Enrich books and products once, from their text plus the cover

Merged
FR
2h ago
+378-44

What & why

A book or product saved with a cover was enriched twice. classify-url queued enrich-item (from the title and description) and analyze-image (the cover) together, and analyze-image then queued a second enrich-item built only from the cover's objects and OCR. Tags and the text vector are last-writer-wins, so the cover pass usually won: a product's search vector came from its cover image rather than its title and description, and the first tag generation and embedding were wasted spend.

  • A book or product with a cover now queues only analyze-image, which takes the item's text as extraSourceText and blends it with the cover's objects and OCR — the same approach analyze-media-cover already takes for tweets (see the comment in handle-twitter-url.ts). A book or product without a cover enriches from its text alone, as before.
  • When item text is present, enrich-item generates the tags from the blend (one gpt-4o-mini call, the same as the text-only enrich this removes) instead of taking the cover's vision tags alone. The cover's objects and OCR lead the blend, so a long description is what the token limit cuts. A plain image upload still uses its vision tags.
  • The cover is an enhancement on a book or product, so once its analysis has failed for good, analyze-image's onFailure enriches the item from its text and the item still completes. It skips that fallback when a re-capture has since replaced the cover.
  • analyze-image's catch now only marks an item failed if it hasn't completed, like enrich-item and analyze-media-cover, so a cover-analysis failure (e.g. on reprocess) can't turn a finished item "Failed".

Checklist

  • User-facing change → added/updated a PostHog event (and a funnel/insight for a new flow), or N/A — N/A: no new action or flow; it changes what enrichment is built from
  • User-facing or service change → docs updated (README features/roadmap/services, .env.example, in-app help, CONTRIBUTING.md, onboarding checklist), or N/A — N/A: no feature, service or setup change
  • New error paths report via captureServerException / the error boundary, or N/A — N/A: no new error paths
  • New behavior is covered by tests, or N/A — classify-url.test.ts (one enrichment per product with a cover, blending its text; text-only for a book without one) and a new analyze-image.integration.test.ts against real Postgres (blended source text and tags; vision tags kept for plain images; the text fallback, and its skip for a replaced cover; a failure never regresses a completed item)
  • User-facing change → demo video attached (record-pr-demo skill); refactor through a user flow → parity run, or N/A — N/A: the change is inside the Trigger.dev pipeline, which isn't configured in the demo stack, so there's nothing to record; the visible effect is in tags and search results
  • If this fixes a recurring defect, considered a guardrail via the learn skill, or N/A — considered: the one-enrichment-per-save rule is now documented on analyze-image and the new triggerEnrichment helper; no deterministic check fits a "don't queue both" rule without over-fitting

Notes

  • Verified: the new tests fail on the previous code and pass now; bun run check:fix clean; the full unit suite passes.
  • Not verified: nothing ran through a real Trigger.dev deployment (not available locally), so the queued runs were checked through the task code, not end to end.
  • #706 is stacked on this branch and moves the failed-status write to after the last retry.