# REST API Async job API alongside the Gradio UI, for callers that need synthesis drafts without a human opening the browser — a CMS, or the [Orchestrator](../../Orchestrator/docs/api.md). - **Base URL (DO)**: `https://archiveai-tts.64-225-26-230.nip.io` - **Auth**: `X-API-Token: ` on everything except `/api/ping`. `/api/drain` requires an operator bearer token. - **Style**: submit returns immediately with a `job_id`; poll for the result. Routes are defined in [`server/api_jobs.py`](../server/api_jobs.py), the source of truth if this drifts. ## Auth `/api/*` is carved out of the droplet's Google OAuth gate so a non-browser caller can reach it, making `X-API-Token` the entire perimeter for these paths (see [the droplet security model](../../Orchestrator/docs/security.md#4-the-api-carve-out)). The check fails closed: an unset `API_TOKEN` gives `503`, never an open API. Comparison is constant-time. | Status | Meaning | |---|---| | `401` | Missing or wrong `X-API-Token`. | | `503` | `API_TOKEN` isn't configured — a deployment problem, not a caller problem. | ## Routes | Method | Path | Auth | Purpose | |---|---|---|---| | `GET` | `/api/ping` | none | Static `{"status":"ok"}`. | | `POST` | `/api/jobs` | token | Submit a turn-generation + synthesis job. | | `GET` | `/api/jobs/{job_id}` | token | Poll status/result. | | `GET` | `/api/jobs?status=` | token | List jobs, optionally filtered. | | `GET` | `/api/projects` | token | List saved projects. Poll to detect UI edits. | | `GET` | `/api/projects/{project_id}` | token | One project's full saved state. | | `GET` | `/api/status` | token | `{active_jobs, draining}`. | | `POST` | `/api/drain` | operator | Stop accepting new jobs before a deploy. | --- ## `POST /api/jobs` `multipart/form-data`. Runs the same pipeline Turn Review's UI runs on upload: parse captions into cues → get a speaker-turn plan → build one segment per turn → synthesise audio for every non-mantra turn. ### Input | Field | Required | Notes | |---|---|---| | `caption_file` | yes | The translated captions (`.vtt`/`.srt`). | | `turns_file` | optional | A pre-computed `turns.json`. Supplying this skips the Gemini turn-plan call: cheaper, deterministic, and the way to re-run synthesis on an approved plan. | | `synthesize_audio` | optional (default `true`) | `false` produces the turn plan and segments without calling ElevenLabs. Use to review splits first. | ### Synthesis parameters All optional. Same knobs the UI exposes. | Field | Default | Notes | |---|---|---| | `model_label` | `Multilingual v2` | ElevenLabs model, by label (see `tts/core/turn_audio.py`'s `MODELS`). | | `speed` | `1.0` | | | `comma_pause` | `0.3` | Seconds inserted at commas. | | `period_pause` | `0.5` | Seconds inserted at sentence ends. | | `stability` | `0.75` | | | `similarity_boost` | `0.75` | | | `style` | `0.05` | | ### Keys, naming, callbacks | Field | Notes | |---|---| | `gemini_api_key` | Per-job override for turn-plan generation. Sanitised before storage. | | `elevenlabs_api_key` | Per-job override for synthesis. Sanitised before storage. | | `label` | Project name; defaults to `untitled`. | | `project_id` | Write into an existing project. Must match `^[A-Za-z0-9_-]+$`. | | `webhook_url` | POSTed to on completion. See [Webhooks](#webhooks). | Returns `202` with `{"job_id": "...", "status": "queued"}`. ```bash # Turn plan only — no ElevenLabs spend, for reviewing splits first curl -X POST https://archiveai-tts.64-225-26-230.nip.io/api/jobs \ -H "X-API-Token: $API_TOKEN" \ -F 'caption_file=@captions.vtt' \ -F 'synthesize_audio=false' \ -F 'label=GR_0150-A' # Synthesise from an already-approved turn plan (skips Gemini) curl -X POST https://archiveai-tts.64-225-26-230.nip.io/api/jobs \ -H "X-API-Token: $API_TOKEN" \ -F 'caption_file=@captions.vtt' -F 'turns_file=@turns.json' \ -F 'project_id=gr-0150-a-3f2a' ``` `synthesize_audio=true` calls Gemini once for the turn plan and ElevenLabs once per non-mantra segment. On a full teaching, cost is material. Prefer `synthesize_audio=false` first, review the plan, then re-submit with `turns_file`. ## `GET /api/jobs/{job_id}` Same shape as the sibling apps: `status` of `queued` | `running` | `done` | `failed`, plus `params` (secrets masked), `progress`, `webhook_delivery`, and on success a `result` carrying the `project_id`. A `404` is permanent. Job state is in-memory, purged an hour after last update, and lost on restart. Resubmit rather than retry. The project is durable — `result.project_id` references it, and it includes the generated audio. ## `GET /api/projects` — the edit-detection endpoint `id`, `name`, `updated_at` per saved project. `updated_at` bumps on every UI edit (`app.py`'s `_autosave_project`); polling this is how a caller detects human edits. There is no push notification out of this app, by design. The Orchestrator polls this every 60s and exposes a merged feed at `/api/changes`. ## `GET /api/projects/{project_id}` The full saved project: settings, segments, per-turn text, timestamps, rationale, and references to each turn's generated audio. `project_id` is validated against `^[A-Za-z0-9_-]+$`; anything else is `422`. That validation matters more here than in TranslationUI: this app's local-file backend builds real filesystem paths from the value. ## `GET /api/status` and `POST /api/drain` `/api/status` returns `{"active_jobs": n, "draining": bool}`. ```bash curl -X POST https://archiveai-tts.64-225-26-230.nip.io/api/drain \ -H "Authorization: Bearer $HF_TOKEN" ``` The bearer value is whatever `HF_TOKEN` is set to on this app, reusing an existing operator value rather than adding a new secret. Draining is in-memory and clears on restart. ## Concurrency: the UI is not on the same queue Unlike TranslationUI and ArchiveAI-STT, this app's UI synthesis handlers are not wrapped through the job store's semaphore. The UI has three separate Generate/Generate All call sites plus a background sweeper thread; wrapping all of them was out of scope when the API was added. Consequences: - API jobs are serialised against each other by `MAX_CONCURRENT_JOBS`. - API jobs and UI-triggered synthesis can run concurrently, competing for the same ElevenLabs rate limit. - `active_jobs` in `/api/status` undercounts real work: a human clicking Generate All in the UI does not show up there. `/api/drain` before a deploy stops API submissions but does not stop a UI batch already running. Confirm the UI is not in use before deploying. ## Webhooks Supply `webhook_url` and a JSON POST — same body as `GET /api/jobs/{job_id}` — is sent on `done` or `failed`, with the outcome in `webhook_delivery`. The destination is re-resolved and IP-pinned immediately before connecting, not merely validated at submission. Synthesis jobs are long-running, so the gap between submission and delivery is exactly the DNS-rebinding window this closes. Private, loopback, and link-local destinations are rejected. A blocked delivery marks `webhook_delivery: "failed"`; the job still completes.