From 03305a57601cd2b5b5a055525339c718d4b51079 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sat, 18 Jul 2026 23:37:51 +0800 Subject: [PATCH 1/3] fix(worker): preserve distinct image placements --- src/lib/image-filtering.ts | 26 ++++++++++++++++++++++++-- tests/image-filtering.test.ts | 33 +++++++++++++++++++++++++++++++-- worker/main.ts | 7 ++++--- 3 files changed, 59 insertions(+), 7 deletions(-) diff --git a/src/lib/image-filtering.ts b/src/lib/image-filtering.ts index 5ae1d3f69..171ae846a 100644 --- a/src/lib/image-filtering.ts +++ b/src/lib/image-filtering.ts @@ -9,7 +9,7 @@ export type CheapImageFilterInput = { bytesLength: number; imageHash: string; seenHashes: Set; - image: Pick; + image: Pick & Partial>; }; export type ClassifiedImage = { @@ -320,6 +320,28 @@ export function normalizeImageBbox(value: unknown): [number, number, number, num return coords.every((coord): coord is number => coord !== null) ? (coords as [number, number, number, number]) : null; } +function stableBboxKey(bbox: unknown) { + const coords = normalizeImageBbox(bbox); + return coords ? coords.map((coord) => Number(coord.toFixed(2))).join(",") : "bbox:null"; +} + +// Exact byte hash alone is too broad for ingestion de-dupe: some PDF extractors can emit +// visually identical table/diagram crops from different page contexts, and dropping every +// later occurrence loses operator-visible clinical evidence. Limit cheap de-dupe to the +// same extracted placement; header/footer/logo filters still remove repeated decorative +// assets across pages. +export function imagePlacementDedupeKey(input: { + imageHash: string; + image: Pick & Partial>; +}) { + return [ + input.imageHash, + input.image.sourceKind ?? "embedded", + input.image.pageNumber ?? "page:null", + stableBboxKey(input.image.bbox), + ].join("|"); +} + function bboxLooksLikeHeaderOrFooter(bbox: unknown) { const coords = normalizeImageBbox(bbox); if (!coords) return false; @@ -335,7 +357,7 @@ export function cheapImageSkipReason(input: CheapImageFilterInput) { const width = image.width ?? null; const height = image.height ?? null; - if (seenHashes.has(imageHash)) return "duplicate image"; + if (seenHashes.has(imagePlacementDedupeKey({ imageHash, image }))) return "duplicate image"; if (sourceKind === "embedded" && bytesLength < 4096) return "small decorative image"; if (width && height) { const shortestSide = Math.min(width, height); diff --git a/tests/image-filtering.test.ts b/tests/image-filtering.test.ts index a948b33e2..b8c4ba2d5 100644 --- a/tests/image-filtering.test.ts +++ b/tests/image-filtering.test.ts @@ -4,6 +4,7 @@ import { cheapImageSkipReason, classifiedImageSkipReason, isClinicalImageEvidence, + imagePlacementDedupeKey, lightweightPerceptualHash, normalizeImageBbox, partitionViewerImages, @@ -11,17 +12,45 @@ import { describe("smart image filtering", () => { it("skips repeated exact image hashes before captioning", () => { - const seenHashes = new Set(["abc"]); + const image = { + sourceKind: "embedded" as const, + width: 600, + height: 400, + pageNumber: 1, + bbox: [10, 20, 610, 420] as [number, number, number, number], + }; + const seenHashes = new Set([imagePlacementDedupeKey({ imageHash: "abc", image })]); expect( cheapImageSkipReason({ bytesLength: 80_000, imageHash: "abc", seenHashes, - image: { sourceKind: "embedded", width: 600, height: 400 }, + image, }), ).toBe("duplicate image"); }); + it("does not drop an exact-byte clinical image from a different page placement", () => { + const firstImage = { + sourceKind: "table_crop" as const, + width: 600, + height: 400, + pageNumber: 1, + bbox: [10, 20, 610, 420] as [number, number, number, number], + }; + const secondImage = { ...firstImage, pageNumber: 2 }; + const seenHashes = new Set([imagePlacementDedupeKey({ imageHash: "same-bytes", image: firstImage })]); + + expect( + cheapImageSkipReason({ + bytesLength: 80_000, + imageHash: "same-bytes", + seenHashes, + image: secondImage, + }), + ).toBeNull(); + }); + it("skips likely header or footer logos", () => { expect( cheapImageSkipReason({ diff --git a/worker/main.ts b/worker/main.ts index bef0660d9..527958eea 100644 --- a/worker/main.ts +++ b/worker/main.ts @@ -24,6 +24,7 @@ import { clinicalImagePolicyVersion, lowSignalImageTextSkipReason, lightweightPerceptualHash, + imagePlacementDedupeKey, } from "../src/lib/image-filtering"; import { isPartialIndexWriteConflict, @@ -857,7 +858,7 @@ async function uploadAndCaptionImages( cropCompleteness: number; ocrTextDensity: number; }> = []; - const seenHashes = new Set(); + const seenImagePlacements = new Set(); let skippedImages = 0; const skipReasons = new Map(); const imageTypeCounts = new Map(); @@ -917,7 +918,7 @@ async function uploadAndCaptionImages( const skipReason = cheapImageSkipReason({ bytesLength: preparedImage.bytesLength, imageHash, - seenHashes, + seenHashes: seenImagePlacements, image, }); if (skipReason) { @@ -925,7 +926,7 @@ async function uploadAndCaptionImages( noteSkippedImage(skipReasons, skipReason); continue; } - seenHashes.add(imageHash); + seenImagePlacements.add(imagePlacementDedupeKey({ imageHash, image })); const nearbyText = image.pageNumber ? pagesByNumber.get(image.pageNumber) : undefined; const tableMetadata = imageTableMetadata(image); From 3bd634daeb14960a0df3393a04ff0427afe87011 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 18 Jul 2026 15:46:32 +0000 Subject: [PATCH 2/3] fix(worker): align enrich de-dupe and keep unknown bboxes unique Update enrich-documents to use imagePlacementDedupeKey with pageNumber, and skip cheap placement de-dupe when bbox is missing or malformed so distinct unknown placements are not collapsed into one shared key. Co-authored-by: BigSimmo --- scripts/enrich-documents.ts | 20 ++++++---- src/lib/image-filtering.ts | 20 +++++++--- tests/image-filtering.test.ts | 70 ++++++++++++++++++++++++++++++++++- worker/main.ts | 3 +- 4 files changed, 96 insertions(+), 17 deletions(-) diff --git a/scripts/enrich-documents.ts b/scripts/enrich-documents.ts index d5b7bb6e3..fba23c9b8 100644 --- a/scripts/enrich-documents.ts +++ b/scripts/enrich-documents.ts @@ -292,6 +292,7 @@ async function classifyExistingImages(supabase: SupabaseAdmin, documentId: strin cheapImageSkipReason, classifiedImageSkipReason, clinicalImagePolicyVersion, + imagePlacementDedupeKey, lightweightPerceptualHash, normalizeImageBbox, }, @@ -332,17 +333,19 @@ async function classifyExistingImages(supabase: SupabaseAdmin, documentId: strin const bytes = Buffer.from(await download.data.arrayBuffer()); const imageHash = image.image_hash ?? hashBytes(bytes); const perceptualHash = lightweightPerceptualHash(imageHash, image.width, image.height); + const filterImage = { + bbox: normalizeImageBbox(image.bbox), + width: image.width, + height: image.height, + pageNumber: typeof image.page_number === "number" ? image.page_number : undefined, + sourceKind: image.source_kind as + "embedded" | "table_crop" | "diagram_crop" | "page_region" | "fallback" | undefined, + }; const cheapSkip = cheapImageSkipReason({ bytesLength: bytes.length, imageHash, seenHashes, - image: { - bbox: normalizeImageBbox(image.bbox), - width: image.width, - height: image.height, - sourceKind: image.source_kind as - "embedded" | "table_crop" | "diagram_crop" | "page_region" | "fallback" | undefined, - }, + image: filterImage, }); if (cheapSkip) { @@ -359,7 +362,8 @@ async function classifyExistingImages(supabase: SupabaseAdmin, documentId: strin skipped += 1; continue; } - seenHashes.add(imageHash); + const placementKey = imagePlacementDedupeKey({ imageHash, image: filterImage }); + if (placementKey) seenHashes.add(placementKey); const baseAssessment = assessClinicalImageUse({ imageType: image.image_type, diff --git a/src/lib/image-filtering.ts b/src/lib/image-filtering.ts index 171ae846a..0600de8bb 100644 --- a/src/lib/image-filtering.ts +++ b/src/lib/image-filtering.ts @@ -320,25 +320,31 @@ export function normalizeImageBbox(value: unknown): [number, number, number, num return coords.every((coord): coord is number => coord !== null) ? (coords as [number, number, number, number]) : null; } -function stableBboxKey(bbox: unknown) { +function stableBboxKey(bbox: unknown): string | null { const coords = normalizeImageBbox(bbox); - return coords ? coords.map((coord) => Number(coord.toFixed(2))).join(",") : "bbox:null"; + // Missing/malformed bboxes must not share one sentinel (e.g. "bbox:null"): that would + // falsely collapse distinct unknown placements on the same page into a single de-dupe key. + // Only comparable coordinates participate in cheap placement de-dupe. + return coords ? coords.map((coord) => Number(coord.toFixed(2))).join(",") : null; } // Exact byte hash alone is too broad for ingestion de-dupe: some PDF extractors can emit // visually identical table/diagram crops from different page contexts, and dropping every // later occurrence loses operator-visible clinical evidence. Limit cheap de-dupe to the // same extracted placement; header/footer/logo filters still remove repeated decorative -// assets across pages. +// assets across pages. Returns null when bbox placement is not comparable so callers can +// skip de-dupe rather than collide unknown placements. export function imagePlacementDedupeKey(input: { imageHash: string; image: Pick & Partial>; -}) { +}): string | null { + const bboxKey = stableBboxKey(input.image.bbox); + if (bboxKey === null) return null; return [ input.imageHash, input.image.sourceKind ?? "embedded", input.image.pageNumber ?? "page:null", - stableBboxKey(input.image.bbox), + bboxKey, ].join("|"); } @@ -357,7 +363,9 @@ export function cheapImageSkipReason(input: CheapImageFilterInput) { const width = image.width ?? null; const height = image.height ?? null; - if (seenHashes.has(imagePlacementDedupeKey({ imageHash, image }))) return "duplicate image"; + const placementKey = imagePlacementDedupeKey({ imageHash, image }); + // Only de-dupe when placement is comparable; unknown/malformed bboxes stay unique. + if (placementKey !== null && seenHashes.has(placementKey)) return "duplicate image"; if (sourceKind === "embedded" && bytesLength < 4096) return "small decorative image"; if (width && height) { const shortestSide = Math.min(width, height); diff --git a/tests/image-filtering.test.ts b/tests/image-filtering.test.ts index b8c4ba2d5..49af17a39 100644 --- a/tests/image-filtering.test.ts +++ b/tests/image-filtering.test.ts @@ -19,7 +19,9 @@ describe("smart image filtering", () => { pageNumber: 1, bbox: [10, 20, 610, 420] as [number, number, number, number], }; - const seenHashes = new Set([imagePlacementDedupeKey({ imageHash: "abc", image })]); + const placementKey = imagePlacementDedupeKey({ imageHash: "abc", image }); + expect(placementKey).toBeTruthy(); + const seenHashes = new Set([placementKey!]); expect( cheapImageSkipReason({ bytesLength: 80_000, @@ -39,7 +41,9 @@ describe("smart image filtering", () => { bbox: [10, 20, 610, 420] as [number, number, number, number], }; const secondImage = { ...firstImage, pageNumber: 2 }; - const seenHashes = new Set([imagePlacementDedupeKey({ imageHash: "same-bytes", image: firstImage })]); + const placementKey = imagePlacementDedupeKey({ imageHash: "same-bytes", image: firstImage }); + expect(placementKey).toBeTruthy(); + const seenHashes = new Set([placementKey!]); expect( cheapImageSkipReason({ @@ -51,6 +55,68 @@ describe("smart image filtering", () => { ).toBeNull(); }); + it("does not collapse missing or malformed bboxes into one shared de-dupe key", () => { + const base = { + sourceKind: "embedded" as const, + width: 600, + height: 400, + pageNumber: 1, + }; + const missingBbox = { ...base, bbox: null as unknown as [number, number, number, number] }; + const malformedBbox = { + ...base, + bbox: { x0: 10, y0: 20, x1: 610, y1: 420 } as unknown as [number, number, number, number], + }; + + expect(imagePlacementDedupeKey({ imageHash: "same-bytes", image: missingBbox })).toBeNull(); + expect(imagePlacementDedupeKey({ imageHash: "same-bytes", image: malformedBbox })).toBeNull(); + + const seenHashes = new Set(); + expect( + cheapImageSkipReason({ + bytesLength: 80_000, + imageHash: "same-bytes", + seenHashes, + image: missingBbox, + }), + ).toBeNull(); + // Even if a caller mistakenly records a sentinel, unknown placements must not de-dupe. + seenHashes.add("same-bytes|embedded|1|bbox:null"); + expect( + cheapImageSkipReason({ + bytesLength: 80_000, + imageHash: "same-bytes", + seenHashes, + image: malformedBbox, + }), + ).toBeNull(); + }); + + it("still de-dupes true identical placements when bbox coordinates match", () => { + const image = { + sourceKind: "table_crop" as const, + width: 600, + height: 400, + pageNumber: 3, + bbox: [12.345, 20.001, 610.004, 420.009] as [number, number, number, number], + }; + const samePlacement = { + ...image, + // Rounding-equivalent coords must share the placement key. + bbox: [12.35, 20, 610, 420.01] as [number, number, number, number], + }; + const placementKey = imagePlacementDedupeKey({ imageHash: "table-bytes", image }); + expect(placementKey).toBe(imagePlacementDedupeKey({ imageHash: "table-bytes", image: samePlacement })); + expect( + cheapImageSkipReason({ + bytesLength: 80_000, + imageHash: "table-bytes", + seenHashes: new Set([placementKey!]), + image: samePlacement, + }), + ).toBe("duplicate image"); + }); + it("skips likely header or footer logos", () => { expect( cheapImageSkipReason({ diff --git a/worker/main.ts b/worker/main.ts index 527958eea..0f035de3f 100644 --- a/worker/main.ts +++ b/worker/main.ts @@ -926,7 +926,8 @@ async function uploadAndCaptionImages( noteSkippedImage(skipReasons, skipReason); continue; } - seenImagePlacements.add(imagePlacementDedupeKey({ imageHash, image })); + const placementKey = imagePlacementDedupeKey({ imageHash, image }); + if (placementKey) seenImagePlacements.add(placementKey); const nearbyText = image.pageNumber ? pagesByNumber.get(image.pageNumber) : undefined; const tableMetadata = imageTableMetadata(image); From 4740462948031e930984f741b4aa15a92c83b698 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 18 Jul 2026 15:59:57 +0000 Subject: [PATCH 3/3] style: prettier image-filtering placement key helpers Co-authored-by: BigSimmo --- src/lib/image-filtering.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/lib/image-filtering.ts b/src/lib/image-filtering.ts index 0600de8bb..967ee60d6 100644 --- a/src/lib/image-filtering.ts +++ b/src/lib/image-filtering.ts @@ -340,12 +340,9 @@ export function imagePlacementDedupeKey(input: { }): string | null { const bboxKey = stableBboxKey(input.image.bbox); if (bboxKey === null) return null; - return [ - input.imageHash, - input.image.sourceKind ?? "embedded", - input.image.pageNumber ?? "page:null", - bboxKey, - ].join("|"); + return [input.imageHash, input.image.sourceKind ?? "embedded", input.image.pageNumber ?? "page:null", bboxKey].join( + "|", + ); } function bboxLooksLikeHeaderOrFooter(bbox: unknown) {