Skip to content

Source-Document Branding Detection (issue #1762)

When a user uploads a source PDF or image, extraction now runs a second, cheap vision pass over page 1 that looks for a header band and a footer band (org name, document title, confidentiality notice, logo graphic). The result is stored on org_charts.suggested_branding (JSONB, migration 224) as a suggestion only β€” it is never applied automatically, mirroring the guardrail that extracted colors are never applied raw.

Flow

  1. POST /api/orgcharts/:id/extract β€” after the PDF page renders exist, detectBrandingSuggestion() (services/org-chart/branding-detector.ts) runs concurrently with the main dual-model extraction on page 1 only. Best-effort: any failure logs a warning and extraction proceeds normally.
  2. The detection prompt returns literal band text plus, when an actual logo graphic is present, a fractional bounding box and model-written alt text.
  3. WCAG 1.4.5 rule: detected text is always suggested as text (headerText / footerText), re-rendered in the chart’s AA-safe palette. Text is never cropped into an image. Only a logo graphic is cropped from the page-1 render (in-browser canvas, same technique as pdf-converter.ts), downscaled to the 2000Γ—400 branding limits, and staged in R2 at users/{userId}/branding/{chartId}/suggested-{header|footer}.png.
  4. The branding page (apps/org-chart/src/app/branding/page.tsx) shows a β€œWe found branding in your document” banner with Apply / Dismiss:
    • POST /api/orgcharts/:id/branding/apply-suggestion β€” writes the chart-level branding columns (header_text, header_image_r2_key + required alt, footer equivalents), copies staged crops to the real branding keys, clears suggested_branding, deletes staged objects.
    • DELETE /api/orgcharts/:id/branding/suggestion β€” dismisses: clears the column and deletes staged objects; real branding untouched.

Guardrails

  • Never auto-applied; the seed color remains the branding source of truth.
  • Logo boxes are sanitized: clamped to the page, slivers and boxes covering

    25% of the page (the chart itself, misreported) are dropped.

  • An applied image always carries alt text (model-written, or the DEFAULT_LOGO_ALT fallback) β€” the existing required-alt rule holds.
  • Detection cost is recorded to cost_ledger under org-chart-extraction with metadata.phase = 'branding-detection' (no new operation type).
  • Re-extraction overwrites the suggestion; staged keys are fixed per chart.

Deploy notes

  • Migration 20260726000000_224_org_chart_suggested_branding.sql (additive) must be applied to prod Supabase before deploying the reading code.
  • API changes ship to BOTH runtimes: Lambda (deploy:lambda) and the Node server (npm run rebuild on 10.1.1.4).