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>/finalizeFinalize is what turns a minted URL into a record something will serve — and it is what the enrichment Workflows key off.
What gets derived
| Type | Workflow | Produces |
|---|---|---|
image | MEDIA_IMAGE_TO_TEXT | Alt text and captions |
audio | MEDIA_AUDIO_TRANSCRIBE | A transcript |
video | MEDIA_VIDEO_TRANSCRIBE | A transcript, from the audio rendition |
document | MEDIA_DOC_EXTRACT | Text 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_failed | The one retryable fault. Workers AI unreachable, overloaded, out of time |
media/enrichment_failed | Deterministic, 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.