Media enrichment

You need: media composed, pithy provision run, and an AI binding.

Upload, then finalize

A backend per media type is which store took the bytes, the enrichment Workflows is what produces the text, provisioning media stands it up, and semantic search is what the text is worth.

POST /media
{ "type": "image", "filename": "hero.png", "contentType": "image/png" }
→ { "id": "…", "url": "…" }

The client PUTs the bytes, then:

POST /media/<id>/finalize

Finalize is what turns a minted URL into a record something will serve — and it is what the enrichment Workflows key off.

What gets derived

TypeWorkflowProduces
imageMEDIA_IMAGE_TO_TEXTAlt text and captions
audioMEDIA_AUDIO_TRANSCRIBEA transcript
videoMEDIA_VIDEO_TRANSCRIBEA transcript, from the audio rendition
documentMEDIA_DOC_EXTRACTText out of a PDF or Word file

Documents extract from pdf, doc and docx — a document outside that set is stored perfectly well and simply has no text to derive. That is not a failure and does not appear as one.

It runs on your binding, on your bill

The inference cost lands on your Cloudflare account, and your users’ files never leave your infrastructure.

The models are parameters

Defaulted from config, so swapping a model is a config edit rather than a code change.

The prompts are constants you can override too — alt text is capped at 80 tokens and asks for one concise sentence, a caption at 512.

They are short on purpose: an alt-text prompt that invites detail produces alt text that is worse at being alt text.

What retries, and what does not

core/upstream_failedThe one retryable fault. Workers AI unreachable, overloaded, out of time
media/enrichment_failedDeterministic, and terminal. A shape the schema does not recognize, a conversion refused, a binding missing a method

The binding’s own words go in the error’s detail, which the HTTP codec strips — an account id or a quota message is not something a caller asking about their own file should be handed.

Video transcription has a trick worth knowing

A video’s audio arrives as a Cloudflare Stream HLS rendition — an init segment plus ordered media segments.

HLS segments are atomic, so grouping is whole-segment, greedy, to a target of 30 seconds.

Then each group backs up 3 seconds into the previous one. A model asked to transcribe a fixed window routinely garbles the words straddling its edges, and a boundary every 30 seconds is a lot of edges.

The overlap is then stripped from the text, so the transcript reads clean. You get the second look without reading anything twice.

Choosing the backend matters here

Video on cf-stream has an audio rendition to read. Video on r2 does not.

So the backend choice reaches further than delivery — see a backend per media type.

Then use it

The derived text is on the record. Feed it to vector and your media becomes searchable by meaning — a transcript is exactly the shape a semantic index wants.

ESC