Enrich books and products once, from their text plus the cover
Merged
FR
2h ago+378+378+012345678901234567890123456789-44-44-01234567890123456789
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 asextraSourceTextand blends it with the cover's objects and OCR — the same approachanalyze-media-coveralready takes for tweets (see the comment inhandle-twitter-url.ts). A book or product without a cover enriches from its text alone, as before. - When item text is present,
enrich-itemgenerates 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'sonFailureenriches 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, likeenrich-itemandanalyze-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 newanalyze-image.integration.test.tsagainst 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-demoskill); 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
learnskill, or N/A — considered: the one-enrichment-per-save rule is now documented onanalyze-imageand the newtriggerEnrichmenthelper; 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:fixclean; 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.