File size: 7,095 Bytes
b2d53f4
 
255731f
b2d53f4
 
255731f
b2d53f4
 
255731f
b2d53f4
 
 
255731f
b2d53f4
 
 
 
 
 
 
 
 
 
 
 
 
 
255731f
b2d53f4
 
 
 
 
 
 
 
255731f
b2d53f4
 
 
 
 
255731f
 
 
b2d53f4
 
 
255731f
b2d53f4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
255731f
b2d53f4
 
255731f
b2d53f4
 
 
 
 
 
 
 
 
 
 
 
 
255731f
b2d53f4
 
 
255731f
b2d53f4
255731f
b2d53f4
 
 
255731f
b2d53f4
 
 
255731f
b2d53f4
 
 
255731f
b2d53f4
 
 
 
 
 
255731f
b2d53f4
 
 
255731f
b2d53f4
255731f
b2d53f4
255731f
 
 
b2d53f4
255731f
b2d53f4
 
 
255731f
b2d53f4
255731f
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# 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: <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.