Skip to content

Textract figure detection

Detects non-text artwork the vision pass never emitted an <img> for, using Amazon Textract’s LAYOUT feature. Module: workers/api/src/services/textract-layout-detector.ts.

Why

Every other figure path requires the vision model to volunteer the figure first:

PathRequires
data-region-pct on an <img>vision emitted the img and a bbox
embedded-image-geometryvision emitted an img to attach a raster to
image-region-recovery (Haiku)vision emitted an <img> placeholder

When the vision pass transcribes a page as pure text and never mentions the artwork, all three are silently inert. Job 8bab5965 (Weatherford Newspaper 1906) shipped 46KB of HTML with zero <img> tags and a clean WCAG pass β€” image-alt reported β€œno images found”, because none were.

Textract LAYOUT is a detector: it segments the page without being told what to look for, so it does not depend on the transcription step noticing anything.

Measured, 2026-09-05

On job 8bab5965 β€” a 1-bit CCITTFax scan of a dense 1906 broadsheet, the hardest real case we have:

DetectorResultBboxIoU vs Textract
Vision pass (production)missed entirelyβ€”β€”
Claude Haiku 4.5, 6 runsdetected 6/61.6×–2.3Γ— too wide0.21–0.37
Textract LAYOUTdetected, 87.2% conftight1.00 (reference)

Haiku’s recall is fine; its localisation is not. At 1.6–2.3Γ— width the crop swallows two columns of body text, shipping an image-of-text (WCAG 1.4.5) β€” worse than emitting nothing. Higher render resolution made Haiku worse, not better.

Haiku remains correct for localising a figure the model already described (image-region-recovery.ts). It is not a detector. Textract is not a describer β€” alt text still comes from the alt-text pass.

Cost

AnalyzeDocument Layout is $0.004/page (first 1M pages/month, us-east-1; $0.003 above), no per-call minimum. Verified against the AWS Pricing API on 2026-09-05.

Option1-page doc4-page docMinimum
Textract Layout$0.004$0.016none
Marker$0.010$0.024$0.01/call
Claude Haiku bbox call~$0.002~$0.008none

Layout is its own cheap SKU β€” do not confuse it with AnalyzeDocument Tables ($0.015/page) or Forms ($0.05/page).

Tables β€” free signal, deliberately not the Tables feature

LAYOUT_TABLE blocks arrive in the same response as the figures, at no extra cost. The module returns tableCountByPage so the assembler can notice that the vision pass flattened a table to <p>/<dl> (a known failure β€” see the comment at routes/convert.ts:1391).

This is detection only β€” LAYOUT_TABLE carries a bounding box, not cell structure. Getting rows, columns, merged cells, and header association requires FeatureTypes: ['TABLES'] at $0.015/page, 3.75Γ— the Layout rate. The cheap path is: use the free count as a tripwire, and only escalate to TABLES on a page where the counts disagree. Not yet wired.

Enabling β€” ZDR prerequisite (SATISFIED 2026-09-07)

The prerequisite is an AWS Organizations AI services opt-out policy. AWS AI services, Textract included, may retain customer content for service improvement unless that policy exists β€” it is not the default, and the Bedrock ZDR posture (bedrock:PutAccountDataRetention: none) does not cover it. The two are separate controls.

Status: attached and verified.

Policyai-services-opt-out (p-90qu1cuzca)
Attached toorg root r-sml2 (inherited by all accounts)
Effective for696081601609 since 2026-09-06 10:33 CDT
Terminal window
aws organizations describe-effective-policy \
--policy-type AISERVICES_OPT_OUT_POLICY --target-id 696081601609
# {"services":{"default":{"opt_out_policy":"optOut"}}}

Note that account-level list-policies-for-target returns [] here β€” it lists only directly attached policies, and this one is attached at the root. describe-effective-policy is the check that matters; do not read the empty list as β€œnot applied”.

Flags were turned on 2026-09-07 once that verify passed:

flagruntimewhere
TEXTRACT_FIGURE_DETECTION=trueLambdainfra/cdk/lib/stacks/api-stack.ts
TEXTRACT_FIGURE_DETECTION=trueNode server .4.env.node-server
PDF_TEXTRACT_ENABLED=trueLambdainfra/cdk/lib/stacks/api-stack.ts
PDF_TEXTRACT_ENABLED=trueNode server .4.env.node-server (pre-existing)

One document (job 8bab5965) was sent to Textract on 2026-09-05 during evaluation, before the policy was in place. Tracked as CRM task 628225a8-a139-4396-993c-33471c20a9d6. Do not close that task because the opt-out policy is now attached: the policy was attached on 2026-09-06 and does not apply retroactively to a call already made, so the task is the only durable record of that exposure.

PDF_TEXTRACT_ENABLED above does not control the flyer-tagger OCR lane (#3415). That lane has its own switch, FLYER_TAGGER_TEXTRACT_ENABLED, deliberately so that an env copy from this file cannot enable it β€” see docs/admin/products/pdf/textract-ocr-lane.md.

Deployment notes

  • IAM: textract:AnalyzeDocument granted to the API Lambda in infra/cdk/lib/stacks/api-stack.ts. AnalyzeDocument has no resource-level ARN, so '*' is the only expressible resource; the action itself is the scope.
  • The chunked pipeline runs on the Node server (10.1.1.4), not the Lambda. The IAM grant above does not cover it. .4 needs AWS credentials with textract:AnalyzeDocument in .env.node-server, plus a rebuild. This is the same runtime-parity trap that has bitten MATHJAX_PRERENDER_ENABLED and PUBLIC_BASE_URL β€” a flag set in one runtime and not the other fails silently down the OFF path.
  • Region defaults to AWS_REGION, then us-east-1.

Wired

Called from chunk-assembler.ts (step 4a-v, ~line 1828) via detectFiguresViaTextract, gated by isTextractDetectionEnabled() and the presence of a browser. It runs before the screenshot fallback, so a precise ~$0.004/pg crop pre-empts a whole-page raster. Fail-open β€” a Textract outage logs a non-blocking warning and never fails an otherwise-good conversion.

textractCandidatePages() implements the gate argued for here: it takes embeddedRectsByPage, assignedRectsByPage and pagesWithAssignedImage to rule out Case A, plus the complexity pages, and returns only pages where Textract is the right tool. The reasoning that produced it is kept below because it is the argument for not widening the gate later.

Two distinct failure cases, with different correct fixes:

Case A β€” discrete image XObjects went unclaimed. 02_human_anatomy (2 pages, 1 image XObject) and Sample of cover issue (4 pages, 4 image XObjects) both produced zero-image output on some runs and correct output on others, from byte-identical input. The raster and its placement geometry are already in the PDF; embedded-image-geometry.ts exists precisely for this. Textract is the wrong tool β€” this should be fixed deterministically, for free, and it appears to be the more common case.

Case B β€” artwork is pixels inside a full-page scan. Weatherford. No separate XObject exists, so region-cropping from the page render is the only option, and detection has to come from somewhere. This is what Textract is for.

Gating Textract on Case A would spend money papering over a bug that has a free, exact fix. Recommended gate: page has no unclaimed usable raster (ruling out Case A) and the vision pass emitted zero figures and the page is backed by a full-page raster.