mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
* fix(pdf): never enlarge on compress, honor redact case, hide same-format convert
- compress-pdf: guard both modes so output is never larger than the input; low-DPI scans could be upsampled and grow. Falls back to the original bytes.
- doc_redact.py: caseSensitive=true now filters PyMuPDF's case-insensitive search to exact-case hits, so the toggle works instead of always over-redacting.
- convert-{document,presentation,spreadsheet}: omit the input's own format from the output dropdown; the backend already rejects same-format conversions.
Verified end-to-end against an isolated Docker stack during a full visual QA sweep of all 37 PDF tools.
* fix(ui): show real multi-file preview thumbnails per modality
The bottom multi-file preview strip rendered a raw <img src=blobUrl> for every file, so audio/video/PDF inputs showed a broken-image icon plus the filename. ThumbnailStrip now branches on FileEntry.previewKind: images use <img> (icon fallback on error), video shows a captured first frame, PDF shows a pdf.js page-1 render, and audio/other show a type icon + extension. Fixes the multi-file preview across all modalities.
Verified in the browser for image/PDF/audio/video.
* fix(modality): make pipeline, batch validation, save/upload, previews & UI modality-aware
The app grew up image-only; several paths still assumed image. They now dispatch on the tool/file modality (image/video/audio/document/file):
- pipeline /execute + /batch: validate+decode input via inputHandlerFor(modality) instead of validateImageBuffer, so PDF/audio/video/data pipelines work (were rejected 'Invalid image').
- batch: non-image inputs now get per-modality validation (ffprobe/qpdf) before the worker instead of passing through unchecked.
- files /upload, user-files /save-result + /thumbnail: accept non-image files (MIME from extension; video-poster / pdf-first-page thumbnails).
- postprocess CONTENT_TYPE_TO_EXT: cover video/audio/pdf/text/zip so output extensions are corrected for all modalities.
- worker pipeline-finalize: attach result payload to the complete SSE event so the sync-window-timeout fallback still delivers a download.
- frontend: batch-ZIP blob MIME by extension (not svg-only); modality-neutral fallback labels/filenames; 'smaller file' not 'smaller image'.
Found via a codebase-wide image-only-assumption audit. Verified: PDF/audio/video pipelines + batch now work; image paths unchanged. canBrowserPreview kept image-only by design (non-image is rendered by dedicated displayMode viewers).
* fix(pipeline): generate a modality-aware preview for pipeline results
processPipelineFinalize now derives the output content type from its extension and runs generatePreview (video poster / pdf first page / image thumb), sets previewRef on the result, and surfaces previewUrl in the /execute sync response and the SSE complete event (via buildLegacyResultPayload). Pipeline outputs get a preview like single-tool results instead of always returning previewUrl: undefined.
Verified: PDF pipeline -> previewUrl returns a valid PNG first-page render; png pipeline correctly has no previewUrl; audio/video/multi-step pipelines all 200.
* fix(worker): auto-save a new library version when processing a library file
The worker hardcoded savedFileId = undefined ('No auto-save') even though the whole versioning feature was wired around it: the frontend sends fileId for library files and reads result.savedFileId, tool-factory threads fileId into ToolJobData, and autoSaveToLibrary implements the new-version save -- but the worker never called it (dead code from the tool-first-workflow merge). processToolJob now calls autoSaveToLibrary with data.fileId; without a fileId it is a no-op, so tool-first uploads are unchanged.
Verified: processing a library PDF with fileId creates version 2 (parent linked, toolChain appended, savedFileId returned); processing without fileId saves nothing.
* fix(library): ownership check + modality-aware dimensions in autoSaveToLibrary
- Only create a new version when the requester owns the parent (parent.userId === opts.userId); prevents versioning another user's file via a known fileId.
- Dimensions are modality-aware: sharp for images, ffprobe (probeMedia) for video, null for audio/document. Previously sharp-only, so non-image versions always got null dims.
* fix(ai): thread fileId + real userId through the 16 AI tool routes
AI custom routes parsed neither the fileId multipart field nor the authenticated user (they hardcoded userId: null), so processing a library file via an AI tool never created a new version, and AI jobs were unattributed. Each route now parses fileId like clientJobId and passes getAuthUser(request)?.id as userId to enqueueToolJob.
Verified: ocr-pdf on a library PDF creates a new version (v2); the ownership check still denies cross-user versioning.
72 lines
2.8 KiB
Python
72 lines
2.8 KiB
Python
"""True redaction: remove every occurrence of the given terms, then VERIFY
|
|
none remain extractable. Args: {"path": in, "out": out, "terms": [".."],
|
|
"caseSensitive": false}. Prints {"found": N, "verified": true}.
|
|
|
|
Case sensitivity: fitz.Page.search_for is ALWAYS case-insensitive, so when
|
|
caseSensitive=true we post-filter the hits to only those whose on-page glyphs
|
|
match the term's exact case (checked with get_textbox). caseSensitive=false
|
|
redacts every case variant. The verification pass applies the same casing
|
|
rule, so it proves the intended occurrences are gone."""
|
|
import json
|
|
import sys
|
|
|
|
|
|
def main():
|
|
args = json.loads(sys.argv[1]) if len(sys.argv) > 1 else {}
|
|
path, out, terms = args.get("path"), args.get("out"), args.get("terms") or []
|
|
case_sensitive = bool(args.get("caseSensitive", False))
|
|
if not path or not out or not isinstance(terms, list) or not terms:
|
|
print(json.dumps({"error": "missing path/out/terms"}))
|
|
sys.exit(1)
|
|
try:
|
|
import fitz
|
|
except ImportError:
|
|
print(json.dumps({"error": "PyMuPDF not installed"}))
|
|
sys.exit(1)
|
|
try:
|
|
doc = fitz.open(path)
|
|
flags = fitz.TEXT_DEHYPHENATE
|
|
found = 0
|
|
for page in doc:
|
|
for term in terms:
|
|
if case_sensitive:
|
|
# search_for is always case-insensitive; keep only the
|
|
# exact-case hits by checking the glyphs under each quad.
|
|
quads = [
|
|
q
|
|
for q in page.search_for(term, quads=True, flags=flags)
|
|
if term in page.get_textbox(q.rect)
|
|
]
|
|
else:
|
|
quads = page.search_for(term, quads=True)
|
|
for quad in quads:
|
|
page.add_redact_annot(quad, fill=(0, 0, 0))
|
|
found += 1
|
|
page.apply_redactions()
|
|
doc.save(out, garbage=4, deflate=True)
|
|
doc.close()
|
|
# Verification pass: reopen and prove no term is extractable anymore.
|
|
check = fitz.open(out)
|
|
leaked = []
|
|
for page in check:
|
|
text = page.get_text()
|
|
haystack = text if case_sensitive else text.lower()
|
|
for term in terms:
|
|
needle = term if case_sensitive else term.lower()
|
|
if needle and needle in haystack:
|
|
leaked.append(term)
|
|
check.close()
|
|
if leaked:
|
|
print(json.dumps({"error": f"verification failed: terms still extractable: {sorted(set(leaked))}"}))
|
|
sys.exit(1)
|
|
print(json.dumps({"found": found, "verified": True}))
|
|
except SystemExit:
|
|
raise
|
|
except Exception as exc: # noqa: BLE001
|
|
print(json.dumps({"error": str(exc)}))
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|