spikes/media-a2e/ (Go, live against a2e: image, video, speech)openspec/changes/add-media-generationtoolnexus can carry media (ContentPart, §1B; §8A emission) but cannot make it. Image,
video, speech, transcription, voice cloning and talking-head generation all reduce to two call
shapes:
Live conversation (realtime duplex audio) is out of scope (owner, 2026-11-02): it is a long-lived stream, not a call that returns a result — neither a tool nor reachable through MCP.
generateImage, generateSpeech, transcribe; experimental_generateVideo
with poll:{intervalMs:5000, timeoutMs:600000}, webhook alternative, abortSignal, URL→bytes
download capped at 2 GiB. One code adapter package per provider.Prefer: wait (≤60 s) then poll urls.get every 1–2 s, or webhook; Cancel-After.subscribe() blocks and polls; queue.submit/status/result/cancel; SSE status; webhooks.POST /prompt → /history/{id} → /view; history shape only in server.py.Everyone who ships video converged on submit → auto-poll → one awaited call. Nobody exposes it as an agent tool, and nobody makes providers data instead of code. Those two are our difference.
One engine, two faces, styles as data, in all eight ports.
┌── function face ─▶ media.image({prompt}) / video / speech … (host decides)
media engine ────┤
(style + poll) └── tool face ─────▶ generate_image / generate_video / … (LLM decides)
createMedia(config) returns an object with fixed verbs:
image, video, speech, transcribe, cloneVoice, talking. Arguments are normalised and
each style maps them to provider fields. Stringly-typed call("image_to_video", …) was rejected
by the owner.generate_image, generate_video, generate_speech, transcribe_audio, clone_voice,
talking_video). Switching provider never renames a tool. Media is a tool source, not a client.a2e, …) live in examples/media-styles/ and are loaded by
path — no vendor names in the public API (scope ADR). A config may route verbs to different styles.done/failed means keep polling.RunResult.files — the client loop returns every media part produced during a run, in call
order, tagged with from (tool name) and callId; toolCalls[].parts carries per-call parts.
RunResult.text never contains markup — rendering is the host's job.| # | Finding | Spec consequence |
|---|---|---|
| 1 | text2image start returns data as an array, image2video as an object | per-verb idPath with numeric segments |
| 2 | Real paths are /api/v1/…, not the documented /v1/… | baseUrl per style |
| 3 | HTTP 200 with code != 0 signals errors | optional errorEnvelope {codePath, okValues, messagePath} |
| 4 | Result URLs expire (3days-…, 30days-… hosts) | download is a planned follow-up, named as a gap |
| 5 | Speech came back .wav, not mp3 | MIME from URL extension first, kind default second |
| 6 | Video reported sent, a status not in the docs | unknown status ⇒ keep polling |
| 7 | negative_prompt documented optional, actually required | fixed in the style alone; every preset verb needs a live smoke |
| 8 | voice → references.0.speaker | arg map targets are dot-paths that create maps and arrays |
Measured: image ~26–29 s, speech ~33 s, image→video ~238 s, all through one ~300-line engine.
Download of expiring URLs, local-file upload flows, ComfyUI graph injection (needs a spike against
a real server), webhooks, price quotes, §10 suspend for long jobs, streaming file events.
RunResult.files.conformance/media/cases.json) replayed by a fake server in every port.Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |