Agent-facing service discovery

Captioner

Audio-first captioning service that turns short-term R2 audio into corrected transcripts, terminology audit data, and YouTube-ready SRT captions.

Agent Workflow

  1. Read this HTML document, then use /openapi.json as the canonical request and response contract.
  2. If you do not already have a credential, follow /auth.md to claim a WorkOS API key.
  3. If the source is video or non-MP3 audio, extract speech audio locally as MP3 before upload.
  4. Upload the extracted MP3 with PUT /audio/:key and the WorkOS API key as bearer auth.
  5. Start POST /workflows/captioner?wait=result with a JSON payload. Use wait=result when you need the completed captions in the response.
  6. Save the returned JSON and SRT near the source media unless the user asks for another destination.

Authentication

Bearer auth with a WorkOS API Key is required for PUT /audio/:key and POST /workflows/captioner?wait=result.

Required scopes: captioner:audio:upload, captioner:caption:run.

Use an existing stored Captioner credential first. If none exists, use /auth.md to claim a WorkOS organization-scoped API key. Store durably only with user consent; never print it or persist it in logs.

macOS Keychain convention: service captioner.chan.dev, account equal to the verified email. After a successful claim, ask once: Store this Captioner API key in macOS Keychain for future runs?

# Store after user consent. Keep the key in CAPTIONER_API_KEY and never echo it.
CAPTIONER_EMAIL="user@example.com"
security add-generic-password -s captioner.chan.dev -a "$CAPTIONER_EMAIL" -U -w "$CAPTIONER_API_KEY"

# Reuse when the verified email is known.
CAPTIONER_API_KEY="$(security find-generic-password -s captioner.chan.dev -a "$CAPTIONER_EMAIL" -w)"

# Remove a revoked or invalid key.
security delete-generic-password -s captioner.chan.dev -a "$CAPTIONER_EMAIL"

Resources

Media Policy

Accepted upload media types: audio/mpeg.

Upload keys must end in .mp3 and files must be no larger than 100 MiB. Non-MP3 uploads return 415 mp3_required; oversized uploads return 413 upload_too_large. Extract the first speech audio track locally as MP3 before upload.

Recommended extraction

ffmpeg -i input.mp4 -map 0:a:0 -vn -ac 1 -ar 16000 -c:a libmp3lame -b:a 32k output.mp3

Fallback for noisy or compressed sources

ffmpeg -i input.mp4 -map 0:a:0 -vn -ac 1 -ar 16000 -c:a libmp3lame -b:a 48k output.mp3

Requests

Upload MP3

curl -X PUT "https://caption.chan.dev/audio/uploads/demo.mp3" \
  -H "Authorization: Bearer $CAPTIONER_API_KEY" \
  -H "Content-Type: audio/mpeg" \
  --data-binary @output.mp3

Run captioner

curl "https://caption.chan.dev/workflows/captioner?wait=result" \
  -H "Authorization: Bearer $CAPTIONER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"audio":{"key":"uploads/demo.mp3","mediaType":"audio/mpeg"},"topic":"MCP Night","glossary":["MCP","Cloudflare","WorkOS"]}'

Minimal payload

{
  "audio": {
    "key": "uploads/demo.mp3",
    "mediaType": "audio/mpeg"
  },
  "topic": "MCP Night",
  "glossary": [
    "MCP",
    "Cloudflare",
    "WorkOS"
  ]
}

Optional fields for terminology packs, provider selection, caption settings, and storage retention are documented in /openapi.json.

Terminology

Use payload.terminology.packs for explicit packs; otherwise packs are selected from topic, filename, glossary, and keyterms.

Available pack IDs:

Fetch /terminology/packs for pack titles, descriptions, triggers, related packs, and term counts.

Result

With wait=result, the workflow response is a JSON envelope. Read plain transcript text from result.transcript.text and the caption file from result.captions.srt.

Caption cues are generated from punctuated and capitalized transcript tokens, so cue text can differ from raw lowercase word fields. Read deterministic cue QC from result.quality.captions.

Useful result paths: result.transcript, result.audit, result.metadata.terminology.observations, and result.metadata.storage.sourceAudio.

Source audio is deleted as soon as transcription has consumed it by default, and deletion is retried if the workflow fails. Set payload.storage.retainSourceAudio=true only when a rerun or debug pass needs the uploaded audio to stay in R2.

Machine-Readable Contract

This HTML includes <script type="application/json" id="captioner-agent-contract"> with the same discovery data. The API schema remains at /openapi.json. When requested with Accept: text/markdown, this page returns Markdown with Content-Type: text/markdown.