{"openapi":"3.1.0","info":{"title":"Sleeper Hit Studio Story API","summary":"Programmatic story generation, source-grounded planning, and durable artifact delivery for AI agents and enterprise integrations.","description":"Authenticated B2B API for programmatic story generation. Customer API keys, scopes, idempotent POSTs, keyed rate limiting, and Studio Credit accounting are stable today. Source ingestion, Series Bibles, StoryPlans, story jobs, table reads, pitch decks, and post-creation artifact mutations are live today. Webhooks and additional artifact adapters are planned follow-on phases. Discover current availability through GET /api/v1/capabilities before integrating a flow.  Authentication: pass an API key as `Authorization: Bearer sh_<prefix>_<secret>`. Errors use a stable envelope `{ error: { code, message, requestId } }` with the codes listed under components.schemas.ErrorCode.","version":"1.0.0-foundation","contact":{"name":"Sleeper Hit Studio","url":"https://sleeperhit.studio"}},"servers":[{"url":"https://sleeperhit.studio/api/v1","description":"Production"}],"security":[{"BearerApiKey":[]}],"tags":[{"name":"Discovery","description":"Machine-readable capability and credit metadata used to plan integration flows."},{"name":"Credits","description":"Studio Credit balance and quota for the authenticated account."},{"name":"Account","description":"The account itself: schedule, follow and cancel its deletion."},{"name":"Projects","description":"Durable workspaces that own ingested sources and generated artifacts."},{"name":"Seasons","description":"Persistent, conversational season orchestration with explicit human approvals and a strict coverage gate before RSS publication."},{"name":"Sources","description":"Source packs attached to a project: inline text/markdown, URL fetch, PDF extraction."},{"name":"Scripts","description":"The writer's own scripts, however they were made — uploaded, written in the Writers Lab or generated from a plan: upload one or a new version, read it, change its title, logline, format, genre or season, delete one version or the whole script, export it as Fountain, Final Draft or PDF, and per-episode settings on one version (scriptUploadId) such as the episode overrides layered over the Series Bible."},{"name":"Plans","description":"Canonical source-grounded StoryPlan: thesis, journey beats, visual + audio system, per-artifact render intents, deterministic quote."},{"name":"Jobs","description":"Queued artifact-generation jobs against an APPROVED StoryPlan: credit reserve/settle, status polling, cancellation."},{"name":"Artifacts","description":"Durable artifact outputs. The table read returns a share token + theater URL + on-demand live-audio URL. Post-creation: refine in-place (POST /refine, scope artifact:publish, 8 credits) or finalize to a durable MP3 (POST /finalize mode=audio) or MP4 (mode=video), both free. The artifact id and share URLs are stable across refines."},{"name":"Publishing","description":"Project-owned publishing series, destinations, releases, assets, targets, RSS feed publication, and provider configuration."},{"name":"Trailer","description":"A trailer moves draft → rendering → review → finishing → complete. Approving the plan is priced first and renders every call at 480p with the cast's own recorded lines (a take that leaves a line unheard or says it in another character's voice, has lips out of step with its lines, or misses its opening framing is retaken once at our cost; a line said in the take's own voice, heard and distinct, is kept; a moderation refusal is released and never retried). In review: read the beats, choose takes, edit and render beats again (priced). The finish is priced first and upscales the selected takes to 720p by default, 1080p on request. Only a complete trailer exports."}],"paths":{"/background-activity":{"get":{"operationId":"getBackgroundActivity","tags":["Jobs"],"summary":"Read background tasks with safe stage and saved asset progress.","description":"Returns owned queued, running and retrying work plus jobs finished in the last ten minutes, each with `projectId` and `projectName` when it works for one of your projects. Optional episode context and completed/total asset progress enrich casting tasks. A deck's or a trailer's tasks are named with what they work on (\"Stitching <deck> · 1:30\", \"Scoring <deck>'s plan (judgment 2 of up to 3)\", \"Watching <deck>'s preview cut\"); an assembly carries its step as `detail` and the chapters or beats joined as `progress` (`unit`: `chapters` / `beats`). Renders, coverage and queued chat tasks are checked against their own owned record, so a render whose story job failed reads `failed` (with `queueState: completed` and the record's `failureReason`), never completed. A task the queue is holding for later (a provider backoff or retry delay) carries `nextAttemptAt`; it is scheduled, not stuck. The same list the app's activity indicator, MCP background_activity and the in-app assistant read. Read-only: never initializes workers, starts work, retries jobs, or exposes raw payloads and provider output.","security":[{"BearerApiKey":["story:read"]}],"responses":{"200":{"description":"Owned background activity.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["jobs"],"properties":{"jobs":{"type":"array","items":{"$ref":"#/components/schemas/BackgroundActivity"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/scripts/{scriptUploadId}":{"parameters":[{"name":"scriptUploadId","in":"path","required":true,"schema":{"type":"string"},"description":"The script: its own id, the id in its Writers Lab URL (read and change), or any of its versions' ids (`scriptUploadId`). A delete takes the whole script either way."}],"get":{"operationId":"getScript","tags":["Scripts"],"summary":"Read a script.","description":"One script you own — a feature, or an episode of a project, however it was made: its title, URL slug, logline, mediaType, genre, season, project, Writers Lab path, and every version (newest first) with what has been made from it. The same read MCP `get_script` and `sleeperhit script show` make, and the answer every script write returns. Read-only; costs no credits.","security":[{"BearerApiKey":["story:read"]}],"responses":{"200":{"description":"The script.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WriterScriptEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"patch":{"operationId":"updateScript","tags":["Scripts"],"summary":"Change a script's title, slug, logline, format, genre or season.","description":"The Writers Lab's edit dialog and season picker, through their core. Partial: only the fields sent change. `title` renames the latest version too; `slug` is refused when reserved or already one of yours; `logline` is the writer's own (the one Slug and the Writers Lab both read — coverage keeps its own reading); `genre` is free text up to 80 characters; `seasonNumber` moves an episode into that season of its project, adding the season to the project's list when it is new (a script in no project is refused). A bad value is refused with 400 `validation_failed`, the same sentence the Writers Lab shows. Free and naturally idempotent, so no `Idempotency-Key`.","security":[{"BearerApiKey":["story:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScriptMetadataPatch"}}}},"responses":{"200":{"description":"The script as it now stands.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WriterScriptEnvelope"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"delete":{"operationId":"deleteScript","tags":["Scripts"],"summary":"Delete a whole script.","description":"Permanently deletes a script: every version and everything scoped to them (character avatars, table reads and their share links, coverage reports, conversations, drafts), through the same core as the Writers Lab's delete. Requires `confirmed: true` (`?confirmed=true` or a JSON body). Refused with 409 `script_in_use`, `details.uses` naming each, while a published or scheduled release, a running job, a season-run slot, a pitch deck or a trailer still depends on it; 404 `script_not_found` for a script that is not yours. Banked audio stays in My Library, and the plan, job and artifact rows that generated it are kept. Irreversible. Free. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"name":"confirmed","in":"query","required":false,"schema":{"type":"boolean"},"description":"Required (here or in a JSON body): the user explicitly agreed to delete the script."}],"responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"type":"object","required":["deleted","scriptId","versions"],"properties":{"deleted":{"type":"boolean","enum":[true]},"scriptId":{"type":"string"},"versions":{"type":"integer","description":"How many versions went with it."}}}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Not your script (`script_not_found`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Something live still depends on the script (`script_in_use`, `details.uses`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/scripts/{scriptUploadId}/overrides":{"parameters":[{"name":"scriptUploadId","in":"path","required":true,"schema":{"type":"string"},"description":"The script version (script upload) — the `scriptUploadId` jobs, artifacts and the Writers Lab use. A new version starts with a copy of the previous version's overrides."}],"get":{"operationId":"getScriptOverrides","tags":["Scripts"],"summary":"Read the five episode overrides.","description":"Returns `avatar`, `theater`, `soundscape`, `pitchDeck` and `locations` — the writer's per-episode direction layered over the Series Bible's visual canon (the soundscape over the studio default, returned as `soundscapeDefault`). All five keys always come back; null means none is set. Works for any Writers Lab episode, with or without a Story API artifact. The same read the Writers Lab, MCP `get_script_overrides` and `sleeperhit script overrides get` make. Read-only; costs no credits.","security":[{"BearerApiKey":["story:read"]}],"responses":{"200":{"description":"The episode overrides.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScriptOverrides"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"patch":{"operationId":"updateScriptOverrides","tags":["Scripts"],"summary":"Set or clear episode overrides.","description":"Partial update. Text sets an override (trimmed); null or blank text clears it back to the Series Bible canon (the soundscape to the studio default); keys left out are untouched; the whole change lands together. An unknown key, a non-text value or text over the cap is refused with 400 `validation_failed` — the same validation the Writers Lab, MCP `update_script_overrides` and `sleeperhit script overrides set` go through. Answers all five as they now stand. Free and naturally idempotent, so no `Idempotency-Key`: it renders nothing, and the next render of that kind uses the new direction.","security":[{"BearerApiKey":["story:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScriptOverridesPatch"}}}},"responses":{"200":{"description":"The episode overrides after the change.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScriptOverrides"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/scripts/{scriptUploadId}/lines/{entryIndex}":{"parameters":[{"name":"scriptUploadId","in":"path","required":true,"schema":{"type":"string"},"description":"The script version (script upload) — the `scriptUploadId` jobs, artifacts and the Writers Lab use."},{"name":"entryIndex","in":"path","required":true,"schema":{"type":"integer","minimum":0},"description":"Which line: the entry's index in the version's parsed screenplay. Find it with GET /scripts/{scriptUploadId} or MCP `get_table_read_script`."}],"get":{"operationId":"getScriptLineTuning","tags":["Scripts"],"summary":"Read one table-read line.","description":"Returns one line's editable state (`line`: text, `ttsText`, `parenthetical`, `sceneHeading`, `emotion` with its `emotions`, `volume` and the `speed` it is spoken at, `pauseAfterMs`, `actingDirection`), the Slug conversation's rolling audits of it (`audits`) and the edit snapshots taken before each change (`versions`, newest first). `line` is null when the index is past the end. The line's `speed` and `actingDirection` are display-only: only re-voicing the line (`revoice_line`) changes them. The same read the Writers Lab's line editor, MCP `get_script_line_tuning` and `sleeperhit script line get` make. Read-only; costs no credits.","security":[{"BearerApiKey":["story:read"]}],"responses":{"200":{"description":"The line's tuning.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScriptLineTuning"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"patch":{"operationId":"updateScriptLine","tags":["Scripts"],"summary":"Edit one table-read line.","description":"Partial update. Only the named fields change; the line's current values are saved to its history first (`versions`); the rest are left alone. There is deliberately no speed and no acting-direction field — a re-voice of the line (`revoice_line`, POST /artifacts/{artifactId}/revoice-line) sets each, so an emotion change keeps the line's existing speed and no edit touches its direction; `actingDirection` here is refused with 400 `validation_failed`, naming that re-voice. An unknown field, a wrong-typed value or an index past the end is refused with 400 `validation_failed` — the same validation the Writers Lab's line editor, MCP `edit_script_line` and `sleeperhit script line set` go through. Answers the line as it now stands. Free and naturally idempotent, so no `Idempotency-Key`: it renders nothing, and the next recording of that line uses it.","security":[{"BearerApiKey":["story:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScriptLineEdit"}}}},"responses":{"200":{"description":"The line after the change.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScriptLineTuning"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/scripts/{scriptUploadId}/share":{"parameters":[{"name":"scriptUploadId","in":"path","required":true,"schema":{"type":"string"},"description":"The script version (script upload) — the `scriptUploadId` jobs, artifacts and the Writers Lab use. The share resolves that version to the script's latest table read."}],"get":{"operationId":"getScriptShare","tags":["Scripts"],"summary":"Read a table read's share link.","description":"Returns ONE screenplay version's (`scriptUploadId`) table-read share state: the active public link if the read is shared — its `shareUrl`, `audience`, playback `descriptor` (mode, scope, scene/entries), `expiresAt`, `includeOnProfile` flag and the render status of its MP4 (theater) or scoped MP3 (audio) — or `share: null` when it is not shared. `jobId` names the table read the share resolves to (the script's latest read), null when there is none. A script that is not yours is 404, the same as one that does not exist. The same read the Writers Lab's share dialog, MCP `get_script_share` and `sleeperhit script share get` make. Read-only; costs no credits.","security":[{"BearerApiKey":["story:read"]}],"responses":{"200":{"description":"The script's table-read share state.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScriptShareState"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"createScriptShare","tags":["Scripts"],"summary":"Share a table read.","description":"Create — or reuse — ONE screenplay version's (`scriptUploadId`) table-read share link: a public, signed URL that plays (theater) or streams (audio) the read. The body is optional; with none it is a public whole-read theater share for the studio's 30-day window. Pass `audience` (public, private_review, team), `includeOnProfile`, `expiresInDays` (1–365) or a `descriptor` (`mode` theater|audio, `scope` whole|scene|entries|conversation, `sceneIndex`, `startEntry`/`endEntry`, `conversationId`) to shape it. Naturally idempotent: a repeat on a script already shared refreshes that link rather than minting another, so no `Idempotency-Key`. A script with no performed read is 400 `validation_failed`; a script that is not yours is 404. Free: a theater MP4 renders only when asked, an audio share's scoped MP3 renders by itself. The same write the Writers Lab's share dialog, MCP `share_script` and `sleeperhit script share create` make.","security":[{"BearerApiKey":["artifact:publish"]}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateScriptShareRequest"}}}},"responses":{"200":{"description":"The share link, created or reused.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateScriptShareResult"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/scripts/{scriptUploadId}/table-read/prepare":{"parameters":[{"name":"scriptUploadId","in":"path","required":true,"schema":{"type":"string"},"description":"The cast episode's script version (script upload) — the `scriptUploadId` the Writers Lab, jobs and artifacts use."}],"post":{"operationId":"prepareScriptTableRead","tags":["Scripts"],"summary":"Prepare an episode's table read (metered — TWO CALLS, price first).","description":"Prepares the read of an episode whose cast is complete (Cast in the Writers Lab, or POST /story-projects/{projectId}/cast): charges its table-read credit — `TABLE_READ_SHORT` up to 60 pages, `TABLE_READ_LONG` above, the same price a job that adopts the screenplay reserves (GET /credits lists both) — and adds the read's scene sound effects and music, the media a Story API read gets. The Writers Lab's \"Continue to Table Read\" on Cast is the same operation, through the same server core. TWO CALLS: sent without `confirmed` it PRICES the preparation and changes nothing (`status: quoted`, `quotedOnly: true`, `credits`, `balanceAfter`, `sufficient`, and a `note` to show the writer; when `sufficient` is false the caller must not confirm); sent again with `confirmed: true` it charges and queues the media (`status: queued`). ONCE PER SCREENPLAY VERSION: a version already charged for its read, on any surface, answers `status: prepared`, and a read whose media is being made answers `preparing` — both free — so a retry never charges twice. A confirmed call the balance cannot cover is refused with 402 `insufficient_credits` and changes nothing. Refused before the price: 404 `script_not_found` (not the caller's screenplay), 409 `cast_precondition_failed` (`details.reason`: `script_not_finalized` for a draft, `script_not_cast_ready` for a cast that is not complete, with `details.missing`), 409 `project_precondition_failed` (the project gate is closed) and 409 `voice_retired` (a voice the read would speak was retired).  Requires `Idempotency-Key`. Scope: `artifact:publish`.","security":[{"BearerApiKey":["artifact:publish"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PrepareTableReadRequest"}}}},"responses":{"200":{"description":"The price (no `confirmed`), the queued preparation, or prepared / preparing.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TableReadPrepareEnvelope"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/InsufficientCredits"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/CastPreconditionFailed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/scripts/{scriptUploadId}/table-read/retry-analysis":{"parameters":[{"name":"scriptUploadId","in":"path","required":true,"schema":{"type":"string"},"description":"The script version (script upload) whose read's creative analysis to retry."}],"post":{"operationId":"retryScriptTableReadAnalysis","tags":["Scripts"],"summary":"Retry a failed table-read creative analysis.","description":"Re-runs a table read's creative analysis after it FAILED. Re-runs the creative worker's provider pre-check against CURRENT sidecar bindings instead of replaying the read's persisted (possibly stale) error message, then re-dispatches the worker, which self-heals the read on success. The read is resolved from `scriptUploadId` (the caller's latest read for that script; a script that is not yours or has no read is 404 `script_not_found`). A read that is NOT in a failed state is refused with 409 `table_read_not_failed`. Free: it renders nothing and reserves nothing. Naturally idempotent, so no `Idempotency-Key`. The Writers Lab's and MCP `retry_creative_analysis` call the same route. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"responses":{"200":{"description":"The read is retrying.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RetryCreativeAnalysisResult"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/scripts/{scriptUploadId}/table-read/characters/remove":{"parameters":[{"name":"scriptUploadId","in":"path","required":true,"schema":{"type":"string"},"description":"The script version (script upload) whose read to remove the character from."}],"post":{"operationId":"removeScriptTableReadCharacter","tags":["Scripts"],"summary":"Remove a character from a table read.","description":"Removes ONE character from a table read's cast roster, so it can be set up again. Clears the character's voice from the read and its `finalized` flag (keeping the rest of its profile) and clears its canonical portrait on the script. The read is resolved from `scriptUploadId` (the caller's latest read for that script; a script that is not yours or has no read is 404 `script_not_found`). An unknown field, or a `character` that is not a non-empty string, is refused with 400 `validation_failed` — the same validation the Writers Lab's cast roster, MCP `remove_character` and `sleeperhit script table-read remove-character` go through. Free and naturally idempotent: removing a character the read no longer has is a no-op, and still answers `removed`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RemoveCharacterRequest"}}}},"responses":{"200":{"description":"The character was removed from the read.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RemoveCharacterResult"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/story-projects/{projectId}/episodes":{"parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string"},"description":"The project id, or the id in its Writers Lab URL."}],"get":{"operationId":"listProjectEpisodes","tags":["Projects"],"summary":"List a project's episodes, versions and videos.","description":"What the Writers Lab's Seasons & episodes and Videos pages show, through their own cores: the project's seasons (named, with their episode counts), every episode — the ones the writer uploaded, started from the Series Bible or wrote in the app included — with its logline, format, genre, season, feed release and every version (with what has been made from it), the Series Bible's planned episodes joined against what is written, and the project's videos. An earlier or failed attempt at an episode is included and marked `supersededBy`, and left out of the season counts. `?season=N` narrows `episodes` and `plannedEpisodes` to one season. Cast lists are left out. The same read MCP `list_project_episodes` and `sleeperhit projects episodes` make. Read-only; costs no credits.","security":[{"BearerApiKey":["story:read"]}],"parameters":[{"name":"season","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":99},"description":"Only this season's episodes and planned episodes."}],"responses":{"200":{"description":"The project's seasons, episodes, versions and videos.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectEpisodesResponse"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"startPlannedEpisode","tags":["Projects"],"summary":"Start a Series Bible episode as a draft.","description":"The Series Bible page's and project page's \"Start\", through their core: mints the planned episode's empty draft (titled, summarized, in its label's season, linked to the Bible entry), or returns the script that already covers it with `created: false` — never a second draft. Write into `scriptUploadId` scene by scene with POST /screenplay-drafts/{scriptUploadId}, or whole with PATCH /scripts/{scriptUploadId}/content, then finish it with POST /scripts/{scriptUploadId}/finish. Refused with 409 `project_precondition_failed` until the project can start an episode, and 400 for an episode no longer in the Bible. Free. Requires `Idempotency-Key`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StartPlannedEpisodeRequest"}}}},"responses":{"200":{"description":"The draft, or the script that already covers the episode.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StartPlannedEpisodeResponse"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/ProjectPreconditionFailed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/story-projects/{projectId}/seasons":{"parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string"},"description":"The project id, or the id in its Writers Lab URL."}],"post":{"operationId":"createProjectSeason","tags":["Projects"],"summary":"Name a new season.","description":"The project page's \"Add season\", through its core: the lowest free season number, named as given (at most 80 characters) or \"Season N\". Move an episode into it with PATCH /scripts/{scriptId} `seasonNumber`. Refused with 409 `project_precondition_failed` until the project can plan (premise coverage, canon and the Mood board). Written under the project's row lock, so a concurrent project update keeps it. Free. Requires `Idempotency-Key`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectSeasonCreateRequest"}}}},"responses":{"200":{"description":"The new season and every season the project now has.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectSeasonCreateResponse"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/ProjectPreconditionFailed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/scripts":{"post":{"operationId":"uploadScript","tags":["Scripts"],"summary":"Upload a screenplay.","description":"The Writers Lab's upload, through its core: a new script WITH ITS PROJECT — it joins the project named in `projectId` (400 if that project is not the writer's live one), or gets a new project named after the script — with its first version, parsed and its characters analysed in the background (read GET /scripts/{scriptId} until `parseStatus` is `complete`). The body goes through the same parser as the Writers Lab, so a bad title, content or format is refused with the same sentence. Refused with 403 `plan_limit_reached` past the plan's page allowance. Free. Requires `Idempotency-Key`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScriptUploadRequest"}}}},"responses":{"200":{"description":"The new script and its first version.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScriptUploadResponse"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/scripts/{scriptUploadId}/versions":{"parameters":[{"name":"scriptUploadId","in":"path","required":true,"schema":{"type":"string"},"description":"The script: its own id, the id in its Writers Lab URL, or any of its versions' ids (`scriptUploadId`)."}],"post":{"operationId":"uploadScriptVersion","tags":["Scripts"],"summary":"Upload a new version of a script.","description":"The Writers Lab's \"upload a new version\", through its core: the next version number, parsed and analysed in the background, with the previous version's episode overrides copied forward. Earlier versions are kept. A new version can come from a file (a PDF, a Final Draft .fdx, or a Fountain or plain-text script) instead of its text. The file is read as a source of the script's project: `fileUrl` (a link to the file) adds it and answers `waiting` with its `sourceId`; check that source until it is READY, then send `sourceId` and its text becomes the new version. A source that is already READY becomes the version at once, and the same source makes its version once (sending it again answers that version). A script in no project has nowhere to keep the file: send its text instead. A source that could not be read is refused (400) with its reason. Refused with 403 `plan_limit_reached` on the free plan (one version per script) or past the page allowance; 404 `script_not_found` for a script that is not yours, `source_not_found` for a source that is not. Free. Requires `Idempotency-Key`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScriptVersionUploadRequest"}}}},"responses":{"200":{"description":"The script and the version this call created (or the version its source already made), or `waiting` while its file is still being read.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ScriptUploadResponse"},{"$ref":"#/components/schemas/ScriptVersionWaiting"}]}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/scripts/{scriptUploadId}/versions/{versionId}":{"parameters":[{"name":"scriptUploadId","in":"path","required":true,"schema":{"type":"string"},"description":"The script: its own id, the id in its Writers Lab URL, or any of its versions' ids (`scriptUploadId`)."},{"name":"versionId","in":"path","required":true,"schema":{"type":"string"},"description":"The version to delete: one of this script's `scriptUploadId`s."}],"delete":{"operationId":"deleteScriptVersion","tags":["Scripts"],"summary":"Delete one version of a script.","description":"The Writers Lab's \"Only version N\", through its core: deletes ONE version and what is scoped to it (character art, table reads and their share links, coverage, conversations); the other versions stay. Requires `confirmed: true` (`?confirmed=true` or a JSON body). Refused with 409 `script_in_use` (`details.uses`) while something live depends on this version — a release in the feed whose read performs it, a running job, an episode draft, a season-run slot, a pitch deck or a trailer — and with 409 `script_state_invalid` for the script's only version (DELETE /scripts/{scriptId} deletes the whole script). 404 `script_not_found` when the version is not one of this script's. Banked audio stays in My Library. Irreversible. Free.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"name":"confirmed","in":"query","required":false,"schema":{"type":"boolean"},"description":"Required (here or in a JSON body): the user explicitly agreed to delete the version."}],"responses":{"200":{"description":"Deleted.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScriptVersionDeleteResponse"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Something live still depends on this version (`script_in_use`, `details.uses`), or it is the script's only version (`script_state_invalid`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/scripts/{scriptUploadId}/export":{"parameters":[{"name":"scriptUploadId","in":"path","required":true,"schema":{"type":"string"},"description":"The version to export (`scriptUploadId`), or a script's id (or Writers Lab URL id) for its latest version."}],"get":{"operationId":"exportScript","tags":["Scripts"],"summary":"Export a screenplay as Fountain, Final Draft or PDF.","description":"The Writers Lab's export menu, through its core. Fountain and Final Draft (`fdx`) come back as text (`encoding: utf8`); a PDF base64-encoded (`encoding: base64`) — all in one JSON envelope with a filename and content type. The title page is kept unless `includeTitlePage=false`; `fromPage` / `toPage` pick a page range; `font` sets a PDF's typeface. Refused with 409 `script_state_invalid` for a version not parsed yet. Read-only; costs no credits.","security":[{"BearerApiKey":["story:read"]}],"parameters":[{"name":"format","in":"query","required":true,"schema":{"type":"string","enum":["fountain","fdx","pdf"]}},{"name":"includeTitlePage","in":"query","required":false,"schema":{"type":"boolean"},"description":"Default true."},{"name":"fromPage","in":"query","required":false,"schema":{"type":"integer","minimum":1}},{"name":"toPage","in":"query","required":false,"schema":{"type":"integer","minimum":1}},{"name":"font","in":"query","required":false,"schema":{"type":"string","enum":["screenplay-sim","courier-prime","courier-new","final-draft"]},"description":"A PDF's typeface."}],"responses":{"200":{"description":"The exported screenplay.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScriptExportResponse"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"This version has not been parsed yet (`script_state_invalid`); export it once its parse has finished.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/scripts/{scriptUploadId}/content":{"parameters":[{"name":"scriptUploadId","in":"path","required":true,"schema":{"type":"string"},"description":"The version (`scriptUploadId`), or a script's id or Writers Lab URL id for its latest version."}],"patch":{"operationId":"writeScriptContent","tags":["Scripts"],"summary":"Write a version's text.","description":"The Writers Lab's editor save, through its core: the version's whole screenplay text. A draft is written as the draft editor writes it; a locked version as the version editor does. With `expectedContent` it is a compare-and-swap: the write lands only if the version still holds exactly that text, else 409 `script_state_invalid` — read it again and write over what is there. A version a season run wrote is not edited here (409). Free and naturally idempotent, so no `Idempotency-Key`.","security":[{"BearerApiKey":["story:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScriptContentWriteRequest"}}}},"responses":{"200":{"description":"Written.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScriptContentWriteResponse"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"The version moved on since you read it, or a season run wrote it (`script_state_invalid`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/scripts/{scriptUploadId}/finish":{"parameters":[{"name":"scriptUploadId","in":"path","required":true,"schema":{"type":"string"},"description":"The version (`scriptUploadId`), or a script's id or Writers Lab URL id for its latest version."}],"post":{"operationId":"finishDraft","tags":["Scripts"],"summary":"Finish a draft into a locked version.","description":"The editor's Finalize, through its core, in two steps. Without `coverage`: Slug's judgment of whether the last passing coverage can be carried onto this version (`status: judged`, `carryover`), and nothing is locked. With `coverage: adopt`: that report is copied onto the version, the draft is locked and the screenplay is parsed and its characters analysed (refused with 409 `script_state_invalid` when no earlier version has passing coverage, before anything is locked). With `coverage: fresh`: the draft is locked and new coverage runs; the screenplay is prepared once it meets the target — refused with 409 `project_precondition_failed` while the project gate holds paid work. A version that is not a draft is refused with 409 `script_state_invalid`. Requires `Idempotency-Key`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FinishDraftRequest"}}}},"responses":{"200":{"description":"The recommendation (`judged`), or the finished version (`finished`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FinishDraftResponse"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Not a draft, no passing coverage to carry forward (`script_state_invalid`), or the project gate holds paid work (`project_precondition_failed`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/artifacts/{artifactId}/coverage/versions":{"parameters":[{"name":"artifactId","in":"path","required":true,"schema":{"type":"string"},"description":"A table-read artifact's id, or a writer's own episode however it was made: its script id, the id in its Writers Lab URL, or a version's scriptUploadId (a script id reads its latest version). The answer's `artifactId` echoes it."}],"get":{"operationId":"listCoverageVersions","tags":["Artifacts"],"summary":"List a screenplay's coverage history.","description":"Every coverage report across every version of the screenplay, newest first — the Coverage tab's version picker, through its core: each report's id, status, score, which version it covers and whether that is the current one. Read one with GET /artifacts/{artifactId}/coverage?reportId=. For a table-read artifact or a writer's own episode. Read-only; costs no credits.","security":[{"BearerApiKey":["artifact:read"]}],"responses":{"200":{"description":"The coverage history.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CoverageVersionsResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/artifacts/{artifactId}/coverage/blacklist":{"parameters":[{"name":"artifactId","in":"path","required":true,"schema":{"type":"string"},"description":"A table-read artifact's id, or a writer's own episode however it was made: its script id, the id in its Writers Lab URL, or a version's scriptUploadId (a script id reads its latest version). The answer's `artifactId` echoes it."}],"get":{"operationId":"getBlackListAnalysis","tags":["Artifacts"],"summary":"Read the Black List analysis of a coverage report.","description":"The Black List analysis of one coverage report (`reportId`, else the latest complete one), as the Coverage tab shows it, through its core. It runs by itself after a coverage report completes; `blackList` is null until then, or when professional coverage add-ons are not available for this episode. A `reportId` that does not cover this screenplay is refused with 400. Read-only; costs no credits.","security":[{"BearerApiKey":["artifact:read"]}],"parameters":[{"name":"reportId","in":"query","required":false,"schema":{"type":"string"},"description":"A coverage report on this version; default the latest complete one."}],"responses":{"200":{"description":"The analysis, or null.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BlackListAnalysisResponse"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/screenplay-drafts/{scriptUploadId}":{"parameters":[{"name":"scriptUploadId","in":"path","required":true,"schema":{"type":"string"},"description":"The empty screenplay draft (script upload) being written scene by scene."}],"get":{"operationId":"getScreenplayDraft","tags":["Jobs"],"summary":"Read scene-by-scene screenplay drafting progress.","description":"Returns the drafting job for this draft (status, completed/total scenes, current act, saved screenplay content, and the screenplay coverage started when drafting completes), or `null` when drafting was never started. Read-only.","security":[{"BearerApiKey":["story:read"]}],"responses":{"200":{"description":"Screenplay drafting status, or null.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScreenplayDraftStatus"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"startScreenplayDraft","tags":["Jobs"],"summary":"Start or resume scene-by-scene screenplay drafting.","description":"With `{ scenes, throughAct? }`, queues an ordered, act-ordered scene plan against an EMPTY editable draft once project setup is approved (until then — and for a script in no project — 409 `project_precondition_failed` names the stage to finish); use `throughAct` to write Act 1 first. With `{ action: \"resume\" }`, continues from the next unfinished scene (also the way to write the remaining acts), preserving the writer's current draft edits. Each completed scene is saved as it lands. Requires `Idempotency-Key`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"oneOf":[{"type":"object","additionalProperties":false,"required":["scenes"],"properties":{"scenes":{"type":"array","minItems":1,"maxItems":100,"items":{"type":"object","additionalProperties":false,"required":["act","heading","direction","pages"],"properties":{"act":{"type":"integer","minimum":1,"maximum":10},"heading":{"type":"string","description":"Scene heading starting INT., EXT., INT./EXT. or I/E."},"direction":{"type":"string","minLength":1,"maxLength":4000},"pages":{"type":"number","minimum":0.5,"maximum":10}}}},"throughAct":{"type":"integer","minimum":1,"maximum":10}}},{"type":"object","additionalProperties":false,"required":["action"],"properties":{"action":{"const":"resume"}}}]}}}},"responses":{"200":{"description":"Screenplay drafting status after queueing.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScreenplayDraftStatus"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/ProjectPreconditionFailed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/capabilities":{"get":{"operationId":"getCapabilities","tags":["Discovery"],"summary":"Discover supported sources, artifacts, scopes, and documentation surfaces.","description":"Returns a machine-readable manifest so agent clients can adapt to current feature availability without hardcoded assumptions. Authenticated to keep the API surface uniformly keyed and to attribute usage for capacity planning.","security":[{"BearerApiKey":["story:read"]}],"responses":{"200":{"description":"Capability manifest.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Capabilities"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/examples":{"get":{"operationId":"getApiExamples","tags":["Discovery"],"summary":"Worked request examples, the auth header format, and the error-code vocabulary.","description":"Public and unauthenticated ON PURPOSE — it is what a client reads BEFORE it has a key, and it names where to obtain one. Returns the API version, base URL, the Authorization header format, the full error-code list, and runnable examples.","responses":{"200":{"description":"Examples, auth format and error vocabulary.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"type":"object","properties":{"version":{"type":"string"},"baseUrl":{"type":"string"},"authentication":{"type":"object","properties":{"header":{"type":"string"},"format":{"type":"string","example":"Bearer sh_<prefix>_<secret>"},"obtainAt":{"type":"string"}}},"errorCodes":{"type":"array","items":{"type":"string"}},"examples":{"type":"array","items":{"type":"object"}}}}}}}}}},"/device/credential":{"post":{"operationId":"exchangeDeviceCode","tags":["Discovery"],"summary":"Exchange an approved device code for an API key (device authorisation flow).","description":"Poll this after starting a device authorisation. ⚠️ THIS ENDPOINT DOES NOT USE THE API's STANDARD ERROR ENVELOPE. It follows RFC 8628 instead, returning `{ error, error_description }` with `authorization_pending` while the user has not yet approved, then `invalid_grant`, `expired_token`, `access_denied`, or `invalid_request`. Every other v1 route returns the common error object.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["device_code"],"properties":{"device_code":{"type":"string"}}}}}},"responses":{"200":{"description":"A newly issued API key. The secret is returned ONCE.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"type":"object","properties":{"access_token":{"type":"string","description":"Shown once and never retrievable again."},"token_type":{"type":"string","example":"bearer"},"key_prefix":{"type":"string"},"scopes":{"type":"array","items":{"type":"string"}},"expires_at":{"type":"string","format":"date-time","nullable":true,"description":"When the key stops working: 90 days after the sign-in. Signing in again mints a new key with a fresh lifetime."}}}}}},"400":{"description":"RFC 8628 polling error — NOT the common error envelope.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"error_description":{"type":"string"}}}}}}}},"delete":{"operationId":"revokeDeviceCredential","tags":["Discovery"],"summary":"Revoke the sign-in key this request is made with (sign out).","description":"The key in the Authorization header revokes ITSELF: `sleeperhit logout`, the MCP `logout` tool, and every new sign-in (which revokes the key the previous one saved) call this, so signing out ends the key on the server rather than only deleting it from one machine. Needs no scope, so a narrowed sign-in key can still end itself. Only a key minted by a browser sign-in (the device flow) can: a key made on the API page answers 409 `not_a_sign_in_key` and keeps working, and an OAuth connector is disconnected from the API page instead. A key that is already revoked or expired answers 401. The key gives up any standing approval it held, as a revoke from the API page does. Uses the common error envelope. Free.","security":[{"BearerApiKey":[]}],"responses":{"200":{"description":"The key is revoked.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"type":"object","required":["revoked"],"properties":{"revoked":{"type":"boolean","enum":[true]},"keyPrefix":{"type":"string","nullable":true,"description":"The revoked key's display prefix."}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"description":"`not_a_sign_in_key` — the credential was not minted by a browser sign-in; nothing was revoked.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/agent-guidance":{"get":{"operationId":"getAgentGuidance","tags":["Discovery"],"summary":"Read backend-managed agent workflow guidance.","description":"Returns the current admin-editable agent guidance sidecar for Story API, MCP, and CLI clients, structured non-blocking development flow warnings, and MCP server instructions plus tool/resource titles and descriptions. Agents should read this before planning, refining, mutating audio, or finalizing table-read artifacts.","security":[{"BearerApiKey":["story:read"]}],"responses":{"200":{"description":"Backend-managed agent guidance.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentGuidance"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/credits":{"get":{"operationId":"getCredits","tags":["Credits"],"summary":"Get Studio Credit balance and what each operation costs.","description":"Read-only summary of the current Studio Credit balance, monthly bucket allocation/use, and perpetual credit balance, plus `costs`: what each operation costs, the same list the Billing page shows (each line names the ledger `ops` its charges carry). A pitch deck is priced by the second: 1 credit a second to render at 480p, plus 1 credit a second to finish at 720p (2 credits a second in all; a 1080p finish is 4 credits a second instead). Planning is free. The exact price is quoted per shot once the plan is ready, before anything renders; location plates and on-screen text are itemized in the same quotes. A trailer is priced by the second: 1 credit a second to render at 480p, plus 1 credit a second to finish at 720p (2 credits a second in all; a 1080p finish is 4 credits a second instead). Planning is free. The exact price is quoted per shot once the plan is ready, before anything renders; location plates and on-screen text are itemized in the same quotes. Use this before submitting expensive generation jobs so agents can short-circuit with `insufficient_credits` guidance instead of triggering reservations.","security":[{"BearerApiKey":["credits:read"]}],"responses":{"200":{"description":"Credit summary.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreditsSummary"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/account/export":{"get":{"operationId":"getAccountExport","tags":["Account"],"summary":"Where the \"Download my data\" archive stands, with a fresh download link when it is ready.","description":"`state`: `none`, `queued`, `building`, `ready`, `failed` (`error` says why) or `expired`. When `ready`, `download.url` is a signed link that works until `download.expiresAt` (an hour); read again for a new one. The archive itself is deleted at `expiresAt` (7 days after it is built). `contents` counts what it holds by section. Scope: `account:read`. Free.","security":[{"BearerApiKey":["account:read"]}],"responses":{"200":{"description":"The latest export.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountExportStatus"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"requestAccountExport","tags":["Account"],"summary":"Download my data: build one zip of everything the account holds.","description":"Scripts (every version as Fountain and Final Draft), projects (Series Bibles, outlines, cast and other documents as JSON and Markdown, sources), coverage (JSON, Markdown and PDF), links to every audio and video master, shows with their episodes and feeds, the voice consent records the account holds, the credit ledger and payments, voices, chats, forum posts and comments. Never secrets, nor another person's personal data held with a consent record. Built in the background: poll GET /account/export. One at a time: one already queued or building is returned. Requires `Idempotency-Key`. Scope: `account:read`. Free.","security":[{"BearerApiKey":["account:read"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"200":{"description":"The export, queued (or the one already building).","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountExportStatus"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/account/deletion":{"get":{"operationId":"getAccountDeletion","tags":["Account"],"summary":"Where the account's deletion stands, with the credit balance.","description":"Read-only: `state` is `none`, `scheduled` (the purge runs at `scheduledFor`; it can be cancelled until then), `purging`, or `blocked` (`blockedReason`; contact help). Also the Studio Credit balance with any top-up still inside its 14-day refund window (request the refund first: unused credits are forfeited at the purge), what deleting removes and what is kept, and `canRequest` / `refusal`. Scope: `account:read`.","security":[{"BearerApiKey":["account:read"]}],"responses":{"200":{"description":"Where the deletion stands.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountDeletionStatus"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"requestAccountDeletion","tags":["Account"],"summary":"Delete the account (scheduled 7 days out; cancellable until then).","description":"Schedules the deletion of the account and everything it made: scripts, projects, Series Bibles, outlines and coverage; table reads, decks, trailers and every stored file; shows, their feeds and public pages; voices, here and at the voice provider; API keys, connected apps and sign-ins. Nothing is removed until `scheduledFor` (7 days); the account owner is emailed and can cancel (DELETE /account/deletion) until the purge starts. Unused Studio Credits are forfeited at the purge. Payment records are kept without a name or email, as tax law requires. Videos on the owner's own YouTube or TikTok channel are listed in the confirmation email for them to remove. `confirmEmail` must be the account's email address (400 `account_email_mismatch`; nothing is scheduled). 409 `account_deletion_refused` for an admin, a workshop host or an organization owner (`message` says why). A deletion already scheduled is returned unchanged.  Requires `Idempotency-Key`. Scope: `account:write`. Free.","security":[{"BearerApiKey":["account:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountDeletionRequest"}}}},"responses":{"200":{"description":"The deletion is scheduled.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountDeletionStatus"}}}},"400":{"$ref":"#/components/responses/AccountEmailMismatch"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"$ref":"#/components/responses/AccountDeletionRefused"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"delete":{"operationId":"cancelAccountDeletion","tags":["Account"],"summary":"Cancel a scheduled account deletion.","description":"Cancels the scheduled deletion; nothing was removed before the purge. 409 `account_deletion_refused` once the purge has started. Scope: `account:write`. Free.","security":[{"BearerApiKey":["account:write"]}],"responses":{"200":{"description":"The deletion is cancelled.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountDeletionStatus"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"$ref":"#/components/responses/AccountDeletionRefused"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/generation-health":{"get":{"operationId":"getGenerationHealth","tags":["Credits"],"summary":"Can the writing pipeline actually run right now?","description":"Reports, per critical sidecar, the resolved provider fallback chain and whether ANY link in it is currently reachable. A chain whose every link is cooling down is the shape that parks jobs indefinitely — the job defers for provider recovery to a retry that cannot succeed — and it is otherwise only visible as a misleading generation timeout. Check this before blaming a failed job on your request, and when triaging repeated failures. A link whose stored API key this server cannot decrypt is never healthy, and a request that reaches it fails there with no fallback tried, so its sidecar is unreachable even with a healthy fallback; its `unreachableReason` and the entry's `reason` name the fix (re-enter the key, or fix the server's AI_ENCRYPTION_KEY). Reports posture only; never provider credentials or base URLs.","security":[{"BearerApiKey":["story:read"]}],"parameters":[{"name":"probe","in":"query","required":false,"schema":{"type":"boolean"},"description":"Also SMOKE TEST each critical binding with a small real model call. Reachability and usability are different questions: a provider can be perfectly reachable while rejecting the sampling parameters this pipeline sends, or while returning output the pipeline cannot use — neither shows up in provider health. Slower and costs a token or two per sidecar; use when triaging repeated failures or immediately after rebinding a provider."}],"responses":{"200":{"description":"Generation health summary.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerationHealthEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/story-projects":{"get":{"operationId":"listStoryProjects","tags":["Projects"],"summary":"List story projects owned by the authenticated account.","description":"Returns a cursor-paginated list of non-deleted story projects, newest first. Use `cursor` from the previous response to page.","security":[{"BearerApiKey":["story:read"]}],"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100},"description":"Max items to return per page (default 25, max 100)."},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"},"description":"Opaque cursor from a previous response."}],"responses":{"200":{"description":"Paginated project list.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StoryProjectList"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"createStoryProject","tags":["Projects"],"summary":"Create a new story project workspace.","description":"Creates an empty project. Requires `Idempotency-Key` for safe retries.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StoryProjectCreate"}}}},"responses":{"200":{"description":"Created project envelope.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StoryProjectEnvelope"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/story-projects/from-script":{"post":{"operationId":"createStoryProjectFromScript","tags":["Projects"],"summary":"Create a project around a script that belongs to no project.","description":"Wraps the script in a new project named after it, so its Series Bible, seasons, videos and publishing become reachable — the same path the Writers Lab uses. Pass exactly one of `scriptId` (the script id, or the id in its Writers Lab URL) or `scriptUploadId`. Idempotent by the script: a script already in a project returns that project with `created: false`. Once in a project, the script's Writers Lab pages open on the project Overview until its Series Bible is complete; `workspacePath` is that Overview. Costs no credits. Requires `Idempotency-Key` for safe retries.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StoryProjectFromScript"}}}},"responses":{"200":{"description":"The project that now holds the script.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StoryProjectFromScriptEnvelope"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/story-projects/{projectId}":{"parameters":[{"$ref":"#/components/parameters/ProjectId"}],"get":{"operationId":"getStoryProject","tags":["Projects"],"summary":"Read a story project by id, with its readiness preflight.","description":"Returns the project, `project.workspaceGate` — `{ ready, stage, reason, missingFields, canPlan, canStartEpisode, fullCoverage, fullCoverageNotice }` — the same position a gated call (plans, drafting, casting, story jobs, the mood board) refuses with as 409 `project_precondition_failed`, and `project.tableReadReadiness` — `{ ready, audioOnly, reason, narratorVoice, members }` — whether the cast canon lets a table_read job start from a plan (the rule a job refuses with as 409 `cast_precondition_failed`). They are the cheap checks a recurring producer runs before it fetches sources, uploads, plans or creates a job: when `canPlan` or `canStartEpisode` is false, skip the run and finish `stage`; when `tableReadReadiness.ready` is false, complete the cast it names. Read-only: reading it never queues or recovers a coverage job.","security":[{"BearerApiKey":["story:read"]}],"responses":{"200":{"description":"Project envelope with its workspace gate.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StoryProjectDetailEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"patch":{"operationId":"updateStoryProject","tags":["Projects"],"summary":"Update a story project.","description":"Partial update. Only the supplied fields are modified. `metadata` is MERGED into what is stored, never written over it: a key you send is set, a key you send as null is removed, a key you leave out is kept, and `metadata: null` removes every key you can set. `metadata.seasons` (the project's named seasons) is kept whatever you send, and a patch that names it is refused with 400 `validation_failed` — create a season with POST /story-projects/{projectId}/seasons and move an episode with PATCH /scripts/{scriptId} `seasonNumber`. Pass `archived: true` to archive (or `false` to unarchive).","security":[{"BearerApiKey":["story:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StoryProjectPatch"}}}},"responses":{"200":{"description":"Updated project envelope.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StoryProjectEnvelope"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"delete":{"operationId":"deleteStoryProject","tags":["Projects"],"summary":"Delete a story project (restorable for 30 days, then purged; its episodes stay as private scripts).","description":"The project is hidden at once: it leaves list responses, reads return `project_not_found`, and its feed, show page, decks and share links stop answering. It can be restored for 30 days (`restorableUntil`; POST /story-projects/{projectId}/restore, GET /story-projects/deleted), then it is purged with its Series Bible, cast, mood board, sources, plans, season runs, show, decks and trailers and their stored files — and with its episodes: during the window each script is private and comes back with the project; at the purge it is deleted with everything under it (`episodesAtPurge`). Archiving (PATCH `archived: true`) only hides a project. Free.","security":[{"BearerApiKey":["story:write"]}],"responses":{"200":{"description":"The project is deleted and restorable until `restorableUntil`.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectDeletionResult"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/story-projects/deleted":{"get":{"operationId":"listDeletedStoryProjects","tags":["Projects"],"summary":"Deleted projects that can still be restored.","description":"Deleted projects inside their 30-day restore window, newest first, each with `restorableUntil` and `episodesAtPurge`. Free.","security":[{"BearerApiKey":["story:read"]}],"responses":{"200":{"description":"Restorable deleted projects.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeletedStoryProjectList"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/story-projects/{projectId}/restore":{"parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","minLength":1}}],"post":{"operationId":"restoreStoryProject","tags":["Projects"],"summary":"Restore a deleted project within its 30-day window.","description":"The project comes back with the episodes its delete took out, private again no longer, with their share links and portfolio places, except any the writer deleted since or moved to another project (`episodesSkipped`). 409 `project_not_restorable` once the window has passed or the purge has started, or for a project that is not deleted. Requires `Idempotency-Key`. Scope: `story:write`. Free.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"200":{"description":"The project is restored.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectRestoreResult"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/ProjectNotRestorable"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/story-projects/{projectId}/series-bible":{"parameters":[{"$ref":"#/components/parameters/ProjectId"}],"get":{"operationId":"getProjectSeriesBible","tags":["Projects"],"summary":"Read the project Series Bible.","description":"Returns the durable project-level canon and continuity document shared by the web UI, Story API, CLI, and MCP. Use it as the preferred non-blocking gate before planning or generating screenplay pages. `document.renderContext.periodNegatives` is read-only: the period world (derived from the Bible once per version) — `indoors` and `outdoors`, the world as it is, which every reference render is told after the series canon, and `negatives`, the anachronisms it never has (e.g. telegraph wires for 1870s Russia), which every take is checked for and which are never sent to the video model; its `status` says whether it is current for this Bible version or will be derived again by the next render.","security":[{"BearerApiKey":["story:read"]}],"responses":{"200":{"description":"Series Bible document envelope.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SeriesBibleEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"patch":{"operationId":"saveProjectSeriesBible","tags":["Projects"],"summary":"Save the project Series Bible.","description":"Stores caller-authored project canon, episode map, character roster, style, and audio direction. This is the same project document the web UI edits and the MCP/CLI tools read. Complete the full bible before screenplay generation when possible; that is guidance, not an enforcement block. One change IS gated: writing the episode map before the project can plan (premise coverage passed, canon complete) is refused with 409 `project_precondition_failed`, and `details.stage` names the stage to finish. Every save returns `gateChange` { before, after, staleCoverage }: what it did to the project's gate. Appending (episodes for a new season, a new character) never re-locks anything. An edit to canon a coverage report scored puts that report out of date (`staleCoverage`) and re-locks what it opened: after full coverage, paid work on every episode already written (coverage, casting, table reads, videos, decks, trailers, publishing) until full coverage passes again. When `after.stage` comes before `before.stage`, tell the user.","security":[{"BearerApiKey":["story:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SeriesBiblePatch"}}}},"responses":{"200":{"description":"The saved Series Bible and what the save did to the project's gate.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SeriesBibleSaveResult"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/ProjectPreconditionFailed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"generateProjectSeriesBible","tags":["Projects"],"summary":"Generate or refresh the project Series Bible.","description":"Synthesizes the Series Bible from the project, sources, plans, scripts, existing document, and optional instructions using the DB sidecar `story_api_series_bible`. Use this when the full bible is missing or incomplete before screenplay generation. Requires `Idempotency-Key`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SeriesBibleGenerateRequest"}}}},"responses":{"200":{"description":"The generated Series Bible and what it did to the project's gate.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SeriesBibleSaveResult"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/story-projects/{projectId}/series-bible/coverage":{"parameters":[{"$ref":"#/components/parameters/ProjectId"}],"get":{"operationId":"getProjectSeriesBibleCoverage","tags":["Projects"],"summary":"Read foundational Series Bible coverage.","description":"Returns the latest qualitative report for the project Bible, including whether it matches the current document revision, its 0–10 score, the first-pass gate, nine craft dimensions, exemplar comparisons, priority fixes, and an operational audio blueprint.","security":[{"BearerApiKey":["story:read"]}],"parameters":[{"name":"phase","in":"query","required":false,"schema":{"type":"string","enum":["premise","full"]},"description":"Which checkpoint to read: `premise` (default) or `full`. Both report separately; `full` exists only after a full-Bible run."}],"responses":{"200":{"description":"Series Bible coverage envelope.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SeriesBibleCoverageEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"generateProjectSeriesBibleCoverage","tags":["Projects"],"summary":"Generate foundational Series Bible coverage.","description":"Queues exemplar-calibrated coverage for the exact current Bible revision. Pick the checkpoint with `phase` in the body: `premise` (default) or `full`, which exists only after the episode map. The first benchmark layer uses 72 provenance-backed analytical profiles across audio docuseries, true crime, documentary, and comedy. Series script coverage waits until the current report passes its 7.0 gate. Requires Idempotency-Key.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SeriesBibleCoverageGenerateRequest"}}}},"responses":{"200":{"description":"Queued Series Bible coverage envelope.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SeriesBibleCoverageEnvelope"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/story-projects/{projectId}/outline-map":{"parameters":[{"$ref":"#/components/parameters/ProjectId"}],"get":{"operationId":"getProjectOutlineMap","tags":["Projects"],"summary":"Read every planned episode's outline.","description":"Every episode of the season map, in order, with its outline and where it came from: `outline_map` when one was written for the episode (its Outline tab, Slug, or PATCH here), `season_map` when nothing is written yet and the episode's season-map note lays it out act by act or scene by scene, so the note stands in. A season-map synopsis or a written screenplay is not an outline. Also `readiness`, exactly what outline coverage reports: whether the outlines can be scored now and, when not, why. Read-only. Scope: `story:read`.","security":[{"BearerApiKey":["story:read"]}],"responses":{"200":{"description":"The outline map.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OutlineMapEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"patch":{"operationId":"saveProjectEpisodeOutline","tags":["Projects"],"summary":"Save ONE episode's outline.","description":"Replaces one season-map episode's outline with `outline` and leaves every other episode as stored — the same writer as the episode's Outline tab and Slug's save_episode_outline, so it is the outline outline coverage scores and the season planner hands the writer. Read the outline map first to carry over what should stay. A structure that is not three acts (six acts and a tag, a scene list) goes in `titleNotes` whole. An `episodeId` the season map does not plan is a 400. Free. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SaveEpisodeOutlineRequest"}}}},"responses":{"200":{"description":"The outline map, with the episode as saved.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OutlineMapEnvelope"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/story-projects/{projectId}/outline-map/coverage":{"parameters":[{"$ref":"#/components/parameters/ProjectId"}],"get":{"operationId":"getProjectOutlineCoverage","tags":["Projects"],"summary":"Read outline coverage.","description":"Returns the latest structural report for the project outline map, including whether it matches the current document revision, its 0–10 score, seven structural dimensions, per-episode notes, priority fixes, and any episodes that were not yet outlined when it ran, plus `readiness` — whether the outlines as they stand can be scored now and, when not, `reason` saying exactly what is missing. Report-only: unlike Series Bible coverage this carries no gate.","security":[{"BearerApiKey":["story:read"]}],"responses":{"200":{"description":"Outline coverage envelope.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OutlineCoverageEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"generateProjectOutlineCoverage","tags":["Projects"],"summary":"Generate outline coverage.","description":"Queues a structural report for the exact current outline-map revision, grading the same beats the season planner hands the script writer. Requires at least one planned episode with an outline (`readiness.canScore` on the read; GET /story-projects/{projectId}/outline-map shows them); otherwise it refuses with 400 `validation_failed` carrying `readiness.reason`, and nothing is queued or charged. Nothing waits on the result. Requires Idempotency-Key.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OutlineCoverageGenerateRequest"}}}},"responses":{"200":{"description":"Queued outline coverage envelope.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OutlineCoverageEnvelope"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/story-projects/{projectId}/season-runs":{"parameters":[{"$ref":"#/components/parameters/ProjectId"}],"get":{"operationId":"listStorySeasonRuns","tags":["Seasons"],"summary":"List persistent season runs for a story project.","description":"Returns the newest 50 non-deleted runs with their full episode maps, coverage audit history, progress, and next human or machine action.","security":[{"BearerApiKey":["story:read"]}],"responses":{"200":{"description":"Season run list.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SeasonRunList"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"createStorySeasonRun","tags":["Seasons"],"summary":"Start a conversational season run from the Series Bible episode map.","description":"Copies the current Series Bible episodes into a durable, editable season map and attaches an audio publishing series. Before calling, inspect document.readiness.development and complete an audio-appropriate Bible; runtime only requires a saved episode map so podcast creation is not blocked by visual/camera fields in the full development profile. Requires story:write and publishing:write because the route may create the audio series and RSS destination. Creation never approves or generates an episode: an agent must discuss each imported brief with the user, PATCH it with confirmed=true, then ask for whole-map approval. Requires Idempotency-Key for HTTP replay safety. Durable identity defaults to project + season number + Series Bible version, or an explicit runKey; a replay with different immutable create inputs is rejected.","security":[{"BearerApiKey":["story:write","publishing:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SeasonRunCreateRequest"}}}},"responses":{"200":{"description":"Created or replayed season run.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SeasonRunEnvelope"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/SeasonRunNotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/season-runs/{runId}":{"parameters":[{"$ref":"#/components/parameters/SeasonRunId"}],"get":{"operationId":"getStorySeasonRun","tags":["Seasons"],"summary":"Read a season run and its durable next action.","description":"Use this as the polling and conversation-resume surface. The response includes every mapped episode, linked plan/job/artifact/coverage/release ids, immutable coverage attempts, and nextAction.","security":[{"BearerApiKey":["story:read"]}],"responses":{"200":{"description":"Season run envelope.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SeasonRunEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/SeasonRunNotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/season-runs/{runId}/episodes/{episodeId}":{"parameters":[{"$ref":"#/components/parameters/SeasonRunId"},{"$ref":"#/components/parameters/SeasonRunEpisodeId"}],"patch":{"operationId":"updateStorySeasonEpisode","tags":["Seasons"],"summary":"Save one agreed episode brief while mapping a season.","description":"Partial update available only while the run is mapping. Set confirmed=true only after the user has discussed and agreed to the title, summary, objective, audience/tone, duration, and notes. expectedMapVersion is required on every PATCH so two agent conversations cannot silently overwrite each other. Map edits are locked permanently after approval.","security":[{"BearerApiKey":["story:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SeasonRunEpisodeUpdateRequest"}}}},"responses":{"200":{"description":"Updated season run.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SeasonRunEnvelope"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/SeasonRunNotFound"},"409":{"$ref":"#/components/responses/SeasonRunStateInvalid"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/season-runs/{runId}/approve":{"parameters":[{"$ref":"#/components/parameters/SeasonRunId"}],"post":{"operationId":"approveStorySeasonMap","tags":["Seasons"],"summary":"Approve and lock the complete season map.","description":"A human approval boundary. Call only after every episode has been discussed, has a complete brief, and is confirmed=true. Send expectedMapVersion from the exact SeasonRun response the user reviewed; a stale value is rejected so a newer map cannot be approved accidentally. Approval locks the map and enables sequential episode planning. It does not approve later StoryPlans, coverage revisions, or RSS publication. Requires Idempotency-Key and repeatable (non-FREE) coverage access.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SeasonRunApprovalRequest"}}}},"responses":{"200":{"description":"Approved season run.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SeasonRunEnvelope"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/SeasonRunNotFound"},"409":{"$ref":"#/components/responses/SeasonRunStateInvalid"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/season-runs/{runId}/advance":{"parameters":[{"$ref":"#/components/parameters/SeasonRunId"}],"post":{"operationId":"advanceStorySeasonRun","tags":["Seasons"],"summary":"Advance one durable step of the next season episode.","description":"Requires story:write, artifact:publish, and publishing:write. `action=publish` additionally requires publishing:publish, checked per-action once the body is read, so a connector that only produces episodes never has to hold publish rights. Performs at most one idempotent queue/reconciliation step, then returns current durable state. It never crosses a human stop automatically: generated StoryPlans require POST /story-plans/{planId}/approve with userConfirmed=true; a terminal failed/rejected/superseded plan requires action=replan with explicit confirmation and can carry an optional correction instruction (a rejected plan defaults to its persisted rejection reason); the instruction is copied into the replacement StoryPlan creative brief and survives dispatch retries; a terminal failed CoverageReport requires action=retry_coverage with explicit confirmation and reuses that same report id/version; a completed report that fails the score gate requires action=revise plus the exact human-approved instruction or an exact replacement screenplay; nextAction=repair_production returns the current artifact and durable music/SFX/voice-effect blockers, which must be repaired through the artifact mutation surfaces before action=advance re-evaluates finalization; a coverage-passed, finalized MP3 requires action=publish before an RSS release is created. Episodes publish in order. The fail-closed gate checks overallScore and premise, structure, characters, dialogue, pacing, and marketFit on the 1–10 integer scale plus the allowed recommendation. Requires Idempotency-Key.","security":[{"BearerApiKey":["story:write","artifact:publish","publishing:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SeasonRunAdvanceRequest"}}}},"responses":{"200":{"description":"Advanced or reconciled season run.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SeasonRunEnvelope"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/InsufficientCredits"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/SeasonRunNotFound"},"409":{"$ref":"#/components/responses/SeasonRunStateInvalid"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/story-projects/{projectId}/cast-voice/design":{"parameters":[{"$ref":"#/components/parameters/ProjectId"}],"post":{"operationId":"designProjectCastVoice","tags":["Projects"],"summary":"Design a cast member's voice from a description (metered — TWO CALLS, price first).","description":"Draws up to four candidate voices for one named cast member (`character`, by canon name, alias or script cue) from `description`, or from the Series Bible's voice line for them when `description` is omitted, on the voice designer (ElevenLabs Voice Design). Each candidate is ONE TAKE of the CHARACTER'S OWN screenplay lines from this project (the design line, then `previewLines` more — and more of their lines when those are too short to clone a voice from), so each preview `url` is the character saying their own words; `sampleText` designs from a line of your own for a person with no lines yet. A description shorter than 20 characters is refused (400). A design never reuses a voice the character already has, so a redesign is never swallowed. TWO CALLS: sent without `confirmed` it PRICES the design and draws nothing (`quotedOnly: true`, `credits` — one casting voice per candidate, the price casting charges — `balanceAfter`, `sufficient`, `note`, and the `description`, `designLine` and `previewLines` it will use); sent again with `confirmed: true` it reserves, draws, and answers with `designId` and the `candidates`. EVERY CANDIDATE DRAWN IS CHARGED, chosen or not; choosing is free. Anyone on the project cast is reached, in an episode or not: a person with no lines in the screenplays (someone only a deck or trailer gives a line) is designed on `sampleText` — the line the deck gives them — or, without it, each take speaks a short passage the designer writes for the description (`designLine` is then null on the price). A key character's candidates are then judged by casting coverage by themselves, free (`coverage` on the answer; GET .../cast-voice/designs/{designId}/coverage). Then choose one with POST /story-projects/{projectId}/cast-voice/choose. Refused before the price with 409 `cast_voice_precondition_failed` when there is no description and no Series Bible voice line, or no voice designer is configured (`details.missing` `voice_design`); 409 `voice_retired` when the provider designed voices speak on has retired its voices; 409 `project_precondition_failed` when the project's gate holds spending.  Requires `Idempotency-Key`. Scope: `artifact:publish` (billable).","security":[{"BearerApiKey":["artifact:publish"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CastVoiceDesignRequest"}}}},"responses":{"200":{"description":"The price (no `confirmed`) or the drawn candidates.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CastVoiceDesignEnvelope"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/InsufficientCredits"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/CastVoicePreconditionFailed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/story-projects/{projectId}/cast-voice":{"parameters":[{"$ref":"#/components/parameters/ProjectId"}],"patch":{"operationId":"setProjectCastVoice","tags":["Projects"],"summary":"Pin an existing voice on one person of the project cast, in an episode or not.","description":"Pins `voiceId` (with `voiceName`, `provider` and `gender`, each read from your voice library when omitted) on the cast canon entry of `character` — by canon name, alias or script cue — anyone on the project cast, including a person no screenplay names yet whom a deck or trailer gives a line. Every new episode, deck, trailer and showcase speaks them with it, and an episode that later names them inherits it instead of casting a second one. A Series Bible character the canon does not hold yet is added to it. Never changes a read: `reads` lists the finished table reads that still speak this person with another voice — re-voice them with POST /artifacts/{artifactId}/revoice (priced). `previous` is the canon voice it replaced; set it again to go back. A NEW voice from a description is POST .../cast-voice/design (priced) then POST .../cast-voice/choose. Refused with 409 `voice_retired` for a voice on a provider that is retiring its voices, and 404 `character_not_found` when nobody in the cast canon or Series Bible answers to `character`. Free. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CastVoiceSetRequest"}}}},"responses":{"200":{"description":"The voice pinned, what it replaced, and the reads still on another voice.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CastVoiceSetResponse"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/CharacterNotFound"},"409":{"$ref":"#/components/responses/VoiceRetired"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/story-projects/{projectId}/cast-voice/choose":{"parameters":[{"$ref":"#/components/parameters/ProjectId"}],"post":{"operationId":"chooseProjectCastVoice","tags":["Projects"],"summary":"Pin one designed candidate voice to the cast canon.","description":"Keeps candidate `candidate` (1-based) of design `designId`: clones its take into the provider that speaks every designed voice (Cartesia), adds it to your voice library as `voiceName` (default \"<project> - <character>\") with where it came from (the description, the take's preview id, the new voice), pins it on the cast canon person (so every new episode, deck, trailer and showcase speaks with it), and lists in `reads` the finished reads that still speak this character with another voice — re-voice them with POST /artifacts/{artifactId}/revoice (priced). Free; choosing another candidate of the same design later is free too, and choosing one already chosen reuses its voice. A candidate drawn before the designer changed cannot be chosen (409 `cast_voice_precondition_failed`, `details.missing` `take`): design again. A candidate its casting coverage FAILED (GET .../cast-voice/designs/{designId}/coverage, `failingCandidates`) is refused with 409 `cast_voice_casting_coverage_failed` unless `acknowledgeCastingCoverage: true`, sent only once the writer has heard it and chosen it anyway; coverage still running or one that could not run never holds a choice (`castingCoverageWarning` says so).  Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CastVoiceChooseRequest"}}}},"responses":{"200":{"description":"The voice pinned, and the reads still on another voice.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CastVoiceChooseResponse"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/VoiceDesignNotFound"},"409":{"$ref":"#/components/responses/CastVoiceChoiceRefused"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/story-projects/{projectId}/cast-voice/designs/{designId}/coverage":{"parameters":[{"$ref":"#/components/parameters/ProjectId"},{"name":"designId","in":"path","required":true,"schema":{"type":"string"},"description":"The voice design (`designId` from POST .../cast-voice/design or GET .../cast-voice/designs)."}],"get":{"operationId":"getProjectCastVoiceCoverage","tags":["Projects"],"summary":"Read a voice design's casting coverage.","description":"A model listened to every candidate take blind (shuffled and relabelled every run), beside the voices already cast in the project, and scored each against the brief and the Series Bible: emblematic fit, truthful performance, period register, accent honesty, clean audio, word accuracy (a blind transcript compared with the lines) and distinctness from the cast. Answers the ranking, each candidate's score and flags, the recommendation, the runner-up, `failingCandidates` and — when every candidate failed — a revised brief. A design with no report reads `missing`, not 404. Free. Scope: `story:read`.","security":[{"BearerApiKey":["story:read"]}],"responses":{"200":{"description":"The casting coverage.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CastingCoverageResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/VoiceDesignNotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"generateProjectCastVoiceCoverage","tags":["Projects"],"summary":"Judge a voice design's candidates again.","description":"Listens to the candidates again as a new casting coverage report, with the cast as it stands now. FREE to the writer: no price and no `confirmed`. Coverage already runs by itself once per key character's design; a report already running is returned as it is. Refused (400) for a design with no hosted takes, or when no provider can listen right now. Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CastingCoverageGenerateRequest"}}}},"responses":{"200":{"description":"The report now running (or the one already running).","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CastingCoverageResponse"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/VoiceDesignNotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/story-projects/{projectId}/cast-voice/designs":{"parameters":[{"$ref":"#/components/parameters/ProjectId"}],"get":{"operationId":"listProjectCastVoiceDesigns","tags":["Projects"],"summary":"List a project's cast voice designs.","description":"The project's cast voice designs, newest first (at most 50), each with its candidates, their preview urls, and the candidate chosen (`chosen`, or null). `?character=` narrows to one person (by name, alias or cue). Free. Scope: `story:read`.","security":[{"BearerApiKey":["story:read"]}],"parameters":[{"name":"character","in":"query","required":false,"schema":{"type":"string"},"description":"One cast member, by canon name, alias or script cue."}],"responses":{"200":{"description":"The designs.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CastVoiceDesignListResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/story-projects/{projectId}/cast-portraits/design":{"parameters":[{"$ref":"#/components/parameters/ProjectId"}],"post":{"operationId":"designProjectCastPortraits","tags":["Projects"],"summary":"Draw headshot candidates for one cast member (metered — TWO CALLS, price first).","description":"Draws N headshot candidates (`candidates`, default 3, at most 4) for one named cast member (`character`, by canon name, alias, cue or Series Bible name), so the writer chooses a face instead of taking the only one the cast draws. Each is drawn from the Series Bible's appearance and wardrobe for them with their LOOK after it: `direction` when given, else their look direction on the cast canon (PATCH /story-projects/{projectId}/cast-look), and `age` likewise. The look outranks the Bible where they differ. TWO CALLS: sent without `confirmed` it PRICES the candidates and draws nothing (`quotedOnly: true`, `credits` — one cast headshot per candidate — `balanceAfter`, `sufficient`, `note`, and the `direction` it will draw from); sent again with `confirmed: true` it reserves them, records the design and queues the draw (`designId`, `status: queued`). Poll GET .../cast-portraits/designs?designId= until `status` is `complete`. EVERY CANDIDATE DRAWN IS CHARGED, chosen or not; a candidate that cannot be drawn is released; choosing is free. RESTYLE KEEPING THE FACE: with `keepFace: true` each candidate is the person's CURRENT HEADSHOT on the cast canon, edited to the look (`direction`, else their saved look) on the model that keeps a face, so their hair, grooming, wardrobe or styling change and the person stays the same (`restyledFrom` names the headshot). Same price per candidate; casting coverage judges them and choosing works the same. Refused before the price: 404 `character_not_found` when nobody in the cast canon or Series Bible answers to the name; 409 `cast_precondition_failed` when there is nothing to draw them from, no image model is bound, or (with `keepFace`) they have no headshot yet (reason `cast_face_missing`) or no bound model keeps a face; 409 `project_precondition_failed` when the project has not reached casting.  Requires `Idempotency-Key`. Scope: `artifact:publish` (billable).","security":[{"BearerApiKey":["artifact:publish"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CastPortraitDesignRequest"}}}},"responses":{"200":{"description":"The price (no `confirmed`) or the queued design.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CastPortraitDesignEnvelope"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/InsufficientCredits"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/CharacterNotFound"},"409":{"$ref":"#/components/responses/CastPreconditionFailed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/story-projects/{projectId}/cast-portraits/choose":{"parameters":[{"$ref":"#/components/parameters/ProjectId"}],"post":{"operationId":"chooseProjectCastPortrait","tags":["Projects"],"summary":"Pin one headshot candidate as the person's face on the cast canon.","description":"Pins candidate `candidate` (1-based) of design `designId` as the person's headshot on the cast canon, with the prompt it was drawn from: every new episode, deck, trailer and showcase draws them from it. Free (the candidates were paid for). A NEW face clears the canon body shot and character sheet, because they were drawn from the previous face (`cleared`; `previous` keeps the old urls): draw them again with POST /story-projects/{projectId}/cast for this character. Choosing the face already pinned changes nothing. A candidate still being drawn, or that could not be drawn, is a 409 `cast_precondition_failed`.  Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CastPortraitChooseRequest"}}}},"responses":{"200":{"description":"The face pinned, and what it cleared.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CastPortraitChooseResponse"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/PortraitDesignNotFound"},"409":{"$ref":"#/components/responses/CastPortraitChoiceRefused"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/story-projects/{projectId}/cast-portraits/designs/{designId}/coverage":{"parameters":[{"$ref":"#/components/parameters/ProjectId"},{"name":"designId","in":"path","required":true,"schema":{"type":"string"},"description":"The portrait design (`designId` from POST .../cast-portraits/design or GET .../cast-portraits/designs)."}],"get":{"operationId":"getProjectCastPortraitCoverage","tags":["Projects"],"summary":"Read a portrait design's casting coverage.","description":"A model looked at every drawn headshot blind, beside the faces already chosen for the cast, and scored each on emblematic fit, period truth, naturalness (not generated) and distinctness; answers the ranking, each headshot's score and flags, the recommendation, `failingCandidates` and — when every one failed — a revised look. A design with no report reads `missing`, not 404. Free. Scope: `story:read`.","security":[{"BearerApiKey":["story:read"]}],"responses":{"200":{"description":"The portrait casting coverage.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CastingPortraitCoverageResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/PortraitDesignNotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"generateProjectCastPortraitCoverage","tags":["Projects"],"summary":"Judge a portrait design's headshots again.","description":"Looks at the headshots again as a new report, with the cast as it stands now. FREE: no price and no `confirmed`. A report already running is returned as it is. Refused (400) for a design with no drawn headshot, or when no provider can look right now. Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CastingCoverageGenerateRequest"}}}},"responses":{"200":{"description":"The report now running (or the one already running).","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CastingPortraitCoverageResponse"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/PortraitDesignNotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/story-projects/{projectId}/cast-portraits/designs":{"parameters":[{"$ref":"#/components/parameters/ProjectId"}],"get":{"operationId":"listProjectCastPortraitDesigns","tags":["Projects"],"summary":"List a project's headshot candidate designs.","description":"The project's portrait designs, newest first (at most 50): each with its `status` (queued, drawing, complete, failed), the person it is for (`character`, `castPersonId`), the direction it was drawn from, its candidates (a stable `candidateId`, `index`, `imageUrl`) and the one chosen. `?designId=` reads one design (poll it after a confirmed design); `?character=` narrows to one person. Free. Scope: `story:read`.","security":[{"BearerApiKey":["story:read"]}],"parameters":[{"name":"designId","in":"query","required":false,"schema":{"type":"string"},"description":"One design."},{"name":"character","in":"query","required":false,"schema":{"type":"string"},"description":"One cast member, by canon name, alias, cue or Series Bible name."}],"responses":{"200":{"description":"The designs.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CastPortraitDesignListResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/PortraitDesignNotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/story-projects/{projectId}/cast-look":{"parameters":[{"$ref":"#/components/parameters/ProjectId"}],"get":{"operationId":"getProjectCastLook","tags":["Projects"],"summary":"Read cast members' look directions at project level.","description":"Every cast member's LOOK on the cast canon — the writer's look direction and the age they are played at — beside what the Series Bible says they look like. Series Bible characters the canon does not hold yet are listed with `inCanon: false` and no look. `?character=` (any name or alias) reads one person; nobody by that name is a 404 `character_not_found`. Free. Scope: `story:read`.","security":[{"BearerApiKey":["story:read"]}],"parameters":[{"name":"character","in":"query","required":false,"schema":{"type":"string"},"description":"One person, by any name or alias."}],"responses":{"200":{"description":"The looks.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CastLookRead"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/CharacterNotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"patch":{"operationId":"setProjectCastLook","tags":["Projects"],"summary":"Set or clear one cast member's look direction and age at project level.","description":"Sets `lookDirection` (how they should look on camera, in the writer's words) and/or `age` (the age they are played at) on the person's cast canon entry, without an episode. Every headshot and portrait candidate drawn for them from then on reads it, outranking the Series Bible's appearance where they differ; body shots and sheets are drawn from the headshot and carry it through it. A field not sent is left alone; `null` or \"\" clears it. A Series Bible character the canon does not hold yet is added to it. Nothing is drawn; free. Unlike a Series Bible edit, it does not re-open Bible coverage. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CastLookPatch"}}}},"responses":{"200":{"description":"The person's look as saved.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CastLookSaved"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/CharacterNotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/story-projects/{projectId}/series-bible/characters":{"parameters":[{"$ref":"#/components/parameters/ProjectId"}],"get":{"operationId":"getSeriesBibleCharacter","tags":["Projects"],"summary":"Read one Series Bible character.","description":"One character of the project's Series Bible (`?character=` its id, or any name the Bible or the cast canon knows them by), with the Bible's `currentVersion` to send back as `expectedVersion`. Nobody by that name is a 404 `character_not_found` naming the characters the Bible has. Scope: `story:read`.","security":[{"BearerApiKey":["story:read"]}],"parameters":[{"name":"character","in":"query","required":true,"schema":{"type":"string"},"description":"The character's id or name."}],"responses":{"200":{"description":"The character and the Bible version.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SeriesBibleCharacterResult"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/CharacterNotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"patch":{"operationId":"updateSeriesBibleCharacter","tags":["Projects"],"summary":"Edit ONE Series Bible character, not the whole array.","description":"Changes only the fields in `content` on one character (found by id, or any name the Bible or the cast canon knows them by) and leaves every other character exactly as stored, under the same lock as every Bible save. `expectedVersion` (the `currentVersion` you read) makes it a compare-and-swap: when the Bible changed since, nothing is saved and it is a 409 `series_bible_version_conflict`. `create: true` adds the character when nobody answers to the name (else 404 `character_not_found`). A note about how someone LOOKS belongs on PATCH /story-projects/{projectId}/cast-look, which never touches the Bible. Every save returns `gateChange` { before, after, staleCoverage }: what it did to the project's gate. Appending (episodes for a new season, a new character) never re-locks anything. An edit to canon a coverage report scored puts that report out of date (`staleCoverage`) and re-locks what it opened: after full coverage, paid work on every episode already written (coverage, casting, table reads, videos, decks, trailers, publishing) until full coverage passes again. When `after.stage` comes before `before.stage`, tell the user. Free. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SeriesBibleCharacterUpdateRequest"}}}},"responses":{"200":{"description":"The character as saved and the Bible version.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SeriesBibleCharacterResult"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/CharacterNotFound"},"409":{"$ref":"#/components/responses/SeriesBibleVersionConflict"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/story-projects/{projectId}/sounds":{"parameters":[{"$ref":"#/components/parameters/ProjectId"}],"get":{"operationId":"listProjectSounds","tags":["Projects"],"summary":"List the sounds the project's decks and trailers use.","description":"Every sound the project's decks and trailers use: each deck's bell on the beat, every music clip and sound effect on a timeline, named as it was used and by its video, numbered in the project by when it was made (\"Deck 2\"). Each entry carries `listenUrl` and `bellSound` (how to strike it as a deck's bell: `{ sound: { fromJobId } }` or `{ assetId }`). Reads only.","security":[{"BearerApiKey":["story:read"]}],"parameters":[{"name":"kind","in":"query","required":false,"schema":{"type":"string","enum":["bell","music","sfx"]},"description":"Only this kind of sound."},{"name":"query","in":"query","required":false,"schema":{"type":"string"},"description":"Words of the sound's name, its video's title, or \"deck 2\"."}],"responses":{"200":{"description":"The project's videos (numbered) and the sounds they use (at most 100; `total` counts every match).","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/story-projects/{projectId}/cast-canon":{"parameters":[{"$ref":"#/components/parameters/ProjectId"}],"get":{"operationId":"getProjectCastCanon","tags":["Projects"],"summary":"Read the project cast canon (pinned portrait, body, and character-sheet assets).","description":"Owner-pinned character assets every episode of the project inherits at creation, with each person's aliases, voice and signature items. Project-wide visual direction lives in the Series Bible. A project with no canon returns an empty document. `identity` warns when one name reaches two people or two people share a body figure, lists characters with no Series Bible appearance, and lists Series Bible names no canon person answers to (`unresolvedBibleNames`, e.g. \"Foma Stepanovich\" beside FOMA, with the people each probably is; add the Bible name to that person's `aliases`); it is read-only.","security":[{"BearerApiKey":["story:read"]}],"responses":{"200":{"description":"Cast canon envelope.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CastCanonEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"patch":{"operationId":"saveProjectCastCanon","tags":["Projects"],"summary":"Save the project cast canon.","description":"Set once per project; new episodes seed each matching character's canonical portrait, body, and character-sheet assets before generation so they are not re-rendered. Project-wide character design and visual language live in the Series Bible. Each character also carries `aliases` (the script cue names that are this person) and `signatureItems` (what only this person wears or carries; videos with two or more people need them for each person). `characters` merges by name or alias; pass `arrayMode: \"replace\"` to send the complete roster. Unknown keys are refused. The answer carries the same `identity` warnings as the read. An explicit cast restyle remains an episode override.","security":[{"BearerApiKey":["story:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CastCanonPatch"}}}},"responses":{"200":{"description":"Saved cast canon envelope.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CastCanonEnvelope"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/story-projects/{projectId}/cast":{"parameters":[{"$ref":"#/components/parameters/ProjectId"}],"patch":{"operationId":"updateEpisodeCharacterDirection","tags":["Projects"],"summary":"Save episode character direction without generating or spending credits.","description":"Existing images and voices remain unchanged. avatarPrompt guides headshots and sheets; figurePrompt guides body shots. signatureItems (what only this person wears or carries) is saved on the project cast canon for every episode. A person on the project cast canon whom the episode does not name (a deck or trailer draws them from the canon) takes `signatureItems` alone, saved on their canon entry (`projectCast: true` in the answer); any other field sent for them is refused by name. When the canon cannot tell who the character is, nothing is saved and a 409 names the `candidates`; the writer answers with `sameAs` (the character IS that canon person, so the script name becomes their alias) or `newPersonName` (someone else, saved under that full name). A separate quote and confirmation are required to replace assets.","security":[{"BearerApiKey":["story:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EpisodeCharacterDirectionRequest"}}}},"responses":{"200":{"description":"Saved direction; generated is always false.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EpisodeCharacterDirectionResult"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"get":{"operationId":"getProjectCast","tags":["Projects"],"summary":"Read project cast and episode casting progress.","security":[{"BearerApiKey":["story:read"]}],"parameters":[{"in":"query","name":"scriptUploadId","required":false,"schema":{"type":"string"},"description":"Include this episode’s casting job and readiness."}],"responses":{"200":{"description":"Cast assets and optional episode status.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectCastReadEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"generateProjectCast","tags":["Projects"],"summary":"Generate the project cast (price first, draw on a confirmed second call).","description":"With scriptUploadId, quote the full episode cast first, then confirm with that quoteId to reserve Studio Credits and enqueue voices plus headshots, body shots and character sheets. Poll GET with the same scriptUploadId; Table Read requires complete casting. Without scriptUploadId this is the separate project Bible visual cast.","security":[{"BearerApiKey":["artifact:publish"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerateProjectCastRequest"}}}},"responses":{"200":{"description":"The quote, durable episode casting job, or completed project Bible visual cast.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectCastEnvelope"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/CastPreconditionFailed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/story-projects/{projectId}/mood-board":{"parameters":[{"$ref":"#/components/parameters/ProjectId"}],"get":{"operationId":"getProjectMoodBoard","tags":["Projects"],"summary":"Read the project mood board (the agreed look, as kept frames).","description":"The kept frames every new character render is made in. Costs no credits. A project that has agreed no look yet returns an empty board rather than a 404, because \"no look agreed yet\" is a normal state. It also answers with `candidates` — frames this project has RENDERED and not kept, which is where the urls keepProjectMoodBoardFrame needs come from — and `renders`, the recent batches and where each one got to, so a poll after renderProjectMoodBoardFrames terminates instead of waiting forever on a batch that failed.","security":[{"BearerApiKey":["story:read"]}],"responses":{"200":{"description":"Mood board envelope.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MoodBoardEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/story-projects/{projectId}/mood-board/frames":{"parameters":[{"$ref":"#/components/parameters/ProjectId"}],"post":{"operationId":"keepProjectMoodBoardFrame","tags":["Projects"],"summary":"Keep one frame on the project mood board.","description":"Pin one frame as part of the agreed look, so every character render afterwards is made in it. Costs no credits and generates nothing: the picture already exists and this writes it into canon. Keeping a frame whose `id` is already kept replaces that entry. Keeping a fifth distinct frame is REFUSED with 409 `mood_board_full` and nothing is trimmed — four is the most that can bind a render, so a fifth frame would silently have no effect. A project that has not passed premise coverage is refused with 409 `project_precondition_failed`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MoodBoardFrameKeepRequest"}}}},"responses":{"200":{"description":"Mood board envelope after the keep.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MoodBoardEnvelope"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/MoodBoardKeepConflict"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/story-projects/{projectId}/mood-board/render-frames":{"parameters":[{"$ref":"#/components/parameters/ProjectId"}],"post":{"operationId":"renderProjectMoodBoardFrames","tags":["Projects"],"summary":"Render candidate mood board frames (metered — TWO CALLS, price first).","description":"Draw candidate frames for the project's look from a brief. METERED: one still image per frame. Sent WITHOUT `confirmed` it prices the batch and renders nothing (`quotedOnly: true`, `credits`, and a `note` to show the writer); sent again with `confirmed: true` it reserves the credit lines and queues the batch. `confirmed` is optional rather than a required `true` because a field that must be true to validate cannot tell a confirmed call from an unconfirmed one, and it would force the price to be announced after the money moved. EVERY CANDIDATE COSTS, KEPT OR NOT — keeping is the free act that agrees the look, so the frames the writer drops were still drawn and still charged; the quote says so and a caller showing the price must show that sentence with it. Preconditions run above the price: a project that has not passed premise coverage, or no image model bound for frames, is 409 `project_precondition_failed` with nothing quoted. A confirmed call answers with a `chatToolJobId` — the frames are NOT in the response. Poll getProjectMoodBoard until the batch leaves `renders` and its frames appear in `candidates`, then keep the ones that are right.","security":[{"BearerApiKey":["artifact:publish"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MoodBoardRenderRequest"}}}},"responses":{"200":{"description":"The price (no `confirmed`) or the queued batch.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MoodBoardRenderEnvelope"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/InsufficientCredits"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/ProjectPreconditionFailed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/story-projects/{projectId}/mood-board/frames/{frameId}":{"parameters":[{"$ref":"#/components/parameters/ProjectId"},{"$ref":"#/components/parameters/MoodBoardFrameId"}],"delete":{"operationId":"dropProjectMoodBoardFrame","tags":["Projects"],"summary":"Drop one frame from the project mood board.","description":"Take one frame back out of the agreed look. Costs no credits and refunds none: the picture stays where it is hosted, and what changes is that it stops binding renders. Dropping a frame id that is not on the board answers with the board unchanged. Returns the whole board after the change.","security":[{"BearerApiKey":["story:write"]}],"responses":{"200":{"description":"Mood board envelope after the drop.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MoodBoardEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/story-projects/{projectId}/sources":{"parameters":[{"$ref":"#/components/parameters/ProjectId"}],"get":{"operationId":"listStorySources","tags":["Sources"],"summary":"List sources attached to a project.","security":[{"BearerApiKey":["source:read"]}],"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":200},"description":"Max items per page (default 50, max 200)."},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"},"description":"Opaque cursor from a previous response."}],"responses":{"200":{"description":"Paginated source list.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StorySourceList"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"createStorySource","tags":["Sources"],"summary":"Attach a source to a project.","description":"Accepts inline `text`, `markdown`, URL, and PDF sources. Requires `Idempotency-Key` for safe retries. Check `/api/v1/capabilities.sources.types` before sending a type. Set `metadata.sourceContextMode` to `full` when planning and final artifact writing must receive the complete integrity-verified retained text instead of the normal digest + preview; full mode fails rather than clipping. A producer that sends one source per item names it with `producer` + `externalId`; a repeat of an item the project already holds returns that source with `deduplicated: true` instead of storing it again.","security":[{"BearerApiKey":["source:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StorySourceCreate"}}}},"responses":{"200":{"description":"The created source, or the live source already holding this item (`deduplicated: true`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StorySourceCreateResult"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"413":{"$ref":"#/components/responses/SourceTooLarge"},"422":{"$ref":"#/components/responses/SourceTypeUnsupported"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/story-projects/{projectId}/story-plans":{"parameters":[{"$ref":"#/components/parameters/ProjectId"}],"get":{"operationId":"listStoryPlans","tags":["Plans"],"summary":"List StoryPlans for a project.","security":[{"BearerApiKey":["story:read"]}],"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Paginated plan list.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StoryPlanList"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"createStoryPlan","tags":["Plans"],"summary":"Create a StoryPlan and enqueue its async generation.","description":"Validates target + artifactRequests + sourceIds synchronously, including source ownership and status READY, persists the plan as `PENDING`, and enqueues the planner backed by DB sidecar `story_api_plan`. Requires `Idempotency-Key` for safe retries. Refused with 409 `project_precondition_failed` until the project passes its full Series Bible coverage — `details.stage` names the stage to finish; retrying will not help. Non-blocking warning gate: before a screenplay/table_read plan, read or generate the full project Series Bible and review the plan against it. The planner auto-loads the saved Series Bible as canonical project truth. The response includes a preliminary quote computed from the artifact requests; the worker overwrites it with a plan-aware quote once the StoryPlan envelope is generated. Table-read plans accept `creativeBrief`, return a generated `scriptBlueprint`, cannot use `autoApprove`, and are limited to one table_read script/installment per plan. Table-read artifact requests may set `narrationPolicy` (`auto | include | suppress`) to make narrator/no-narrator a user-level creation choice instead of implying it from mode.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StoryPlanCreate"}}}},"responses":{"200":{"description":"Created plan envelope.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StoryPlanEnvelopeResponse"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/ProjectPreconditionFailed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/story-plans/{planId}":{"parameters":[{"$ref":"#/components/parameters/StoryPlanId"}],"get":{"operationId":"getStoryPlan","tags":["Plans"],"summary":"Read a StoryPlan by id.","security":[{"BearerApiKey":["story:read"]}],"responses":{"200":{"description":"Plan envelope.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StoryPlanEnvelopeResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"patch":{"operationId":"renameStoryPlan","tags":["Plans"],"summary":"Rename a plan (free).","description":"Renames a plan: the name its list and its review show. Free and reversible (rename it again). Nothing is planned, priced or made again, and a table read, deck or trailer already made from it keeps its own name. Any status. A name that is empty or over 200 characters is refused (400 `validation_failed`). Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ArtifactRenameRequest"}}}},"responses":{"200":{"description":"The renamed plan.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StoryPlanEnvelopeResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/story-plans/{planId}/resume":{"parameters":[{"$ref":"#/components/parameters/StoryPlanId"}],"post":{"operationId":"resumeStoryPlan","tags":["Plans"],"summary":"Resume planning under the same durable StoryPlan id.","description":"State-aware and replay-safe. Ordinary PENDING plans repair a missed queue dispatch, stale RUNNING plans reclaim a dead worker, and FAILED plans without generated output re-enter generation under the same id after clearing their failure. A PENDING plan with `failureCode: provider_capacity_backoff` already has a Sleeper Hit-managed delayed retry and is returned unchanged. Active, review-ready, and approved plans are also unchanged. REJECTED and SUPERSEDED plans remain human-terminal and cannot be resumed. Requires `Idempotency-Key`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"200":{"description":"Resumed plan envelope.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StoryPlanEnvelopeResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/StoryPlanStateInvalid"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/story-plans/{planId}/approve":{"parameters":[{"$ref":"#/components/parameters/StoryPlanId"}],"post":{"operationId":"approveStoryPlan","tags":["Plans"],"summary":"Approve a generated StoryPlan.","description":"Mandatory human approval boundary. After showing the complete generated plan to the user, send userConfirmed=true to transition it to `APPROVED`. Valid only from `REQUIRES_APPROVAL` or `READY`. Requires `Idempotency-Key`; re-approving an already-approved plan is a no-op but still requires confirmation in the request. The one exception is a show's STANDING APPROVAL: the API key it is bound to may omit `userConfirmed` for a table-read plan in that show's project created after the grant (not a season-run plan, not while the show is paused or archived). Every other caller still needs it.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StoryPlanApprovalRequest"}}}},"responses":{"200":{"description":"Approved plan.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StoryPlanEnvelopeResponse"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/StoryPlanStateInvalid"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/story-plans/{planId}/reject":{"parameters":[{"$ref":"#/components/parameters/StoryPlanId"}],"post":{"operationId":"rejectStoryPlan","tags":["Plans"],"summary":"Reject a generated StoryPlan.","description":"Transitions the plan to `REJECTED`. Valid only from `REQUIRES_APPROVAL` or `READY`. Pass an optional `reason` in the body. Requires `Idempotency-Key`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"reason":{"type":"string","maxLength":2000}}}}}},"responses":{"200":{"description":"Rejected plan.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StoryPlanEnvelopeResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/StoryPlanStateInvalid"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/story-jobs":{"get":{"operationId":"listStoryJobs","tags":["Jobs"],"summary":"List story jobs owned by the authenticated account.","description":"Cursor-paginated list of non-deleted story jobs, newest first. Filter with optional `projectId` and `storyPlanId` query params.","security":[{"BearerApiKey":["story:read"]}],"parameters":[{"name":"projectId","in":"query","required":false,"schema":{"type":"string"},"description":"Restrict to jobs in this project."},{"name":"storyPlanId","in":"query","required":false,"schema":{"type":"string"},"description":"Restrict to jobs for this plan."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100},"description":"Max items per page (default 25, max 100)."},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"},"description":"Opaque cursor from a previous response."}],"responses":{"200":{"description":"Paginated job list.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StoryJobList"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"createStoryJob","tags":["Jobs"],"summary":"Create an artifact-generation job against an APPROVED StoryPlan.","description":"Requires the referenced plan to be `APPROVED`. Non-blocking warning gates: if this generates screenplay pages, confirm the full Series Bible was completed/reviewed first; the screenplay adapter auto-loads the saved bible as canonical project truth. After the table-read artifact is READY, read/poll the initial coverage report before finalization, pitch packaging, or later-installment work. Table-read jobs queue that first coverage pass when pages are created. For table_read, the plan must include a reviewed `scriptBlueprint` (unless the request adopts a finalized screenplay with `scriptUploadId`) and the job may contain only one table_read artifact request. A table read PERFORMS the project's cast and never makes one: from a plan it starts when the project's cast canon, plus the request's `voiceMap`, covers every canon character — a voice for everyone and, unless the project is audio-only, a headshot, body shot and sheet — and a narrated read also covers the narrator; `GET /story-projects/{projectId}` reports this as `project.tableReadReadiness`. With `scriptUploadId` it performs that finalized screenplay as written, which needs its own episode cast complete. Once the job has written its screenplay, a speaker the cast does not cover fails the job (`failureCode: cast_precondition_failed`) before any audio and releases its credits. Every refusal runs BEFORE the job row is created — a plan not `APPROVED`, a project that has not reached the stage the artifacts need (409 `project_precondition_failed`, `details.stage`), a table read whose cast is incomplete (409 `cast_precondition_failed`, `details.reason` and `details.missing`), an adopted screenplay outside the project (404 `script_not_found`), a balance that cannot cover the quote (402 `insufficient_credits`) — so a refused call leaves nothing behind. Then it persists the job, reserves Studio Credits for the artifact line items, and enqueues the async runner. Requires `Idempotency-Key`, and the job is BOUND to it: retrying a key whose attempt failed resumes that same job (a 402 names it in `details.jobId`), it never creates a second one. `artifactRequests` defaults to the plan's own requests when omitted; table-read overrides may include `narrationPolicy` (`auto | include | suppress`) and a `voiceMap` using saved IDs from GET /voices for voices the cast canon does not carry. A `trailer` request: A trailer job always stops at its plan: it plans the beats onto a `draft` trailer (the job's `progress.trailerJobId`) and renders and reserves nothing, so planning is free. The next step is approve_trailer_plan (POST /trailers/{jobId}/plan/approve; CLI: trailer approve-plan <jobId>), priced first: its quote (quote_approve_trailer_plan, or the route without `confirmed`) returns the 480p render quote and reserves nothing; with `confirmed: true` it reserves exactly the quoted calls and renders them. The job completes READY with `progress.stage: 'plan_ready'`, `progress.trailerJobId` and `progress.nextStep: 'approve_trailer_plan'`, and its quote prices the planning only (0 credits). A `pitch_deck` request: A pitch deck job always stops at its plan: it plans the chapters onto a `draft` deck (the job's `progress.pitchDeckJobId`) and renders and reserves nothing, so planning is free. The next step is approve_pitch_deck_plan (POST /pitch-decks/{jobId}/plan/approve; CLI: pitch-deck approve-plan <jobId>), priced first: its quote (quote_approve_pitch_deck_plan, or the route without `confirmed`) returns the 480p render quote and reserves nothing; with `confirmed: true` it reserves exactly the quoted calls and renders them. The job completes READY with `progress.stage: 'plan_ready'`, `progress.pitchDeckJobId` and `progress.nextStep: 'approve_pitch_deck_plan'`, and its quote prices the planning only (0 credits).","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StoryJobCreate"}}}},"responses":{"200":{"description":"Created job envelope.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StoryJobEnvelope"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/StoryJobInsufficientCredits"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/StoryJobCreateConflict"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/story-jobs/{jobId}":{"parameters":[{"$ref":"#/components/parameters/StoryJobId"}],"get":{"operationId":"getStoryJob","tags":["Jobs"],"summary":"Read a story job by id, including its artifacts.","description":"Poll this to follow status + progress. Terminal states: `READY`, `PARTIAL`, `FAILED`, `CANCELED`. A `RESERVED` job with `progress.stage: provider_backoff` is nonterminal: Sleeper Hit has retained its credits/checkpoint and scheduled the retry, so clients should keep polling rather than implement provider backoff. A `RUNNING` job with `runStale: true` lost its worker mid-run: POST /story-jobs/{jobId}/resume sends it on from its last checkpoint at no new cost (Sleeper Hit also does when its workers restart). Each artifact carries its manifest URLs once `status: READY`.","security":[{"BearerApiKey":["story:read"]}],"responses":{"200":{"description":"Job envelope.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StoryJobEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/StoryJobNotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/story-jobs/{jobId}/cancel":{"parameters":[{"$ref":"#/components/parameters/StoryJobId"}],"post":{"operationId":"cancelStoryJob","tags":["Jobs"],"summary":"Cancel a story job and release its reserved credits.","description":"Valid only while the job is `PENDING` or `RESERVED`, or `FAILED` before it ever started (a refused reservation or a lost enqueue) — canceling that one means it can no longer be resumed. A `FAILED` job that had started generating is refused. Releases any reserved (not-yet-settled) credit line items. Requires `Idempotency-Key`; canceling an already-canceled job is a no-op.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"200":{"description":"Canceled job envelope.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StoryJobEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/StoryJobNotFound"},"409":{"$ref":"#/components/responses/StoryJobStateInvalid"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/story-jobs/{jobId}/resume":{"parameters":[{"$ref":"#/components/parameters/StoryJobId"}],"post":{"operationId":"resumeStoryJob","tags":["Jobs"],"summary":"Resume a failed job from its last durable boundary.","description":"State-aware and replay-safe. Active jobs—including `RESERVED` jobs in centralized provider backoff—are returned unchanged, except a `RUNNING` table read with `runStale: true`, whose run is sent again from its last checkpoint at no new cost (`generation_requeued`). Terminal legacy/pre-worker provider failures may reserve their original quote and re-enter generation under the same job id. READY/PARTIAL jobs with a failed table-read finalize retry that existing artifact without regenerating it. Other started generation failures are not blindly restarted because they may own durable intermediate resources; canceled jobs cannot be resumed. A resume that would reserve or re-enter generation asks the project's gate again first: when it has closed since the job was created, the call is refused with 409 `project_precondition_failed` and nothing is reserved. A running job asks it again before each paid step too, and a gate that closed mid-run fails the job with `failureCode: project_precondition_failed`, releasing its credits. Requires `Idempotency-Key`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"200":{"description":"Job recovery result.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StoryJobResumeResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/InsufficientCredits"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/StoryJobNotFound"},"409":{"$ref":"#/components/responses/StoryJobStateInvalid"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/story-jobs/{jobId}/artifacts":{"parameters":[{"$ref":"#/components/parameters/StoryJobId"}],"get":{"operationId":"listStoryJobArtifacts","tags":["Artifacts"],"summary":"List the artifacts produced by a story job.","security":[{"BearerApiKey":["artifact:read"]}],"responses":{"200":{"description":"Artifact list.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StoryArtifactListResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/StoryJobNotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/artifacts/{artifactId}":{"parameters":[{"$ref":"#/components/parameters/StoryArtifactId"}],"get":{"operationId":"getStoryArtifact","tags":["Artifacts"],"summary":"Read a single artifact and its manifest.","description":"For a `table_read`, the manifest carries the share token, theater URL, on-demand live-audio start URL, refine versioning, and finalize state. Pass `?revision=N` to fetch a prior revision (v1 = initial generation; each refine increments). `currentVersion` and `refineCount` are always the current values regardless of the revision queried. Poll this endpoint for `manifest.audio.finalize.status` (audio finalize) and `manifest.video.status` (video finalize/render).","security":[{"BearerApiKey":["artifact:read"]}],"parameters":[{"name":"revision","in":"query","required":false,"schema":{"type":"integer","minimum":1},"description":"Revision number to fetch. Omit for the current (latest) revision. v1 is the initial generation; each POST /artifacts/{id}/refine increments the version."}],"responses":{"200":{"description":"Artifact envelope.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StoryArtifactEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/ArtifactNotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"patch":{"operationId":"renameStoryArtifact","tags":["Artifacts"],"summary":"Rename a result: a table read, or a finished pitch deck or trailer (free).","description":"Renames a result. A table read is called by the script it performs, so its new name is that script's title (the episode's, where it is one), as its share page and the Writers Lab show it; the pages are never touched. A finished pitch deck or trailer is renamed as its deck or trailer is. Free and reversible (rename it again). Any other kind of result is refused (400 `validation_failed`). Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ArtifactRenameRequest"}}}},"responses":{"200":{"description":"The renamed result.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RenamedArtifact"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/ArtifactNotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/artifacts/{artifactId}/script":{"parameters":[{"name":"artifactId","in":"path","required":true,"schema":{"type":"string"},"description":"A table-read artifact's id, or a writer's own episode however it was made: its script id, the id in its Writers Lab URL, or a version's scriptUploadId (a script id reads its latest version). The answer's `artifactId` echoes it."}],"get":{"operationId":"getArtifactScript","tags":["Artifacts"],"summary":"Read actual script content for a table read artifact.","description":"Returns the parsed backing entries used by theater playback, music, SFX, and coverage. Use `scope=page`, `scope=scene`, `scope=character`, or `scope=range` for focused reads. Terminal agents should call this before answering questions about what is on a page, which scene is strongest, or what a character says. Scope: `artifact:read`.","security":[{"BearerApiKey":["artifact:read"]}],"parameters":[{"name":"scope","in":"query","required":false,"schema":{"type":"string","enum":["all","page","scene","character","range"]},"description":"Selection scope. Defaults to `all`."},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1},"description":"Required when `scope=page`."},{"name":"sceneIndex","in":"query","required":false,"schema":{"type":"integer","minimum":0},"description":"Required when `scope=scene`. Scene indexes are zero-based and listed in the response `scenes` array."},{"name":"character","in":"query","required":false,"schema":{"type":"string"},"description":"Required when `scope=character`. Use names from the response `characters` array."},{"name":"startEntry","in":"query","required":false,"schema":{"type":"integer","minimum":0},"description":"Required when `scope=range`."},{"name":"endEntry","in":"query","required":false,"schema":{"type":"integer","minimum":0},"description":"Required when `scope=range`."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":500},"description":"Maximum selected entries returned. Default 250, max 500."}],"responses":{"200":{"description":"Script content envelope.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ArtifactScriptContentEnvelope"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/ArtifactNotFound"},"409":{"$ref":"#/components/responses/ArtifactNotReady"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"replaceArtifactScript","tags":["Artifacts"],"summary":"Replace a table-read screenplay exactly.","description":"Creates a new revision from the supplied screenplay without an AI rewrite. The normal revision worker still parses the script, preserves existing voices where character names match, scores music and SFX, refreshes the stable share, and invalidates prior durable media. Set deferMusic=true when an operator will attach exact soundtrack clips through the music surface before finalizing; SFX generation still runs. Set deferAudioRender=true when the operator will run their own post-production and call POST /artifacts/{artifactId}/finalize later — without it the whole read is synthesised twice, once for a mix they discard. Season-run artifacts must use POST /season-runs/{runId}/advance with action=revise and screenplay so the durable episode state stays synchronized. Returns a StoryJob to poll. Requires Idempotency-Key.","security":[{"BearerApiKey":["artifact:publish"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ArtifactScriptReplacementRequest"}}}},"responses":{"200":{"description":"Exact screenplay replacement job.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StoryJobEnvelope"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/InsufficientCredits"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/ArtifactNotFound"},"409":{"$ref":"#/components/responses/ArtifactNotReady"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/artifacts/{artifactId}/render-video":{"parameters":[{"$ref":"#/components/parameters/StoryArtifactId"}],"get":{"operationId":"getArtifactRenderStatus","tags":["Artifacts"],"summary":"Read the durable render status + shareable link for an artifact.","description":"Returns the current theater (MP4) / audio (MP3) render status and the shareable link to watch or listen. Poll after POST /artifacts/{artifactId}/render-video until status is `complete`. A render that stopped with no job behind it reads `failed` (or `stale` over an earlier render) with its error saying so; POST render-video again to queue it. Scope: `artifact:read`.","security":[{"BearerApiKey":["artifact:read"]}],"responses":{"200":{"description":"Current render status + shareable link.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ArtifactRenderStatus"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/ArtifactNotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"renderArtifactVideo","tags":["Artifacts"],"summary":"Trigger the opt-in MP4 render for a table read artifact.","description":"Queues the Remotion MP4 export for a `table_read` artifact. This output is never produced by default; calling it incurs a separate Studio Credit charge. Returns `artifact_not_ready` if the underlying share link does not exist yet. Requires `Idempotency-Key`. The same render is also reachable via POST /artifacts/{id}/finalize with `{ mode: \"video\" }`. Scope: `artifact:publish` (billable + mutating).","security":[{"BearerApiKey":["artifact:publish"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"200":{"description":"Render acknowledgement.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ArtifactRenderAck"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/ArtifactNotFound"},"409":{"$ref":"#/components/responses/ArtifactNotReady"},"422":{"$ref":"#/components/responses/ArtifactGenerationFailed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/artifacts/{artifactId}/publish-state":{"parameters":[{"$ref":"#/components/parameters/StoryArtifactId"}],"get":{"operationId":"getArtifactPublishState","tags":["Artifacts"],"summary":"Read a render's publishable state.","description":"Returns whether the table-read render has been sent to Publishing (`publishable`), the durable full-mix render's status (`renderStatus`, `recordingReady`), and whether a live/published RSS release backs it (`inFeed`). An artifact that is not yours, or does not exist, is a 404 `artifact_not_found`. Costs nothing. Scope: `artifact:read`.","security":[{"BearerApiKey":["artifact:read"]}],"responses":{"200":{"description":"The render's publish/render state.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ArtifactPublishState"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/ArtifactNotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"setArtifactPublishState","tags":["Artifacts"],"summary":"Send a render to Publishing, or remove it.","description":"Marks the table-read render publishable (`action: \"mark\"`) so it surfaces in Publishing and becomes eligible for an RSS/podcast feed — ensuring the durable full-mix render exists first, free and replay-guarded — or removes it (`action: \"unmark\"`). FREE and naturally idempotent: no `Idempotency-Key`. An `unmark` a live/published RSS release backs comes back `{ requiresConfirm: true, inFeed: true }` until sent again with `confirm: true`. An artifact that is not yours, or does not exist, is a 404 `artifact_not_found`; a `mark` whose durable render cannot start refuses before the flag is set. Scope: `artifact:publish`.","security":[{"BearerApiKey":["artifact:publish"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ArtifactPublishStateRequest"}}}},"responses":{"200":{"description":"The render's publish state after the change.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ArtifactPublishStateResult"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/ArtifactNotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/artifacts/{artifactId}/theater-motion":{"parameters":[{"$ref":"#/components/parameters/StoryArtifactId"}],"post":{"operationId":"directTheaterMotionBeat","tags":["Artifacts"],"summary":"Add or refine a motion-graphics beat in a theater scene.","description":"Authors or refines a single MOTION BEAT (kinetic on-screen text, a lower-third, or a title card) anchored to one spoken line and baked into the durable theater video. Returns `shareUrl` — a watchable link to the enhanced theater video. Requires `Idempotency-Key`. Scope: `artifact:publish`.","security":[{"BearerApiKey":["artifact:publish"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TheaterMotionBeatRequest"}}}},"responses":{"200":{"description":"The motion beat + a shareable theater video link.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TheaterMotionResult"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/ArtifactNotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/artifacts/{artifactId}/theater-motion-clip":{"parameters":[{"$ref":"#/components/parameters/StoryArtifactId"}],"post":{"operationId":"generateTheaterMotionClip","tags":["Artifacts"],"summary":"Generate the background clip for a motion-over-clip theater beat.","description":"Generates the BACKGROUND CLIP a \"motion-over-clip\" theater motion beat plays its text over (author the beat first with POST /artifacts/{artifactId}/theater-motion, compositionKind \"motion-over-clip\"). Async — poll GET /artifacts/{artifactId} for the clip status. Returns `shareUrl`. Requires `Idempotency-Key`. Scope: `artifact:publish`.","security":[{"BearerApiKey":["artifact:publish"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TheaterMotionClipRequest"}}}},"responses":{"200":{"description":"The queued clip + a shareable theater video link.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TheaterMotionResult"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/ArtifactNotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/artifacts/{artifactId}/theater-canvas":{"parameters":[{"$ref":"#/components/parameters/StoryArtifactId"}],"post":{"operationId":"applyTheaterCanvas","tags":["Artifacts"],"summary":"Commit a whole theater scene as a motion-graphics canvas.","description":"Designs a whole table-read theater scene as one immersive canvas: an ordered list (1–12) of full-canvas motion-graphic beats anchored to spoken lines and baked into the durable theater video. Returns `shareUrl`. Requires `Idempotency-Key`. Scope: `artifact:publish`.","security":[{"BearerApiKey":["artifact:publish"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TheaterCanvasRequest"}}}},"responses":{"200":{"description":"The committed canvas + a shareable theater video link.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TheaterMotionResult"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/ArtifactNotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/artifacts/{artifactId}/theater":{"parameters":[{"$ref":"#/components/parameters/StoryArtifactId"}],"get":{"operationId":"getArtifactTheater","tags":["Artifacts"],"summary":"Read the table read's theater presentation and every scene's theater.","description":"Returns the whole read's presentation (theater mode default, backgrounds, scene visuals, the active style set, the scene theme and transitions, the script pane, fonts, the title screen, fullscreen chrome, noun imagery), every style set `applyStyleSetId` takes, and each scene's theater (text reveal, look, transition, backdrop prompt and media, FX cues). Read-only. Scope: `artifact:read`.","security":[{"BearerApiKey":["artifact:read"]}],"responses":{"200":{"description":"The theater presentation.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TableReadTheaterResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/ArtifactNotFound"},"409":{"$ref":"#/components/responses/ArtifactNotReady"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"updateArtifactTheater","tags":["Artifacts"],"summary":"Change the table read's theater presentation.","description":"A partial update of the whole read's presentation: keys left out are untouched and theme objects merge field by field. `applyStyleSetId` applies a built-in or saved style set and marks it active (applied first, then any theme field beside it); `styleSets` replaces the saved custom sets; `nounImagery.displayMode` sets how spoken-noun images appear. The scene soundtrack's master switch is `soundtrackEnabled` on POST /artifacts/{artifactId}/music. The same save the Table Read page's theater settings make. Free: renders nothing. Requires `Idempotency-Key`. Scope: `artifact:publish`.","security":[{"BearerApiKey":["artifact:publish"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TableReadTheaterUpdateRequest"}}}},"responses":{"200":{"description":"The presentation as it now stands.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TableReadTheaterUpdateResponse"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/ArtifactNotFound"},"409":{"$ref":"#/components/responses/ArtifactNotReady"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/artifacts/{artifactId}/theater-scene":{"parameters":[{"$ref":"#/components/parameters/StoryArtifactId"}],"post":{"operationId":"updateArtifactTheaterScene","tags":["Artifacts"],"summary":"Change one scene's theater.","description":"Sets one scene's text reveal, look (`visualStyle`), transition, backdrop prompt and media, and atmospheric FX cues (a list replaces the scene's cues; `[]` clears them). Fields left out are kept. The same save as the Table Read page's per-scene controls and Slug's update_scene_theater. Free: renders nothing. Requires `Idempotency-Key`. Scope: `artifact:publish`.","security":[{"BearerApiKey":["artifact:publish"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TableReadTheaterSceneRequest"}}}},"responses":{"200":{"description":"The scene's theater as it now stands.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TableReadTheaterSceneResponse"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/ArtifactNotFound"},"409":{"$ref":"#/components/responses/ArtifactNotReady"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/artifacts/{artifactId}/theater-fx-cues":{"parameters":[{"$ref":"#/components/parameters/StoryArtifactId"}],"post":{"operationId":"generateArtifactTheaterFxCues","tags":["Artifacts"],"summary":"Write one scene's atmospheric FX cues from its lines.","description":"Writes a bounded set of atmospheric, non-text FX cues (light bursts, washes, weather, darkness) for one scene from its own lines and mood, replacing the cues it had. Synchronous. While the project's gate is closed it is refused with 409 `project_precondition_failed` before anything is written. Requires `Idempotency-Key`. Scope: `artifact:publish`.","security":[{"BearerApiKey":["artifact:publish"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TableReadFxCuesRequest"}}}},"responses":{"200":{"description":"The scene's new FX cues.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TableReadFxCuesResponse"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/ArtifactNotFound"},"409":{"$ref":"#/components/responses/ArtifactNotReady"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/artifacts/{artifactId}/theater-backdrop-prompts":{"parameters":[{"$ref":"#/components/parameters/StoryArtifactId"}],"post":{"operationId":"writeArtifactTheaterBackdropPrompts","tags":["Artifacts"],"summary":"Write backdrop prompts for scenes that have none.","description":"Writes a backdrop prompt (the setting only: no characters, no on-screen text) for every scene that has none, or for `sceneIndexes`; `force` writes over a prompt a scene has. Each comes from the scene's heading, lines and mood in the show's look. Synchronous. While the project's gate is closed it is refused with 409 `project_precondition_failed` before anything is written. Requires `Idempotency-Key`. Scope: `artifact:publish`.","security":[{"BearerApiKey":["artifact:publish"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TableReadBackdropPromptsRequest"}}}},"responses":{"200":{"description":"The prompts written and the scenes skipped.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TableReadBackdropPromptsResponse"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/ArtifactNotFound"},"409":{"$ref":"#/components/responses/ArtifactNotReady"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/artifacts/{artifactId}/noun-imagery":{"parameters":[{"$ref":"#/components/parameters/StoryArtifactId"}],"get":{"operationId":"getArtifactNounImagery","tags":["Artifacts"],"summary":"Read the noun-imagery library.","description":"Returns how spoken-noun images appear in the live read (`displayMode`: insets, replace_backdrop or off), how many lines name a depictable noun, and each noun's image and status (queued, ready, failed). Read-only. Scope: `artifact:read`.","security":[{"BearerApiKey":["artifact:read"]}],"responses":{"200":{"description":"The noun-imagery library.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TableReadNounImageryResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/ArtifactNotFound"},"409":{"$ref":"#/components/responses/ArtifactNotReady"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"generateArtifactNounImagery","tags":["Artifacts"],"summary":"Build the noun-imagery library, or draw one noun again.","description":"With no body (or `{ force: true }` to draw every noun again), finds the depictable nouns on every spoken line and draws one image per noun, reused on every later mention. With `noun`, draws that one noun again. Queued: poll GET until each noun is `ready`. While the project's gate is closed it is refused with 409 `project_precondition_failed` before the job is queued. Requires `Idempotency-Key`. Scope: `artifact:publish`.","security":[{"BearerApiKey":["artifact:publish"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TableReadNounImageryRequest"}}}},"responses":{"200":{"description":"The queued job.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TableReadNounImageryQueued"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/ArtifactNotFound"},"409":{"$ref":"#/components/responses/ArtifactNotReady"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/artifacts/{artifactId}/soundscapes":{"parameters":[{"$ref":"#/components/parameters/StoryArtifactId"}],"get":{"operationId":"listArtifactSoundscapes","tags":["Artifacts"],"summary":"List the read's soundscape beds and custom regions.","description":"Returns every scene's soundscape bed (over the lines it spans, `kind: \"scene_bed\"`, id = the scene index) and every custom region (`kind: \"region\"`), in script order, with each one's sound, level, fades and play mode. Read-only. Scope: `artifact:read`.","security":[{"BearerApiKey":["artifact:read"]}],"responses":{"200":{"description":"The soundscapes.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TableReadSoundscapesResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/ArtifactNotFound"},"409":{"$ref":"#/components/responses/ArtifactNotReady"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"manageArtifactSoundscapes","tags":["Artifacts"],"summary":"Edit a soundscape bed or region.","description":"One edit by `op`: `update_bed` tunes a scene's bed (label, start/end offsets in lines, level, fades, loop or stretch); `add_region` adds a custom region over a range of lines (optionally with library audio, `assetId`); `update_region` moves or re-levels one; `attach` gives a bed or region library audio (`assetId`, from GET /library/audio); `remove` takes a bed's sound off or deletes a region. A region may not overlap another soundscape. Free; a finalized MP3/MP4 is marked stale. Requires `Idempotency-Key`. Scope: `artifact:publish`.","security":[{"BearerApiKey":["artifact:publish"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TableReadSoundscapeRequest"}}}},"responses":{"200":{"description":"The edit and the bed or region as it now stands.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TableReadSoundscapeResponse"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/ArtifactNotFound"},"409":{"$ref":"#/components/responses/ArtifactNotReady"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/artifacts/{artifactId}/avatar-video":{"parameters":[{"$ref":"#/components/parameters/StoryArtifactId"}],"post":{"operationId":"animateArtifactAvatar","tags":["Artifacts"],"summary":"Animate one character's portrait into a talking loop.","description":"Makes a short animated talking portrait from the character's portrait (never the narrator). Queued: poll GET /artifacts/{artifactId}/cast until the character's `avatarVideoStatus` is `ready` and `avatarVideoUrl` is set. While the project's gate is closed it is refused with 409 `project_precondition_failed` before the job is queued. Requires `Idempotency-Key`. Scope: `artifact:publish`.","security":[{"BearerApiKey":["artifact:publish"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TableReadAvatarVideoRequest"}}}},"responses":{"200":{"description":"The queued animation.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TableReadAvatarVideoResponse"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/ArtifactNotFound"},"409":{"$ref":"#/components/responses/ArtifactNotReady"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/artifacts/{artifactId}/refine":{"parameters":[{"$ref":"#/components/parameters/StoryArtifactId"}],"post":{"operationId":"refineArtifact","tags":["Artifacts"],"summary":"Conversationally revise a table read artifact in place.","description":"Submits a free-form instruction that rewrites the screenplay and/or re-grounds the StoryPlan, producing a NEW REVISION of the SAME artifact. The artifact id, share token, and all three player URLs (theaterUrl / theaterFullscreenUrl / audio.liveUrl) remain STABLE — no re-embed, no new links.  `scope` controls the rewrite strategy: - `\"auto\"` (default) — the model self-routes; most instructions hit `screenplay`. - `\"screenplay\"` — rewrites the screenplay and recasts voices in place; faster. - `\"plan\"` — re-grounds the StoryPlan against sources then re-adapts; use for major structural changes.  TWO CALLS: a price and a spend are two tools. Sent without `confirmed` it returns the QUOTE and reserves and enqueues NOTHING — `{ quotedOnly: true, credits, lines, balanceAfter, sufficient, note }`; the same request with `confirmed: true` reserves exactly the quoted amount and returns `{ storyJob }`. Sent confirmed, poll GET /story-jobs/{jobId} until terminal (READY / FAILED), exactly like a standard artifact generation job. Once complete, GET /artifacts/{id} returns the updated `currentVersion` and `refineCount`. Fetch a prior revision with GET /artifacts/{id}?revision=N (v1 = initial generation).  Reserves TABLE_READ_REFINE (8 Studio Credits) only when `confirmed: true`. Every precondition is refused BEFORE the quote. Requires `Idempotency-Key`.","security":[{"BearerApiKey":["artifact:publish"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RefineArtifactRequest"}}}},"responses":{"200":{"description":"Without `confirmed`: the price (`quotedOnly: true`), nothing reserved or enqueued. With `confirmed: true`: the refine job to poll until terminal.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RefineArtifactEnvelope"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/InsufficientCredits"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/ArtifactNotFound"},"409":{"$ref":"#/components/responses/ArtifactNotReady"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/artifacts/{artifactId}/voice":{"parameters":[{"$ref":"#/components/parameters/StoryArtifactId"}],"post":{"operationId":"recastArtifactVoice","tags":["Artifacts"],"summary":"Recast a single character's voice on a table read artifact.","description":"Reassigns one character's voice on the table read backing this artifact, in place. The artifact id, share token, and all player URLs remain STABLE — no new revision, no charge.  Body: `{ character, voiceId, voiceName, gender?, provider?, pinToCanon? }`. `provider` is the TTS provider for the voice (e.g. `cartesia`, `openai`); omit to infer it (your library voice's own, else the read's, else the configured table-read provider). A voice whose provider retired it is refused (409 `voice_retired`). THE CAST CANON FOLLOWS: renders (decks, trailers, showcases) prefer the canon voice, so the recast also pins it on the canon person the character names (by name or alias) unless `pinToCanon: false`. `canon` names the person pinned. NOTHING IS LOST: when the voice actually changes on a read that has a recording, the previous voice, the voices-only track and the finished mix are kept before the recording is invalidated, and POST /artifacts/{artifactId}/voice/restore puts them back (`restore` describes what it would restore). A recast to the voice the character already has changes nothing (`changed: false`). To keep every other line and re-voice only this character in a finished read, use POST /artifacts/{artifactId}/revoice instead. Returns the updated `voiceMap` (character → voice). The current cast is also readable on `manifest.audio.voiceMap` via GET /artifacts/{id}.  Requires `Idempotency-Key`. Scope: `artifact:publish`.","security":[{"BearerApiKey":["artifact:publish"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["character","voiceId","voiceName"],"additionalProperties":false,"properties":{"character":{"type":"string","minLength":1,"description":"Character name as it appears in the cast (e.g. \"NARRATOR\")."},"voiceId":{"type":"string","minLength":1,"description":"Provider voice id to assign."},"voiceName":{"type":"string","minLength":1,"description":"Human-readable voice name (shown in the cast)."},"gender":{"type":"string","description":"Optional voice gender hint. Preserves the existing value when omitted."},"provider":{"type":"string","enum":["cartesia","elevenlabs","openai","deepgram","hume"],"description":"Optional TTS provider for this voice. Inferred when omitted (your library voice's own, else the read's, else the configured table-read provider)."},"pinToCanon":{"type":"boolean","description":"Also pin this voice on the cast canon person (default true), so decks, trailers and showcases speak with it too. false changes this read only."}}}}}},"responses":{"200":{"description":"Voice recast. Returns the updated cast as `voiceMap`.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"type":"object","required":["artifactId","character","voiceMap","changed","canon","restore"],"additionalProperties":false,"properties":{"artifactId":{"type":"string"},"character":{"type":"string"},"voiceMap":{"type":"object","additionalProperties":true,"description":"Full updated cast: character → { voiceId, voiceName, gender, provider }."},"changed":{"type":"boolean","description":"false when the character already had this voice: nothing was invalidated or remembered."},"canon":{"oneOf":[{"type":"null"},{"type":"object","required":["projectId","person"],"properties":{"projectId":{"type":"string"},"person":{"type":"string"}}}],"description":"The cast canon person now pinned to this voice; null when the canon was not touched (`pinToCanon: false`, or a script in no project)."},"restore":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/RestorableVoice"}],"description":"What POST .../voice/restore would put back for this character."}}}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/ArtifactNotFound"},"409":{"$ref":"#/components/responses/ArtifactNotReady"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/artifacts/{artifactId}/voice/restore":{"parameters":[{"$ref":"#/components/parameters/StoryArtifactId"}],"post":{"operationId":"restoreArtifactVoice","tags":["Artifacts"],"summary":"Restore a character's previous voice on a table read artifact.","description":"Undoes the newest recast of `character` (or of any character when omitted): the previous voice comes back with the recording the read had before the recast — its voices-only track, and the finished mix and video while they still match — and the cast canon voice when the canon still holds the one the recast pinned. Free. Restoring again returns to the voice this restore replaced. GET /artifacts/{artifactId}/cast lists what is restorable (`restorable`). 409 `cast_voice_precondition_failed` when the character has not been recast on this read.  Requires `Idempotency-Key`. Scope: `artifact:publish`.","security":[{"BearerApiKey":["artifact:publish"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VoiceRestoreRequest"}}}},"responses":{"200":{"description":"The previous voice and what came back with it.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VoiceRestoreResponse"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/ArtifactNotFound"},"409":{"$ref":"#/components/responses/CastVoicePreconditionFailed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/artifacts/{artifactId}/revoice":{"parameters":[{"$ref":"#/components/parameters/StoryArtifactId"}],"post":{"operationId":"revoiceArtifactCharacter","tags":["Artifacts"],"summary":"Re-voice one character in a finished table read (metered — TWO CALLS, price first).","description":"Speaks only `character`'s lines again in a new voice and keeps every other line byte for byte from the recording the read already has, then mixes the read again with its music and sound (`TABLE_READ_REVOICE`, priced on GET /credits). The voice is `voiceId` (with `voiceName`/`provider`), or the cast canon voice for the person when omitted (after POST /story-projects/{projectId}/cast-voice/choose). The recast inside it pins the canon too and keeps the previous voice and recording restorable (POST .../voice/restore). TWO CALLS: sent without `confirmed` it PRICES the re-voice and changes nothing (`quotedOnly: true`, `credits`, `lines` and `chars` it speaks again, `keptLines`, `balanceAfter`, `sufficient`, and a `note` to show the writer; when `sufficient` is false the caller must not confirm); sent again with `confirmed: true` it reserves, recasts and queues the finalize. Poll GET /artifacts/{artifactId} until `manifest.audio.finalize.status` is `complete`; the credit settles only then and is released if it fails. Refused before the price with 409 `cast_voice_precondition_failed`: a read with no finished recording (recast and finalize instead), a character with no lines in it, a voice it already speaks with, a finalize already running, or a recording whose lines overlap (it cannot be cut line by line). Refused with 409 `voice_retired` when the new voice, or the voice of any line spoken again, is one its provider has retired; lines kept from the recording never count, so a cast on a retired provider can be moved off it one character at a time.  Requires `Idempotency-Key`. Scope: `artifact:publish`.","security":[{"BearerApiKey":["artifact:publish"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RevoiceRequest"}}}},"responses":{"200":{"description":"The price (no `confirmed`) or the queued re-voice.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RevoiceEnvelope"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/InsufficientCredits"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/ArtifactNotFound"},"409":{"$ref":"#/components/responses/CastVoicePreconditionFailed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/artifacts/{artifactId}/revoice-line":{"parameters":[{"$ref":"#/components/parameters/StoryArtifactId"}],"post":{"operationId":"revoiceArtifactLine","tags":["Artifacts"],"summary":"Re-voice one line of a table read at a new speed or with a new acting direction (free; TWO CALLS, quote first).","description":"Speaks line `entryIndex` again from its voice's provider with a new `speed`, a new `actingDirection`, or both (the line is spoken once). The speed is sent as that provider's OWN speed parameter (Cartesia `generation_config.speed` 0.6–1.5, ElevenLabs `voice_settings.speed` 0.7–1.2, Deepgram `speed` 0.7–1.5) — never a time-stretch — and brought inside the provider's range; the answer says the range (`range`) and whether it did (`clamped`). The direction is free text in the writer's words, sent through the provider's OWN expressive control: OpenAI `instructions`, and ElevenLabs Eleven v3 and later as a leading audio tag (`direction` says which); an empty string or null takes it off. Every other line keeps its recording byte for byte, then the read is mixed again with its music and sound. FREE: a line's speed and direction cost no credits — nothing is held, charged or booked, and `credits` is always 0. Three outcomes (`mode`), all quoted first: `revoice` — the read has a finished recording, and the line is spoken again; `restore_take` — the line had a take with this speed and direction before (the take it has is kept whenever it is re-voiced), and it is brought back; `save` — the read has no recording yet: they are saved and the line is spoken that way when the read is recorded. TWO CALLS: sent without `confirmed` it QUOTES and changes nothing (`quotedOnly: true`, `credits` 0, `mode`, `changes`, `speed`, `previousSpeed`, `range`, `actingDirection`, `previousActingDirection`, `keptLines`, `balanceAfter`, `sufficient`, and a `note` to show the writer); sent again with `confirmed: true` it does it. Poll GET /artifacts/{artifactId} until `manifest.audio.finalize.status` is `complete`. Refused before the quote with 409 `cast_voice_precondition_failed` (`details.reason`): `provider_has_no_speed` (an OpenAI or Hume voice, or a model or voice with no speed setting — Eleven v3, Cartesia before sonic-3, a Deepgram Aura-1 voice or a Cartesia professional clone — named in `details.model` and `details.noSpeed`: recast the character first), `provider_has_no_direction` (a Deepgram, Hume, or OpenAI tts-1 voice, ElevenLabs before Eleven v3, or a Cartesia model before sonic-3 — none reads a written direction, and Cartesia before sonic-3 has no delivery controls at all — `details.model`, `details.noDirection`), `same_speed`, `same_direction`, `nothing_spoken`, `no_voice`, `modified_take` (the line plays a voice effect or a performance take), `finalize_running`, `not_spliceable` / `not_in_recording` (a recording that cannot be cut line by line), `line_changed` (the line was edited since it was priced). 409 `voice_retired` when the line's voice is one its provider retired.  Requires `Idempotency-Key`. Scope: `artifact:publish`.","security":[{"BearerApiKey":["artifact:publish"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RevoiceLineRequest"}}}},"responses":{"200":{"description":"The quote, 0 credits (no `confirmed`), or the queued re-voice / the saved speed and direction.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RevoiceLineEnvelope"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/ArtifactNotFound"},"409":{"$ref":"#/components/responses/CastVoicePreconditionFailed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/artifacts/{artifactId}/update-recording":{"parameters":[{"$ref":"#/components/parameters/StoryArtifactId"}],"post":{"operationId":"updateArtifactRecording","tags":["Artifacts"],"summary":"Update a table read's recording with the lines changed since it was made (free; TWO CALLS, quote first).","description":"Brings the read's finished recording up to the read as it is now. The read knows what each line of its recording was spoken from — its voice, its words and its delivery (emotion, speed, volume, acting direction) — and the pause after it. A line whose words, voice or delivery changed since (a line edit, refine_performance) is spoken again by its voice's provider; every other line is kept byte for byte; a line whose only change is the pause after it is not spoken at all — the track is stitched again with the new pause; a line the read no longer speaks is stitched out. Then the read is mixed again with its music and sound, and the old recording keeps playing until the new one lands. FREE: 0 credits however many lines change — nothing is held, charged or booked, and `credits` is always 0. `mode` says what it would do: `respeak`, `restitch` (nothing spoken), `remix` (only the music or sound moved), `record` (no recording yet) or `current` (nothing to update). A recording with lines said over each other was mixed into one track, so no line can be cut from it (`spliceable: false`): every line is made again. TWO CALLS: sent without `confirmed` it QUOTES and changes nothing (`quotedOnly: true`, `credits` 0, `mode`, `changedLines`, `respokenLines`, `pauseChanges`, `removedLines`, `keptLines`, the changed `lines`, `balanceAfter`, `sufficient`, and a `note` to show the writer); sent again with `confirmed: true` it does it. Poll GET /artifacts/{artifactId} until `manifest.audio.finalize.status` is `complete`. Refused before the quote with 409 `cast_voice_precondition_failed` (`details.reason`): `finalize_running` (the recording is being made right now), `no_recording` (a read with no table-read artifact yet). 409 `voice_retired` when a line it must speak again is in a voice its provider retired.  Requires `Idempotency-Key`. Scope: `artifact:publish`.","security":[{"BearerApiKey":["artifact:publish"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateRecordingRequest"}}}},"responses":{"200":{"description":"The quote, 0 credits (no `confirmed`), or the queued update.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateRecordingEnvelope"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/ArtifactNotFound"},"409":{"$ref":"#/components/responses/CastVoicePreconditionFailed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/artifacts/{artifactId}/review-items":{"parameters":[{"$ref":"#/components/parameters/StoryArtifactId"}],"get":{"operationId":"listLineReviews","tags":["Artifacts"],"summary":"List the lines flagged for review on a table read.","description":"Returns the lines a writer flagged for review on one `table_read` artifact and the notes typed with them, plus the read's `status`. Each item names the line it marks (`entryIndex`), who speaks it (`character`, null for narration), its `lineText` and the writer's `note`. The same flags the Writers Lab's review panel, MCP `list_line_reviews` and `sleeperhit script review list` read. Read-only; costs no credits.","security":[{"BearerApiKey":["artifact:read"]}],"responses":{"200":{"description":"The read's flagged lines.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LineReviewList"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/ArtifactNotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"flagLineForReview","tags":["Artifacts"],"summary":"Flag one table-read line for review.","description":"Flags ONE line of a `table_read` artifact for the writer's review, with an optional `note`: pass the line's `entryIndex`, the `lineText` as it stands, and optionally `character` (null or omitted for narration) and `note`. One flag per line (keyed by `(jobId, entryIndex)`) — flagging a line again replaces its note rather than adding a second, so the same flag always lands and a retry is safe. An unknown field, a wrong-typed value or a missing `lineText` is refused with 400 `validation_failed` — the same validation MCP `flag_line_for_review` and `sleeperhit script review flag` go through. Answers the flag as it now stands. Free and naturally idempotent, so no `Idempotency-Key`: it renders nothing.","security":[{"BearerApiKey":["artifact:publish"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LineReviewFlag"}}}},"responses":{"200":{"description":"The flag as it now stands.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LineReviewFlagResult"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/ArtifactNotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/artifacts/{artifactId}/review-items/{entryIndex}":{"parameters":[{"$ref":"#/components/parameters/StoryArtifactId"},{"name":"entryIndex","in":"path","required":true,"schema":{"type":"integer","minimum":0},"description":"Which line: the entry's index in the read's parsed screenplay."}],"delete":{"operationId":"unflagLineForReview","tags":["Artifacts"],"summary":"Remove a table-read line's review flag.","description":"Removes the review flag on ONE line of a `table_read` artifact, named by its `entryIndex`. Answers `removed` (whether a flag was there) and `entryIndex`; removing a flag the line does not have is the same no-op state, so a retry is always safe. A non-numeric or negative `entryIndex` is refused with 400 `validation_failed`. The same removal MCP `unflag_line_for_review` and `sleeperhit script review unflag` make. Free and naturally idempotent, so no `Idempotency-Key`.","security":[{"BearerApiKey":["artifact:publish"]}],"responses":{"200":{"description":"Whether a flag was removed.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["artifactId","entryIndex","removed"],"properties":{"artifactId":{"type":"string"},"entryIndex":{"type":"integer","minimum":0},"removed":{"type":"boolean"}}}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/ArtifactNotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/artifacts/{artifactId}/avatar":{"parameters":[{"$ref":"#/components/parameters/StoryArtifactId"}],"post":{"operationId":"recastArtifactAvatar","tags":["Artifacts"],"summary":"Regenerate a single character's avatar portrait on a table read artifact.","description":"Re-renders ONE character's avatar portrait on the table read backing this `table_read` artifact, in place. The artifact id, share token, and all player URLs remain STABLE — no new revision, no charge, like a voice recast.  Body: `{ character, refinement?, style?, referenceImageUrl? }`. `refinement` is a freeform art-direction nudge; `style` persists an episode portrait override without changing project Series Bible canon; `referenceImageUrl` seeds the image model from an already-uploaded image. Async + queue-backed: enqueues a `regenerate_avatar` job and returns `{ character, status, chatToolJobId }`. Poll GET /artifacts/{artifactId}/cast and watch that character's `avatarStatus` (`queued` → `rendering` → `ready`).  Requires `Idempotency-Key`. Scope: `artifact:publish`.","security":[{"BearerApiKey":["artifact:publish"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AvatarRecastRequest"}}}},"responses":{"200":{"description":"Without `confirmed`: the price (3 credits), nothing drawn. With `confirmed: true`: avatar regeneration dispatched and charged. Poll GET /artifacts/{artifactId}/cast until the character's avatarStatus is ready.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/CastArtQuote"},{"$ref":"#/components/schemas/AvatarRecastResponse"}]}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/ArtifactNotFound"},"409":{"$ref":"#/components/responses/ArtifactNotReady"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/artifacts/{artifactId}/figure":{"parameters":[{"$ref":"#/components/parameters/StoryArtifactId"}],"post":{"operationId":"recastArtifactFigure","tags":["Artifacts"],"summary":"Regenerate a single character's full-body figure on a table read artifact.","description":"Re-renders ONE character's FULL-BODY FIGURE on the table read backing this `table_read` artifact, in place. The figure is the full-body cutout a render uses for that character; the avatar (POST /artifacts/{artifactId}/avatar) is the headshot the identity gate judges faces against. THEY MUST DEPICT THE SAME PERSON: when they drift apart, frames build from the figure and are judged against the avatar, so that character fails identity on every render while the rest of the cast passes.  Body: `{ character, refinement?, referenceImageUrl? }`. The figure is seeded from the character's own avatar unless `referenceImageUrl` overrides it. There is no `style` field — figure art direction follows the avatar. Async + queue-backed: enqueues a `generate_figure` job and returns `{ character, status, chatToolJobId }`.  Requires `Idempotency-Key`. Scope: `artifact:publish`.","security":[{"BearerApiKey":["artifact:publish"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FigureRecastRequest"}}}},"responses":{"200":{"description":"Without `confirmed`: the price (1 credit), nothing drawn. With `confirmed: true`: figure regeneration dispatched and charged.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/CastArtQuote"},{"$ref":"#/components/schemas/FigureRecastResponse"}]}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/ArtifactNotFound"},"409":{"$ref":"#/components/responses/ArtifactNotReady"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/artifacts/{artifactId}/cast":{"parameters":[{"$ref":"#/components/parameters/StoryArtifactId"}],"post":{"operationId":"updateArtifactCast","tags":["Artifacts"],"summary":"Batch-update the cast (voices + avatars) on a table read artifact.","description":"Batch-updates the cast on the table read backing this `table_read` artifact in one call, in place. Each `entries` item targets one character and may reassign its voice (`voiceId` / `voiceName` / `gender` / `provider`), request an avatar re-render (`regenerateAvatar: true`, with an optional `avatarRefinement` art-direction nudge), or both. A top-level `avatarStyle` restyles ALL portraits (re-renders every character against the new art-direction). Provide at least one `entries` item OR a top-level `avatarStyle` — a body with neither is rejected as `validation_failed`. Voice reassignments apply immediately. Avatar work is async + queue-backed: the response returns the updated `voiceMap` plus one avatar op `{ character, status, chatToolJobId }` per queued render — poll GET /artifacts/{artifactId}/cast until each character's `avatarStatus` reaches `ready`. No new revision — the artifact id and share URLs stay stable, like a voice recast (no charge).  Requires `Idempotency-Key`. Scope: `artifact:publish`.","security":[{"BearerApiKey":["artifact:publish"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CastBatchRequest"}}}},"responses":{"200":{"description":"A batch that draws portraits, sent without `confirmed`: the price of the whole batch, nothing applied. Otherwise: cast updated, returning the merged voiceMap plus the in-flight avatar ops to poll and the credits charged.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/CastArtQuote"},{"$ref":"#/components/schemas/CastBatchResponse"}]}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/ArtifactNotFound"},"409":{"$ref":"#/components/responses/ArtifactNotReady"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"get":{"operationId":"getArtifactCast","tags":["Artifacts"],"summary":"Read the enriched cast for a table read artifact.","description":"Returns the enriched cast for the read backing this artifact: each character's assigned voice (`voiceId` / `voiceName` / `gender` / `provider`) merged with its avatar state (`avatarUrl` / `avatarStyle` / `avatarStatus`), plus the artifact's resolved top-level `avatarStyle`. `avatarStatus` is `none` (no avatar), `queued` / `rendering` (a render is in flight), `ready`, or `failed` — poll this endpoint after recast_avatar / update_cast until in-flight renders reach `ready`. The cast is assigned automatically when the table read becomes performable; an empty `cast` means it is not performable yet (poll GET /artifacts/{artifactId}). `identity` warns when one character reaches two canon people or two people share a body figure, lists characters with no Series Bible appearance, and lists Series Bible names no canon person answers to (`unresolvedBibleNames`, with candidates). It is read-only; fix it by declaring aliases on the cast canon. `restorable` lists each character recast on this read whose previous voice (and recording, when `recordingRecoverable`) POST /artifacts/{artifactId}/voice/restore can put back.  Scope: `artifact:read`.","security":[{"BearerApiKey":["artifact:read"]}],"responses":{"200":{"description":"Enriched cast envelope.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CastEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/ArtifactNotFound"},"409":{"$ref":"#/components/responses/ArtifactNotReady"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/artifacts/{artifactId}/coverage":{"parameters":[{"name":"artifactId","in":"path","required":true,"schema":{"type":"string"},"description":"A table-read artifact's id, or a writer's own episode however it was made: its script id, the id in its Writers Lab URL, or a version's scriptUploadId (a script id reads its latest version). The answer's `artifactId` echoes it."}],"post":{"operationId":"generateArtifactCoverage","tags":["Artifacts"],"summary":"Generate professional script coverage for a table read artifact.","description":"Recommended first development round once a screenplay-backed table-read artifact exists; use the report for a revision pass before finalization, pitch packaging, or later-installment work.  Runs a queued, async coverage analysis of the screenplay backing this `table_read` artifact: logline, premise, structure, characters, dialogue, pacing, market fit, an overall score, top fixes, and comparables.  Returns `{ reportId, status }` immediately (`status: \"generating\"`). Poll GET /artifacts/{artifactId}/coverage (optionally `?reportId=<id>`) until `status` is `complete` or `failed`.  Coverage produces a separate report — it does NOT change the artifact, its revision, or its share URLs. Free-plan accounts may generate one coverage report; additional reports require an upgrade (403 `plan_limit_reached`). Every plan has a monthly cap on coverage reports: past it the request is refused with 403 `usage_limit_reached`, whose `details.resetsAt` says when it resets — do not retry before then.  Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"focusPrompt":{"type":"string","maxLength":2000,"description":"Optional free-form lens the analysis should weave throughout (e.g. \"focus on whether the second act earns its midpoint\")."}}}}}},"responses":{"200":{"description":"Coverage generation queued. Poll GET /artifacts/{artifactId}/coverage until terminal.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CoverageDispatchAck"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/ArtifactNotFound"},"409":{"$ref":"#/components/responses/ArtifactNotReady"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"get":{"operationId":"getArtifactCoverage","tags":["Artifacts"],"summary":"Read the coverage report for a table read artifact.","description":"Returns the latest coverage report for the screenplay backing this artifact, or a specific report via `?reportId=<id>`. The report carries a `status` field (`generating` → `complete` / `failed`) so you can poll it to terminal. When `status: complete`, `report` carries the structured coverage payload; it is `null` while generating or on failure. `overallScore` is the GATED score: a first judgment within `judging.nearGateBand` of `gateThreshold` is followed by another, two that disagree on the bar by a third, and the score is the MEDIAN of the judgments — `summary` says so (\"the median of 3 judgments\"), `judging` records each one. A report the platform queues by itself (a table-read job's coverage gate and first-pass report, a season run) on a script whose text, rubric and Series Bible are identical to an earlier report's reuses that report's judgments (`judging.reusedFrom`) instead of paying for them again; a report you request (this POST) is always judged afresh. Returns `{ coverage: null }` when no coverage has been requested yet.  Scope: `artifact:read`.","security":[{"BearerApiKey":["artifact:read"]}],"parameters":[{"name":"reportId","in":"query","required":false,"schema":{"type":"string"},"description":"Read a specific coverage report by id. Omit for the latest."}],"responses":{"200":{"description":"Coverage report envelope (`coverage` may be null).","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CoverageReportEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/ArtifactNotFound"},"409":{"$ref":"#/components/responses/ArtifactNotReady"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/brand-coverage":{"post":{"operationId":"analyzeBrandCoverage","tags":["Brand"],"summary":"Queue a brand-storytelling coverage analysis.","description":"Analyzes a piece of brand/marketing narrative (landing copy, a brand film script, a founder pitch, a case study) across eight craft dimensions. Async + queued: returns `{ reportId, status: \"generating\" }`; poll GET /brand-coverage/{reportId} until terminal. Content is capped at 60k chars. Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BrandCoverageRequest"}}}},"responses":{"200":{"description":"Brand coverage analysis queued.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BrandCoverageAck"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/brand-narrative":{"post":{"operationId":"generateBrandNarrative","tags":["Brand"],"summary":"Generate a piece of brand storytelling from a brief.","description":"The \"create\" companion to brand coverage. Turns a brief (+ optional source facts) into a finished piece of brand storytelling held to the studio craft bar. Will NOT invent stats/quotes/claims beyond the supplied facts. Synchronous — returns the narrative in the response. Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BrandNarrativeRequest"}}}},"responses":{"200":{"description":"The generated brand narrative.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BrandNarrativeResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/brand-coverage/{reportId}":{"parameters":[{"name":"reportId","in":"path","required":true,"schema":{"type":"string"},"description":"Brand coverage report id."}],"get":{"operationId":"getBrandCoverage","tags":["Brand"],"summary":"Read a brand-storytelling coverage report.","description":"Returns the brand coverage report for the given id. Poll after POST /brand-coverage until the report status is terminal. Scope: `story:read`.","security":[{"BearerApiKey":["story:read"]}],"responses":{"200":{"description":"The brand coverage report.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BrandCoverageReport"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/artifacts/{artifactId}/voice-modification":{"parameters":[{"$ref":"#/components/parameters/StoryArtifactId"}],"post":{"operationId":"modifyArtifactVoice","tags":["Artifacts"],"summary":"Apply a voice effect to a range of lines on a table read artifact.","description":"Applies a voice effect (currently only `autotune`) to a contiguous range of dialogue entries `[startEntryIndex..endEntryIndex]` on the live read backing a `table_read` artifact. No new revision — the artifact id and share URLs stay stable, like a voice recast. The autotune recipe (`params`) is optional and partial: any omitted field falls back to the proven defaults (key `D`, scale `minpent`, strength `1.0`, smooth `1`, reverb `chapel`). Async + queued: returns `{ artifactId, modificationId, status }`; the rendered audio is projected onto the read once the modification reaches `status: \"ready\"`.  Requires `Idempotency-Key`. Scope: `artifact:publish`.","security":[{"BearerApiKey":["artifact:publish"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VoiceModificationRequest"}}}},"responses":{"200":{"description":"Voice modification dispatched.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VoiceModificationResponse"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/ArtifactNotFound"},"409":{"$ref":"#/components/responses/ArtifactNotReady"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/voices":{"get":{"operationId":"listStudioVoices","tags":["Voices"],"summary":"List the caller's reusable studio voices.","description":"Lists the studio voices owned by the caller: designed voices (text-description voice design) and cloned voices (recorded sample, `sourceKind: \"cloned\"`), each with the provider that speaks it. Each entry carries `{ voiceId, name, provider, sourceKind, isCloned, retiresAt, retired, consent }` — assign one to a character via the cast/voice surfaces using `voiceId` + `provider`. A voice with `retiresAt` is on a provider that is shutting its voices down: migrate it before then (POST /voice-migrations); once `retired`, it can no longer speak. A cloned voice speaks only while `consent.status` is `recorded`: one with `required` needs its subject to confirm (POST /voices/{voiceId}/consent-link), and one with `revoked` can never be used again. Scope: `story:read`.","security":[{"BearerApiKey":["story:read"]}],"responses":{"200":{"description":"Studio voices.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StudioVoiceListResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/voices/clone-link":{"post":{"operationId":"createVoiceCloneLink","tags":["Voices"],"summary":"Mint a short-lived browser recording link for voice cloning.","description":"Voice cloning needs a microphone, so headless surfaces (API/CLI/MCP) mint this short-lived (30 min) browser URL and hand it to the human. Opening the link first asks the person speaking (who may not be the account holder) whose voice it is, to confirm they are 18 or older, and to read a consent statement aloud and agree; then they record a 15-60 second sample and name the voice. The clone lands in this account's studio as a reusable voice (provider `cartesia`, tagged `cloned`, `consent.status: \"recorded\"`) and appears in GET /voices. The signed link itself is the auth for the recording page — it is bound to the minting user.  Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"200":{"description":"Recording link minted.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VoiceCloneLinkResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/voices/{voiceId}/consent-link":{"parameters":[{"name":"voiceId","in":"path","required":true,"schema":{"type":"string","minLength":1},"description":"The library voice's `voiceId` (or its library `id`) from GET /voices."}],"post":{"operationId":"createVoiceConsentLink","tags":["Voices"],"summary":"Mint a link for a cloned voice's subject to confirm their consent.","description":"A cloned voice speaks only with the recorded consent of the person whose voice it is. A clone made before consent records began is listed with `consent.status: \"required\"` and refused everywhere (409 `voice_consent_required`) until they confirm. This mints a 7-day browser link: the person opens it, says whose voice it is (their own, or a deceased person's on the estate's behalf), confirms they are 18 or older, reads a consent statement aloud and agrees. The link is the auth, so hand it to them (or open it yourself if it is your voice). A designed voice needs no consent (400); a voice whose consent was withdrawn can never be confirmed again (409 `voice_consent_revoked`). Free.  Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"200":{"description":"The consent link.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VoiceConsentLinkResponse"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/VoiceNotFound"},"409":{"$ref":"#/components/responses/VoiceConsentRevoked"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/voices/{voiceId}/revoke-consent":{"parameters":[{"name":"voiceId","in":"path","required":true,"schema":{"type":"string","minLength":1},"description":"The library voice's `voiceId` (or its library `id`) from GET /voices."}],"post":{"operationId":"revokeVoiceConsent","tags":["Voices"],"summary":"Withdraw a cloned voice's consent: no new use, provider copy deleted.","description":"Withdraws every consent on record for one of your cloned voices. From now on every cast, pin, preview and speak of it is refused (409 `voice_consent_revoked`), and its copy at the voice provider (with any Pro dataset) is deleted within 7 days (`providerDeletionBy`). Work already published stays up. Final: the voice can never be confirmed again. The library record stays as the record; delete it with DELETE /voices/{voiceId} if wanted. Free.  Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"200":{"description":"Consent withdrawn.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VoiceConsentRevokeResponse"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/VoiceNotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/likeness-consents":{"get":{"operationId":"listLikenessConsents","tags":["Voices"],"summary":"List the likeness approvals this account holds.","description":"A picture or video that would show a real, identifiable person who is living, or who died within the last 70 years, is made only while this account holds an active approval naming them (otherwise 422 `likeness_consent_required`). Fictional characters and long-dead historical figures need none, and nobody real is ever shown in a sexual or intimate way. Each approval says whose likeness it is, who approved (`self`: the account holder; `living_third_party`: the person; `estate`: a recently deceased person's estate), when, and whether it is `active` or `revoked`. Newest first.  Scope: `story:read`.","security":[{"BearerApiKey":["story:read"]}],"responses":{"200":{"description":"The approvals.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LikenessConsentList"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/likeness-consents/links":{"post":{"operationId":"createLikenessConsentLink","tags":["Voices"],"summary":"Mint a link for a real person (or their estate) to approve pictures and videos of them.","description":"Mints a 7-day, single-use browser link for the person named in `subjectName`. They open it, confirm who they are (or that they represent the estate of someone who died within the last 70 years, with the date of death and an authorization), confirm they are 18 or older and agree to the exact words; the approval is then recorded for this account. The link is the auth, so hand it to them. An agent cannot approve on anyone's behalf: the account holder approves their own likeness in the dashboard (My Library → Cast → Likeness approvals). Free.  Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["subjectName"],"additionalProperties":false,"properties":{"subjectName":{"type":"string","minLength":1,"maxLength":120,"description":"The person whose likeness the pictures would show, as you would name them."},"subjectEmail":{"type":"string","format":"email","description":"Their email, kept with the link (optional; the page asks them for it again)."}}}}}},"responses":{"200":{"description":"The approval link.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LikenessConsentLinkResponse"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/likeness-consents/{consentId}/revoke":{"parameters":[{"name":"consentId","in":"path","required":true,"schema":{"type":"string","minLength":1},"description":"The approval's `id` from GET /likeness-consents."}],"post":{"operationId":"revokeLikenessConsent","tags":["Voices"],"summary":"Withdraw a likeness approval: no new pictures or videos of that person.","description":"From now on, a picture or video that would show that person is refused (422 `likeness_consent_required`). Work already published stays up. Final and idempotent. 404 `likeness_consent_not_found` for an id that is not this account's. Free.  Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"200":{"description":"The approval, withdrawn.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StudioLikenessConsent"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/ValidationFailed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/voices/preview":{"post":{"operationId":"previewStudioVoice","tags":["Voices"],"summary":"Hear a voice say a line before assigning it.","description":"Voices any voice — one of yours (GET /voices) or a catalog voice — saying `text`, or a character's own first line in a read (`artifactId` + `character`), and answers with a playable `url`. Free. The same words in the same voice are voiced once: a repeat (or a line a deck already voiced) is served from cache (`cached: true`). Assign the voice afterwards with POST /artifacts/{artifactId}/voice or pin it with PATCH /story-projects/{projectId}/cast-canon.  Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VoicePreviewRequest"}}}},"responses":{"200":{"description":"The preview.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VoicePreviewResponse"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/ArtifactNotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/voices/{voiceId}":{"parameters":[{"name":"voiceId","in":"path","required":true,"schema":{"type":"string","minLength":1},"description":"The library voice's `voiceId` (or its library `id`) from GET /voices."}],"patch":{"operationId":"renameStudioVoice","tags":["Voices"],"summary":"Rename one of your library voices.","description":"Renames a voice in your library. The cast canon people pinned to it carry the new name (`canonRenamed` counts them); a read's own cast label stays as it is. Free. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StudioVoiceRenameRequest"}}}},"responses":{"200":{"description":"The renamed voice.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StudioVoiceRenameResponse"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/VoiceNotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"delete":{"operationId":"deleteStudioVoice","tags":["Voices"],"summary":"Delete one of your library voices.","description":"Deletes a voice from your library and at its provider. A voice still pinned in a cast canon or spoken in a read (the read behind a share link) is refused with 409 `voice_in_use`, `details.uses` naming every use; send the delete again with `confirmed: true` (`?confirmed=true`, or a JSON body) to delete it anyway — those characters then have no voice until they are recast. Free. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"name":"confirmed","in":"query","required":false,"schema":{"type":"boolean"},"description":"Delete a voice that is still in use."}],"responses":{"200":{"description":"Deleted.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StudioVoiceDeleteResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/VoiceNotFound"},"409":{"$ref":"#/components/responses/VoiceInUse"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/voice-migrations":{"get":{"operationId":"listVoiceMigrations","tags":["Voices"],"summary":"List your voice migrations.","description":"Your recorded voice migrations, newest first, each with what it re-pointed (`changes`), the finished reads it left on the old voice (`skipped`), and whether it was undone. `voiceId` narrows to migrations from or to one voice. Scope: `story:read`.","security":[{"BearerApiKey":["story:read"]}],"parameters":[{"name":"voiceId","in":"query","required":false,"schema":{"type":"string"},"description":"Only migrations from or to this voice."}],"responses":{"200":{"description":"Voice migrations.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VoiceMigrationListResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"migrateVoice","tags":["Voices"],"summary":"Migrate a voice to a replacement, everywhere it is used.","description":"Re-points every use of `fromVoiceId` to `toVoiceId`: each cast canon person pinned to it (which is also a recurring show's pinned voice, and the voice every new episode, deck, trailer and showcase speaks with), the narrator pinned on a trailer or pitch deck, and the voice map of every read with no finished recording yet. `projectId` (and `character`) narrow it to one project (and one person, under any name the canon knows them by). A read with a finished recording keeps it — published audio stays as listeners heard it — and is listed in `skipped`: re-voice its characters (POST /artifacts/{artifactId}/revoice, priced) to finalize it again on the new voice. Free, and TWO CALLS: without `confirmed` it answers `applied: false` with what would change and writes nothing; with `confirmed: true` it re-points and records the migration (`migrationId`), which POST /voice-migrations/{migrationId}/undo reverses. The replacement must be on a provider that is neither retiring nor retired (409 `voice_retired`).  Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VoiceMigrationRequest"}}}},"responses":{"200":{"description":"The plan, or the recorded migration.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VoiceMigrationEnvelope"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/VoiceRetired"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/voice-migrations/{migrationId}/undo":{"parameters":[{"name":"migrationId","in":"path","required":true,"schema":{"type":"string","minLength":1},"description":"The `migrationId` from POST /voice-migrations or GET /voice-migrations."}],"post":{"operationId":"undoVoiceMigration","tags":["Voices"],"summary":"Undo a voice migration.","description":"Every use the migration re-pointed that still holds the replacement goes back to the old voice; a use changed again since, or a read finished on the new voice since, is kept and listed in `migration.undo.kept` with the reason. A migration is undone once. Refused (409 `voice_retired`) once the old voice's provider has retired it, because the undo would pin a voice that can no longer speak. Free. Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"200":{"description":"The undone migration.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VoiceMigrationUndoResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/VoiceNotFound"},"409":{"$ref":"#/components/responses/VoiceRetired"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/table-read/performance-booth":{"post":{"operationId":"startPerformanceBoothSession","tags":["Table Read"],"summary":"Mint a Performance Booth recording link for one table-read job.","description":"Audio Acting is ADDITIVE: a performed line keeps the writer's own timing and intonation in the assigned character voice, and every line NOT performed stays on TTS. Mints a 7-day browser recording link bound to the job — the link IS the auth, so hand it to the person performing. Capture spans many lines and sessions. Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["jobId"],"properties":{"jobId":{"type":"string","description":"Table-read job the recording link is bound to."}}}}}},"responses":{"200":{"description":"A recording link, its expiry, and the record id.","content":{"application/json":{"schema":{"type":"object","properties":{"recordingUrl":{"type":"string","description":"Give this to the person performing — the link is the auth."},"expiresAt":{"type":"string","format":"date-time"},"recordId":{"type":"string"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"get":{"operationId":"getPerformanceBoothStatus","tags":["Table Read"],"summary":"Which table-read lines have a performed take.","description":"Lines carrying a human performance, and those still on TTS. A line with no take is not an error — it is the default. Scope: `story:read`.","security":[{"BearerApiKey":["story:read"]}],"parameters":[{"name":"jobId","in":"query","required":true,"schema":{"type":"string"},"description":"Table-read job id."}],"responses":{"200":{"description":"Per-line performance status.","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/voices/pro/start":{"post":{"operationId":"startProVoiceClone","tags":["Voices"],"summary":"Start a studio-grade (Pro) voice clone and mint a recording link.","description":"Begins a professional voice clone (fine-tuned on 30 min–2 hr of audio). Mints a long-lived browser recording URL bound to the clone job — give it to the user to record at least 30 minutes of clear speech, then call POST /voices/pro/train with the returned jobId. Premium, metered, async (~3 hours, 250 credits charged only on success). Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProVoiceCloneStartRequest"}}}},"responses":{"200":{"description":"The recording link + clone job id.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProVoiceCloneStartResult"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/voices/pro/script":{"get":{"operationId":"getProVoiceCloneScript","tags":["Voices"],"summary":"Read the Pro voice clone recording script.","description":"Returns the ~30-minute mixed-register script the user reads aloud to collect Pro-clone audio: `{ title, intro, totalEstSeconds, passages }`. Scope: `story:read`.","security":[{"BearerApiKey":["story:read"]}],"responses":{"200":{"description":"The recording script.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProVoiceCloneScript"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/voices/pro/train":{"post":{"operationId":"startProVoiceCloneTraining","tags":["Voices"],"summary":"Start the fine-tune for a Pro voice clone.","description":"Kicks off the ~3 hour fine-tune once at least 30 minutes of audio is recorded (check GET /voices/pro/{jobId}). Charges the Pro voice clone price, settled only if training succeeds (a failed train is free). Returns `{ status: \"training\" }`; a short balance is 402 `insufficient_credits`. Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProVoiceCloneTrainRequest"}}}},"responses":{"200":{"description":"Training acknowledgement.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProVoiceCloneTrainResult"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/voices/pro/{jobId}":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Pro voice clone job id."}],"get":{"operationId":"getProVoiceClone","tags":["Voices"],"summary":"Poll a Pro voice clone job.","description":"Returns the clone status (collecting → training → ready | failed), audio collected vs required, trainability, and the resulting voiceId once ready. Scope: `story:read`.","security":[{"BearerApiKey":["story:read"]}],"responses":{"200":{"description":"Current Pro voice clone status.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProVoiceCloneStatus"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/pitch-decks/{jobId}":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Pitch deck job id."}],"delete":{"operationId":"deletePitchDeck","tags":["Pitch Deck"],"summary":"Delete a pitch deck job you own.","description":"Removes a pitch deck you will not pursue: a failed or abandoned attempt, a draft, or one rendering, finishing or in review. Irreversible: its takes, masters and versions go with it. Deleting it first stops anything still rendering or finishing, and every credit held for work that had not run comes back to your balance (`releasedCredits`). A PUBLISHED pitch deck is refused with a validation error: unpublish it first, its share link is live.","security":[{"BearerApiKey":["story:write"]}],"responses":{"200":{"description":"The deleted job id and the credits that came back.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VideoDeleteResult"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"patch":{"operationId":"renamePitchDeck","tags":["Pitch Deck"],"summary":"Rename a pitch deck (free).","description":"Renames a pitch deck: the name every list of the project's videos shows it by. A display name only: its plan, its review and its video never read it, so nothing goes out of date and nothing renders. Free and reversible; `title: null` gives it back the name its plan gives it. Versions of a deck that share a name are listed together, so a new name lists this one on its own. Any status. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ArtifactRenameRequest"}}}},"responses":{"200":{"description":"The deck's name now.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RenamedVideo"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/pitch-decks/{jobId}/duplicate":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"The pitch deck to copy the plan from."}],"post":{"operationId":"duplicatePitchDeck","tags":["Pitch Deck"],"summary":"Duplicate a deck from its current plan onto a new draft (free).","description":"Duplicating a deck makes a NEW `draft` deck that carries the source deck's CURRENT plan — every chapter with its edits, the deck's intent, its black interludes and montage marks — and the choices made on the deck itself (the narrator cast for it, its delivery class, its strategic target), and leaves the source deck untouched. Nothing rendered rides along: no takes, cuts, masters, share links or coverage reports. It is free and renders and reserves nothing; the copy's plan is queued for coverage, and its next step is approve_pitch_deck_plan on the NEW deck's jobId (CLI: pitch-deck approve-plan <newJobId>). Any status may be duplicated — a draft, a deck in review or complete, a failed deck — as long as its plan reads as a plan today; one that does not is refused with 409 `pitch_deck_state_invalid` (`details.refusal.code: pitch_deck_duplicate_no_plan`) and nothing is created. `title` (optional) renames the copy. NOT metered and NOT confirmed. Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PitchDeckDuplicateRequest"}}}},"responses":{"200":{"description":"The new draft deck.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PitchDeckDuplicate"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/PitchDeckStateInvalid"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/pitch-decks/{jobId}/publish":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"The pitch deck to publish. It must be complete."}],"post":{"operationId":"publishPitchDeck","tags":["Pitch Deck"],"summary":"Publish a finished pitch deck: its share link goes live.","description":"Publish a finished deck: its share link goes live at the writer's public slug. ONLY A `complete` DECK PUBLISHES — a draft or a deck in review is refused and nothing is written. The deck's page is screened for outbound safety first (a held account publishes nothing). A share token is minted when the deck has none, and a master that is missing, idle, stale or failed is re-stitched. NOT metered and NOT confirmed. Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"200":{"description":"The published deck: its full share link (relay it verbatim), share token, writer slug and master state.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PitchDeckPublish"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/pitch-decks/{jobId}/unpublish":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"The pitch deck to take down."}],"post":{"operationId":"unpublishPitchDeck","tags":["Pitch Deck"],"summary":"Unpublish a pitch deck: its share link goes dark.","description":"Take a published deck down: its share link goes dark and the deck can be deleted again (DELETE /pitch-decks/{jobId}). Its own edits stay. No status gate — a deck taken live can always be taken back down, and a deck that is not published is a no-op. NOT metered and NOT confirmed. Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"200":{"description":"The deck is unpublished.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PitchDeckUnpublish"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/pitch-decks/{jobId}/pdf":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"The pitch deck whose PDF to render. It must be complete."}],"post":{"operationId":"requestPitchDeckPdf","tags":["Pitch Deck"],"summary":"Render a finished pitch deck's PDF on demand.","description":"Render the PDF of a finished deck on demand. The first PDF comes with the deck only when the create request carried `includePdf: true`; this asks for one afterwards — a deck made without it, or an earlier PDF that failed. ONLY A `complete` DECK RENDERS ONE. A queued or rendering export short-circuits instead of stacking, and a deck whose PDF is already ready is the export worker's own no-op. NOT metered and NOT confirmed. Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"200":{"description":"The PDF state after the request.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PitchDeckPdf"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/pitch-decks/{jobId}/chapters":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Pitch deck job id."}],"get":{"operationId":"getPitchDeckChapters","tags":["Pitch Deck"],"summary":"Read a pitch deck's chapters, their calls and every take.","description":"Returns the deck's chapters as they render: each chapter's plan (what it delivers and shows, its words, who speaks its line — a cast member in their own voice (`speaker`; `speakerOnCamera` when they are in the picture and say it on camera, lip-synced, rather than as a voice-over) or the narrator (`narrated`) — its cast and the location key it is set in) and its render calls (at most 10s each) — duration, audio mode, the plate rung the render opens on, the cast on camera, the shots and the lines spoken in them, the speech track — and every take with its checks: whether each line was heard in a voice of its own (`voiceMissLineRefs`: a line not heard, or said in a voice too like another character's — a flag; `ownReadingLineRefs`: a line said in the take's own voice, heard and distinct, kept and never a flag), whether it opened on its planned framing (`openingMiss`), whether a mouth did not do what its line asks — the speaker's not following an on-camera line, or lips moving under someone else's voice (`mouthMiss`, with the `mouths` check) — whether it was the call's one automatic retake at our cost (`atOurCost`), whether it was not checked for a person shown twice (`duplicateNotChecked`: the check could not judge it even on its retry — never a pass, and it ranks below checked takes), whether its lip sync was not checked (`mouthNotChecked`: no on-camera speaker's mouth could be read, even after the face locator's denser retry — never a pass and never a retake, and it ranks below a take whose mouths were measured passing), whether its lip sync is uncertain (`mouthUncertain`: measured, but a reading sat inside the band around its limit, so the same file could read either side of it — never a pass and never a retake, ranked below a measured pass and above a measured miss), each criterion the mouth check missed or was uncertain on, with its line, face, number and limit (`mouthFindings`), a content refusal (released, never retried; `reference_render_provider_ip_refused` is a shot refused as possibly someone else's intellectual property, whose notice names what to edit before it renders again), the selection, what a priced retake would cost, and the repair engine's state: what the take watcher saw (`watch`), the remedy decided and the rule that fired (`repair`), where a take came from (`lineage`: repaired from, trimmed from, or a new take after, an earlier take), what a repair cost (`repairCost`), what repairing it in place would cost now (`repairQuote`), what a free trim of it may keep (`trimBounds`: never into a spoken line) and, on a trimmed take, the span it kept (`trim`: its cut plays that length). Also the chapter's cut and poster frame, `stale` when the plan changed since it rendered, the master (a 480p preview in review, finished once the finish completes), the delivery class, the warnings the finish and export carry, `videoModelPreflight` in draft and review (what each chapter's render needs and what is missing; a missing location plate reads as made at render), and `reviewUrl`. Each chapter also carries `montage` (a rapid intercut run: brief shots cut tight across the people and places it names), and the deck its `interludes` — its black interludes in cut order, each with the chapter it follows, its seconds, its length in the master, its line and speaker or its sound, and what the master made of them (replace them with POST /pitch-decks/{jobId}/interludes). The deck also carries its `titleSequence` (its epigraph and source credit, its title and the people its flash cuts go to, with what each flash cut resolved to and each person's name super when `nameSupers` is on — set it with POST /pitch-decks/{jobId}/title-sequence) its `closingSequence` (the closing quote, \"Coming Soon\" and the production credit after the last chapter, with each card's length, start, bell beat and the look it wears, and where the music and the bell fade out — set it with POST /pitch-decks/{jobId}/closing-sequence) and its `finish` (the film finish laid on the picture at master assembly: its look, intensity and adjustments — set it with POST /pitch-decks/{jobId}/look). There are no stills and no storyboard or draft approval. A read: it never compiles, renders, voices or makes a plate. Scope: `story:read`.","security":[{"BearerApiKey":["story:read"]}],"responses":{"200":{"description":"The deck's chapters, calls and takes + a browser review link.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PitchDeckChapters"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/pitch-decks/{jobId}/storyboard/coverage":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Pitch deck job id."}],"get":{"operationId":"getPitchDeckStoryboardCoverage","tags":["Pitch Deck"],"summary":"Read pitch deck plan coverage.","description":"Returns the pre-render report for the exact journey plan this deck would render right now: whether it still describes the current plan, the 0–10 score, the 8.0 gate and every reason it withheld a pass, seven dimensions, machine-checked plan defects, per-chapter notes, and priority fixes. While the latest draft edit's coverage waits to start it answers `status: \"queued\"`; a plan nobody has graded yet answers `status: \"missing\"` with the reason — NOT a 404, which would read as \"no such pitch deck\". This never returns the chapters. Scope: `story:read`.","security":[{"BearerApiKey":["story:read"]}],"responses":{"200":{"description":"Pitch deck plan coverage envelope.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"type":"object","required":["coverage"],"additionalProperties":false,"properties":{"coverage":{"type":"object","required":["status","planKind","jobId","current","stale","gatePassed","gateThreshold","gateReasons","gateNotes","heldBy","reportId","planRevision","reportedPlanRevision","reportVersion","rubricVersion","bibleVersion","exemplarIds","overallScore","progressDetail","failureMessage","report","judging","judgingIncomplete","defects","gateOverride","writerOverride","createdAt","updatedAt"],"properties":{"status":{"type":"string","enum":["missing","queued","generating","complete","failed"],"description":"`queued`: the latest draft edit's coverage waits to start — it starts once the plan has been left alone for three minutes (a burst of edits is one run, on the revision it ends on), and the plan approval starts it at once. `generating`: scoring — including while the same revision is judged again near the bar (`judging.pending`), when the approval waits for the median. A run a later edit replaced is dropped, never stored, and never the status."},"planKind":{"type":"string","enum":["pitch_deck","trailer"]},"jobId":{"type":"string"},"current":{"type":"boolean","description":"The stored report graded the plan as it stands right now (a `queued` run is for the plan as it stands)."},"stale":{"type":"boolean","description":"The plan changed after this report was produced."},"gatePassed":{"type":"boolean","description":"Complete, current, over threshold, and no veto dimension or blocking defect. A stale pass reads false."},"gateThreshold":{"type":"number","minimum":0,"maximum":10,"description":"The score this report had to clear (8.0 by default — stricter than the 7.0 Series Bible gate, because renders cost money)."},"gateReasons":{"type":"array","items":{"type":"string"},"description":"Every reason the gate withheld a pass, in the order they were checked. Empty when it passed."},"reportId":{"oneOf":[{"type":"string"},{"type":"null"}]},"planRevision":{"type":"integer","minimum":0,"description":"Fingerprint of the plan as it stands right now."},"reportedPlanRevision":{"oneOf":[{"type":"integer","minimum":0},{"type":"null"}],"description":"Fingerprint of the plan the stored report actually graded."},"reportVersion":{"oneOf":[{"type":"integer","minimum":1},{"type":"null"}]},"rubricVersion":{"oneOf":[{"type":"integer","minimum":1},{"type":"null"}],"description":"The rubric version the report was graded with. A report graded under an older rubric than the current one cannot authorize a render."},"bibleVersion":{"oneOf":[{"type":"integer","minimum":1},{"type":"null"}],"description":"The Series Bible revision the report was graded against, when the project has one."},"exemplarIds":{"type":"array","items":{"type":"string"},"description":"Ids of the benchmark exemplars the report was calibrated against; empty when the surface has no corpus."},"gateNotes":{"type":"array","items":{"type":"string"},"description":"Vetoes that were suppressed by policy, surfaced beside the reasons and never merged into them — the finding a reader most often wants when a gate passed narrowly."},"heldBy":{"type":"array","description":"What holds the plan beside its overall score, structured: each dimension that fails the plan on its own and sits under the bar (a story check such as `buildup`; `score` null when the judge gave it no usable score), then each blocking plan defect. Empty on a pass and on a report that does not describe the plan as it stands. A plan whose overall clears the bar is still held by any of these: say \"7.6 overall, but Buildup scored 6.5, below the 7.0 bar\", never that 7.6 is below the bar.","items":{"oneOf":[{"type":"object","required":["kind","dimension","label","score"],"properties":{"kind":{"type":"string","enum":["dimension"]},"dimension":{"type":"string"},"label":{"type":"string","description":"The dimension as the app names it, e.g. \"Buildup\"."},"score":{"oneOf":[{"type":"number","minimum":0,"maximum":10},{"type":"null"}]}}},{"type":"object","required":["kind","code","detail"],"properties":{"kind":{"type":"string","enum":["defect"]},"code":{"type":"string"},"detail":{"type":"string"}}}]}},"overallScore":{"oneOf":[{"type":"number","minimum":0,"maximum":10},{"type":"null"}]},"progressDetail":{"oneOf":[{"type":"string"},{"type":"null"}]},"failureMessage":{"oneOf":[{"type":"string"},{"type":"null"}]},"report":{"oneOf":[{"type":"object","description":"The graded report (rubric v13): overallScore, verdict, oneLineDiagnosis, canonSummary, showSold, opening (the judge's note on the deck's opening title sequence: present, whatItSays, servesIntent, nextMove; null on a report judged before it existed), the twenty scored dimensions — first the four that judge the plan against its intent and veto in every mode (intentClarity, centralQuestion, buildup, themeCarried), then subtext (does the plan pose its question rather than recite its plot; a veto in every mode), transitions (does every part follow the one before it for a reason a first-time viewer can feel; a veto in every mode) and voices (do the characters speak, rather than the narrator carrying everything), then sequenceArchitecture, storyThroughLine, seriesRepresentation, emotionalArchitecture, narrativeVoice, promiseControl, beatDistinctness, characterIntroduction, seriesBibleConsistency, referenceCompleteness, motionDirection, shotLadderLegibility, continuity — exemplarComparisons, emotionalJourney, narrationThroughline, beatNotes, characterNotes, weakTransitions (the weakest cuts, worst first: toBeatIndex — the unit the cut goes into — problem and fix), strengths, risks, priorityFixes, and viewerTakeaway: what a first-time viewer takes away from the plan (aboutTheShow, centralQuestion, whereItBuilds, toldVersusShown — how much of it was told in words rather than shown — and lostTheThread — where they could not tell why one part came after the last), written BEFORE the judge was shown the intent.","properties":{"viewerTakeaway":{"$ref":"#/components/schemas/VideoViewerTakeaway"},"opening":{"oneOf":[{"$ref":"#/components/schemas/StoryboardCoverageOpening"},{"type":"null"}]}}},{"type":"null"}]},"judgingIncomplete":{"oneOf":[{"type":"object","required":["judgments","maxJudgments"],"additionalProperties":false,"properties":{"judgments":{"type":"integer","minimum":1,"description":"The judgments that did run (their scores are on `judging`)."},"maxJudgments":{"type":"integer","minimum":1,"description":"The most judgments one revision takes."}}},{"type":"null"}],"description":"A revision near the bar whose median could not be completed: the further judgments it needed could not run, even queued again. The report is `failed` and is NEITHER a pass nor a veto (\"Plan coverage couldn't finish judging — Score again\"); `judging` lists the judgments that ran, with `stoppedBecause: judging_incomplete`. Score the plan again. Null otherwise."},"judging":{"oneOf":[{"$ref":"#/components/schemas/StoryboardCoverageJudging"},{"type":"null"}],"description":"How the verdict was reached. One judgment is noisy near the bar, so a revision whose first judgment lands within `nearGateBand` of a gating line (the overall, or a veto dimension) is judged again — the SAME revision, up to `maxJudgments`, free to the writer — and the gate reads the MEDIAN of each score, vetoes included. A plan edit starts a fresh set. While another judgment is pending, `pending` is true and the judgments so far are listed. Null before a judgment is recorded."},"defects":{"type":"array","description":"Plan defects decided in code, not by a model. `blocking` ones fail the gate on their own; `advisory` ones are evidence handed to the rubric.","items":{"type":"object","required":["code","severity","beatIndexes","subject","detail"],"properties":{"code":{"type":"string","enum":["missing_motion_direction","missing_shot_type","missing_location","shot_ladder_collapsed","shot_ladder_duplicate_framing","location_flip_flop","missing_character_reference","missing_location_plate","character_never_featured","opens_on_arrival","ends_on_exit","cut_ladder_no_range","promised_never_shown","canon_lead_absent","canon_lead_never_subject","canon_character_not_castable","approved_beat_dropped_silently","video_intent_missing","arc_stage_missing","question_not_advanced","arc_out_of_order","arc_stage_unserved","ending_off_question","transition_unexplained","characters_never_speak","character_lines_under_share"]},"severity":{"type":"string","enum":["blocking","advisory"]},"beatIndexes":{"type":"array","items":{"type":"integer","minimum":0}},"subject":{"type":"string"},"detail":{"type":"string"}}}},"gateOverride":{"type":"object","required":["allowed","why"],"additionalProperties":false,"description":"Whether a failed gate may be rendered over on the writer's word (`acknowledgePlanCoverage` on the plan approval). Allowed ONLY when a complete report of the plan as it stands, scored on the current rubric, failed on the judge's scores alone (the overall score, or a judged veto dimension, under the bar); never over a blocking defect decided in code, and never for a missing, queued, running, failed, stale or old-rubric report. `why` says which, in the writer's terms.","properties":{"allowed":{"type":"boolean"},"why":{"type":"string"}}},"writerOverride":{"oneOf":[{"$ref":"#/components/schemas/PlanCoverageOverride"},{"type":"null"}],"description":"The override the writer recorded on THIS report by approving over its gate — who (and via which surface and key), when, why, and the revision, score and reasons it overrode. Every surface shows it as \"approved over the plan gate: <reason>\". Null when no approval overrode this report."},"createdAt":{"oneOf":[{"type":"string","format":"date-time"},{"type":"null"}]},"updatedAt":{"oneOf":[{"type":"string","format":"date-time"},{"type":"null"}]}}}}}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"generatePitchDeckStoryboardCoverage","tags":["Pitch Deck"],"summary":"Generate pitch deck plan coverage.","description":"Queues a pre-render report for the exact current journey plan. It grades the PLAN, never a rendered frame, so it spends no render credits and is the cheap step before the expensive one. Requires at least one planned chapter. The gate is 8.0 — stricter than the 7.0 foundational Bible gate — and Series Bible consistency fails the report on its own whatever the other dimensions score. Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"focusPrompt":{"type":"string","maxLength":5000,"description":"Optional writer focus for this coverage pass."}}}}}},"responses":{"200":{"description":"Queued pitch deck plan coverage envelope.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"type":"object","required":["coverage"],"additionalProperties":false,"properties":{"coverage":{"type":"object","required":["status","planKind","jobId","current","stale","gatePassed","gateThreshold","gateReasons","gateNotes","heldBy","reportId","planRevision","reportedPlanRevision","reportVersion","rubricVersion","bibleVersion","exemplarIds","overallScore","progressDetail","failureMessage","report","judging","judgingIncomplete","defects","gateOverride","writerOverride","createdAt","updatedAt"],"properties":{"status":{"type":"string","enum":["missing","queued","generating","complete","failed"],"description":"`queued`: the latest draft edit's coverage waits to start — it starts once the plan has been left alone for three minutes (a burst of edits is one run, on the revision it ends on), and the plan approval starts it at once. `generating`: scoring — including while the same revision is judged again near the bar (`judging.pending`), when the approval waits for the median. A run a later edit replaced is dropped, never stored, and never the status."},"planKind":{"type":"string","enum":["pitch_deck","trailer"]},"jobId":{"type":"string"},"current":{"type":"boolean","description":"The stored report graded the plan as it stands right now (a `queued` run is for the plan as it stands)."},"stale":{"type":"boolean","description":"The plan changed after this report was produced."},"gatePassed":{"type":"boolean","description":"Complete, current, over threshold, and no veto dimension or blocking defect. A stale pass reads false."},"gateThreshold":{"type":"number","minimum":0,"maximum":10,"description":"The score this report had to clear (8.0 by default — stricter than the 7.0 Series Bible gate, because renders cost money)."},"gateReasons":{"type":"array","items":{"type":"string"},"description":"Every reason the gate withheld a pass, in the order they were checked. Empty when it passed."},"reportId":{"oneOf":[{"type":"string"},{"type":"null"}]},"planRevision":{"type":"integer","minimum":0,"description":"Fingerprint of the plan as it stands right now."},"reportedPlanRevision":{"oneOf":[{"type":"integer","minimum":0},{"type":"null"}],"description":"Fingerprint of the plan the stored report actually graded."},"reportVersion":{"oneOf":[{"type":"integer","minimum":1},{"type":"null"}]},"rubricVersion":{"oneOf":[{"type":"integer","minimum":1},{"type":"null"}],"description":"The rubric version the report was graded with. A report graded under an older rubric than the current one cannot authorize a render."},"bibleVersion":{"oneOf":[{"type":"integer","minimum":1},{"type":"null"}],"description":"The Series Bible revision the report was graded against, when the project has one."},"exemplarIds":{"type":"array","items":{"type":"string"},"description":"Ids of the benchmark exemplars the report was calibrated against; empty when the surface has no corpus."},"gateNotes":{"type":"array","items":{"type":"string"},"description":"Vetoes that were suppressed by policy, surfaced beside the reasons and never merged into them — the finding a reader most often wants when a gate passed narrowly."},"heldBy":{"type":"array","description":"What holds the plan beside its overall score, structured: each dimension that fails the plan on its own and sits under the bar (a story check such as `buildup`; `score` null when the judge gave it no usable score), then each blocking plan defect. Empty on a pass and on a report that does not describe the plan as it stands. A plan whose overall clears the bar is still held by any of these: say \"7.6 overall, but Buildup scored 6.5, below the 7.0 bar\", never that 7.6 is below the bar.","items":{"oneOf":[{"type":"object","required":["kind","dimension","label","score"],"properties":{"kind":{"type":"string","enum":["dimension"]},"dimension":{"type":"string"},"label":{"type":"string","description":"The dimension as the app names it, e.g. \"Buildup\"."},"score":{"oneOf":[{"type":"number","minimum":0,"maximum":10},{"type":"null"}]}}},{"type":"object","required":["kind","code","detail"],"properties":{"kind":{"type":"string","enum":["defect"]},"code":{"type":"string"},"detail":{"type":"string"}}}]}},"overallScore":{"oneOf":[{"type":"number","minimum":0,"maximum":10},{"type":"null"}]},"progressDetail":{"oneOf":[{"type":"string"},{"type":"null"}]},"failureMessage":{"oneOf":[{"type":"string"},{"type":"null"}]},"report":{"oneOf":[{"type":"object","description":"The graded report (rubric v13): overallScore, verdict, oneLineDiagnosis, canonSummary, showSold, opening (the judge's note on the deck's opening title sequence: present, whatItSays, servesIntent, nextMove; null on a report judged before it existed), the twenty scored dimensions — first the four that judge the plan against its intent and veto in every mode (intentClarity, centralQuestion, buildup, themeCarried), then subtext (does the plan pose its question rather than recite its plot; a veto in every mode), transitions (does every part follow the one before it for a reason a first-time viewer can feel; a veto in every mode) and voices (do the characters speak, rather than the narrator carrying everything), then sequenceArchitecture, storyThroughLine, seriesRepresentation, emotionalArchitecture, narrativeVoice, promiseControl, beatDistinctness, characterIntroduction, seriesBibleConsistency, referenceCompleteness, motionDirection, shotLadderLegibility, continuity — exemplarComparisons, emotionalJourney, narrationThroughline, beatNotes, characterNotes, weakTransitions (the weakest cuts, worst first: toBeatIndex — the unit the cut goes into — problem and fix), strengths, risks, priorityFixes, and viewerTakeaway: what a first-time viewer takes away from the plan (aboutTheShow, centralQuestion, whereItBuilds, toldVersusShown — how much of it was told in words rather than shown — and lostTheThread — where they could not tell why one part came after the last), written BEFORE the judge was shown the intent.","properties":{"viewerTakeaway":{"$ref":"#/components/schemas/VideoViewerTakeaway"},"opening":{"oneOf":[{"$ref":"#/components/schemas/StoryboardCoverageOpening"},{"type":"null"}]}}},{"type":"null"}]},"judgingIncomplete":{"oneOf":[{"type":"object","required":["judgments","maxJudgments"],"additionalProperties":false,"properties":{"judgments":{"type":"integer","minimum":1,"description":"The judgments that did run (their scores are on `judging`)."},"maxJudgments":{"type":"integer","minimum":1,"description":"The most judgments one revision takes."}}},{"type":"null"}],"description":"A revision near the bar whose median could not be completed: the further judgments it needed could not run, even queued again. The report is `failed` and is NEITHER a pass nor a veto (\"Plan coverage couldn't finish judging — Score again\"); `judging` lists the judgments that ran, with `stoppedBecause: judging_incomplete`. Score the plan again. Null otherwise."},"judging":{"oneOf":[{"$ref":"#/components/schemas/StoryboardCoverageJudging"},{"type":"null"}],"description":"How the verdict was reached. One judgment is noisy near the bar, so a revision whose first judgment lands within `nearGateBand` of a gating line (the overall, or a veto dimension) is judged again — the SAME revision, up to `maxJudgments`, free to the writer — and the gate reads the MEDIAN of each score, vetoes included. A plan edit starts a fresh set. While another judgment is pending, `pending` is true and the judgments so far are listed. Null before a judgment is recorded."},"defects":{"type":"array","description":"Plan defects decided in code, not by a model. `blocking` ones fail the gate on their own; `advisory` ones are evidence handed to the rubric.","items":{"type":"object","required":["code","severity","beatIndexes","subject","detail"],"properties":{"code":{"type":"string","enum":["missing_motion_direction","missing_shot_type","missing_location","shot_ladder_collapsed","shot_ladder_duplicate_framing","location_flip_flop","missing_character_reference","missing_location_plate","character_never_featured","opens_on_arrival","ends_on_exit","cut_ladder_no_range","promised_never_shown","canon_lead_absent","canon_lead_never_subject","canon_character_not_castable","approved_beat_dropped_silently","video_intent_missing","arc_stage_missing","question_not_advanced","arc_out_of_order","arc_stage_unserved","ending_off_question","transition_unexplained","characters_never_speak","character_lines_under_share"]},"severity":{"type":"string","enum":["blocking","advisory"]},"beatIndexes":{"type":"array","items":{"type":"integer","minimum":0}},"subject":{"type":"string"},"detail":{"type":"string"}}}},"gateOverride":{"type":"object","required":["allowed","why"],"additionalProperties":false,"description":"Whether a failed gate may be rendered over on the writer's word (`acknowledgePlanCoverage` on the plan approval). Allowed ONLY when a complete report of the plan as it stands, scored on the current rubric, failed on the judge's scores alone (the overall score, or a judged veto dimension, under the bar); never over a blocking defect decided in code, and never for a missing, queued, running, failed, stale or old-rubric report. `why` says which, in the writer's terms.","properties":{"allowed":{"type":"boolean"},"why":{"type":"string"}}},"writerOverride":{"oneOf":[{"$ref":"#/components/schemas/PlanCoverageOverride"},{"type":"null"}],"description":"The override the writer recorded on THIS report by approving over its gate — who (and via which surface and key), when, why, and the revision, score and reasons it overrode. Every surface shows it as \"approved over the plan gate: <reason>\". Null when no approval overrode this report."},"createdAt":{"oneOf":[{"type":"string","format":"date-time"},{"type":"null"}]},"updatedAt":{"oneOf":[{"type":"string","format":"date-time"},{"type":"null"}]}}}}}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/pitch-decks/{jobId}/intent":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Pitch deck job id."}],"get":{"operationId":"getPitchDeckIntent","tags":["Pitch Deck"],"summary":"Read the pitch deck's intent and what a first-time viewer takes away.","description":"Returns the pitch deck's intent, each chapter's arc stage and how it advances the central question, where the chapters break the arc, and what plan coverage says: the first-time-viewer takeaway it wrote before seeing the intent, and the story checks. Every deck and trailer is planned from an explicit intent — what the video is for, its logline, theme, central question, stakes, what a first-time viewer is promised, a four-stage arc (setup, escalation, turn, closing question) and how the ending lands the question. Every chapter or beat names the arc stage it serves (`arcStage`), how it advances the central question (`advancesQuestion`) and, after the first, why it follows the one before it (`whyItFollows`: the visible link — a cause, an answer, a contrast, a match cut, a look, an object, a sound — and what it moves forward; one line, never shown on screen). Plan coverage first writes what a first-time viewer would take away from the plan without seeing the intent (`viewerTakeaway`, with how much of the story the words recited rather than let them work out: `toldVersusShown`, and where they lost the thread: `lostTheThread`), then compares it with the intent: intent clarity, the central question, the buildup and whether the theme is carried each fail the plan on their own, and so do subtext — a plan that recites its plot instead of posing its question — and transitions — a part that follows the one before it for no reason a first-time viewer can feel (`weakTransitions` names the weakest, each with its fix); voices (do the characters speak, rather than the narrator carrying everything) is scored beside them. A read. Scope: `story:read`.","security":[{"BearerApiKey":["story:read"]}],"responses":{"200":{"description":"The intent beside what the plan says.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VideoIntent"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"updatePitchDeckIntent","tags":["Pitch Deck"],"summary":"Edit a draft pitch deck's intent.","description":"Edits the intent of a DRAFT pitch deck: each field given replaces the stored one (`arc` the whole four-stage arc), and the merged intent must be complete and build (the arc's intensity rises to the turn). Editing the intent is free and only while the plan is a draft (approving the plan approves its intent): it saves the new intent and queues plan coverage again for the latest revision — a burst of edits is one coverage run, started once the plan has been left alone for three minutes. The chapters or beats are not re-planned; edit them (their `arcStage`, `advancesQuestion` and `whyItFollows` included) to follow the new intent. A plan planned before intents existed takes a complete intent. Answers the saved intent, the changed plan's `planCoverage` and `arcProblems` — where the chapters now break the new arc (fix them with the chapter refine's `arcStage` and `advancesQuestion`). Refused once the plan is approved (409). Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VideoIntentEditRequest"}}}},"responses":{"200":{"description":"The saved intent.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VideoIntentEditAck"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/PitchDeckStateInvalid"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/pitch-decks/{jobId}/narrator":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Pitch deck job id."}],"get":{"operationId":"getPitchDeckNarrator","tags":["Pitch Deck"],"summary":"Read the deck's narrator.","description":"Returns the voice every narrated line of the deck (chapter narration, lines over black) is spoken in, and where it comes from (`source`): `override` (cast for this deck), `cast_canon` (the cast canon's NARRATOR), `table_read_voice_map` (the table read's NARRATOR) or `default` (the default narrator). A deck's narrator speaks every line the narrator carries (chapter narration and lines over black). It resolves in one order: the voice cast for THIS deck (`set_pitch_deck_narrator`), else the cast canon's NARRATOR, else the table read's NARRATOR, else the default narrator. Casting or clearing it is free; on a rendered deck the narrated lines are voiced again in the new voice and the master is re-assembled, at no charge. Scope: `story:read`.","security":[{"BearerApiKey":["story:read"]}],"responses":{"200":{"description":"The deck's resolved narrator.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"type":"object","required":["narrator"],"properties":{"narrator":{"$ref":"#/components/schemas/PitchDeckNarrator"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"setPitchDeckNarrator","tags":["Pitch Deck"],"summary":"Cast the narrator for this deck.","description":"Casts the voice that narrates THIS deck, or send `voiceId: null` to drop back to the default (the cast canon's NARRATOR, else the table read's, else the default narrator). A deck's narrator speaks every line the narrator carries (chapter narration and lines over black). It resolves in one order: the voice cast for THIS deck (`set_pitch_deck_narrator`), else the cast canon's NARRATOR, else the table read's NARRATOR, else the default narrator. Casting or clearing it is free; on a rendered deck the narrated lines are voiced again in the new voice and the master is re-assembled, at no charge. Allowed while the deck is a draft plan, in review or complete (409 otherwise). Answers the resolved `narrator` and `reassembling`. Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["voiceId"],"additionalProperties":false,"properties":{"voiceId":{"type":"string","nullable":true,"description":"Studio voice id, or null to drop back to the default narrator."},"provider":{"type":"string","nullable":true,"enum":["cartesia","elevenlabs","openai","deepgram","hume",null],"description":"The voice's TTS provider. Inferred when omitted."}}}}}},"responses":{"200":{"description":"The deck's resolved narrator.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"type":"object","required":["narrator","reassembling"],"properties":{"narrator":{"$ref":"#/components/schemas/PitchDeckNarrator"},"reassembling":{"type":"boolean","description":"True when a rendered deck's narration is voiced again and its master re-assembled (free)."}}}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/PitchDeckStateInvalid"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/pitch-decks/{jobId}/interludes":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Pitch deck job id."}],"post":{"operationId":"updatePitchDeckInterludes","tags":["Pitch Deck"],"summary":"Replace the deck's black interludes.","description":"Replaces the deck's black interludes with the list given (`[]` removes them all; the chapters read returns the current ones as `interludes`). A black interlude is a beat of pure black between two chapters (0.5 to 2 seconds), keyed by the chapter it follows (`afterChapterIndex`), that may carry one sound (a breath, a bell) or one short line heard over the dark, the narrator's or a character's own, always as a voice-over. It costs no render: the master draws the black and voices its line and its sound at no charge. The whole list is validated: each follows a chapter the plan has and never the last one, at most one after any chapter, a line of at most 12 words, and a cast speaker who has a voice (400 otherwise). Free. On a draft plan the changed plan is queued for plan coverage again (`planCoverage`); in review or complete the master is re-assembled with the black, its lines and its sounds (`reassembling`). Refused while a render or finish pass runs (409). Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PitchDeckInterludesUpdateRequest"}}}},"responses":{"200":{"description":"The deck's interludes, as saved.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PitchDeckInterludesUpdateAck"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/PitchDeckStateInvalid"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/pitch-decks/{jobId}/title-sequence":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Pitch deck job id."}],"post":{"operationId":"updatePitchDeckTitleSequence","tags":["Pitch Deck"],"summary":"Set the deck's title sequence.","description":"Sets the deck's title sequence whole (`null` removes it; the chapters read returns the current one as `titleSequence`, with what each flash cut goes to and each person's name super, `supers`). A deck's title sequence opens its master, before the first chapter: optionally an epigraph (`epigraph`: its words and where they are from) and a source credit (`sourceCredit`), each quiet on black and held long enough to read; then flash cuts of six to ten frames to the people it names (`cast`, in order; `inShadow` darkens one), each the person's LIVING PORTRAIT — looking into the lens, still, breathing, rendered for the flash (priced first, its own step) — and until it is made a still, silent close from the deck's own takes or another rendered deck of the project (free), never one in which anyone speaks or moves; a still canon portrait image only when the writer asks for one (`flashSource: 'portrait'`); then the title (`title`, one word, in capitals) in a bold sans on black. The flash cuts and the title run `seconds` (3 to 5). With `nameSupers: true` each flash cut carries the person's NAME in the TITLE'S LOOK — the face and colour the title is drawn in (blood red, the heavy condensed sans, when the title is directed so), uppercase, large enough to read at 480p — on the bell beat with the face, set in the black BESIDE the picture (or over the top of it), never over the face: LABEL is the first word of the person's canon name in capitals unless their cast entry sets `label`, and `role` (up to 40 characters, optional) is a smaller line under it; a person `inShadow` gets no name unless their `label` is set (the shadow withholds who they are); how the names look and where they sit is directed with `titleElement: nameSupers`. With `beatGrid: true` the intros cut on the bell's beat: from the sequence's first frame a beat every `beatSeconds` (0.5 to 4; 2 when left out), each flash cut starting on a beat and holding exactly one beat (the one in shadow too), the title on the next beat for `titleBeats` beats (1 to 8; 2 when left out) — their seconds are then derived (beats × beatSeconds), and a score composed to the deck strikes its bell on every intro and on the title. Saving it is free: nothing is rendered for it (a motion close is its own priced step). `enabled: false` keeps it without playing it. How its words look and move is directed separately (`titleElement` on the direct-text-motion call; read back as `textMotion`), and a save keeps those directions. Validated whole: a one-word title, nobody named twice, and everyone in the cast canon when the project has one (400 otherwise). Free. On a draft plan the changed plan is queued for plan coverage again (`planCoverage`: the story judge reads the title); in review or complete the master is re-assembled with it (`reassembling`). Refused while a render or finish pass runs (409). Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PitchDeckTitleSequenceUpdateRequest"}}}},"responses":{"200":{"description":"The deck's title sequence, as saved.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PitchDeckTitleSequenceUpdateAck"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/PitchDeckStateInvalid"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/pitch-decks/{jobId}/closing-sequence":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Pitch deck job id."}],"post":{"operationId":"updatePitchDeckClosingSequence","tags":["Pitch Deck"],"summary":"Set the deck's closing sequence.","description":"Sets the deck's closing sequence whole (`null` removes it; the chapters read returns the current one as `closingSequence`, with each card's length, start, bell beat and the look it wears, and `fadeOut`). A deck's closing sequence ends its master, after the last chapter, and mirrors its title sequence: optionally a closing quote (`quote`: its words, quoted exactly, and where they are from, cited exactly — one that answers the opening's epigraph, often from the same source), then \"Coming Soon\" (`comingSoon`, up to 40 characters; \"Coming Soon\" when left out), then the production credit (`productionCredit`; \"Sleeper Hit Studio\" when left out), each quiet on black, faded in and out and held long enough to read. It ADDS to the deck's length: no chapter is shortened for it. When the title sequence cuts on its bell beat grid, each card starts on a beat of that same grid and the bell lane accents it. Across it the music (the score or the beds) and the bell lane fade out smoothly to silence on the master's last frame: what still sounds at the last chapter's cut plays on under the cards. A card with no direction of its own wears the opening's look (the quote the epigraph's, the credit the source credit's, \"Coming Soon\" the title's or the epigraph's background), so the deck is sandwiched by one style. It is free: nothing is rendered for it, and a rendered deck's master is stitched again with it. `enabled: false` keeps it without playing it. How its words look and move is directed separately (`closingElement` on the direct-text-motion call; read back as `textMotion`), and a save keeps those directions. Validated whole; an unknown field is refused by name (400). Free. On a draft plan it is saved on the plan; in review or complete the master is re-assembled with it (`reassembling`). Refused while a render or finish pass runs (409). Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PitchDeckClosingSequenceUpdateRequest"}}}},"responses":{"200":{"description":"The deck's closing sequence, as saved.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PitchDeckClosingSequenceUpdateAck"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/PitchDeckStateInvalid"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/pitch-decks/{jobId}/visual-grammar":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Pitch deck job id."}],"post":{"operationId":"updatePitchDeckVisualGrammar","tags":["Pitch Deck"],"summary":"Set the deck's visual grammar.","description":"Replaces the deck's visual grammar (the chapters read returns the current one as `visualGrammar`). The visual grammar is the deck's one camera language across every chapter: how its world is lit and framed, how the camera moves or holds, and how chapters hand off. It is plan words: plan coverage judges every chapter against it, and nothing rendered reads it, so changing it never makes a take out of date. An unknown field is refused by name (400). Free. On a draft plan the changed plan is queued for plan coverage again (`planCoverage`); in review or complete it is saved on the plan and nothing else happens. The answer's `edited` says which version changed. Refused while a render or finish pass runs (409). Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PitchDeckVisualGrammarUpdateRequest"}}}},"responses":{"200":{"description":"The deck's visual grammar, as saved.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PitchDeckVisualGrammarUpdateAck"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/PitchDeckStateInvalid"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/pitch-decks/{jobId}/title-sequence/motion-closes":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Pitch deck job id."}],"post":{"operationId":"renderPitchDeckTitleMotionCloses","tags":["Pitch Deck"],"summary":"Price, then make, living portraits for the title intros.","description":"A motion close is a person's LIVING PORTRAIT for their title intro: one WAN shot of their face and shoulders from their cast portrait, the person looking straight into the lens and holding still — breath, an occasional blink, lips closed — lit and graded like the deck (its Series Bible's look), at 480p, silent, 3 seconds on the 2 s bell (the flash's beat plus room), in shadow when the sequence keeps them in shadow. PRICED FIRST — TWO CALLS on the API and the CLI, two tools on MCP and in chat (quote_render_pitch_deck_title_motion_closes, then render_pitch_deck_title_motion_closes): without `confirmed` it prices every person whose intro is not yet their living portrait (`motionClosesNeeded` in the read: about 6 people × 3 s at 480p for a father and his sons), or the people named in `names` (a new take of someone who has one is priced again), and spends nothing; with `confirmed: true` it reserves exactly those lines and renders them. Allowed while the deck is in review or complete. When each lands it is checked like a take (a file that decodes, silent, its length) and the master is re-assembled with it, free; a render that fails or is refused is released (nothing charged). Read each one back as `titleSequence.motionCloses` (`livingPortrait` false: a moving close made before 2026-10-07, which no longer opens an intro). Refused before the price: a deck that has not rendered, no title sequence, every intro already its living portrait, a name the sequence does not have (400), a person whose living portrait is already on its way, a person with no cast portrait to render from, or a portrait prompt row not yet rolled out — with the fix in `details.refusal`. Requires `Idempotency-Key`. Scope: `artifact:publish` — a read-only key can neither spend nor ask what spending would cost.","security":[{"BearerApiKey":["artifact:publish"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PitchDeckTitleMotionClosesRequest"}}}},"responses":{"200":{"description":"The price (no `confirmed`) or the queued living portraits.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PitchDeckTitleMotionClosesAnswer"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/InsufficientCredits"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/PitchDeckStateInvalid"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/pitch-decks/{jobId}/motion-blur":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Pitch deck job id."}],"get":{"operationId":"getPitchDeckMotionBlur","tags":["Pitch Deck"],"summary":"Read the deck's motion blur.","description":"Returns every chapter's shots, numbered as its cut plays them (with who speaks on camera), which are chosen, the strength, the seconds chosen against the cap (`capSeconds`), and, chapter by chapter, whether the last master laid the blur (`state`: on, waiting, skipped with a `reason`, off) with the sentence the review shows; `renderBox` says whether a render box answers now. Motion blur makes chosen shots move like film: what moves in them blurs the way a camera's shutter blurs it, while still frames, faces held in frame and straight edges stay sharp. It is chosen per shot (or per chapter or beat: its moving shots, never a shot where someone speaks on camera unless that shot is named), at one of two strengths (standard, subtle; standard by default). It is free, up to 300 seconds of blurred picture per deck or trailer, and it is laid when the master is assembled, before the film finish. The sound and timing are never touched. Scope: `story:read`.","security":[{"BearerApiKey":["story:read"]}],"responses":{"200":{"description":"The deck's motion blur.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MotionBlurView"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"setPitchDeckMotionBlur","tags":["Pitch Deck"],"summary":"Choose the deck's shots to blur.","description":"Sets which shots are blurred: `{ units: [{ unit, shots? }], strength?, off? }` — `unit` is the chapter's `chapterIndex`, `shots` its shot numbers from the read, `moving` (the default: every shot where nobody speaks on camera) or `[]` to clear it; chapters not listed keep their choice; `off: true` clears every choice. Refused (400, one sentence naming the fix): a chapter with no cut, a shot number it does not have, a choice over 300 seconds. Motion blur makes chosen shots move like film: what moves in them blurs the way a camera's shutter blurs it, while still frames, faces held in frame and straight edges stay sharp. It is chosen per shot (or per chapter or beat: its moving shots, never a shot where someone speaks on camera unless that shot is named), at one of two strengths (standard, subtle; standard by default). It is free, up to 300 seconds of blurred picture per deck or trailer, and it is laid when the master is assembled, before the film finish. The sound and timing are never touched. Allowed in any state: before a master the choice is kept for the first assembly; in review, finishing or complete the master is assembled again with it (`reassembling`). Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MotionBlurSetRequest"}}}},"responses":{"200":{"description":"The motion blur now in force.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MotionBlurSetAck"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/pitch-decks/{jobId}/versions":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Pitch deck job id."}],"get":{"operationId":"listPitchDeckVersions","tags":["Pitch Deck"],"summary":"List the deck's versions.","description":"Returns every version of the deck, newest first: its `number`, `stage` (preview, finished, current; `earlyLook` when an early look), when it was published, its master, the chapters and takes it played, its `folder` of raw clips with where that copy stands (`archive.status`: pending, archiving, archived, partial, failed), and its zip (`zip.status`: none, queued, building, ready, failed; a signed `zip.url` while ready). Every time a deck's master is published (a preview, an early look or the finished deck) it is kept as a numbered version: the master, and for every chapter in order the take each shot played (with its trim, and its finished copy on a finished deck), the chapter's cut before the film finish, its narration, speech, music, sound effects and text layer, and the finish it was laid with. Each version's cuts and takes are copied, untouched, into a folder of their own, one per chapter (pitch-decks/<deck>/versions/<n>-<YYYYMMDD-HHMM>/chapters/<NN>-<title>/), with a manifest.json, so a later re-render never loses the clips an earlier version played. Reading versions is free, and a zip of a version's clips is made on request, free. Scope: `story:read`.","security":[{"BearerApiKey":["story:read"]}],"responses":{"200":{"description":"The deck's versions.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PitchDeckVersions"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/pitch-decks/{jobId}/versions/{number}":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Pitch deck job id."},{"name":"number","in":"path","required":true,"schema":{"type":"integer","minimum":1},"description":"The version number (from the list)."}],"get":{"operationId":"getPitchDeckVersion","tags":["Pitch Deck"],"summary":"Read one version's manifest and clips.","description":"Returns one version: every chapter in journey order with its cut before the film finish and the take each shot played (`takeUrl`, its `trim`, the finished `deliveryUrl` on a finished master), each with its `path` in the version's folder and its `archivedUrl` once copied (a scene group's takes sit under the chapter that holds them: `takesHeldBy`); its narration, speech clips, music, sound effects and text layer; the deck's audio timeline, interludes and title flashes; the finish, camera and motion blur it was laid with; every file of the folder (`files`, `manifest.json` first); and its zip. A number the deck does not have is 404 `pitch_deck_version_not_found`. Every time a deck's master is published (a preview, an early look or the finished deck) it is kept as a numbered version: the master, and for every chapter in order the take each shot played (with its trim, and its finished copy on a finished deck), the chapter's cut before the film finish, its narration, speech, music, sound effects and text layer, and the finish it was laid with. Each version's cuts and takes are copied, untouched, into a folder of their own, one per chapter (pitch-decks/<deck>/versions/<n>-<YYYYMMDD-HHMM>/chapters/<NN>-<title>/), with a manifest.json, so a later re-render never loses the clips an earlier version played. Reading versions is free, and a zip of a version's clips is made on request, free. Scope: `story:read`.","security":[{"BearerApiKey":["story:read"]}],"responses":{"200":{"description":"The version.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PitchDeckVersionRead"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/pitch-decks/{jobId}/versions/{number}/zip":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Pitch deck job id."},{"name":"number","in":"path","required":true,"schema":{"type":"integer","minimum":1},"description":"The version number (from the list)."}],"post":{"operationId":"requestPitchDeckVersionZip","tags":["Pitch Deck"],"summary":"Ask for a version's zip.","description":"Asks for a zip of the version's folder: its manifest.json and every clip, stored as they are (never re-encoded). Made once, off the request path, and then kept: a ready zip answers its signed `zip.url` (valid for an hour); otherwise the zip is queued (`queued: true`, `zip.status: queued`) — read the version again (`GET /pitch-decks/{jobId}/versions/{number}`) until `zip.status` is `ready`. Free. Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"200":{"description":"The zip, or that it is being made.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VideoVersionZipAck"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/pitch-decks/{jobId}/quiet-track":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Pitch deck job id."}],"get":{"operationId":"getPitchDeckQuietTrack","tags":["Pitch Deck"],"summary":"Read the deck's quiet track.","description":"Returns what the deck's renders with no spoken line are sent as their voice track (`quietTrack`: silence, room_tone, or null: none), the renders it reaches (`quietCalls`), the quiet renders too long for it (`tooLong`, over 15 s), and the takes already rendered with each track (`takesWithTrack`). The quiet track sends every render of the deck that has no spoken line a voice track with nobody speaking in it (silence, or a soft room tone), the way a spoken render is sent the cast's lines, to test whether quiet faces keep their lips closed when the render hears a track. It is off by default and free, and it changes only the takes rendered after it is set: each take records the track it was sent, and a take already made keeps what it had. A render with a spoken line, and a chapter that keeps its own sound (bells, singers, a crowd), never gets one. A chapter that played its render's own room tone plays the track (silence, or the hush) instead. Scope: `story:read`.","security":[{"BearerApiKey":["story:read"]}],"responses":{"200":{"description":"The deck's quiet track.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuietTrackView"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"setPitchDeckQuietTrack","tags":["Pitch Deck"],"summary":"Set the deck's quiet track.","description":"Sets what the deck's renders with no spoken line are sent as their voice track: `{ quietTrack }` — silence or room_tone, or null to send none again. The field is required (an absent one is refused, 400). The quiet track sends every render of the deck that has no spoken line a voice track with nobody speaking in it (silence, or a soft room tone), the way a spoken render is sent the cast's lines, to test whether quiet faces keep their lips closed when the render hears a track. It is off by default and free, and it changes only the takes rendered after it is set: each take records the track it was sent, and a take already made keeps what it had. A render with a spoken line, and a chapter that keeps its own sound (bells, singers, a crowd), never gets one. A chapter that played its render's own room tone plays the track (silence, or the hush) instead. Free and allowed in any state; nothing is rendered by setting it. Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuietTrackSetRequest"}}}},"responses":{"200":{"description":"The quiet track now in force.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuietTrackSetAck"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/pitch-decks/{jobId}/video-model":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Pitch deck job id."}],"get":{"operationId":"getPitchDeckVideoModel","tags":["Pitch Deck"],"summary":"Read the deck's video model.","description":"Returns which video model renders every chapter of the deck (`videoModel`: wan3-prime or vidu-q4; wan3-prime is the default), the rendered chapters made on the other model (`chaptersOnAnotherModel`: out of date), and the rendered chapters in all (`renderedChapters`). The video model renders every chapter of the deck: WAN 3 (the default) or Vidu Q4. Changing it keeps the plan and every edit, and marks every rendered chapter out of date: the next render of the deck is quoted again and re-renders them on the new model. Takes already made are kept as they are. A render is priced in Studio Credits per second exactly as before; the quote states the model's own cost per second. Scope: `story:read`.","security":[{"BearerApiKey":["story:read"]}],"responses":{"200":{"description":"The deck's video model.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PitchDeckVideoModelView"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"setPitchDeckVideoModel","tags":["Pitch Deck"],"summary":"Set the deck's video model.","description":"Sets which video model renders every chapter of the deck: `{ videoModel }` — wan3-prime or vidu-q4. The field is required (an absent or unknown one is refused, 400). The video model renders every chapter of the deck: WAN 3 (the default) or Vidu Q4. Changing it keeps the plan and every edit, and marks every rendered chapter out of date: the next render of the deck is quoted again and re-renders them on the new model. Takes already made are kept as they are. A render is priced in Studio Credits per second exactly as before; the quote states the model's own cost per second. Free and allowed in any state; nothing is rendered by setting it. Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PitchDeckVideoModelSetRequest"}}}},"responses":{"200":{"description":"The video model now in force.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PitchDeckVideoModelSetAck"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/pitch-decks/{jobId}/camera":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Pitch deck job id."}],"get":{"operationId":"getPitchDeckCamera","tags":["Pitch Deck"],"summary":"Read the deck's camera motion.","description":"Returns every chapter's shots, numbered as its cut plays them, each with its camera (`held`, `moving`, `handheld`), who speaks on camera and who the call renders; the handheld strength; each chapter's move; and, chapter by chapter, what the last master laid (`state`: on, waiting, partly with a `moveReason`, off) with the sentence the review shows. Camera motion gives the held shots an operator, the way a documentary camera moves: a handheld sway on the shots that were rendered still (off, subtle, medium; never on a shot whose camera already moves or was filmed handheld, and never so far that a speaking mouth leaves the frame), and at most one move per chapter or beat, on a held shot where nobody speaks on camera: a snap zoom onto a reaction (up to 1.5×, right after a line) or a slow reframe from one face to another already in the frame. It is free, and laid when the master is assembled, as the first step of the film finish and before any type. The sound and timing are never touched. Scope: `story:read`.","security":[{"BearerApiKey":["story:read"]}],"responses":{"200":{"description":"The deck's camera motion.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CameraView"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"setPitchDeckCamera","tags":["Pitch Deck"],"summary":"Set the deck's camera motion.","description":"Sets the camera motion: `{ handheld?, moves?: [{ unit, move, shot?, target?, from?, zoom? }], off? }` — `handheld` sways every held shot; a move names its chapter by `unit` (the chapter's `chapterIndex`), its shot by number from the read, `move` `snap` (onto `target`, `zoom` up to 1.5) or `reframe` (from `from` to `target`), or `none` to clear the chapter; one move per chapter; chapters not listed keep theirs; `off: true` turns it all off. Refused (400, one sentence naming the fix): a chapter with no cut, a shot number it does not have, a shot whose camera already moves or is handheld, a shot where someone speaks on camera, a shot too short for the move, a face not in the shot's cast. Camera motion gives the held shots an operator, the way a documentary camera moves: a handheld sway on the shots that were rendered still (off, subtle, medium; never on a shot whose camera already moves or was filmed handheld, and never so far that a speaking mouth leaves the frame), and at most one move per chapter or beat, on a held shot where nobody speaks on camera: a snap zoom onto a reaction (up to 1.5×, right after a line) or a slow reframe from one face to another already in the frame. It is free, and laid when the master is assembled, as the first step of the film finish and before any type. The sound and timing are never touched. Allowed in any state: before a master the choice is kept for the first assembly; in review, finishing or complete the master is assembled again with it (`reassembling`). Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CameraSetRequest"}}}},"responses":{"200":{"description":"The camera motion now in force.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CameraSetAck"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/pitch-decks/{jobId}/look":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Pitch deck job id."}],"get":{"operationId":"getPitchDeckLook","tags":["Pitch Deck"],"summary":"Read the deck's finish.","description":"Returns the film finish laid on the deck's picture at master assembly (`finish`: its look, intensity and adjustments; a new deck starts on `camera`), each effect's level in it (`effects`), and every look and intensity there is with what it does. The finish is a film look laid on the picture when the master is assembled: a look (camera, period-35mm, night-interior, none), an intensity (light, standard, strong; standard by default) and, optionally, single effects set to a level (adjust: { effect: off | subtle | medium | strong }; effects: steady-light, match-shots, halation, mist, shoulder, colour, vignette, weave, grain, soften). New decks and trailers start on camera. Titles and captions are laid after it, so type stays crisp, and the sound and timing are never touched. Setting it is free; on a rendered deck or trailer the master is assembled again with it (free), and setting the same finish again changes nothing. Scope: `story:read`.","security":[{"BearerApiKey":["story:read"]}],"responses":{"200":{"description":"The finish, each effect's level in it, and every look and intensity there is.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FinishView"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"setPitchDeckLook","tags":["Pitch Deck"],"summary":"Set the deck's finish.","description":"Sets the film finish laid on the picture when the master is assembled — the whole finish `{ finish: { look, intensity?, adjust? } }`, looks `camera`, `period-35mm`, `night-interior`, `none` (`none`: no finish); an unknown look, intensity, effect or level is refused by name. The finish is a film look laid on the picture when the master is assembled: a look (camera, period-35mm, night-interior, none), an intensity (light, standard, strong; standard by default) and, optionally, single effects set to a level (adjust: { effect: off | subtle | medium | strong }; effects: steady-light, match-shots, halation, mist, shoulder, colour, vignette, weave, grain, soften). New decks and trailers start on camera. Titles and captions are laid after it, so type stays crisp, and the sound and timing are never touched. Setting it is free; on a rendered deck or trailer the master is assembled again with it (free), and setting the same finish again changes nothing. Allowed in any state: a draft or a deck still rendering keeps it for its first master; in review, finishing or complete the master is assembled again with it (`reassembling`). Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FinishSetRequest"}}}},"responses":{"200":{"description":"The finish now in force.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FinishSetAck"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/pitch-decks/{jobId}/chapters/refine":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Pitch deck job id."}],"post":{"operationId":"refinePitchDeckChapter","tags":["Pitch Deck"],"summary":"Refine one pitch deck chapter.","description":"Edits one chapter's plan while the deck is a draft plan, in review or complete: its title (`title`, plan words that never make a take stale; never empty), what it delivers (`storyBeat`), what it shows on screen (`visualPlan`), its shot size (`shotType`, which must name a scale on the shot ladder — establishing, wide, medium or close, the scale word deciding, so \"medium close\" is a medium; a shot type that names none is refused with a 400; the render is told it, so it stales a rendered chapter's takes; '' clears it), where it is set (`location`, a canonical location key; null clears it), the camera motion and the hand-off into the next chapter, its words (narration and who speaks it, caption, text mode and treatment), composition, duration, who is in the picture (`visibleCharacters`, the named characters on camera, each with an identity reference; `featuredCharacter`), the stage of the deck's intent it serves (`arcStage`, `advancesQuestion` — plan words that never make a take stale), whether it is a rapid intercut run (`montage` — brief shots cut tight; fed to the render, so it stales the takes), and its continuity — `propContinuity`, `wardrobeContinuity` and `screenDirection`, which the render is told (\"Props: …\"), so a prop the writer is steering away from must leave them too; and `continuityNotes`, which it is never told ('' clears any of them). `imagePrompt` and `castReferenceMode` are gone — a chapter has no still, and its cast reaches the render as body figures — and either is refused by name (400). Free — it saves the plan. On a DRAFT plan nothing has rendered, so nothing goes stale: the next plan approval recompiles just this chapter, and the changed plan is queued for plan coverage again (`planCoverage`: `queued` for the latest edit — one run per burst of edits, started once the plan has been left alone for three minutes; the plan approval starts it at once and waits for its score). This is how a chapter the plan approval refused (`pitch_deck_render_blocked`, for example a sentence too long for one render) is fixed: refine it, then price the approval again. In review or complete an edit to what the chapter films marks its takes out of date (they stay playable) and a caption edit marks only its text layer; render the chapter again with `chapters/render-clip` (priced). `chapterIndex`: The chapter's `chapterIndex`, exactly as the chapters read returns it: a key, not a position — a plan may key its chapters from 0 or from 1. A person's \"chapter 2\" is the chapter whose `chapterNumber` is 2; pass that chapter's `chapterIndex`. Returns `needsRerender`, `chapterIndex`, `chapterNumber`, `planCoverage`, `shotLadderRung` (when it set a shot type: the rung it landed on), `edited` (which version changed: `draft_plan` or `rendered_deck` — tell the writer which), the deck's `jobId` and `status`, and `previewUrl`. Provide at least one editable field. Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PitchDeckChapterRefineRequest"}}}},"responses":{"200":{"description":"The saved chapter edit + a shareable chapter preview link.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PitchDeckChapterRefineAck"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/PitchDeckStateInvalid"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/pitch-decks/{jobId}/chapters/render-clip":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Pitch deck job id."}],"post":{"operationId":"renderPitchDeckChapterClip","tags":["Pitch Deck"],"summary":"Price, then render, one pitch deck chapter again.","description":"The writer's priced retake: a new 480p take of one call (`callId`) or of every call of the chapter. A chapter whose plan changed since it rendered (or that never rendered, such as one inserted in review) is recompiled first (free) and its new calls are priced. Each new take is checked like every take and may earn the call's one automatic retake at our cost (a line not heard or said in another character's voice, or an opening off its planned framing); a moderation refusal is released and never retried. TWO CALLS, PRICE FIRST: sent WITHOUT `confirmed` it prices the render and reserves nothing (`quotedOnly: true`, the `lines`, `credits`); sent again with `confirmed: true` it reserves exactly the quoted lines and queues the takes. The flag is optional rather than a required `true` because a field that must be true to validate cannot separate a confirmed call from an unconfirmed one. Refused before the price: a text-only chapter, a chapter not in the deck, a call id the chapter does not have (or a call of a changed chapter), and a cast member with no body figure — with the action that fixes it in `details.refusal` (and `details.blocked`). Review or complete only. Returns a shareable per-chapter `previewUrl`. Requires `Idempotency-Key`. Scope: `artifact:publish` — a read-only key can neither spend nor ask what spending would cost.","security":[{"BearerApiKey":["artifact:publish"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PitchDeckRenderClipRequest"}}}},"responses":{"200":{"description":"The price (no `confirmed`) or the queued takes, plus a shareable chapter preview link.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PitchDeckRenderAnswer"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/InsufficientCredits"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/PitchDeckStateInvalid"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/pitch-decks/{jobId}/chapters/{chapterIndex}/select-take":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Pitch deck job id."},{"name":"chapterIndex","in":"path","required":true,"schema":{"type":"integer","minimum":0},"description":"The chapter the call belongs to. The chapter's `chapterIndex`, exactly as the chapters read returns it: a key, not a position — a plan may key its chapters from 0 or from 1. A person's \"chapter 2\" is the chapter whose `chapterNumber` is 2; pass that chapter's `chapterIndex`."}],"post":{"operationId":"selectPitchDeckTake","tags":["Pitch Deck"],"summary":"Select a take of one of a chapter's calls.","description":"Chooses which take of one call the cut plays and the finish delivers. NOT metered and NOT confirmed: it renders nothing and is reversible (select another take), and the chapter's preview cut is re-assembled from the new selection. Any accepted take may be selected, a flagged one included; the answer's `warning` names what it is flagged for, and the finish quote and export carry it too. A take that produced no file cannot be selected, and a call of another chapter is refused (400). In a complete deck a new selection sends the deck back to review, and the next finish quotes just that take. Review or complete only. Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PitchDeckSelectTakeRequest"}}}},"responses":{"200":{"description":"The take now selected.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PitchDeckSelectTakeAck"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/PitchDeckStateInvalid"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/pitch-decks/{jobId}/chapters/{chapterIndex}/repair-take":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Pitch deck job id."},{"name":"chapterIndex","in":"path","required":true,"schema":{"type":"integer","minimum":0},"description":"The chapter the call belongs to. The chapter's `chapterIndex`, exactly as the chapters read returns it: a key, not a position — a plan may key its chapters from 0 or from 1. A person's \"chapter 2\" is the chapter whose `chapterNumber` is 2; pass that chapter's `chapterIndex`."}],"post":{"operationId":"repairPitchDeckTake","tags":["Pitch Deck"],"summary":"Price, then repair, one take of a chapter's call in place.","description":"Every rendered take is watched with its sound after its checks. A cut nobody asked for at the start or end of an otherwise good take is trimmed off first, before any new take: free, frame for frame, only where the cut is measured in the file, outside every spoken line (each kept whole with a breath after it), and only when what is left keeps its chapter or beat within a quarter of its planned length. When the shot's automatic new take is spent and the take it plays still has such a cut outside its line, that end is trimmed as a last resort: past the quarter, as long as the line stays whole, at least 2 seconds are left and the chapter or beat keeps half its planned length. A cut inside a spoken line is never trimmed. A speaker whose lips keep moving past the take's last line, where nothing is heard, is trimmed half a second after the line, free and frame for frame. Lips moving where no line is heard are never retaken on their own: the same staging brings them back, so the shot is offered for restaging (one face, framed closer), a trim, or your call. Any other defect is remedied by rule during processing, at our cost: first a new take told what to avoid; a repair in place when the same problem comes back after a new take, or when the take passed every other check and only something removable (a period anachronism, garbled lettering, a stray or doubled person) or the light is wrong. Camera movement and its speed (a move that rushes, or dies into a hold at the end), timing, cuts and a crowd moving as one (extras in step, everyone turning or looking down at once, repeated faces) are only ever fixed by a new take, or by the free trim when they sit only at an end of the take; spoken shots are not repaired in place. A line the speaker's mouth did not speak on two takes is no longer retaken on camera: the next take stages it as a voice-over, the speaker seen from behind, the line heard over the picture. Every take plays its own sound under its own picture, so the lips follow what is heard: a line said in the take's own reading is kept, and only a line not heard, or said in a voice too like another character's, is a defect (a new take). Where a speaker's mouth cannot be measured (or the face is too small to trust a miss), the watcher's own judgment of the lips decides the line: a mouth it saw not speak is a miss, one it saw speak is uncertain. Each shot gets at most one automatic new take, one automatic repair in place and one automatic voice-over re-stage, so a new take whose removable defect came back is repaired in the same pass; a repaired or trimmed take is never remedied again on its own. A repair that fails on the repair service's own side (never one declined at its content check) and cost nothing is sent again once; if that fails too, a new take instead. A repaired take keeps the take's own sound, is judged again, and is kept as a new take linked to the one it repairs; it plays only when it passes. A trimmed take plays over the take it came from when it passes, or when it shows only part of what that take showed, with the same speech and faces: it is judged by time window, so anything seen inside the span it keeps counts as that take's own footage too, and it wins when the trim cut something off. This is the writer's own repair of one take. TWO CALLS, PRICE FIRST: sent WITHOUT `confirmed` it prices the repair (the seconds it sends — the whole take, or only the shots its defect sits in — at the repair rung, two seconds at least) and reserves nothing (`quotedOnly: true`, `credits`, `summary`, `note`); sent again with `confirmed: true` it reserves exactly that line and queues the repair as a new take linked to the one it repairs (`repairTake`). The repaired take keeps the take's own sound, is checked again, and plays only if it passes; a repair that does not land is released (nothing charged). Refused before anything is reserved: the project's spend gate (`project_precondition_failed`), a take that is not an accepted take of the call, a repair of a repair (`repair_of_a_repair`), a take whose repair was declined at the content check (`repair_refused_before`, never sent again unchanged), a spoken shot (`repair_spoken_shot_held`: not repaired in place yet), repair not set up (`repair_unavailable`), a length the repair cannot take (`repair_not_possible`) — each a 409 with the fixing sentence. To render a new take instead, use the unit's render-clip with `callId`: a new take is told what the watcher saw on the takes before it. Review or complete only. Requires `Idempotency-Key`. Scope: `artifact:publish`.","security":[{"BearerApiKey":["artifact:publish"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TakeRepairRequest"}}}},"responses":{"200":{"description":"The repair's price (no `confirmed`) or the queued repair.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TakeRepairAnswer"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/InsufficientCredits"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/PitchDeckStateInvalid"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/pitch-decks/{jobId}/chapters/{chapterIndex}/take-repair-override":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Pitch deck job id."},{"name":"chapterIndex","in":"path","required":true,"schema":{"type":"integer","minimum":0},"description":"The chapter the call belongs to. The chapter's `chapterIndex`, exactly as the chapters read returns it: a key, not a position — a plan may key its chapters from 0 or from 1. A person's \"chapter 2\" is the chapter whose `chapterNumber` is 2; pass that chapter's `chapterIndex`."}],"post":{"operationId":"overridePitchDeckTakeRepair","tags":["Pitch Deck"],"summary":"Override the repair engine on one take of a chapter's call.","description":"NOT metered and NOT confirmed: free. `decision: keep` keeps the take as it is — what was found on it no longer ranks it down or warns, and the engine never acts on it; `decision: restore` lets the engine's decision stand again (both reversible). The call's default selection re-runs unless the writer chose its take explicitly, and the cut re-assembles when the take it plays changed. `decision: rewatch` watches again, at our cost, a take whose watch could not finish (its decision `unwatched`: never clean, never remedied on its own) — its checks are queued, it reads checks pending until the watch lands, and a take already watched is refused (409). Review or complete only. Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TakeRepairOverrideRequest"}}}},"responses":{"200":{"description":"The override, the take's decision after it, and the take the call plays now.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TakeRepairOverrideAck"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/PitchDeckStateInvalid"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/pitch-decks/{jobId}/chapters/{chapterIndex}/stage-voice-over":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Pitch deck job id."},{"name":"chapterIndex","in":"path","required":true,"schema":{"type":"integer","minimum":0},"description":"The chapter the call belongs to. The chapter's `chapterIndex`, exactly as the chapters read returns it: a key, not a position — a plan may key its chapters from 0 or from 1. A person's \"chapter 2\" is the chapter whose `chapterNumber` is 2; pass that chapter's `chapterIndex`."}],"post":{"operationId":"stageVoiceOverPitchDeckTake","tags":["Pitch Deck"],"summary":"Price, then stage, one take's line of a chapter's call as a voice-over in a new take.","description":"NOT a free call: METERED like a new take of the call, TWO CALLS, PRICE FIRST. Stages the line of one take as a VOICE-OVER in a new take of its call: the speaker seen from behind, the line heard over the picture, so no mouth has to follow it. The lines staged are the call's on-camera lines whose speaker's lips failed them on a take of it, else every line it says on camera. The engine does this itself, at our cost, once the speaker's lips fail the same line on two takes of a call. Sent WITHOUT `confirmed` it prices the new take and reserves nothing (`quotedOnly: true`, `credits`, `voiceOverTake`, `lineRefs`, `summary`, `balanceAfter`, `sufficient`, `note`); sent again with `confirmed: true` it reserves exactly that line, records the request on the take and queues the new take, which is checked like every take and plays only when it is the best take of the call. Refused before anything is reserved: the spend gate, a take that is not an accepted take of the call, and a call that says nothing on camera (`voice_over_nothing_to_stage`). Review or complete only.","security":[{"BearerApiKey":["artifact:publish"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StageVoiceOverRequest"}}}},"responses":{"200":{"description":"The new take's price (no `confirmed`) or the queued voice-over take.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StageVoiceOverAnswer"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/InsufficientCredits"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/PitchDeckStateInvalid"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/pitch-decks/{jobId}/chapters/{chapterIndex}/trim-take":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Pitch Deck job id."},{"name":"chapterIndex","in":"path","required":true,"schema":{"type":"integer","minimum":0},"description":"The chapter the call belongs to. The chapter's `chapterIndex`, exactly as the chapters read returns it: a key, not a position — a plan may key its chapters from 0 or from 1. A person's \"chapter 2\" is the chapter whose `chapterNumber` is 2; pass that chapter's `chapterIndex`."}],"post":{"operationId":"trimPitchDeckTake","tags":["Pitch Deck"],"summary":"Trim one take of a chapter's call to a span (free).","description":"NOT metered and NOT confirmed: FREE, one call. Trims one take to the span `[keepStartSeconds, keepEndSeconds)` — \"trim this take to <start>–<end>\" — snapped to the take's frames and kept as a NEW take linked to it (`trimTake`); the take itself stays. Never into a spoken line: a span that cuts into a line placed on the take's speech track is refused (`trim_cuts_speech`), as is a span outside the take, shorter than the shortest a take is trimmed to, or the whole take (`trim_span_invalid`) — each before anything is queued. The trimmed take is checked again and watched, and plays over the take it came from only if it passes; its cut then plays its real, shorter length. The repair engine does the same on its own when a cut nobody asked for sits at the start or end of an otherwise good take (rule `trim_edge`, measured in the file: a hard cut or the first frame of a dissolve), and when a speaker's lips run on past the take's last line where nothing is heard (rule `trim_overrun`: cut half a second after the line as the take says it). Each take's `trimBounds` says what it may keep. Review or complete only. Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TakeTrimRequest"}}}},"responses":{"200":{"description":"The trim, queued as a new take.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TakeTrimQueued"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/PitchDeckStateInvalid"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/pitch-decks/{jobId}/chapters/{chapterIndex}/drop-line":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Pitch Deck job id."},{"name":"chapterIndex","in":"path","required":true,"schema":{"type":"integer","minimum":0},"description":"The chapter whose line is cut out. The chapter's `chapterIndex`, exactly as the chapters read returns it: a key, not a position — a plan may key its chapters from 0 or from 1. A person's \"chapter 2\" is the chapter whose `chapterNumber` is 2; pass that chapter's `chapterIndex`."}],"post":{"operationId":"dropPitchDeckChapterLine","tags":["Pitch Deck"],"summary":"Cut a chapter's line out of its footage and keep the rest (free).","description":"NOT metered and NOT confirmed: FREE, one call. Cut the line, keep the rest (free, one step): the chapter's spoken line leaves its plan (its words, its voice and how it is said), and each take that says it is trimmed to the footage on one side of it — after the line when the line opens the take, before it when it closes it — measured where the take says it, with a small pad, so nothing of the line is heard. The trimmed take is the same footage, so the chapter stays current: nothing is rendered or charged, the trimmed take plays as soon as it lands, and its sound is the take's own sound with the line cut out. A line with footage on both sides is refused until you choose keep before or keep after; so is a side shorter than 1.5 s, and a trim that would leave another chapter of its scene group with no shot. Name a chapter to move the line to and its words, speaker and delivery go there, to be rendered with that chapter's own staging; otherwise add it there with a refine. The chapters read's `lineDrop` says what it would do now (the takes it trims and the span each keeps) or why it cannot; `droppedLine` shows a line already cut out and each trim's state. Calling it again on a chapter whose line was already cut out queues again only the trims that never landed (`resumed`). Refused before anything changes (409, `details.refusal.code`): `drop_line_in_middle` (choose `keep`), `drop_line_too_short`, `drop_line_empties_chapter`, `drop_line_cuts_other_line`, `drop_line_narrated`, `drop_line_not_rendered`, `drop_line_no_take`, `drop_line_unplaced`, `drop_line_target_has_line`. A complete deck goes back to review. Review or complete only. Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PitchDeckDropLineRequest"}}}},"responses":{"200":{"description":"The line, cut out: each take's trim queued as a new take.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PitchDeckDropLineAnswer"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/PitchDeckStateInvalid"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/pitch-decks/{jobId}/chapters/insert":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Pitch deck job id."}],"post":{"operationId":"insertPitchDeckChapter","tags":["Pitch Deck"],"summary":"Insert a blank chapter.","description":"Inserts a new blank chapter: `atIndex` is the position it takes (0 = first — a position, not a key), `afterChapterIndex` puts it just after the chapter with that `chapterIndex` (its key from the chapters read), and omitting both appends it. NOT metered: it renders nothing — the new chapter has no footage until it is refined and rendered (`chapters/render-clip`, priced) — and every other chapter keeps its takes. A complete deck goes back to review. A draft plan, review or complete. On a draft plan nothing has rendered: the plan is renumbered (its black interludes follow their chapters) and the changed plan is queued for plan coverage again (`planCoverage`). Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PitchDeckInsertChapterRequest"}}}},"responses":{"200":{"description":"Where the new chapter landed.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PitchDeckInsertChapterAck"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/PitchDeckStateInvalid"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/pitch-decks/{jobId}/chapters/remove":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Pitch deck job id."}],"post":{"operationId":"removePitchDeckChapter","tags":["Pitch Deck"],"summary":"Remove one chapter.","description":"Removes one chapter with everything already rendered on it; the remaining chapters are renumbered and keep their takes. NOT metered — and nothing is refunded: what the removed chapter bought is gone. A deck keeps at least one chapter. A draft plan, review or complete. On a draft plan nothing has rendered: the plan is renumbered (its black interludes follow their chapters) and the changed plan is queued for plan coverage again (`planCoverage`). Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PitchDeckRemoveChapterRequest"}}}},"responses":{"200":{"description":"The deck's length after the edit.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PitchDeckRemoveChapterAck"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/PitchDeckStateInvalid"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/pitch-decks/{jobId}/chapters/reorder":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Pitch deck job id."}],"post":{"operationId":"reorderPitchDeckChapters","tags":["Pitch Deck"],"summary":"Put the chapters in a new order.","description":"Puts the deck's chapters in a new order: `order` names every chapter's chapterIndex once, in the order they will play, or `move` puts one chapter just after another (`afterChapterIndex`) or first (`afterChapterIndex: null`). Free: nothing renders and every chapter keeps its takes; a rendered deck's video is stitched again in the new order. Chapters filmed in one render (a scene group: a chapter whose `renderedWith` names another, and that chapter) move as one block, side by side in their order; an order that pulls them apart is refused (`pitch_deck_scene_group_split`) and a move of one of them moves the whole group. The chapters are renumbered from 0 in their new order (each answer chapter's `previousChapterIndex` is its key before). How a chapter hands into the next and the black after it travel with it (a black whose chapter now ends the deck is taken out and named in `droppedInterludes`); music and sound laid on one chapter move with it, and a score composed to the old order keeps playing but no longer lands on the chapters (`score: no_longer_fits`) until it is scored again, priced. A draft plan, review or complete; on a draft the changed plan is queued for plan coverage again (`planCoverage`). A rendered deck waits while its video is being put together or its automatic fixes are running (409 `pitch_deck_reorder_assembly_running`, `pitch_deck_reorder_auto_fix_running`). A scene group pulled apart is refused with 409 `pitch_deck_state_invalid` (`details.refusal.code: pitch_deck_scene_group_split`), as is a rendered deck whose video is being put together (`pitch_deck_reorder_assembly_running`) or whose automatic fixes are reviewing or fixing it (`pitch_deck_reorder_auto_fix_running`); nothing changes. An order that does not name every chapter once is a 400. NOT metered and NOT confirmed. Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PitchDeckReorderChaptersRequest"}}}},"responses":{"200":{"description":"The chapters in their new order, and what the move did to the cut.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PitchDeckReorderChaptersAck"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/PitchDeckStateInvalid"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/pitch-decks/{jobId}/storyboard/regenerate-text":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Pitch deck job id."}],"post":{"operationId":"regeneratePitchDeckText","tags":["Pitch Deck"],"summary":"Price, then render, one chapter's text / motion-graphic layer.","description":"Renders one chapter's on-screen text / motion-graphic layer with its current caption + treatment: a text-only chapter on its own, a text-over-footage chapter composited over its cut. Metered — charges a single per-text credit (returned as `credits`). Poll GET /pitch-decks/{jobId}/chapters. TWO CALLS, PRICE FIRST: sent WITHOUT `confirmed` it prices the render and reserves nothing (`quotedOnly: true`); sent again with `confirmed: true` it reserves the single credit line and queues the render. The flag is optional rather than a required `true` because a field that must be true to validate cannot separate a confirmed call from an unconfirmed one, and it would force the price to be announced after the money moved. Preconditions run above the price: a footage-only chapter carries no on-screen text and is refused with no quote at all. Review or complete only. A confirmed call returns `reviewUrl` + a shareable per-chapter `previewUrl`. Requires `Idempotency-Key`. Scope: `artifact:publish` — a read-only key can neither spend nor ask what spending would cost.","security":[{"BearerApiKey":["artifact:publish"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PitchDeckChapterIndexRequest"}}}},"responses":{"200":{"description":"The price (no `confirmed`) or the queued render, plus a shareable chapter preview link.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PitchDeckTextRenderAnswer"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/PitchDeckStateInvalid"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/pitch-decks/{jobId}/storyboard/direct-text-motion":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Pitch deck job id."}],"post":{"operationId":"directPitchDeckTextMotion","tags":["Pitch Deck"],"summary":"Direct how a deck's text looks and moves (a chapter's caption, the title sequence's words, or the closing sequence's cards).","description":"Say in plain words how one chapter's on-screen text should move — \"type it out letter by letter, then draw an underline beneath it\" — and it is compiled into the bounded motion spec that chapter's text layer executes. NOT metered and NOT confirmed: it costs no credits and renders nothing, and re-directing REPLACES the treatment rather than adding to it, so there is nothing a confirmation would protect. Your own words are saved beside the compiled spec, so the next direction is not authored blind. Nothing MOVES on screen until the text layer is re-rendered with `storyboard/regenerate-text`, which IS metered. A chapter with nothing to move is refused with a 409 before anything is compiled — a chapter that is footage only, or one whose caption is still empty — and the refusal names the fix. THE TITLE SEQUENCE'S WORDS: send `titleElement` (title, epigraph, sourceCredit, nameSupers) instead of `chapterIndex` to direct how one looks and moves (\"KARAMAZOV huge, blood red, the ends cut off by the frame\"), or `clear: true` to put it back in the sequence's own type; the title sequence is drawn by the master, so a rendered deck's master is stitched again (free, `reassembling`) and no chapter renders. A title sequence with no such words (no epigraph yet, name supers off) is refused with a 409. THE CLOSING SEQUENCE'S CARDS: send `closingElement` (quote, comingSoon, productionCredit) to direct one the same way, or `clear: true` to put it back in the opening's look; a closing with no quote yet refuses `quote` with a 409. Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PitchDeckDirectTextMotionRequest"}}}},"responses":{"200":{"description":"The direction now on the chapter (plus a link to watch it), or on the title sequence's word (with the sequence as every read shows it).","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PitchDeckDirectTextMotionAck"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/PitchDeckSceneHasNothingToMove"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/pitch-decks/{jobId}/video-export":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Pitch deck job id."}],"post":{"operationId":"exportPitchDeckVideo","tags":["Pitch Deck"],"summary":"Re-stitch the finished pitch deck master from its current chapters and audio.","description":"Rebuilds the finished MP4 by re-running the composition over the finished chapter cuts, narration and music ALREADY on the job. Generates nothing and renders no take, so it is the supported way to fold in a music bed or a narration track that landed after the finish. Only a complete deck exports — a 480p preview is never the deliverable; finish the deck first (`finish`, priced). A queued or rendering export short-circuits rather than stacking. Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"200":{"description":"Master re-stitch queued; poll `renderedVideoStatus`.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PitchDeckExportAck"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/PitchDeckStateInvalid"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/pitch-decks/{jobId}/plan/approve":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Pitch deck job id."}],"post":{"operationId":"approvePitchDeckPlan","tags":["Pitch Deck"],"summary":"Price, then start, the render of a draft deck's plan.","description":"Approves a draft deck's chapter plan into the render — the next step of EVERY deck job: A pitch deck job always stops at its plan: it plans the chapters onto a `draft` deck (the job's `progress.pitchDeckJobId`) and renders and reserves nothing, so planning is free. The next step is approve_pitch_deck_plan (POST /pitch-decks/{jobId}/plan/approve; CLI: pitch-deck approve-plan <jobId>), priced first: its quote (quote_approve_pitch_deck_plan, or the route without `confirmed`) returns the 480p render quote and reserves nothing; with `confirmed: true` it reserves exactly the quoted calls and renders them. Approving THIS job renders the deck that was scored; creating a new plan re-runs the stochastic planner and renders an ungraded draw instead. TWO CALLS, PRICE FIRST: sent WITHOUT `confirmed` it runs the coverage gate, compiles and voices every footage chapter (free to the writer) and prices the 480p render — a line per call at its voiced seconds and a line per location plate the renders make first — reserving nothing (`quotedOnly: true`); sent again with `confirmed: true` it reserves exactly the quoted calls and starts the render (`rendering`, then `review`). A chapter's line is spoken by a cast member in their own voice when its `narrationVoice` names one — on camera, lip-synced, when they are on its `visibleCharacters`, else as a voice-over — otherwise by the narrator over the cut; a take that leaves a line unheard or says it in another character's voice, has lips out of step with its lines, or misses its opening framing is retaken once automatically at our cost. Refused before the price, with the action that fixes it (`details.refusal`, and `details.blocked` per chapter): an unscored or failing plan, a chapter whose cast has no body figure or whose speaker has no cast voice, a chapter whose words one render cannot carry. Each blocked chapter carries its `chapterIndex` and `chapterNumber`: fix it with `chapters/refine` (free, on the draft) and price the approval again — the deck is not planned again. A plan the gate refuses on the judge's scores alone says so (`details.overridable: true`, `details.overrideWhy`): the writer may render it anyway by sending `acknowledgePlanCoverage: { reason }` on the price and the confirm alike — only on their explicit choice. `details.overridable: false` cannot be rendered over (a blocking defect decided in code, or a missing, running, failed, stale or old-rubric report). The confirmed approval records the override on the report (the coverage read's `writerOverride`); the answer carries it as `planCoverageOverride`. Draft only. Requires `Idempotency-Key`. Scope: `story:write`. A pitch deck is priced by the second: 1 credit a second to render at 480p, plus 1 credit a second to finish at 720p (2 credits a second in all; a 1080p finish is 4 credits a second instead). Planning is free. The exact price is quoted per shot once the plan is ready, before anything renders; location plates and on-screen text are itemized in the same quotes.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PitchDeckPlanApproveRequest"}}}},"responses":{"200":{"description":"The 480p render quote (no `confirmed`) or the started render.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PitchDeckRenderAnswer"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/InsufficientCredits"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/PitchDeckStateInvalid"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/pitch-decks/{jobId}/finish":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Pitch deck job id."}],"post":{"operationId":"finishPitchDeck","tags":["Pitch Deck"],"summary":"Price, then run, the finish of a pitch deck.","description":"Finishes the deck: every selected take is upscaled from its 480p render to the delivery class — 720p unless the writer chose 1080p (`deliveryResolution` here, or the deck's stored choice; the plan never raises it) — the on-screen text layers are laid over the finished cuts, the finished master is stitched, and the deck's artifact follows, with its PDF only when the request asked for one (`includePdf`, off by default). Nothing is generated again, and a take already finished at the class is reused, not charged twice. TWO CALLS, PRICE FIRST: sent WITHOUT `confirmed` it prices the finish and reserves nothing (`quotedOnly: true`, the `lines`, and a `warnings` entry per flagged take the writer kept); sent again with `confirmed: true` it reserves exactly those lines and starts the finish (`finishing`, then `complete`). Refused before the price while any call has no selected take (`pitch_deck_finish_missing_takes`: stage the line as a voice-over, or render a new take) or any footage chapter has not rendered, and while the master the writer is looking at FAILED its video coverage (`pitch_deck_video_coverage_failed`, `details.videoCoverage`: the score, the bar and the findings with their fixes) — unless `acknowledgeVideoCoverage: true`, which only the writer's own choice to finish anyway may set. Coverage still running, one that could not run, or one of an earlier master never holds the finish: the quote's `warnings` say so. Review or complete (finishing again at a new class) only. Requires `Idempotency-Key`. Scope: `artifact:publish`.","security":[{"BearerApiKey":["artifact:publish"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PitchDeckFinishRequest"}}}},"responses":{"200":{"description":"The finish price (no `confirmed`) or the running finish.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PitchDeckFinishAnswer"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/InsufficientCredits"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/PitchDeckStateInvalid"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/pitch-decks/{jobId}/reassemble":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Pitch deck job id."}],"post":{"operationId":"reassemblePitchDeck","tags":["Pitch Deck"],"summary":"Re-assemble a deck whose assembly failed, or rebuild its master in review or complete (free).","description":"Puts a FAILED deck back together from the takes it already has. A deck whose assembly failed for good — `assemble`, the 480p preview, or `finish_assemble`, the finished master — lost nothing it paid for: every call's takes (and their finished upscales) are kept. This re-runs THAT stage: the deck goes back to `rendering` (then `review`) or `finishing` (then `complete`). NOT metered and NOT confirmed: nothing is rendered, upscaled or reserved. Refused with 409 `pitch_deck_state_invalid` (`details.refusal.code`: `pitch_deck_reassemble_not_failed`, `pitch_deck_reassemble_not_an_assembly_failure`, `pitch_deck_reassemble_pass_incomplete`, `pitch_deck_reassemble_missing_takes`) for a deck that is not failed, one that failed while its shots were being sent to render or to be finished, a finished master missing a finished take, or a preview with no footage chapter whose takes are all in hand — GET /pitch-decks/{jobId}/chapters `reassembly` says which before you call. The PREVIEW follows the master's own rule: a chapter whose shot has no accepted take has no footage, so it is LEFT OUT of the master (the master plays the chapters around it) rather than refusing — the answer names each one in `leftOut` and in `message` (`reassembly.leftOut` says so before you call); tell the writer which chapter is missing, and render it again to put it back. The finished master never leaves a chapter out. A deck in `review` or `complete` has its master REBUILT the same way, free and FRESH, with the current sound rules (the only voices in a master are the cast's): in review the preview assembly runs (`stage: assemble`) — every cut an older recipe made is re-cut with the current one — and the deck STAYS in review; complete, the finished master is stitched again at its class (`stage: restitch`): the finished cuts as they are (their text layers are laid over them), the narration, a black's sound and the score laid again with the current rules, and the deck STAYS complete (a new finish re-cuts the finished cuts). The answer's `status` is where the deck stays (`review` / `complete`). The selected takes are never changed; the new master is watched for video coverage at our cost. A rebuild is refused with 409 `pitch_deck_reassemble_assembly_in_flight` while a master is already being assembled for the deck (queued or running), `pitch_deck_reassemble_take_in_flight` while a shot has a take being rendered or finished, and `pitch_deck_reassemble_missing_takes` for a finished deck whose selected take is not finished at its class (finish it again). Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"200":{"description":"The stage it runs and the status the deck is in while it runs (where it stays, for a rebuild).","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RenderReassembleAck"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/PitchDeckStateInvalid"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/pitch-decks/{jobId}/chapters/{chapterIndex}/abandon-take":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Pitch deck job id."},{"name":"chapterIndex","in":"path","required":true,"schema":{"type":"integer","minimum":0},"description":"The chapter the call belongs to. The chapter's `chapterIndex`, exactly as the chapters read returns it: a key, not a position — a plan may key its chapters from 0 or from 1. A person's \"chapter 2\" is the chapter whose `chapterNumber` is 2; pass that chapter's `chapterIndex`."}],"post":{"operationId":"abandonPitchDeckTake","tags":["Pitch Deck"],"summary":"Let one take still out at the provider go (free; it gives back what was held).","description":"Let ONE take of the chapter's call go when it is still out at the provider (handed back pending between two polls) — the per-take counterpart of Stop render (`POST /pitch-decks/{jobId}/cancel-render`), on the same core. Its claim comes off the shot, the take is recorded failed with a plain notice on the chapter, the writer's credits held for it are given back (`releasedCredits`), and the automatic fixes carry on. The provider may still bill it: that is ours, never the writer's. Every other take is kept. A take that is not out at a provider answers `abandoned: false` and changes nothing; one being polled right now is refused (409, `details.refusal.code` `reference_render_take_busy`: try again in a minute). The engine does the same on its own when a take's polls run out, and tells the writer. Answers `{ jobId, surface, callId, take, abandoned, kind, taskId, releasedCredits, note }`; read `GET /pitch-decks/{jobId}/chapters` after it. NOT metered and NOT confirmed: free. Requires `Idempotency-Key`. Scope: `artifact:publish` — the key that can start and spend on renders is the key that can let paid work in flight go.","security":[{"BearerApiKey":["artifact:publish"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TakeAbandonRequest"}}}},"responses":{"200":{"description":"Whether the take was let go, what it was waiting on, and what was given back.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TakeAbandonResult"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/PitchDeckStateInvalid"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/pitch-decks/{jobId}/cancel-render":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Pitch deck job id."}],"post":{"operationId":"cancelPitchDeckRender","tags":["Pitch Deck"],"summary":"Stop the deck's render in flight (free; it gives back what was held).","description":"Stop the deck's render in flight — its first pass, its finish, or a retake or repair still running. Nothing more is sent to a provider: its queued and running render jobs are cancelled, and a job already running stops at its next step. The writer's reservations for work that had not run are released (`released`, `releasedCredits`); every accepted take, selection, cut, finished take and master is kept (`keptTakes`). A shot already out at the provider is stopped and recorded on its chapter (`abandoned`): not charged to the writer (the provider may still bill it, at our cost), and never resumed. It lands in `review` with what rendered, back at `draft` when nothing did (approving again quotes the same shots), back in `review` after a stopped finish, or as it was after a stopped retake. Free: no price and nothing to confirm. With nothing rendering it answers `stopped: false` and changes nothing. Answers `{ jobId, surface, stopped, previousStatus, status, cancelledJobs, abandoned, released, releasedCredits, keptTakes, note }`; read `GET /pitch-decks/{jobId}/chapters` after it. NOT metered and NOT confirmed. Requires `Idempotency-Key`. Scope: `artifact:publish` — the key that can start and spend on renders is the key that can stop them.","security":[{"BearerApiKey":["artifact:publish"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"200":{"description":"What the stop did: the status it lands in, what it gave back, what it abandoned and kept.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RenderCancelResult"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/PitchDeckStateInvalid"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/pitch-decks/{jobId}/more-fixes":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Pitch deck job id."}],"post":{"operationId":"allowMorePitchDeckFixes","tags":["Pitch Deck"],"summary":"Let the deck's automatic fixes try more (priced: one more allowance grant).","description":"The render approval carries an automatic-fix allowance: the engine watches the 480p preview and retakes the shots its review flags, inside that ceiling, until the preview is clean or it stops short (GET /pitch-decks/{jobId}/chapters `autoFix`: `state: stopped`, `stopReason`, `stoppedBecause`). When it stopped for want of credits or rounds, or because a flagged shot reached its tries, `autoFix.moreFixes` offers ONE MORE GRANT and `verdict.choices` carries `more_fixes`. METERED, TWO CALLS: without `confirmed` it PRICES the grant and reserves nothing (`quotedOnly: true`, `credits`, `costLabel`, `balanceAfter`, `sufficient`, `note`; when `sufficient` is false do not confirm); with `confirmed: true` it reserves it, raises the loop's caps (a round of fixes and a try per shot more) and the loop answers the same review again (`quotedOnly: false`, `queued: true`, `message`, `autoFix`). Only what the fixes draw is charged; the rest comes back when the loop stops. Refused with 409 `pitch_deck_state_invalid` (`details.refusal.code`: `auto_fix_not_in_review`, `auto_fix_nothing_more` — still fixing, or nothing a new take or a text render would fix: finish anyway instead — `auto_fix_already_running`), 402 when the balance cannot hold the grant. Review only. Requires `Idempotency-Key`. Scope: `artifact:publish`.","security":[{"BearerApiKey":["artifact:publish"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MoreFixesRequest"}}}},"responses":{"200":{"description":"The price of one more grant, or the grant reserved and the loop running again.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MoreFixesAnswer"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/InsufficientCredits"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/PitchDeckStateInvalid"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/pitch-decks/{jobId}/video-coverage":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Pitch deck job id."}],"get":{"operationId":"getPitchDeckVideoCoverage","tags":["Pitch Deck"],"summary":"Read the deck's video coverage.","description":"Returns the latest VIDEO COVERAGE report on the deck: the stitched master (the 480p preview, then the finished master) watched with its sound and scored on its quality and its adherence to the shot list — lip sync, prompt adherence, visual fidelity, motion naturalness, identity continuity, text placement, audio sync — never its story. It says which master it watched and whether that is the master the deck has now (`current`), the score against the bar (`gateThreshold`, with `vetoDimensions` that fail it on their own), every finding with its timecode located in code on chapters, calls and shots and the remedy that fixes it, and whether it holds the paid finish (`blocksFinish`; then the finish is refused with `pitch_deck_video_coverage_failed` unless the writer finishes anyway). It runs by itself after every stitch; a deck with no report answers `status: \"missing\"`, never a 404. The same object is `videoCoverage` on GET /pitch-decks/{jobId}/chapters. Scope: `story:read`.","security":[{"BearerApiKey":["story:read"]}],"responses":{"200":{"description":"Pitch deck video coverage envelope.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VideoCoverageEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"generatePitchDeckVideoCoverage","tags":["Pitch Deck"],"summary":"Cover the deck's current master again (free).","description":"Watches the deck's CURRENT stitched master again as a new report version. NOT metered and NOT confirmed: the watch costs the writer nothing, so there is no price and no `confirmed`. Coverage already runs by itself, idempotent per master, when the preview is stitched (review) and again when the finished master is (complete); call this to cover the master again (with an optional `focusPrompt`). A report already running on this master is returned as it is. Refused with 400 for a deck with no stitched master, one that is not in review or complete, or when no provider can watch a video right now (that never holds the finish). Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VideoCoverageGenerateRequest"}}}},"responses":{"200":{"description":"The queued (or already running) video coverage envelope.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VideoCoverageEnvelope"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/pitch-decks/{jobId}/resolution":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Pitch deck job id."}],"get":{"operationId":"getPitchDeckResolution","tags":["Pitch Deck"],"summary":"Read the deck's delivery class.","description":"Returns the class the finish delivers the deck at: `deliveryResolution` (720p unless the writer chose 1080p — the plan never raises it), `isDefault` (true when nobody chose on the deck, so the 720p default applies — say so rather than reporting the default as a decision), `renderResolution` (every take renders at 480p first, whatever the class), `options`, and `editable` (false while a render or finish pass is running). Raising the class upscales the selected takes at finish: it quotes a finish, never a render. Scope: `story:read`.","security":[{"BearerApiKey":["story:read"]}],"responses":{"200":{"description":"The deck's delivery class.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"type":"object","required":["resolution"],"additionalProperties":false,"properties":{"resolution":{"$ref":"#/components/schemas/PitchDeckDeliveryResolution"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"setPitchDeckResolution","tags":["Pitch Deck"],"summary":"Choose the delivery class for this deck.","description":"Choose the class the finish delivers the deck at — 720p (the default) or 1080p — or send `resolution: null` to clear the choice back to the default. Storing it is free and changes no render: every shot renders at 480p first, and raising the class upscales the selected takes at finish, so it quotes a finish of the takes you keep, never a render. A finished (complete) deck goes back to review so it can be finished again at the new class. Every class renders its first pass at 480p, finished by upscaling: about $0.068/s at 480p, $0.078/s at 720p and $0.088/s at 1080p (the 480p first pass plus $0.01/s or $0.02/s to upscale; confirmed rates), so a 90-second cut comes to about $6.12, $7.02 or $7.92. Refused (409) while a render or finish pass is running. Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["resolution"],"additionalProperties":false,"properties":{"resolution":{"type":"string","nullable":true,"enum":["720p","1080p",null],"description":"The delivery class, or null to clear the choice back to the default."}}}}}},"responses":{"200":{"description":"The delivery class now in force.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"type":"object","required":["resolution"],"additionalProperties":false,"properties":{"resolution":{"$ref":"#/components/schemas/PitchDeckDeliveryResolution"}}}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/PitchDeckStateInvalid"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/trailers/{jobId}/beats":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Trailer job id."}],"get":{"operationId":"getTrailerBeats","tags":["Trailer"],"summary":"Read a trailer's beats, their calls and every take.","description":"Returns the trailer's beats as they render: each beat's plan fields and its render calls (at most 10s each) — duration, audio mode, the plate rung the render opens on, the cast on camera, the shots and the lines spoken in them, the cast's speech track — and every take with its checks: whether each line was heard in a voice of its own (`voiceMissLineRefs`: a line not heard, or said in a voice too like another character's — a flag; `ownReadingLineRefs`: a line said in the take's own voice, heard and distinct, kept and never a flag), whether it opened on its planned framing (`openingMiss`), whether a mouth did not do what its line asks — the speaker's not following an on-camera line, or lips moving under someone else's voice (`mouthMiss`, with the `mouths` check) — whether it was the call's one automatic retake at our cost (`atOurCost`), whether it was not checked for a person shown twice (`duplicateNotChecked`: the check could not judge it even on its retry — never a pass, and it ranks below checked takes), whether its lip sync was not checked (`mouthNotChecked`: no on-camera speaker's mouth could be read, even after the face locator's denser retry — never a pass and never a retake, and it ranks below a take whose mouths were measured passing), whether its lip sync is uncertain (`mouthUncertain`: measured, but a reading sat inside the band around its limit, so the same file could read either side of it — never a pass and never a retake, ranked below a measured pass and above a measured miss), each criterion the mouth check missed or was uncertain on, with its line, face, number and limit (`mouthFindings`), a content refusal (released, never retried; `reference_render_provider_ip_refused` is a shot refused as possibly someone else's intellectual property, whose notice names what to edit before it renders again), the selection, what a priced retake would cost, and the repair engine's state: what the take watcher saw (`watch`), the remedy decided and the rule that fired (`repair`), where a take came from (`lineage`: repaired from, trimmed from, or a new take after, an earlier take), what a repair cost (`repairCost`), what repairing it in place would cost now (`repairQuote`), what a free trim of it may keep (`trimBounds`: never into a spoken line) and, on a trimmed take, the span it kept (`trim`: its cut plays that length). Also the beat's cut and poster, `stale` when the plan changed since it rendered, the master (a 480p preview in review, finished once the finish completes), the delivery class, the warnings the finish and export carry, and `reviewUrl`. A read: it never compiles, renders or makes a plate. Scope: `story:read`.","security":[{"BearerApiKey":["story:read"]}],"responses":{"200":{"description":"The trailer's beats, calls and takes + a browser review link.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrailerBeats"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/trailers/{jobId}/render-states":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Trailer job id."}],"get":{"operationId":"getTrailerRenderStates","tags":["Trailer"],"summary":"Read every trailer beat's render state and credit trail.","description":"Returns every beat's clip and text render state as the queue, the manifest and the credit ledger actually see it, with each attempt's own credit trail. Where GET /trailers/{jobId}/beats shows the plan, the takes and the cut, THIS shows why a beat reads `queued` / `running` / `ready` / `failed` / `never_submitted` and whether an attempt cost money. Each `state` is DERIVED, never asserted — `ready` (the asset is in the manifest, the only state that means done), `queued` / `running` (the queue still holds the recorded job id), `completed` (the queue finished but the asset is NOT in the manifest: the worker ran and wrote nothing durable), `failed` (the queue marked it failed/cancelled/expired; `error` carries the worker's payload), `missing` (a job id was recorded but the queue has no such job), `unknown` (the queue could not be reached — say \"unknown\", never guess), `never_submitted` (no job id and no asset). `attempt` is the recorded attempt INDEPENDENT of the asset — a re-render over an existing still reads `state: ready` while the new attempt is still queued — with its own queue state, error, `creditIdempotencyKey`, `providerTaskId`, `providerModelId` and its `timeline` (submitted → enqueued → provider-started → completed → persisted). `credits` is THE CREDIT TRAIL for the beat, newest first: each row's `reason` is its current ledger position — `RESERVE` (held, the worker still owes the render), `CONSUME_PROVISION` (settled: the render landed and the credits are spent), `RELEASE` (returned: the attempt died before spending). A read: it never compiles, renders, voices or makes a plate. Scope: `story:read`.","security":[{"BearerApiKey":["story:read"]}],"responses":{"200":{"description":"Every beat's clip and text render state and credit trail.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrailerBeatRenderStates"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/trailers/{jobId}/duplicate":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"The trailer to copy the plan from."}],"post":{"operationId":"duplicateTrailer","tags":["Trailer"],"summary":"Duplicate a trailer from its current plan onto a new draft (free).","description":"Duplicating a trailer makes a NEW `draft` trailer that carries the source trailer's CURRENT plan — every beat with its edits, its hook, its intent, its montage marks, its black interludes and its closing sequence — and the choices made on the trailer itself (its shape, its audio policy, the narrator cast for it, its delivery class, its film finish, its quiet track and its name), and leaves the source trailer untouched. Nothing rendered rides along: no takes, cuts, masters, versions, share links or coverage reports. It is free and renders and reserves nothing; the copy's plan is queued for coverage, and its next step is approve_trailer_plan on the NEW trailer's jobId (CLI: trailer approve-plan <newJobId>). Any status may be duplicated — a draft, a trailer in review or complete, a failed one — as long as its plan reads as a plan today; one that does not is refused with 409 `trailer_state_invalid` (`details.refusal.code: trailer_duplicate_no_plan`) and nothing is created. `title` (optional) names the copy. NOT metered and NOT confirmed. Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrailerDuplicateRequest"}}}},"responses":{"200":{"description":"The new draft trailer.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrailerDuplicate"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/TrailerStateInvalid"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/trailers/{jobId}":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Trailer job id."}],"delete":{"operationId":"deleteTrailer","tags":["Trailer"],"summary":"Delete a trailer job you own.","description":"Removes a trailer you will not pursue: a failed or abandoned attempt, a draft, or one rendering, finishing or in review. Irreversible: its takes and masters go with it. Deleting it first stops anything still rendering or finishing, and every credit held for work that had not run comes back to your balance (`releasedCredits`). A PUBLISHED trailer is refused with a validation error: unpublish it first, its share link is live.","security":[{"BearerApiKey":["story:write"]}],"responses":{"200":{"description":"The deleted job id and the credits that came back.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VideoDeleteResult"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"patch":{"operationId":"renameTrailer","tags":["Trailer"],"summary":"Rename a trailer (free).","description":"Renames a trailer: the name every list of the project's videos shows it by. A display name only: its plan, its review and its video never read it, so nothing goes out of date and nothing renders. Free and reversible; `title: null` gives it back the name made from its theme and length. Versions that share a name are listed together, so a new name lists this one on its own. Any status. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ArtifactRenameRequest"}}}},"responses":{"200":{"description":"The trailer's name now.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RenamedVideo"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/trailers/{jobId}/motion-blur":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Trailer job id."}],"get":{"operationId":"getTrailerMotionBlur","tags":["Trailer"],"summary":"Read the trailer's motion blur.","description":"Returns every beat's shots, numbered as its cut plays them (with who speaks on camera), which are chosen, the strength, the seconds chosen against the cap (`capSeconds`), and, beat by beat, whether the last master laid the blur (`state`: on, waiting, skipped with a `reason`, off) with the sentence the review shows; `renderBox` says whether a render box answers now. Motion blur makes chosen shots move like film: what moves in them blurs the way a camera's shutter blurs it, while still frames, faces held in frame and straight edges stay sharp. It is chosen per shot (or per chapter or beat: its moving shots, never a shot where someone speaks on camera unless that shot is named), at one of two strengths (standard, subtle; standard by default). It is free, up to 300 seconds of blurred picture per deck or trailer, and it is laid when the master is assembled, before the film finish. The sound and timing are never touched. Scope: `story:read`.","security":[{"BearerApiKey":["story:read"]}],"responses":{"200":{"description":"The trailer's motion blur.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MotionBlurView"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"setTrailerMotionBlur","tags":["Trailer"],"summary":"Choose the trailer's shots to blur.","description":"Sets which shots are blurred: `{ units: [{ unit, shots? }], strength?, off? }` — `unit` is the beat's `beatIndex`, `shots` its shot numbers from the read, `moving` (the default: every shot where nobody speaks on camera) or `[]` to clear it; beats not listed keep their choice; `off: true` clears every choice. Refused (400, one sentence naming the fix): a beat with no cut, a shot number it does not have, a choice over 300 seconds. Motion blur makes chosen shots move like film: what moves in them blurs the way a camera's shutter blurs it, while still frames, faces held in frame and straight edges stay sharp. It is chosen per shot (or per chapter or beat: its moving shots, never a shot where someone speaks on camera unless that shot is named), at one of two strengths (standard, subtle; standard by default). It is free, up to 300 seconds of blurred picture per deck or trailer, and it is laid when the master is assembled, before the film finish. The sound and timing are never touched. Allowed in any state: before a master the choice is kept for the first assembly; in review, finishing or complete the master is assembled again with it (`reassembling`). Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MotionBlurSetRequest"}}}},"responses":{"200":{"description":"The motion blur now in force.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MotionBlurSetAck"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/trailers/{jobId}/versions":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Trailer job id."}],"get":{"operationId":"listTrailerVersions","tags":["Trailer"],"summary":"List the trailer's versions.","description":"Returns every version of the trailer, newest first: its `number`, `stage` (preview, finished, current; `earlyLook` when an early look), when it was published, its master, the beats and takes it played, its `folder` of raw clips with where that copy stands (`archive.status`: pending, archiving, archived, partial, failed), and its zip (`zip.status`: none, queued, building, ready, failed; a signed `zip.url` while ready). Every time a trailer's master is published (a preview, an early look or the finished trailer) it is kept as a numbered version, so re-assembling a trailer never loses the master it replaces: the master, and for every beat in order the take each shot played (with its trim, and its finished copy on a finished trailer), the beat's cut before the film finish, its narration, speech, music, sound effects and text layer, and the finish it was laid with. Each version's cuts and takes are copied, untouched, into a folder of their own, one per beat (trailers/<trailer>/versions/<n>-<YYYYMMDD-HHMM>/beats/<NN>-<label>/), with a manifest.json, so a later re-render never loses the clips an earlier version played. Reading versions is free, and a zip of a version's clips is made on request, free. Scope: `story:read`.","security":[{"BearerApiKey":["story:read"]}],"responses":{"200":{"description":"The trailer's versions.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrailerVersions"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/trailers/{jobId}/versions/{number}":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Trailer job id."},{"name":"number","in":"path","required":true,"schema":{"type":"integer","minimum":1},"description":"The version number (from the list)."}],"get":{"operationId":"getTrailerVersion","tags":["Trailer"],"summary":"Read one trailer version's manifest and clips.","description":"Returns one version: every beat in plan order with its cut before the film finish and the take each shot played (`takeUrl`, its `trim`, the finished `deliveryUrl` on a finished master), each with its `path` in the version's folder and its `archivedUrl` once copied; its narration, speech clips, music, sound effects and text layer; the trailer's audio timeline; the finish, camera and motion blur it was laid with; every file of the folder (`files`, `manifest.json` first); and its zip. A number the trailer does not have is 404 `trailer_version_not_found`. Every time a trailer's master is published (a preview, an early look or the finished trailer) it is kept as a numbered version, so re-assembling a trailer never loses the master it replaces: the master, and for every beat in order the take each shot played (with its trim, and its finished copy on a finished trailer), the beat's cut before the film finish, its narration, speech, music, sound effects and text layer, and the finish it was laid with. Each version's cuts and takes are copied, untouched, into a folder of their own, one per beat (trailers/<trailer>/versions/<n>-<YYYYMMDD-HHMM>/beats/<NN>-<label>/), with a manifest.json, so a later re-render never loses the clips an earlier version played. Reading versions is free, and a zip of a version's clips is made on request, free. Scope: `story:read`.","security":[{"BearerApiKey":["story:read"]}],"responses":{"200":{"description":"The version.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrailerVersionRead"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/trailers/{jobId}/versions/{number}/zip":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Trailer job id."},{"name":"number","in":"path","required":true,"schema":{"type":"integer","minimum":1},"description":"The version number (from the list)."}],"post":{"operationId":"requestTrailerVersionZip","tags":["Trailer"],"summary":"Ask for a trailer version's zip.","description":"Asks for a zip of the version's folder: its manifest.json and every clip, stored as they are (never re-encoded). Made once, off the request path, and then kept: a ready zip answers its signed `zip.url` (valid for an hour); otherwise the zip is queued (`queued: true`, `zip.status: queued`) — read the version again (`GET /trailers/{jobId}/versions/{number}`) until `zip.status` is `ready`. Free. Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"200":{"description":"The zip, or that it is being made.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VideoVersionZipAck"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/trailers/{jobId}/camera":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Trailer job id."}],"get":{"operationId":"getTrailerCamera","tags":["Trailer"],"summary":"Read the trailer's camera motion.","description":"Returns every beat's shots, numbered as its cut plays them, each with its camera (`held`, `moving`, `handheld`), who speaks on camera and who the call renders; the handheld strength; each beat's move; and, beat by beat, what the last master laid (`state`: on, waiting, partly with a `moveReason`, off) with the sentence the review shows. Camera motion gives the held shots an operator, the way a documentary camera moves: a handheld sway on the shots that were rendered still (off, subtle, medium; never on a shot whose camera already moves or was filmed handheld, and never so far that a speaking mouth leaves the frame), and at most one move per chapter or beat, on a held shot where nobody speaks on camera: a snap zoom onto a reaction (up to 1.5×, right after a line) or a slow reframe from one face to another already in the frame. It is free, and laid when the master is assembled, as the first step of the film finish and before any type. The sound and timing are never touched. Scope: `story:read`.","security":[{"BearerApiKey":["story:read"]}],"responses":{"200":{"description":"The trailer's camera motion.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CameraView"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"setTrailerCamera","tags":["Trailer"],"summary":"Set the trailer's camera motion.","description":"Sets the camera motion: `{ handheld?, moves?: [{ unit, move, shot?, target?, from?, zoom? }], off? }` — `handheld` sways every held shot; a move names its beat by `unit` (the beat's `beatIndex`), its shot by number from the read, `move` `snap` (onto `target`, `zoom` up to 1.5) or `reframe` (from `from` to `target`), or `none` to clear the beat; one move per beat; beats not listed keep theirs; `off: true` turns it all off. Refused (400, one sentence naming the fix): a beat with no cut, a shot number it does not have, a shot whose camera already moves or is handheld, a shot where someone speaks on camera, a shot too short for the move, a face not in the shot's cast. Camera motion gives the held shots an operator, the way a documentary camera moves: a handheld sway on the shots that were rendered still (off, subtle, medium; never on a shot whose camera already moves or was filmed handheld, and never so far that a speaking mouth leaves the frame), and at most one move per chapter or beat, on a held shot where nobody speaks on camera: a snap zoom onto a reaction (up to 1.5×, right after a line) or a slow reframe from one face to another already in the frame. It is free, and laid when the master is assembled, as the first step of the film finish and before any type. The sound and timing are never touched. Allowed in any state: before a master the choice is kept for the first assembly; in review, finishing or complete the master is assembled again with it (`reassembling`). Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CameraSetRequest"}}}},"responses":{"200":{"description":"The camera motion now in force.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CameraSetAck"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/trailers/{jobId}/quiet-track":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Trailer job id."}],"get":{"operationId":"getTrailerQuietTrack","tags":["Trailer"],"summary":"Read the trailer's quiet track.","description":"Returns what the trailer's renders with no spoken line are sent as their voice track (`quietTrack`: silence, room_tone, or null: none), the renders it reaches (`quietCalls`), the quiet renders too long for it (`tooLong`, over 15 s), and the takes already rendered with each track (`takesWithTrack`). The quiet track sends every render of the trailer that has no spoken line a voice track with nobody speaking in it (silence, or a soft room tone), the way a spoken render is sent the cast's lines, to test whether quiet faces keep their lips closed when the render hears a track. It is off by default and free, and it changes only the takes rendered after it is set: each take records the track it was sent, and a take already made keeps what it had. A render with a spoken line never gets one. A beat that played its render's own room tone plays the track (silence, or the hush) instead. Scope: `story:read`.","security":[{"BearerApiKey":["story:read"]}],"responses":{"200":{"description":"The trailer's quiet track.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuietTrackView"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"setTrailerQuietTrack","tags":["Trailer"],"summary":"Set the trailer's quiet track.","description":"Sets what the trailer's renders with no spoken line are sent as their voice track: `{ quietTrack }` — silence or room_tone, or null to send none again. The field is required (an absent one is refused, 400). The quiet track sends every render of the trailer that has no spoken line a voice track with nobody speaking in it (silence, or a soft room tone), the way a spoken render is sent the cast's lines, to test whether quiet faces keep their lips closed when the render hears a track. It is off by default and free, and it changes only the takes rendered after it is set: each take records the track it was sent, and a take already made keeps what it had. A render with a spoken line never gets one. A beat that played its render's own room tone plays the track (silence, or the hush) instead. Free and allowed in any state; nothing is rendered by setting it. Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuietTrackSetRequest"}}}},"responses":{"200":{"description":"The quiet track now in force.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuietTrackSetAck"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/trailers/{jobId}/look":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Trailer job id."}],"get":{"operationId":"getTrailerLook","tags":["Trailer"],"summary":"Read the trailer's finish.","description":"Returns the film finish laid on the trailer's picture at master assembly (`finish`: its look, intensity and adjustments; a new trailer starts on `camera`), each effect's level in it (`effects`), and every look and intensity there is with what it does. The finish is a film look laid on the picture when the master is assembled: a look (camera, period-35mm, night-interior, none), an intensity (light, standard, strong; standard by default) and, optionally, single effects set to a level (adjust: { effect: off | subtle | medium | strong }; effects: steady-light, match-shots, halation, mist, shoulder, colour, vignette, weave, grain, soften). New decks and trailers start on camera. Titles and captions are laid after it, so type stays crisp, and the sound and timing are never touched. Setting it is free; on a rendered deck or trailer the master is assembled again with it (free), and setting the same finish again changes nothing. Scope: `story:read`.","security":[{"BearerApiKey":["story:read"]}],"responses":{"200":{"description":"The finish, each effect's level in it, and every look and intensity there is.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FinishView"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"setTrailerLook","tags":["Trailer"],"summary":"Set the trailer's finish.","description":"Sets the film finish laid on the picture when the master is assembled — the whole finish `{ finish: { look, intensity?, adjust? } }`, looks `camera`, `period-35mm`, `night-interior`, `none` (`none`: no finish); an unknown look, intensity, effect or level is refused by name. The finish is a film look laid on the picture when the master is assembled: a look (camera, period-35mm, night-interior, none), an intensity (light, standard, strong; standard by default) and, optionally, single effects set to a level (adjust: { effect: off | subtle | medium | strong }; effects: steady-light, match-shots, halation, mist, shoulder, colour, vignette, weave, grain, soften). New decks and trailers start on camera. Titles and captions are laid after it, so type stays crisp, and the sound and timing are never touched. Setting it is free; on a rendered deck or trailer the master is assembled again with it (free), and setting the same finish again changes nothing. Allowed in any state: a draft or a trailer still rendering keeps it for its first master; in review, finishing or complete the master is assembled again with it (`reassembling`). Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FinishSetRequest"}}}},"responses":{"200":{"description":"The finish now in force.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FinishSetAck"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/trailers/{jobId}/resolution":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Trailer job id."}],"get":{"operationId":"getTrailerResolution","tags":["Trailer"],"summary":"Read the cut's delivery class.","description":"Returns the class the finish delivers the cut at: `deliveryResolution` (720p unless the writer asked for 1080p), `isDefault` (true when nobody has chosen — say so rather than reporting the default as a decision), `renderResolution` (every take renders at 480p first, whatever the class), `options`, and `editable` (false while a render or finish pass is running). Raising the class upscales the approved takes at finish: it quotes a finish, never a render. Scope: `story:read`.","security":[{"BearerApiKey":["story:read"]}],"responses":{"200":{"description":"The cut's delivery class.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"type":"object","required":["resolution"],"additionalProperties":false,"properties":{"resolution":{"$ref":"#/components/schemas/TrailerDeliveryResolution"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"setTrailerResolution","tags":["Trailer"],"summary":"Choose the delivery class for this cut.","description":"Choose the class the finish delivers the cut at — 720p (the default) or 1080p — or send `resolution: null` to clear the choice back to 720p. Storing it is free and changes no render: every shot renders at 480p first, and raising the class upscales the approved takes at finish, so it quotes a finish of the takes you keep, never a render. A finished (complete) cut goes back to review so it can be finished again at the new class. Every class renders its first pass at 480p, finished by upscaling: about $0.068/s at 480p, $0.078/s at 720p and $0.088/s at 1080p (the 480p first pass plus $0.01/s or $0.02/s to upscale; confirmed rates), so a 90-second cut comes to about $6.12, $7.02 or $7.92. Refused (409) while a render or finish pass is running. Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["resolution"],"additionalProperties":false,"properties":{"resolution":{"type":"string","nullable":true,"enum":["720p","1080p",null],"description":"The delivery class, or null to clear the choice back to the default (720p)."}}}}}},"responses":{"200":{"description":"The delivery class now in force.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"type":"object","required":["resolution"],"additionalProperties":false,"properties":{"resolution":{"$ref":"#/components/schemas/TrailerDeliveryResolution"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/TrailerStateInvalid"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/trailers/{jobId}/narrator":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Trailer job id."}],"get":{"operationId":"getTrailerNarrator","tags":["Trailer"],"summary":"Read the cut's narrator.","description":"Returns the voice that narrates this cut and the cut's audio policy. A `narrate` trailer speaks each beat's `narrationText` in ONE voice, rendered as a separate per-beat track and mixed over the finished picture. The narrator is the voice cast for this cut, else — by default — the cast canon's NARRATOR (`source`: `override` or `cast_canon`; `overridden` true for the first). `voiceId: null` means neither exists — correct for a caption-carried (`suppress`) cut, and blocking for a narrated one, because every beat's voiceover is gated on a narrator existing. Scope: `story:read`.","security":[{"BearerApiKey":["story:read"]}],"responses":{"200":{"description":"The cut's narrator and audio policy.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"setTrailerNarrator","tags":["Trailer"],"summary":"Cast the narrator for this cut.","description":"Choose the voice that narrates this trailer, or send `voiceId: null` to clear it (the cut then narrates in the cast canon's NARRATOR when the canon casts one). The narrator belongs to the TRAILER: it need not be a cast member and need not match the table read's narrator. Storing the choice costs nothing — the voice is spoken at render — and re-casting is cheap, because narration is a separate track mixed over the finished cut, so a change re-renders audio and never a frame. Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["voiceId"],"additionalProperties":false,"properties":{"voiceId":{"type":"string","nullable":true,"description":"Studio voice id, or null to clear the narrator."},"provider":{"type":"string","nullable":true,"enum":["cartesia","elevenlabs","openai","deepgram","hume",null],"description":"The voice's provider. Inferred when omitted."}}}}}},"responses":{"200":{"description":"The cast narrator.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/trailers/{jobId}/aspect":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Trailer job id."}],"get":{"operationId":"getTrailerAspect","tags":["Trailer"],"summary":"Read the cut's aspect ratio.","description":"Returns the shape the cut renders at: `aspectRatio` (the shape in force), `chosen` (the writer's own pick, or null while the default holds), and `width`/`height` (the frame this shape delivers at, at the cut's delivery class). The shape is a cut-level choice, read back by the compile, the render, the quote, the boards and the master alike. Scope: `story:read`.","security":[{"BearerApiKey":["story:read"]}],"responses":{"200":{"description":"The cut's shape.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"type":"object","required":["aspect"],"additionalProperties":false,"properties":{"aspect":{"$ref":"#/components/schemas/TrailerAspectView"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"setTrailerAspect","tags":["Trailer"],"summary":"Choose the cut's aspect ratio.","description":"Choose the cut's shape — 9:16 (the default), 16:9 or 1:1 — sent as `{ aspectRatio }`. Free to store, and the useful moment is BEFORE the first render: a shape change never re-renders on its own. On a cut that already rendered, every beat whose takes are at the old shape is kept playable but flagged `clipStale` (its text layer goes, because it was burned at the old frame); rendering it again recompiles at the new shape, and `invalidatedBeats` names the beats now stale. Refused (409) while a render or finish pass is running. Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["aspectRatio"],"additionalProperties":false,"properties":{"aspectRatio":{"type":"string","enum":["9:16","16:9","1:1"],"description":"The shape to switch the cut to."}}}}}},"responses":{"200":{"description":"The cut's shape and the beats now stale at the old one.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"type":"object","required":["aspect"],"additionalProperties":false,"properties":{"aspect":{"$ref":"#/components/schemas/TrailerAspectView"}}}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/TrailerStateInvalid"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/trailers/{jobId}/audio-policy":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Trailer job id."}],"post":{"operationId":"setTrailerAudioPolicy","tags":["Trailer"],"summary":"Switch the cut's audio policy.","description":"Switch the cut between `narrate` and `suppress` — sent as `{ audioPolicy }`. A narrated cut speaks each beat's `narrationText` in ONE voice (the cast narrator), rendered as a separate per-beat track mixed over the finished picture; a suppressed cut is caption-carried. The policy is read once per render stage, so it is refused (409) while a render or finish pass is running. The read is `GET /trailers/{jobId}/narrator`, which carries `audioPolicy`. Free to store; the answer carries the policy now in force and the cast narrator so a narrated cut with no narrator can be warned about before approval. Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["audioPolicy"],"additionalProperties":false,"properties":{"audioPolicy":{"type":"string","enum":["suppress","narrate"],"description":"`narrate` speaks each beat's narration in one voice; `suppress` is caption-carried."}}}}}},"responses":{"200":{"description":"The audio policy now in force and the cast narrator.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"type":"object","required":["audioPolicy"],"additionalProperties":true,"properties":{"audioPolicy":{"type":"string"},"narratorVoiceId":{"type":"string","nullable":true},"narratorVoiceProvider":{"type":"string","nullable":true}}}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/TrailerStateInvalid"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/trailers/{jobId}/storyboard/audio":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Trailer job id."}],"get":{"operationId":"getTrailerSceneAudio","tags":["Trailer"],"summary":"Read per-beat music and sound effects.","description":"Returns each beat's music bed and the sound-effect cues placed within it. This is the per-beat view: every bed is generated for one beat on its own, so beds read here share no tempo, key or phrase with their neighbours. Read the audio timeline instead for anything that spans or crossfades across beats. A fully-scored cut normally shows `music: null` on every beat here: most cuts are scored on the project TIMELINE rather than beat by beat, because a per-beat bed is generated on its own and so shares no tempo, key or phrase with its neighbours. Read `audioTimeline.musicClips` in the response before concluding a cut is silent. Scope: `story:read`.","security":[{"BearerApiKey":["story:read"]}],"responses":{"200":{"description":"Per-beat music beds and placed sound-effect cues, plus `audioTimeline` counts for what is scored on the project timeline instead.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"properties":{"audioTimeline":{"type":"object","description":"What the per-beat view is NOT showing: music clips, SFX cues and layer count on the project timeline. Non-zero `musicClips` with every beat bed null is the normal state of a scored cut.","properties":{"musicClips":{"type":"integer"},"sfxCues":{"type":"integer"},"musicLayers":{"type":"integer"}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/trailers/{jobId}/storyboard/music":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Trailer job id."}],"post":{"operationId":"editTrailerSceneMusic","tags":["Trailer"],"summary":"Score one beat's music bed.","description":"One endpoint, selected by `action`: `score` generates a new bed from a prompt, `apply-library` reuses a track already in My Library (no generation, and the way several beats come to share a tempo and key), `update` relevels or retimes the bed, `remove` clears it. Beds render with the Lyria engine, so they are never blocked by the fal balance; describe the SOUND rather than naming a real artist. Generating — `score`, or an `update` that changes the prompt or vocals — waits for the project's gate: while it is closed the call is refused with 409 `project_precondition_failed` and no bed is written or queued; `apply-library`, a level or timing `update` and `remove` stay open. Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["action","sceneIndex"],"additionalProperties":true,"properties":{"action":{"type":"string","enum":["score","update","remove","apply-library"]},"sceneIndex":{"type":"integer","minimum":0,"description":"Zero-based index of the beat."}}}}}},"responses":{"200":{"description":"The updated music bed, plus a shareable per-beat preview link.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/trailers/{jobId}/storyboard/sfx":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Trailer job id."}],"post":{"operationId":"editTrailerSceneSfx","tags":["Trailer"],"summary":"Place a sound effect on one beat.","description":"One endpoint, selected by `action`: `add` generates a new cue from a prompt, `apply-library` places a track already in My Library (no generation, and the only way a recurring motif sounds IDENTICAL every time), `update` moves, relevels or disables a placed cue, `remove` deletes it. Offsets are measured from the beat's own start. Generating — `add` — waits for the project's gate: while it is closed the call is refused with 409 `project_precondition_failed` and nothing is generated or placed; `apply-library`, `update` and `remove` generate nothing and stay open. Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["action","sceneIndex"],"additionalProperties":true,"properties":{"action":{"type":"string","enum":["add","update","remove","apply-library"]},"sceneIndex":{"type":"integer","minimum":0,"description":"Zero-based index of the beat."}}}}}},"responses":{"200":{"description":"The placed cue, plus a shareable per-beat preview link.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/pitch-decks/{jobId}/storyboard/audio":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Pitch deck job id."}],"get":{"operationId":"getPitchDeckSceneAudio","tags":["Pitch Deck"],"summary":"Read per-scene music and sound effects.","description":"Returns each scene's music bed and the sound-effect cues placed within it. This is the per-scene view: every bed is generated for one scene on its own, so beds read here share no tempo, key or phrase with their neighbours. Read the audio timeline instead for anything that spans or crossfades across scenes. A fully-scored cut normally shows `music: null` on every scene here: most cuts are scored on the project TIMELINE rather than scene by scene, because a per-scene bed is generated on its own and so shares no tempo, key or phrase with its neighbours. Read `audioTimeline.musicClips` in the response before concluding a cut is silent. Scope: `story:read`.","security":[{"BearerApiKey":["story:read"]}],"responses":{"200":{"description":"Per-scene music beds and placed sound-effect cues, plus `audioTimeline` counts for what is scored on the project timeline instead.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"type":"object","additionalProperties":true,"properties":{"audioTimeline":{"type":"object","description":"What the per-scene view is NOT showing: music clips, SFX cues and layer count on the project timeline. Non-zero `musicClips` with every scene bed null is the normal state of a scored cut.","properties":{"musicClips":{"type":"integer"},"sfxCues":{"type":"integer"},"musicLayers":{"type":"integer"}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/pitch-decks/{jobId}/storyboard/music":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Pitch deck job id."}],"post":{"operationId":"editPitchDeckSceneMusic","tags":["Pitch Deck"],"summary":"Score one scene's music bed.","description":"One endpoint, selected by `action`: `score` generates a new bed from a prompt, `apply-library` reuses a track already in My Library (no generation, and the way several scenes come to share a tempo and key), `update` relevels or retimes the bed, `remove` clears it. Beds render with the Lyria engine, so they are never blocked by the fal balance; describe the SOUND rather than naming a real artist. Generating — `score`, or an `update` that changes the prompt or vocals — waits for the project's gate: while it is closed the call is refused with 409 `project_precondition_failed` and no bed is written or queued; `apply-library`, a level or timing `update` and `remove` stay open. Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["action","sceneIndex"],"additionalProperties":true,"properties":{"action":{"type":"string","enum":["score","update","remove","apply-library"]},"sceneIndex":{"type":"integer","minimum":0,"description":"Zero-based index of the scene."}}}}}},"responses":{"200":{"description":"The updated music bed, plus a shareable per-scene preview link.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/pitch-decks/{jobId}/storyboard/sfx":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Pitch deck job id."}],"post":{"operationId":"editPitchDeckSceneSfx","tags":["Pitch Deck"],"summary":"Place a sound effect on one scene.","description":"One endpoint, selected by `action`: `add` generates a new cue from a prompt, `apply-library` places a track already in My Library (no generation, and the only way a recurring motif sounds IDENTICAL every time), `update` moves, relevels or disables a placed cue, `remove` deletes it. Offsets are measured from the scene's own start. Generating — `add` — waits for the project's gate: while it is closed the call is refused with 409 `project_precondition_failed` and nothing is generated or placed; `apply-library`, `update` and `remove` generate nothing and stay open. Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["action","sceneIndex"],"additionalProperties":true,"properties":{"action":{"type":"string","enum":["add","update","remove","apply-library"]},"sceneIndex":{"type":"integer","minimum":0,"description":"Zero-based index of the scene."}}}}}},"responses":{"200":{"description":"The placed cue, plus a shareable per-scene preview link.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/trailers/{jobId}/storyboard/audio-timeline":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Trailer job id."}],"get":{"operationId":"getTrailerAudioTimeline","tags":["Trailer"],"summary":"Read the project-level audio timeline.","description":"Returns the multi-track audio timeline: music layers with their clips — a clip may span several beats and crossfade into the next — plus sound-effect cues placed at an absolute offset within a beat. Every sound-effect cue in the answer carries how loud it really is: `assetLufs` (its file: a bed's body, the median of its sounding 400 ms windows; a spot's loudest 400 ms), `normGainDb` (what the mix lifts or lowers the file by before its `level`: a bed is levelled to -17 LUFS, a spot to -18, never past -1.5 dBTP or more than +30 dB), `mixLufs` (where it sits in the mix at its level; the sound map reads it against its scene's own mix), a bed's `continuity` (the share of its span within 10 dB of its body) and `loudnessFlag` (`too_quiet`: its file came out too quiet to bring up, make it again; `clipped`; `gappy`: a bed that drops out or fades, make it again as one continuous sound; `unmeasured`: measured at the next stitch). Also returns the beat axis (label, duration, start image) that clips are positioned against. This is the through-composed alternative to per-beat beds, which are generated independently and therefore share no tempo, key or phrase. Scope: `story:read`.","security":[{"BearerApiKey":["story:read"]}],"responses":{"200":{"description":"The audio timeline and the beat axis it is placed against.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"editTrailerAudioTimeline","tags":["Trailer"],"summary":"Edit the project-level audio timeline.","description":"One endpoint, selected by `action`: add-music-layer, remove-music-layer, generate-music-clip, add-music-clip-from-library, update-music-clip, remove-music-clip, generate-sfx-cue, add-sfx-cue-from-library, update-sfx-cue, update-sfx-cues, remove-sfx-cue. Music clips render with the Lyria engine, so they are never blocked by the fal balance; describe the SOUND rather than naming a real artist. A sound-effect cue (generate-sfx-cue, add-sfx-cue-from-library) takes `kind` (`spot`: one event, its own length; `ambience`: a bed under the picture that, with no `lengthMs`, fills its scene from the offset or through `endSceneIndex`, repeating the effect crossfaded, at 0.2 with short fades unless set), `lengthMs` (how long it sounds, 100..3600000; longer than the effect repeats it), `fadeInMs` / `fadeOutMs` (0..60000) and `level`; `durationSeconds` is how long the effect is generated (up to 22). update-sfx-cue changes any of them, `muted` silences a cue without removing it, and null `lengthMs` / `endSceneIndex` put it back on its default span. update-sfx-cues changes or removes several cues in ONE write (a re-balance): `cues`, 1 to 40 entries, each update-sfx-cue's fields with its `cueId`, or `{ cueId, remove: true }`; the answer's `results` says what each did (one naming no cue changes nothing while the rest land). Every cue's file is measured as it is placed and played levelled before its `level`, so a level is a place in the mix: the sound map's `suggestedLevels` and each cue's `targetLevel` put a bed 10–14 dB and a spot 3–8 dB under its scene's own mix. The cue in the answer, and every cue in its timeline, carries `assetLufs`, `normGainDb`, `mixLufs` and `loudnessFlag` (see the timeline read). Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["action"],"additionalProperties":true,"properties":{"action":{"type":"string"}}}}}},"responses":{"200":{"description":"The updated timeline, plus a shareable per-beat preview link where the change touched one beat.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/pitch-decks/{jobId}/storyboard/audio-timeline":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Pitch deck job id."}],"get":{"operationId":"getPitchDeckAudioTimeline","tags":["Pitch Deck"],"summary":"Read the project-level audio timeline.","description":"Returns the multi-track audio timeline: music layers with their clips — a clip may span several scenes and crossfade into the next — plus sound-effect cues placed at an absolute offset within a scene. Every sound-effect cue in the answer carries how loud it really is: `assetLufs` (its file: a bed's body, the median of its sounding 400 ms windows; a spot's loudest 400 ms), `normGainDb` (what the mix lifts or lowers the file by before its `level`: a bed is levelled to -17 LUFS, a spot to -18, never past -1.5 dBTP or more than +30 dB), `mixLufs` (where it sits in the mix at its level; the sound map reads it against its scene's own mix), a bed's `continuity` (the share of its span within 10 dB of its body) and `loudnessFlag` (`too_quiet`: its file came out too quiet to bring up, make it again; `clipped`; `gappy`: a bed that drops out or fades, make it again as one continuous sound; `unmeasured`: measured at the next stitch). Also returns the scene axis (label, duration, start image) that clips are positioned against. This is the through-composed alternative to per-scene beds, which are generated independently and therefore share no tempo, key or phrase. Scope: `story:read`.","security":[{"BearerApiKey":["story:read"]}],"responses":{"200":{"description":"The audio timeline and the scene axis it is placed against.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"editPitchDeckAudioTimeline","tags":["Pitch Deck"],"summary":"Edit the project-level audio timeline.","description":"One endpoint, selected by `action`: add-music-layer, remove-music-layer, generate-music-clip, add-music-clip-from-library, update-music-clip, remove-music-clip, generate-sfx-cue, add-sfx-cue-from-library, update-sfx-cue, update-sfx-cues, remove-sfx-cue. Music clips render with the Lyria engine, so they are never blocked by the fal balance; describe the SOUND rather than naming a real artist. A sound-effect cue (generate-sfx-cue, add-sfx-cue-from-library) takes `kind` (`spot`: one event, its own length; `ambience`: a bed under the picture that, with no `lengthMs`, fills its scene from the offset or through `endSceneIndex`, repeating the effect crossfaded, at 0.2 with short fades unless set), `lengthMs` (how long it sounds, 100..3600000; longer than the effect repeats it), `fadeInMs` / `fadeOutMs` (0..60000) and `level`; `durationSeconds` is how long the effect is generated (up to 22). update-sfx-cue changes any of them, `muted` silences a cue without removing it, and null `lengthMs` / `endSceneIndex` put it back on its default span. update-sfx-cues changes or removes several cues in ONE write (a re-balance): `cues`, 1 to 40 entries, each update-sfx-cue's fields with its `cueId`, or `{ cueId, remove: true }`; the answer's `results` says what each did (one naming no cue changes nothing while the rest land). Every cue's file is measured as it is placed and played levelled before its `level`, so a level is a place in the mix: the sound map's `suggestedLevels` and each cue's `targetLevel` put a bed 10–14 dB and a spot 3–8 dB under its scene's own mix. The cue in the answer, and every cue in its timeline, carries `assetLufs`, `normGainDb`, `mixLufs` and `loudnessFlag` (see the timeline read). Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["action"],"additionalProperties":true,"properties":{"action":{"type":"string"}}}}}},"responses":{"200":{"description":"The updated timeline, plus a shareable per-scene preview link where the change touched one scene.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/trailers/{jobId}/storyboard/sound-map":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Trailer job id."}],"get":{"operationId":"getTrailerSoundMap","tags":["Trailer"],"summary":"Read the sound map a sound pass is placed from.","description":"For every beat, or one (`sceneIndex`, 0-based): its length in the master (`cutSeconds`, the window a cue's offset is measured in), every shot's `fromSeconds`/`toSeconds` with its framing, who is on camera and its setup and action, every spoken line's speaker and seconds (`how`: on_camera, laid, narration or approx), what the cut already carries under each call (`cutSound[].heard`: silence, cast_lines, room_tone or scene_sound) and the compiled `roomTone`, its own mix (`chapterMixLufs`: voice, kept scene sound and score without the sound effects, measured on the master at its last stitch; `chapterHeardLufs` with them; null until a stitch measured it) and the levels a new bed and spot take under it (`suggestedLevels`), the sound-effect cues already placed on the timeline (`cueId`, kind, offset, length, level, fades, muted; how loud each really is: `assetLufs`, `normGainDb`, `mixLufs`; where it sits against its own mix: `underChapterMixDb` and `targetLevel`, the level that lands it in the middle of the map's `targets` — a bed 10–14 dB, a spot 3–8 dB under; a bed's `continuity`; `insideLine` for a spot inside a spoken line; and `loudnessFlag` too_quiet, clipped, gappy or unmeasured). `masterLufs` is context only; `chapterMix` names the master the mixes were measured on and whether it is still the current one and the music sounding over it, plus the bell lane. Lay cues with POST `/trailers/{jobId}/storyboard/audio-timeline` (`generate-sfx-cue`) and re-balance them in one write (`update-sfx-cues`). Read-only. Scope: `story:read`.","security":[{"BearerApiKey":["story:read"]}],"parameters":[{"name":"sceneIndex","in":"query","required":false,"schema":{"type":"integer","minimum":0},"description":"Only this beat."}],"responses":{"200":{"description":"The sound map: `masterLufs`, `targets`, `chapterMix`, `scenes` (one entry per beat) and `bell`.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/pitch-decks/{jobId}/storyboard/sound-map":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Pitch deck job id."}],"get":{"operationId":"getPitchDeckSoundMap","tags":["Pitch Deck"],"summary":"Read the sound map a sound pass is placed from.","description":"For every chapter, or one (`sceneIndex`, its chapterIndex): its length in the master (`cutSeconds`, the window a cue's offset is measured in), whether its picture carries straight into the next (`hardCutIntoNext`), every shot's `fromSeconds`/`toSeconds` with its framing, who is on camera and its setup and action, every spoken line's speaker and seconds (`how`: on_camera, laid, narration or approx), what the cut already carries under each call (`cutSound[].heard`: silence, cast_lines, room_tone or scene_sound) and the compiled `roomTone`, its own mix (`chapterMixLufs`: voice, kept scene sound and score without the sound effects, measured on the master at its last stitch; `chapterHeardLufs` with them; null until a stitch measured it) and the levels a new bed and spot take under it (`suggestedLevels`), the sound-effect cues already placed on the timeline (`cueId`, kind, offset, length, level, fades, muted; how loud each really is: `assetLufs`, `normGainDb`, `mixLufs`; where it sits against its own mix: `underChapterMixDb` and `targetLevel`, the level that lands it in the middle of the map's `targets` — a bed 10–14 dB, a spot 3–8 dB under; a bed's `continuity`; `insideLine` for a spot inside a spoken line; and `loudnessFlag` too_quiet, clipped, gappy or unmeasured). `masterLufs` is context only; `chapterMix` names the master the mixes were measured on and whether it is still the current one and the music sounding over it, plus the bell lane. Lay cues with POST `/pitch-decks/{jobId}/storyboard/audio-timeline` (`generate-sfx-cue`) and re-balance them in one write (`update-sfx-cues`). Read-only. Scope: `story:read`.","security":[{"BearerApiKey":["story:read"]}],"parameters":[{"name":"sceneIndex","in":"query","required":false,"schema":{"type":"integer","minimum":0},"description":"Only this chapter (its chapterIndex)."}],"responses":{"200":{"description":"The sound map: `masterLufs`, `targets`, `chapterMix`, `scenes` (one entry per chapter) and `bell`.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/pitch-decks/{jobId}/score-to-picture":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Pitch deck job id."}],"get":{"operationId":"getPitchDeckScoreToPicture","tags":["Pitch Deck"],"summary":"Read the pitch deck's score to picture.","description":"The score composed to the pitch deck's cut and whether one can be composed now: `status` (none, waiting_for_master, queued, composing, laid, failed, reverted), `canScore` and `blocked` (why not — music the writer placed is never replaced), `runtimeMs`, `fitsCut` (false when the cut changed after the score was composed: it still plays, off the cuts) and the `cueSheet`: its concept and palette, its sections by cut number with their windows in ms, mood, instrumentation and intensity, its hit points (the cuts the music lands on), its intensity curve and where speech sits; and `silentGap` (null unless it happened): the laid score fell silent when it was first composed (under −50 dBFS for longer than 1.5 s, the ending's fade-out aside), so it was composed again — or what stayed silent was filled from the music beside it — with where (`found`, ms of the master), the cue-sheet `sections`, the `action` (`recomposed` / `filled`) and the writer's `note`, which also ends `message`. A score with dead air is never laid. Before any master, `plannedCut` is true (`runtimeMs` and `cuts` are the planned cut, every unit at its planned length) and `sketch` is the sketch composed to it for a score that waits for the master: `status` (`composing`, `ready`, `failed`), `url` to listen to, `composedAt`, `fitsCut` (false when the plan changed since) and `error` — never laid, never charged; null once the score is on its way to the real cut. On a deck whose title sequence cuts on the bell's beat grid, the `cueSheet` carries `beatGrid` (the beat, the tempo, and the required bell strikes: every intro cut and the title, with hit points marked `required`), and `beatAlignment` (null otherwise) is the score check: each strike's cut, the nearest bell onset measured in the laid score, the offset in ms and in frames and whether it is within a frame (`aligned`), the constant `shiftMs` applied to put them on the cuts (a shift, never a retime), and the `note` — a `warning` (\"Intro bell strikes not on the cuts: …\") when they are not, which also ends `message`. Nothing is composed again for it: scoring again is the writer's priced choice. When `status` is failed, `error` is why (the Details). A score asked for with a `direction` that could not be composed is NEVER replaced by the table read's scene beds: they come off the cut, the bell lane and the sound effects stay, and `message` says so plainly — \"The score couldn't be composed (reason in Details). The bell plays alone. Score to picture to try again.\" (with no bell lane, no music plays under the cut; music the writer placed is named as still on it). Say that to the writer. Scoring to picture again without a new `direction` keeps the one it had; a score asked for without a direction that fails still leaves the scene beds. Scope: `story:read`.","security":[{"BearerApiKey":["story:read"]}],"responses":{"200":{"description":"The score's state and its cue sheet.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"scorePitchDeckToPicture","tags":["Pitch Deck"],"summary":"Price, then compose, a score to the pitch deck's cut.","description":"Composes ONE score to the pitch deck's cut — a cue sheet planned over its chapters (sections that change on the cuts, an intensity curve, hit points at chosen cuts, room under every spoken line, the Series Bible's tone and period), a composition conformed to the master's length to the frame — and lays it under the whole master in place of the table read's scene beds, then re-stitches the master through its serialized assembly. TWO CALLS, PRICE FIRST: sent WITHOUT `confirmed` it prices the score and holds nothing (`quotedOnly: true`, `credits`, `balanceAfter`, `sufficient`, `note`); sent again with `confirmed: true` it holds the quoted credits and starts composing. On a DRAFT plan (so the music direction is set before anything renders) or while the first render pass is still running, it is priced on the PLANNED cut and waits for that pass's master (`waiting_for_master`), and a SKETCH to the planned cut is composed right away by the same planner and composer — free, never laid — to listen to (`sketch.url` on the read); the table read's scene beds are not laid under the cut while a score is asked for. Priced like the app's other music generation: included today (0 credits), still quoted first. Refused with a 409 before any price when the writer placed music of their own or edited an earlier score (the writer's music is never replaced), or while a score is already composing. `direction` is an optional note on the music the writer wants. Requires `Idempotency-Key`. Scope: `artifact:publish`.","security":[{"BearerApiKey":["artifact:publish"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"confirmed":{"type":"boolean","description":"Omit (or false) for the price; true only after the writer has seen it and said go."},"direction":{"type":"string","maxLength":600,"description":"An optional note on the music the writer wants."}}}}}},"responses":{"200":{"description":"The price (no `confirmed`), or the started score.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/InsufficientCredits"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/PitchDeckStateInvalid"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/pitch-decks/{jobId}/score-to-picture/revert":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Pitch deck job id."}],"post":{"operationId":"revertPitchDeckScoreToPicture","tags":["Pitch Deck"],"summary":"Revert the pitch deck to its scene beds.","description":"Takes the score to picture off the pitch deck's timeline, puts back the table read's scene beds it replaced, and re-stitches the master — or cancels a score still waiting for the master. Free. Nothing else on the timeline is touched. Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"200":{"description":"The score is off the timeline (and whether the scene beds came back).","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/PitchDeckStateInvalid"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/pitch-decks/{jobId}/bell-lane":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Pitch deck job id."}],"get":{"operationId":"getPitchDeckBellLane","tags":["Pitch Deck"],"summary":"Read the pitch deck's bell on the beat.","description":"Read the deck's bell on the beat: `available` (the deck has a bell beat grid — its title sequence cuts on the bell's beat; `unavailable.message` says how to turn it on), the `grid` (the beat in seconds, where beat 0 is in the master, the intros' and the title's starts), the `lane` (its one sound, `every` 1 or 2, `fromSeconds`/`toSeconds`, `skipSceneIndexes`, `accent`, `level`, `duckUnderVoice`), every strike on the cut (`strikes`: beat, `atMs`, the part it falls in, `accent`; on the PLANNED cut before any master, `plannedCut: true`), the `parts` it can skip (the title sequence, each chapter, each black interlude, by their `sceneIndex` key, with the strikes each gets) and handy start/end `points`, and `check`: every strike found in the published master after assembly by matching the lane's own bell sound, and timed against its beat — `verdict` (`on`: every strike it could time is within a frame; `off`: some are off their beat, a real timing error; `unmeasured`: none could be timed), `label` (the verdict in words, the badge every surface shows), `strikes`, `aligned`, `offBeat`, `tooQuiet` (strikes too quiet in the mix to time — buried under a line or the score; neither on nor off), `worstOffsetMs`, the `off` ones (each with its `offsetMs`, `offsetFrames` and `match`), `ok` and a `note` (a `warning` when strikes are off their beat); `checkCurrent` is false when it was measured on an earlier master or lane. `audibility` is how loud the bell sits against the score (a median dB per strike over the 400 ms after it, `on` the master or the sketch), with a `note` when most strikes are more than 12 dB under it. `scoreMadeRoom` is false when the score on the timeline was composed before the lane and carries its own bell. Scope: `story:read`.","security":[{"BearerApiKey":["story:read"]}],"responses":{"200":{"description":"The bell lane, its strikes, the parts it can skip and its check.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"strikePitchDeckBellOnTheBeat","tags":["Pitch Deck"],"summary":"Strike a bell on every beat of the pitch deck.","description":"Strike ONE bell sound on every beat of the deck's bell beat grid, frame-exact, as one lane on its audio timeline — or change the lane. The sound is `assetId` (a My Library sound effect), `sound: { fromJobId }` (another deck's bell of the same project, exactly as it struck there: the same bell as v2), or `prompt` (words for one to be made with the sound-effect generator and banked in My Library; `label`, `durationSeconds`), or — on a change — the lane's own. `every` 1 (every beat, default) or 2 (every 2nd beat); `fromSeconds`/`toSeconds` in seconds of the master (null: its first/last frame, the default); `skipSceneIndexes` the parts it stays out of (black interludes, chapters, the title sequence); `accent` (the intros and the title louder, default true); `level` 0–1 (0.12: every bell is struck levelled to a -1 dBFS peak, and 0.12 sits a few dB under a laid score); `duckUnderVoice` (default true: it ducks under the spoken lines like the score). Fields left out keep the lane's; an unknown field is refused by name. Free: nothing is rendered again — a deck in review or finished re-stitches its master for its sound (`reassembling`), a sketch waiting for its master is mixed again with the bell (`sketchRemixing`), and a score to picture composed with a lane leaves room for the struck bell (no bell of its own, nothing on the downbeat). Refused with a 409 when the deck has no bell beat grid. Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"assetId":{"type":"string","description":"A My Library sound effect to strike."},"sound":{"type":"object","additionalProperties":false,"required":["fromJobId"],"properties":{"fromJobId":{"type":"string","description":"A deck of the same project whose bell to strike here."}},"description":"Or another deck's bell of the same project (\"the same bell as v2\"), exactly as it struck there, levelled into this deck. Only the sound; this deck's timing stays. GET /story-projects/{projectId}/sounds lists them."},"prompt":{"type":"string","minLength":3,"maxLength":600,"description":"Or words for the bell to be made from (one sound effect, banked in My Library)."},"label":{"type":"string","maxLength":120,"description":"The made bell's name."},"durationSeconds":{"type":"number","minimum":0.5,"maximum":22,"description":"The made bell's length in seconds (5 when left out)."},"every":{"type":"integer","enum":[1,2],"description":"1: every beat (default). 2: every 2nd beat."},"fromSeconds":{"type":["number","null"],"minimum":0,"description":"Where it starts, in seconds of the master; null for its first frame."},"toSeconds":{"type":["number","null"],"minimum":0,"description":"Where it stops (inclusive), in seconds of the master; null for its last frame."},"skipSceneIndexes":{"type":"array","items":{"type":"number"},"description":"The parts it stays out of, by the `sceneIndex` the read lists in `parts`."},"accent":{"type":"boolean","description":"Accent the strike on each intro and on the title (default true)."},"level":{"type":"number","minimum":0,"maximum":1,"description":"Level 0..1 (0.12 by default) on a bell levelled to a -1 dBFS peak."},"duckUnderVoice":{"type":"boolean","description":"Duck under the spoken lines like the score (default true)."}}}}}},"responses":{"200":{"description":"The bell lane as set, and whether the master is re-stitched and the sketch mixed again.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/PitchDeckStateInvalid"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/pitch-decks/{jobId}/bell-lane/remove":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Pitch deck job id."}],"post":{"operationId":"removePitchDeckBellLane","tags":["Pitch Deck"],"summary":"Take the bell off the pitch deck.","description":"Takes the bell lane off the pitch deck's timeline. Free; nothing else on the timeline is touched (the bell sound stays in My Library); a deck in review or finished re-stitches its master for its sound. Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"200":{"description":"The bell is off the timeline.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/PitchDeckStateInvalid"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/trailers/{jobId}/score-to-picture":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Trailer job id."}],"get":{"operationId":"getTrailerScoreToPicture","tags":["Trailer"],"summary":"Read the trailer's score to picture.","description":"The score composed to the trailer's cut and whether one can be composed now: `status` (none, waiting_for_master, queued, composing, laid, failed, reverted), `canScore` and `blocked` (why not — music the writer placed is never replaced), `runtimeMs`, `fitsCut` (false when the cut changed after the score was composed: it still plays, off the cuts) and the `cueSheet`: its concept and palette, its sections by cut number with their windows in ms, mood, instrumentation and intensity, its hit points (the cuts the music lands on), its intensity curve and where speech sits; and `silentGap` (null unless it happened): the laid score fell silent when it was first composed (under −50 dBFS for longer than 1.5 s, the ending's fade-out aside), so it was composed again — or what stayed silent was filled from the music beside it — with where (`found`, ms of the master), the cue-sheet `sections`, the `action` (`recomposed` / `filled`) and the writer's `note`, which also ends `message`. A score with dead air is never laid. Before any master, `plannedCut` is true (`runtimeMs` and `cuts` are the planned cut, every unit at its planned length) and `sketch` is the sketch composed to it for a score that waits for the master: `status` (`composing`, `ready`, `failed`), `url` to listen to, `composedAt`, `fitsCut` (false when the plan changed since) and `error` — never laid, never charged; null once the score is on its way to the real cut. When `status` is failed, `error` is why (the Details). A score asked for with a `direction` that could not be composed is NEVER replaced by the table read's scene beds: they come off the cut, the bell lane and the sound effects stay, and `message` says so plainly — \"The score couldn't be composed (reason in Details). The bell plays alone. Score to picture to try again.\" (with no bell lane, no music plays under the cut; music the writer placed is named as still on it). Say that to the writer. Scoring to picture again without a new `direction` keeps the one it had; a score asked for without a direction that fails still leaves the scene beds. Scope: `story:read`.","security":[{"BearerApiKey":["story:read"]}],"responses":{"200":{"description":"The score's state and its cue sheet.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"scoreTrailerToPicture","tags":["Trailer"],"summary":"Price, then compose, a score to the trailer's cut.","description":"Composes ONE score to the trailer's cut — a cue sheet planned over its beats (sections that change on the cuts, an intensity curve, hit points at chosen cuts, room under every spoken line, the Series Bible's tone and period), a composition conformed to the master's length to the frame — and lays it under the whole master in place of the table read's scene beds, then re-stitches the master through its serialized assembly. TWO CALLS, PRICE FIRST: sent WITHOUT `confirmed` it prices the score and holds nothing (`quotedOnly: true`, `credits`, `balanceAfter`, `sufficient`, `note`); sent again with `confirmed: true` it holds the quoted credits and starts composing. On a DRAFT plan (so the music direction is set before anything renders) or while the first render pass is still running, it is priced on the PLANNED cut and waits for that pass's master (`waiting_for_master`), and a SKETCH to the planned cut is composed right away by the same planner and composer — free, never laid — to listen to (`sketch.url` on the read). Priced like the app's other music generation: included today (0 credits), still quoted first. Refused with a 409 before any price when the writer placed music of their own or edited an earlier score (the writer's music is never replaced), or while a score is already composing. `direction` is an optional note on the music the writer wants. Requires `Idempotency-Key`. Scope: `artifact:publish`.","security":[{"BearerApiKey":["artifact:publish"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"confirmed":{"type":"boolean","description":"Omit (or false) for the price; true only after the writer has seen it and said go."},"direction":{"type":"string","maxLength":600,"description":"An optional note on the music the writer wants."}}}}}},"responses":{"200":{"description":"The price (no `confirmed`), or the started score.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/InsufficientCredits"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/TrailerStateInvalid"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/trailers/{jobId}/score-to-picture/revert":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Trailer job id."}],"post":{"operationId":"revertTrailerScoreToPicture","tags":["Trailer"],"summary":"Revert the trailer to its scene beds.","description":"Takes the score to picture off the trailer's timeline, puts back the table read's scene beds it replaced, and re-stitches the master — or cancels a score still waiting for the master. Free. Nothing else on the timeline is touched. Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"200":{"description":"The score is off the timeline (and whether the scene beds came back).","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/TrailerStateInvalid"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/trailers/{jobId}/storyboard/coverage":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Trailer job id."}],"get":{"operationId":"getTrailerStoryboardCoverage","tags":["Trailer"],"summary":"Read trailer plan coverage.","description":"Returns the pre-render report for the exact beat plan this trailer would render right now: whether it still describes the current plan, the 0–10 score, the 8.0 gate and every reason it withheld a pass, seven dimensions, machine-checked plan defects, per-beat notes, and priority fixes. While the latest draft edit's coverage waits to start it answers `status: \"queued\"`; a plan nobody has graded yet answers `status: \"missing\"` with the reason — NOT a 404, which would read as \"no such trailer\". This never returns storyboard boards. Scope: `story:read`.","security":[{"BearerApiKey":["story:read"]}],"responses":{"200":{"description":"Trailer plan coverage envelope.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"type":"object","required":["coverage"],"additionalProperties":false,"properties":{"coverage":{"type":"object","required":["status","planKind","jobId","current","stale","gatePassed","gateThreshold","gateReasons","gateNotes","heldBy","reportId","planRevision","reportedPlanRevision","reportVersion","rubricVersion","bibleVersion","exemplarIds","overallScore","progressDetail","failureMessage","report","judging","judgingIncomplete","defects","gateOverride","writerOverride","createdAt","updatedAt"],"properties":{"status":{"type":"string","enum":["missing","queued","generating","complete","failed"],"description":"`queued`: the latest draft edit's coverage waits to start — it starts once the plan has been left alone for three minutes (a burst of edits is one run, on the revision it ends on), and the plan approval starts it at once. `generating`: scoring — including while the same revision is judged again near the bar (`judging.pending`), when the approval waits for the median. A run a later edit replaced is dropped, never stored, and never the status."},"planKind":{"type":"string","enum":["pitch_deck","trailer"]},"jobId":{"type":"string"},"current":{"type":"boolean","description":"The stored report graded the plan as it stands right now (a `queued` run is for the plan as it stands)."},"stale":{"type":"boolean","description":"The plan changed after this report was produced."},"gatePassed":{"type":"boolean","description":"Complete, current, over threshold, and no veto dimension or blocking defect. A stale pass reads false."},"gateThreshold":{"type":"number","minimum":0,"maximum":10,"description":"The score this report had to clear (8.0 by default — stricter than the 7.0 Series Bible gate, because renders cost money)."},"gateReasons":{"type":"array","items":{"type":"string"},"description":"Every reason the gate withheld a pass, in the order they were checked. Empty when it passed."},"reportId":{"oneOf":[{"type":"string"},{"type":"null"}]},"planRevision":{"type":"integer","minimum":0,"description":"Fingerprint of the plan as it stands right now."},"reportedPlanRevision":{"oneOf":[{"type":"integer","minimum":0},{"type":"null"}],"description":"Fingerprint of the plan the stored report actually graded."},"reportVersion":{"oneOf":[{"type":"integer","minimum":1},{"type":"null"}]},"rubricVersion":{"oneOf":[{"type":"integer","minimum":1},{"type":"null"}],"description":"The rubric version the report was graded with. A report graded under an older rubric than the current one cannot authorize a render."},"bibleVersion":{"oneOf":[{"type":"integer","minimum":1},{"type":"null"}],"description":"The Series Bible revision the report was graded against, when the project has one."},"exemplarIds":{"type":"array","items":{"type":"string"},"description":"Ids of the benchmark exemplars the report was calibrated against; empty when the surface has no corpus."},"gateNotes":{"type":"array","items":{"type":"string"},"description":"Vetoes that were suppressed by policy, surfaced beside the reasons and never merged into them — the finding a reader most often wants when a gate passed narrowly."},"heldBy":{"type":"array","description":"What holds the plan beside its overall score, structured: each dimension that fails the plan on its own and sits under the bar (a story check such as `buildup`; `score` null when the judge gave it no usable score), then each blocking plan defect. Empty on a pass and on a report that does not describe the plan as it stands. A plan whose overall clears the bar is still held by any of these: say \"7.6 overall, but Buildup scored 6.5, below the 7.0 bar\", never that 7.6 is below the bar.","items":{"oneOf":[{"type":"object","required":["kind","dimension","label","score"],"properties":{"kind":{"type":"string","enum":["dimension"]},"dimension":{"type":"string"},"label":{"type":"string","description":"The dimension as the app names it, e.g. \"Buildup\"."},"score":{"oneOf":[{"type":"number","minimum":0,"maximum":10},{"type":"null"}]}}},{"type":"object","required":["kind","code","detail"],"properties":{"kind":{"type":"string","enum":["defect"]},"code":{"type":"string"},"detail":{"type":"string"}}}]}},"overallScore":{"oneOf":[{"type":"number","minimum":0,"maximum":10},{"type":"null"}]},"progressDetail":{"oneOf":[{"type":"string"},{"type":"null"}]},"failureMessage":{"oneOf":[{"type":"string"},{"type":"null"}]},"report":{"oneOf":[{"type":"object","description":"The graded report (rubric v13): overallScore, verdict, oneLineDiagnosis, canonSummary, showSold, opening (the judge's note on the deck's opening title sequence: present, whatItSays, servesIntent, nextMove; null on a report judged before it existed), the twenty scored dimensions — first the four that judge the plan against its intent and veto in every mode (intentClarity, centralQuestion, buildup, themeCarried), then subtext (does the plan pose its question rather than recite its plot; a veto in every mode), transitions (does every part follow the one before it for a reason a first-time viewer can feel; a veto in every mode) and voices (do the characters speak, rather than the narrator carrying everything), then sequenceArchitecture, storyThroughLine, seriesRepresentation, emotionalArchitecture, narrativeVoice, promiseControl, beatDistinctness, characterIntroduction, seriesBibleConsistency, referenceCompleteness, motionDirection, shotLadderLegibility, continuity — exemplarComparisons, emotionalJourney, narrationThroughline, beatNotes, characterNotes, weakTransitions (the weakest cuts, worst first: toBeatIndex — the unit the cut goes into — problem and fix), strengths, risks, priorityFixes, and viewerTakeaway: what a first-time viewer takes away from the plan (aboutTheShow, centralQuestion, whereItBuilds, toldVersusShown — how much of it was told in words rather than shown — and lostTheThread — where they could not tell why one part came after the last), written BEFORE the judge was shown the intent.","properties":{"viewerTakeaway":{"$ref":"#/components/schemas/VideoViewerTakeaway"},"opening":{"oneOf":[{"$ref":"#/components/schemas/StoryboardCoverageOpening"},{"type":"null"}]}}},{"type":"null"}]},"judgingIncomplete":{"oneOf":[{"type":"object","required":["judgments","maxJudgments"],"additionalProperties":false,"properties":{"judgments":{"type":"integer","minimum":1,"description":"The judgments that did run (their scores are on `judging`)."},"maxJudgments":{"type":"integer","minimum":1,"description":"The most judgments one revision takes."}}},{"type":"null"}],"description":"A revision near the bar whose median could not be completed: the further judgments it needed could not run, even queued again. The report is `failed` and is NEITHER a pass nor a veto (\"Plan coverage couldn't finish judging — Score again\"); `judging` lists the judgments that ran, with `stoppedBecause: judging_incomplete`. Score the plan again. Null otherwise."},"judging":{"oneOf":[{"$ref":"#/components/schemas/StoryboardCoverageJudging"},{"type":"null"}],"description":"How the verdict was reached. One judgment is noisy near the bar, so a revision whose first judgment lands within `nearGateBand` of a gating line (the overall, or a veto dimension) is judged again — the SAME revision, up to `maxJudgments`, free to the writer — and the gate reads the MEDIAN of each score, vetoes included. A plan edit starts a fresh set. While another judgment is pending, `pending` is true and the judgments so far are listed. Null before a judgment is recorded."},"defects":{"type":"array","description":"Plan defects decided in code, not by a model. `blocking` ones fail the gate on their own; `advisory` ones are evidence handed to the rubric.","items":{"type":"object","required":["code","severity","beatIndexes","subject","detail"],"properties":{"code":{"type":"string","enum":["missing_motion_direction","missing_shot_type","missing_location","shot_ladder_collapsed","shot_ladder_duplicate_framing","location_flip_flop","missing_character_reference","missing_location_plate","character_never_featured","opens_on_arrival","ends_on_exit","cut_ladder_no_range","promised_never_shown","canon_lead_absent","canon_lead_never_subject","canon_character_not_castable","approved_beat_dropped_silently","video_intent_missing","arc_stage_missing","question_not_advanced","arc_out_of_order","arc_stage_unserved","ending_off_question","transition_unexplained","characters_never_speak","character_lines_under_share"]},"severity":{"type":"string","enum":["blocking","advisory"]},"beatIndexes":{"type":"array","items":{"type":"integer","minimum":0}},"subject":{"type":"string"},"detail":{"type":"string"}}}},"gateOverride":{"type":"object","required":["allowed","why"],"additionalProperties":false,"description":"Whether a failed gate may be rendered over on the writer's word (`acknowledgePlanCoverage` on the plan approval). Allowed ONLY when a complete report of the plan as it stands, scored on the current rubric, failed on the judge's scores alone (the overall score, or a judged veto dimension, under the bar); never over a blocking defect decided in code, and never for a missing, queued, running, failed, stale or old-rubric report. `why` says which, in the writer's terms.","properties":{"allowed":{"type":"boolean"},"why":{"type":"string"}}},"writerOverride":{"oneOf":[{"$ref":"#/components/schemas/PlanCoverageOverride"},{"type":"null"}],"description":"The override the writer recorded on THIS report by approving over its gate — who (and via which surface and key), when, why, and the revision, score and reasons it overrode. Every surface shows it as \"approved over the plan gate: <reason>\". Null when no approval overrode this report."},"createdAt":{"oneOf":[{"type":"string","format":"date-time"},{"type":"null"}]},"updatedAt":{"oneOf":[{"type":"string","format":"date-time"},{"type":"null"}]}}}}}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"generateTrailerStoryboardCoverage","tags":["Trailer"],"summary":"Generate trailer plan coverage.","description":"Queues a pre-render report for the exact current beat plan. It grades the PLAN, never a rendered frame, so it spends no render credits and is the cheap step before the expensive one. Requires at least one planned beat. The gate is 8.0 — stricter than the 7.0 foundational Bible gate — and Series Bible consistency fails the report on its own whatever the other dimensions score. Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"focusPrompt":{"type":"string","maxLength":5000,"description":"Optional writer focus for this coverage pass."}}}}}},"responses":{"200":{"description":"Queued trailer plan coverage envelope.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"type":"object","required":["coverage"],"additionalProperties":false,"properties":{"coverage":{"type":"object","required":["status","planKind","jobId","current","stale","gatePassed","gateThreshold","gateReasons","gateNotes","heldBy","reportId","planRevision","reportedPlanRevision","reportVersion","rubricVersion","bibleVersion","exemplarIds","overallScore","progressDetail","failureMessage","report","judging","judgingIncomplete","defects","gateOverride","writerOverride","createdAt","updatedAt"],"properties":{"status":{"type":"string","enum":["missing","queued","generating","complete","failed"],"description":"`queued`: the latest draft edit's coverage waits to start — it starts once the plan has been left alone for three minutes (a burst of edits is one run, on the revision it ends on), and the plan approval starts it at once. `generating`: scoring — including while the same revision is judged again near the bar (`judging.pending`), when the approval waits for the median. A run a later edit replaced is dropped, never stored, and never the status."},"planKind":{"type":"string","enum":["pitch_deck","trailer"]},"jobId":{"type":"string"},"current":{"type":"boolean","description":"The stored report graded the plan as it stands right now (a `queued` run is for the plan as it stands)."},"stale":{"type":"boolean","description":"The plan changed after this report was produced."},"gatePassed":{"type":"boolean","description":"Complete, current, over threshold, and no veto dimension or blocking defect. A stale pass reads false."},"gateThreshold":{"type":"number","minimum":0,"maximum":10,"description":"The score this report had to clear (8.0 by default — stricter than the 7.0 Series Bible gate, because renders cost money)."},"gateReasons":{"type":"array","items":{"type":"string"},"description":"Every reason the gate withheld a pass, in the order they were checked. Empty when it passed."},"reportId":{"oneOf":[{"type":"string"},{"type":"null"}]},"planRevision":{"type":"integer","minimum":0,"description":"Fingerprint of the plan as it stands right now."},"reportedPlanRevision":{"oneOf":[{"type":"integer","minimum":0},{"type":"null"}],"description":"Fingerprint of the plan the stored report actually graded."},"reportVersion":{"oneOf":[{"type":"integer","minimum":1},{"type":"null"}]},"rubricVersion":{"oneOf":[{"type":"integer","minimum":1},{"type":"null"}],"description":"The rubric version the report was graded with. A report graded under an older rubric than the current one cannot authorize a render."},"bibleVersion":{"oneOf":[{"type":"integer","minimum":1},{"type":"null"}],"description":"The Series Bible revision the report was graded against, when the project has one."},"exemplarIds":{"type":"array","items":{"type":"string"},"description":"Ids of the benchmark exemplars the report was calibrated against; empty when the surface has no corpus."},"gateNotes":{"type":"array","items":{"type":"string"},"description":"Vetoes that were suppressed by policy, surfaced beside the reasons and never merged into them — the finding a reader most often wants when a gate passed narrowly."},"heldBy":{"type":"array","description":"What holds the plan beside its overall score, structured: each dimension that fails the plan on its own and sits under the bar (a story check such as `buildup`; `score` null when the judge gave it no usable score), then each blocking plan defect. Empty on a pass and on a report that does not describe the plan as it stands. A plan whose overall clears the bar is still held by any of these: say \"7.6 overall, but Buildup scored 6.5, below the 7.0 bar\", never that 7.6 is below the bar.","items":{"oneOf":[{"type":"object","required":["kind","dimension","label","score"],"properties":{"kind":{"type":"string","enum":["dimension"]},"dimension":{"type":"string"},"label":{"type":"string","description":"The dimension as the app names it, e.g. \"Buildup\"."},"score":{"oneOf":[{"type":"number","minimum":0,"maximum":10},{"type":"null"}]}}},{"type":"object","required":["kind","code","detail"],"properties":{"kind":{"type":"string","enum":["defect"]},"code":{"type":"string"},"detail":{"type":"string"}}}]}},"overallScore":{"oneOf":[{"type":"number","minimum":0,"maximum":10},{"type":"null"}]},"progressDetail":{"oneOf":[{"type":"string"},{"type":"null"}]},"failureMessage":{"oneOf":[{"type":"string"},{"type":"null"}]},"report":{"oneOf":[{"type":"object","description":"The graded report (rubric v13): overallScore, verdict, oneLineDiagnosis, canonSummary, showSold, opening (the judge's note on the deck's opening title sequence: present, whatItSays, servesIntent, nextMove; null on a report judged before it existed), the twenty scored dimensions — first the four that judge the plan against its intent and veto in every mode (intentClarity, centralQuestion, buildup, themeCarried), then subtext (does the plan pose its question rather than recite its plot; a veto in every mode), transitions (does every part follow the one before it for a reason a first-time viewer can feel; a veto in every mode) and voices (do the characters speak, rather than the narrator carrying everything), then sequenceArchitecture, storyThroughLine, seriesRepresentation, emotionalArchitecture, narrativeVoice, promiseControl, beatDistinctness, characterIntroduction, seriesBibleConsistency, referenceCompleteness, motionDirection, shotLadderLegibility, continuity — exemplarComparisons, emotionalJourney, narrationThroughline, beatNotes, characterNotes, weakTransitions (the weakest cuts, worst first: toBeatIndex — the unit the cut goes into — problem and fix), strengths, risks, priorityFixes, and viewerTakeaway: what a first-time viewer takes away from the plan (aboutTheShow, centralQuestion, whereItBuilds, toldVersusShown — how much of it was told in words rather than shown — and lostTheThread — where they could not tell why one part came after the last), written BEFORE the judge was shown the intent.","properties":{"viewerTakeaway":{"$ref":"#/components/schemas/VideoViewerTakeaway"},"opening":{"oneOf":[{"$ref":"#/components/schemas/StoryboardCoverageOpening"},{"type":"null"}]}}},{"type":"null"}]},"judgingIncomplete":{"oneOf":[{"type":"object","required":["judgments","maxJudgments"],"additionalProperties":false,"properties":{"judgments":{"type":"integer","minimum":1,"description":"The judgments that did run (their scores are on `judging`)."},"maxJudgments":{"type":"integer","minimum":1,"description":"The most judgments one revision takes."}}},{"type":"null"}],"description":"A revision near the bar whose median could not be completed: the further judgments it needed could not run, even queued again. The report is `failed` and is NEITHER a pass nor a veto (\"Plan coverage couldn't finish judging — Score again\"); `judging` lists the judgments that ran, with `stoppedBecause: judging_incomplete`. Score the plan again. Null otherwise."},"judging":{"oneOf":[{"$ref":"#/components/schemas/StoryboardCoverageJudging"},{"type":"null"}],"description":"How the verdict was reached. One judgment is noisy near the bar, so a revision whose first judgment lands within `nearGateBand` of a gating line (the overall, or a veto dimension) is judged again — the SAME revision, up to `maxJudgments`, free to the writer — and the gate reads the MEDIAN of each score, vetoes included. A plan edit starts a fresh set. While another judgment is pending, `pending` is true and the judgments so far are listed. Null before a judgment is recorded."},"defects":{"type":"array","description":"Plan defects decided in code, not by a model. `blocking` ones fail the gate on their own; `advisory` ones are evidence handed to the rubric.","items":{"type":"object","required":["code","severity","beatIndexes","subject","detail"],"properties":{"code":{"type":"string","enum":["missing_motion_direction","missing_shot_type","missing_location","shot_ladder_collapsed","shot_ladder_duplicate_framing","location_flip_flop","missing_character_reference","missing_location_plate","character_never_featured","opens_on_arrival","ends_on_exit","cut_ladder_no_range","promised_never_shown","canon_lead_absent","canon_lead_never_subject","canon_character_not_castable","approved_beat_dropped_silently","video_intent_missing","arc_stage_missing","question_not_advanced","arc_out_of_order","arc_stage_unserved","ending_off_question","transition_unexplained","characters_never_speak","character_lines_under_share"]},"severity":{"type":"string","enum":["blocking","advisory"]},"beatIndexes":{"type":"array","items":{"type":"integer","minimum":0}},"subject":{"type":"string"},"detail":{"type":"string"}}}},"gateOverride":{"type":"object","required":["allowed","why"],"additionalProperties":false,"description":"Whether a failed gate may be rendered over on the writer's word (`acknowledgePlanCoverage` on the plan approval). Allowed ONLY when a complete report of the plan as it stands, scored on the current rubric, failed on the judge's scores alone (the overall score, or a judged veto dimension, under the bar); never over a blocking defect decided in code, and never for a missing, queued, running, failed, stale or old-rubric report. `why` says which, in the writer's terms.","properties":{"allowed":{"type":"boolean"},"why":{"type":"string"}}},"writerOverride":{"oneOf":[{"$ref":"#/components/schemas/PlanCoverageOverride"},{"type":"null"}],"description":"The override the writer recorded on THIS report by approving over its gate — who (and via which surface and key), when, why, and the revision, score and reasons it overrode. Every surface shows it as \"approved over the plan gate: <reason>\". Null when no approval overrode this report."},"createdAt":{"oneOf":[{"type":"string","format":"date-time"},{"type":"null"}]},"updatedAt":{"oneOf":[{"type":"string","format":"date-time"},{"type":"null"}]}}}}}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/trailers/{jobId}/intent":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Trailer job id."}],"get":{"operationId":"getTrailerIntent","tags":["Trailer"],"summary":"Read the trailer's intent and what a first-time viewer takes away.","description":"Returns the trailer's intent, each beat's arc stage and how it advances the central question, where the beats break the arc, and what plan coverage says: the first-time-viewer takeaway it wrote before seeing the intent, and the story checks. Every deck and trailer is planned from an explicit intent — what the video is for, its logline, theme, central question, stakes, what a first-time viewer is promised, a four-stage arc (setup, escalation, turn, closing question) and how the ending lands the question. Every chapter or beat names the arc stage it serves (`arcStage`), how it advances the central question (`advancesQuestion`) and, after the first, why it follows the one before it (`whyItFollows`: the visible link — a cause, an answer, a contrast, a match cut, a look, an object, a sound — and what it moves forward; one line, never shown on screen). Plan coverage first writes what a first-time viewer would take away from the plan without seeing the intent (`viewerTakeaway`, with how much of the story the words recited rather than let them work out: `toldVersusShown`, and where they lost the thread: `lostTheThread`), then compares it with the intent: intent clarity, the central question, the buildup and whether the theme is carried each fail the plan on their own, and so do subtext — a plan that recites its plot instead of posing its question — and transitions — a part that follows the one before it for no reason a first-time viewer can feel (`weakTransitions` names the weakest, each with its fix); voices (do the characters speak, rather than the narrator carrying everything) is scored beside them. A read. Scope: `story:read`.","security":[{"BearerApiKey":["story:read"]}],"responses":{"200":{"description":"The intent beside what the plan says.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VideoIntent"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"updateTrailerIntent","tags":["Trailer"],"summary":"Edit a draft trailer's intent.","description":"Edits the intent of a DRAFT trailer: each field given replaces the stored one (`arc` the whole four-stage arc), and the merged intent must be complete and build (the arc's intensity rises to the turn). Editing the intent is free and only while the plan is a draft (approving the plan approves its intent): it saves the new intent and queues plan coverage again for the latest revision — a burst of edits is one coverage run, started once the plan has been left alone for three minutes. The chapters or beats are not re-planned; edit them (their `arcStage`, `advancesQuestion` and `whyItFollows` included) to follow the new intent. A plan planned before intents existed takes a complete intent. Answers the saved intent, the changed plan's `planCoverage` and `arcProblems` — where the beats now break the new arc (fix them with the beat refine's `arcStage` and `advancesQuestion`). Refused once the plan is approved (409). Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VideoIntentEditRequest"}}}},"responses":{"200":{"description":"The saved intent.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VideoIntentEditAck"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/TrailerStateInvalid"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/trailers/{jobId}/beats/refine":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Trailer job id."}],"post":{"operationId":"refineTrailerBeat","tags":["Trailer"],"summary":"Refine one trailer beat.","description":"Edits one trailer beat's plan: what it delivers (`storyBeat`), BOTH sides of its seam — `transitionPrompt` outgoing and `transitionIn` incoming — its framing (`shotType`), motion direction, its staging (`characterAction` — what its people physically do — and `stagingNotes`, which a beat held as one shot is filmed from; '' clears either), on-screen caption, composition, duration, who is in the picture (`visibleCharacters` is the named characters on camera; unnamed people need no listing), where it is set (`location`, stored as a canonical location key), the stage of the trailer's intent it serves (`arcStage`, `advancesQuestion` — plan words that never make a take stale; on a draft the changed plan is queued for plan coverage, `planCoverage`), whether it is a rapid intercut run (`montage` — brief shots cut tight, as short as 1 s; it stales the takes), and `shots` — the beat's CUT LIST of shots with hard cuts between them, each with whatever camera move it wants, which is how a beat is made to contain an EVENT rather than an image. A beat names where it is set in its `location`: a canonical location key, the place its location image is made of. A beat may travel: a shot set somewhere else names that place in its own `location` (a key, or a place in plain words), and the render follows it there. Allowed in draft, review and complete. Free — it saves the plan and marks what went stale: an edit to what the beat films marks its takes out of date (they stay playable), a caption edit marks only its text layer; render the beat again with `beats/render-clip` (priced). Returns `needsRerender`, `planCoverage` and `previewUrl`. Provide at least one editable field. Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrailerBeatRefineRequest"}}}},"responses":{"200":{"description":"The saved beat + a shareable beat preview link.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrailerBeatPreview"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/TrailerStateInvalid"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/trailers/{jobId}/beats/render-clip":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Trailer job id."}],"post":{"operationId":"renderTrailerBeatClip","tags":["Trailer"],"summary":"Price, then render, one trailer beat again.","description":"The writer's priced retake: a new 480p take of one call (`callId`) or of every call of the beat. A beat whose plan changed since it rendered is recompiled first (free) and its new calls are priced. Each new take is checked like every take and may earn the call's one automatic retake at our cost (a line not heard or said in another character's voice, or an opening off its planned framing); a moderation refusal is released and never retried. TWO CALLS, PRICE FIRST: sent WITHOUT `confirmed` it prices the render and reserves nothing (`quotedOnly: true`, the `lines`, `credits`); sent again with `confirmed: true` it reserves exactly the quoted lines and queues the takes. The flag is optional rather than a required `true` because a field that must be true to validate cannot separate a confirmed call from an unconfirmed one. Refused before the price: a text-only beat, a beat not in the cut, a call id the beat does not have (or a call of a changed beat), and a cast member with no body figure — with the action that fixes it in `details.refusal`. Review or complete only. Returns a shareable per-beat `previewUrl`. Requires `Idempotency-Key`. Scope: `artifact:publish` — a read-only key can neither spend nor ask what spending would cost.","security":[{"BearerApiKey":["artifact:publish"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrailerRenderClipRequest"}}}},"responses":{"200":{"description":"The price (no `confirmed`) or the queued takes, plus a shareable beat preview link.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrailerRenderAnswer"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/InsufficientCredits"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/TrailerStateInvalid"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/trailers/{jobId}/beats/{beatIndex}/select-take":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Trailer job id."},{"name":"beatIndex","in":"path","required":true,"schema":{"type":"integer","minimum":0},"description":"Zero-based beat the call belongs to."}],"post":{"operationId":"selectTrailerTake","tags":["Trailer"],"summary":"Select a take of one of a beat's calls.","description":"Chooses which take of one call the cut plays and the finish delivers. NOT metered and NOT confirmed: it renders nothing and is reversible (select another take), and the beat's preview cut is re-assembled from the new selection. Any accepted take may be selected, a flagged one included; the answer's `warning` names what it is flagged for, and the finish quote and export carry it too. A take that produced no file cannot be selected. In a complete trailer a new selection sends the cut back to review, and the next finish quotes just that take. Review or complete only. Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrailerSelectTakeRequest"}}}},"responses":{"200":{"description":"The take now selected.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrailerSelectTakeAck"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/TrailerStateInvalid"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/trailers/{jobId}/beats/{beatIndex}/repair-take":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Trailer job id."},{"name":"beatIndex","in":"path","required":true,"schema":{"type":"integer","minimum":0},"description":"Zero-based beat the call belongs to."}],"post":{"operationId":"repairTrailerTake","tags":["Trailer"],"summary":"Price, then repair, one take of a beat's call in place.","description":"Every rendered take is watched with its sound after its checks. A cut nobody asked for at the start or end of an otherwise good take is trimmed off first, before any new take: free, frame for frame, only where the cut is measured in the file, outside every spoken line (each kept whole with a breath after it), and only when what is left keeps its chapter or beat within a quarter of its planned length. When the shot's automatic new take is spent and the take it plays still has such a cut outside its line, that end is trimmed as a last resort: past the quarter, as long as the line stays whole, at least 2 seconds are left and the chapter or beat keeps half its planned length. A cut inside a spoken line is never trimmed. A speaker whose lips keep moving past the take's last line, where nothing is heard, is trimmed half a second after the line, free and frame for frame. Lips moving where no line is heard are never retaken on their own: the same staging brings them back, so the shot is offered for restaging (one face, framed closer), a trim, or your call. Any other defect is remedied by rule during processing, at our cost: first a new take told what to avoid; a repair in place when the same problem comes back after a new take, or when the take passed every other check and only something removable (a period anachronism, garbled lettering, a stray or doubled person) or the light is wrong. Camera movement and its speed (a move that rushes, or dies into a hold at the end), timing, cuts and a crowd moving as one (extras in step, everyone turning or looking down at once, repeated faces) are only ever fixed by a new take, or by the free trim when they sit only at an end of the take; spoken shots are not repaired in place. A line the speaker's mouth did not speak on two takes is no longer retaken on camera: the next take stages it as a voice-over, the speaker seen from behind, the line heard over the picture. Every take plays its own sound under its own picture, so the lips follow what is heard: a line said in the take's own reading is kept, and only a line not heard, or said in a voice too like another character's, is a defect (a new take). Where a speaker's mouth cannot be measured (or the face is too small to trust a miss), the watcher's own judgment of the lips decides the line: a mouth it saw not speak is a miss, one it saw speak is uncertain. Each shot gets at most one automatic new take, one automatic repair in place and one automatic voice-over re-stage, so a new take whose removable defect came back is repaired in the same pass; a repaired or trimmed take is never remedied again on its own. A repair that fails on the repair service's own side (never one declined at its content check) and cost nothing is sent again once; if that fails too, a new take instead. A repaired take keeps the take's own sound, is judged again, and is kept as a new take linked to the one it repairs; it plays only when it passes. A trimmed take plays over the take it came from when it passes, or when it shows only part of what that take showed, with the same speech and faces: it is judged by time window, so anything seen inside the span it keeps counts as that take's own footage too, and it wins when the trim cut something off. This is the writer's own repair of one take. TWO CALLS, PRICE FIRST: sent WITHOUT `confirmed` it prices the repair (the seconds it sends — the whole take, or only the shots its defect sits in — at the repair rung, two seconds at least) and reserves nothing (`quotedOnly: true`, `credits`, `summary`, `note`); sent again with `confirmed: true` it reserves exactly that line and queues the repair as a new take linked to the one it repairs (`repairTake`). The repaired take keeps the take's own sound, is checked again, and plays only if it passes; a repair that does not land is released (nothing charged). Refused before anything is reserved: the project's spend gate (`project_precondition_failed`), a take that is not an accepted take of the call, a repair of a repair (`repair_of_a_repair`), a take whose repair was declined at the content check (`repair_refused_before`, never sent again unchanged), a spoken shot (`repair_spoken_shot_held`: not repaired in place yet), repair not set up (`repair_unavailable`), a length the repair cannot take (`repair_not_possible`) — each a 409 with the fixing sentence. To render a new take instead, use the unit's render-clip with `callId`: a new take is told what the watcher saw on the takes before it. Review or complete only. Requires `Idempotency-Key`. Scope: `artifact:publish`.","security":[{"BearerApiKey":["artifact:publish"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TakeRepairRequest"}}}},"responses":{"200":{"description":"The repair's price (no `confirmed`) or the queued repair.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TakeRepairAnswer"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/InsufficientCredits"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/TrailerStateInvalid"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/trailers/{jobId}/beats/{beatIndex}/take-repair-override":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Trailer job id."},{"name":"beatIndex","in":"path","required":true,"schema":{"type":"integer","minimum":0},"description":"Zero-based beat the call belongs to."}],"post":{"operationId":"overrideTrailerTakeRepair","tags":["Trailer"],"summary":"Override the repair engine on one take of a beat's call.","description":"NOT metered and NOT confirmed: free. `decision: keep` keeps the take as it is — what was found on it no longer ranks it down or warns, and the engine never acts on it; `decision: restore` lets the engine's decision stand again (both reversible). The call's default selection re-runs unless the writer chose its take explicitly, and the cut re-assembles when the take it plays changed. `decision: rewatch` watches again, at our cost, a take whose watch could not finish (its decision `unwatched`: never clean, never remedied on its own) — its checks are queued, it reads checks pending until the watch lands, and a take already watched is refused (409). Review or complete only. Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TakeRepairOverrideRequest"}}}},"responses":{"200":{"description":"The override, the take's decision after it, and the take the call plays now.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TakeRepairOverrideAck"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/TrailerStateInvalid"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/trailers/{jobId}/beats/{beatIndex}/stage-voice-over":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Trailer job id."},{"name":"beatIndex","in":"path","required":true,"schema":{"type":"integer","minimum":0},"description":"Zero-based beat the call belongs to."}],"post":{"operationId":"stageVoiceOverTrailerTake","tags":["Trailer"],"summary":"Price, then stage, one take's line of a beat's call as a voice-over in a new take.","description":"NOT a free call: METERED like a new take of the call, TWO CALLS, PRICE FIRST. Stages the line of one take as a VOICE-OVER in a new take of its call: the speaker seen from behind, the line heard over the picture, so no mouth has to follow it. The lines staged are the call's on-camera lines whose speaker's lips failed them on a take of it, else every line it says on camera. The engine does this itself, at our cost, once the speaker's lips fail the same line on two takes of a call. Sent WITHOUT `confirmed` it prices the new take and reserves nothing (`quotedOnly: true`, `credits`, `voiceOverTake`, `lineRefs`, `summary`, `balanceAfter`, `sufficient`, `note`); sent again with `confirmed: true` it reserves exactly that line, records the request on the take and queues the new take, which is checked like every take and plays only when it is the best take of the call. Refused before anything is reserved: the spend gate, a take that is not an accepted take of the call, and a call that says nothing on camera (`voice_over_nothing_to_stage`). Review or complete only.","security":[{"BearerApiKey":["artifact:publish"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StageVoiceOverRequest"}}}},"responses":{"200":{"description":"The new take's price (no `confirmed`) or the queued voice-over take.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StageVoiceOverAnswer"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/InsufficientCredits"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/TrailerStateInvalid"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/trailers/{jobId}/beats/{beatIndex}/drop-line":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Trailer job id."},{"name":"beatIndex","in":"path","required":true,"schema":{"type":"integer","minimum":0},"description":"The beat whose line is cut out."}],"post":{"operationId":"dropTrailerBeatLine","tags":["Trailer"],"summary":"Cut one of a beat's lines out of its footage and keep the rest (free).","description":"NOT metered and NOT confirmed: FREE, one call. Cut the line, keep the rest (free, one step): one of the beat's spoken lines leaves its plan (its words and who says it), and each take that says it is trimmed to the footage on one side of it — after the line when the line opens the take, before it when it closes it — measured where the take says it, with a small pad, so nothing of the line is heard. The trimmed take is the same footage, so the beat stays current: nothing is rendered or charged, the trimmed take plays as soon as it lands, and its sound is the take's own sound with the line cut out. A beat with several lines names the one to cut (`line`, from 1, as its `lineDrops` list them). A line with footage on both sides is refused until you choose keep before or keep after; so is a side shorter than 1.5 s. Name a beat to move the line to and its words and speaker go there, to be rendered with that beat's own staging; otherwise add it there with a refine. The beats read's `lineDrops` says, line by line, what it would do now (the takes it trims and the span each keeps) or why it cannot; `droppedLines` shows the lines already cut out and each trim's state. Calling it again (without `line`) on a beat whose line was already cut out queues again only the trims that never landed (`resumed`). Refused before anything changes (409, `details.refusal.code`): `drop_line_which_line` (name `line`), `drop_line_in_middle` (choose `keep`), `drop_line_too_short`, `drop_line_cuts_other_line`, `drop_line_not_rendered`, `drop_line_no_take`, `drop_line_unplaced`, `drop_line_no_line`, `drop_line_target_has_line`. A complete trailer goes back to review. Review or complete only. Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrailerDropLineRequest"}}}},"responses":{"200":{"description":"The line, cut out: each take's trim queued as a new take.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrailerDropLineAnswer"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/TrailerStateInvalid"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/trailers/{jobId}/beats/{beatIndex}/trim-take":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Trailer job id."},{"name":"beatIndex","in":"path","required":true,"schema":{"type":"integer","minimum":0},"description":"Zero-based beat the call belongs to."}],"post":{"operationId":"trimTrailerTake","tags":["Trailer"],"summary":"Trim one take of a beat's call to a span (free).","description":"NOT metered and NOT confirmed: FREE, one call. Trims one take to the span `[keepStartSeconds, keepEndSeconds)` — \"trim this take to <start>–<end>\" — snapped to the take's frames and kept as a NEW take linked to it (`trimTake`); the take itself stays. Never into a spoken line: a span that cuts into a line placed on the take's speech track is refused (`trim_cuts_speech`), as is a span outside the take, shorter than the shortest a take is trimmed to, or the whole take (`trim_span_invalid`) — each before anything is queued. The trimmed take is checked again and watched, and plays over the take it came from only if it passes; its cut then plays its real, shorter length. The repair engine does the same on its own when a cut nobody asked for sits at the start or end of an otherwise good take (rule `trim_edge`, measured in the file: a hard cut or the first frame of a dissolve), and when a speaker's lips run on past the take's last line where nothing is heard (rule `trim_overrun`: cut half a second after the line as the take says it). Each take's `trimBounds` says what it may keep. Review or complete only. Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TakeTrimRequest"}}}},"responses":{"200":{"description":"The trim, queued as a new take.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TakeTrimQueued"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/TrailerStateInvalid"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/trailers/{jobId}/finish":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Trailer job id."}],"post":{"operationId":"finishTrailer","tags":["Trailer"],"summary":"Price, then run, the finish of a trailer.","description":"Finishes the cut: every selected take is upscaled from its 480p render to the delivery class — 720p unless the writer asked for 1080p (`deliveryResolution`, else the trailer's own class) — the on-screen text layers are composited over the finished cut, and the finished master is stitched. Nothing is generated again, and a take already finished at the class is reused, not charged twice. TWO CALLS, PRICE FIRST: sent WITHOUT `confirmed` it prices the finish and reserves nothing (`quotedOnly: true`, the `lines`, and a `warnings` entry per flagged take the writer kept); sent again with `confirmed: true` it reserves exactly those lines and starts the finish (`finishing`, then `complete`). Refused before the price while any call has no selected take (a call with no accepted take: render it again), and while the master the writer is looking at FAILED its video coverage (`trailer_video_coverage_failed`, `details.videoCoverage`: the score, the bar and the findings with their fixes) — unless `acknowledgeVideoCoverage: true`, which only the writer's own choice to finish anyway may set. Coverage still running, one that could not run, or one of an earlier master never holds the finish: the quote's `warnings` say so. Review or complete (finishing again at a new class) only. Requires `Idempotency-Key`. Scope: `artifact:publish`.","security":[{"BearerApiKey":["artifact:publish"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrailerFinishRequest"}}}},"responses":{"200":{"description":"The finish price (no `confirmed`) or the running finish.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrailerFinishAnswer"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/InsufficientCredits"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/TrailerStateInvalid"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/trailers/{jobId}/reassemble":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Trailer job id."}],"post":{"operationId":"reassembleTrailer","tags":["Trailer"],"summary":"Re-assemble a trailer whose assembly failed, or rebuild its master in review or complete (free).","description":"Puts a FAILED trailer back together from the takes it already has. A trailer whose assembly failed for good — `assemble`, the 480p preview, or `finish_assemble`, the finished master — lost nothing it paid for: every call's takes (and their finished upscales) are kept. This re-runs THAT stage: the trailer goes back to `rendering` (then `review`) or `finishing` (then `complete`). NOT metered and NOT confirmed: nothing is rendered, upscaled or reserved. Refused with 409 `trailer_state_invalid` (`details.refusal.code`: `trailer_reassemble_not_failed`, `trailer_reassemble_not_an_assembly_failure`, `trailer_reassemble_pass_incomplete`, `trailer_reassemble_missing_takes`) for a trailer that is not failed, one that failed while its shots were being sent to render or to be finished, or one missing an accepted take — GET /trailers/{jobId}/beats `reassembly` says which before you call. A trailer in `review` or `complete` has its master REBUILT the same way, free and FRESH, with the current sound rules (the only voices in a master are the cast's): in review the preview assembly runs (`stage: assemble`) — every cut an older recipe made is re-cut with the current one — and the trailer STAYS in review; complete, the finished master is stitched again at its class (`stage: restitch`): the finished cuts as they are (their text layers are laid over them), the narration, a black's sound and the score laid again with the current rules, and the trailer STAYS complete (a new finish re-cuts the finished cuts). The answer's `status` is where the trailer stays (`review` / `complete`). The selected takes are never changed; the new master is watched for video coverage at our cost. A rebuild is refused with 409 `trailer_reassemble_assembly_in_flight` while a master is already being assembled for the trailer (queued or running), `trailer_reassemble_take_in_flight` while a shot has a take being rendered or finished, and `trailer_reassemble_missing_takes` when a selected take is missing (for a finished trailer: not finished at its class — finish it again). Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"200":{"description":"The stage it runs and the status the trailer is in while it runs (where it stays, for a rebuild).","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RenderReassembleAck"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/TrailerStateInvalid"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/trailers/{jobId}/beats/{beatIndex}/abandon-take":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Trailer job id."},{"name":"beatIndex","in":"path","required":true,"schema":{"type":"integer","minimum":0},"description":"Zero-based beat the call belongs to."}],"post":{"operationId":"abandonTrailerTake","tags":["Trailer"],"summary":"Let one take still out at the provider go (free; it gives back what was held).","description":"Let ONE take of the beat's call go when it is still out at the provider (handed back pending between two polls) — the per-take counterpart of Stop render (`POST /trailers/{jobId}/cancel-render`), on the same core. Its claim comes off the shot, the take is recorded failed with a plain notice on the beat, the writer's credits held for it are given back (`releasedCredits`), and the automatic fixes carry on. The provider may still bill it: that is ours, never the writer's. Every other take is kept. A take that is not out at a provider answers `abandoned: false` and changes nothing; one being polled right now is refused (409, `details.refusal.code` `reference_render_take_busy`: try again in a minute). The engine does the same on its own when a take's polls run out, and tells the writer. Answers `{ jobId, surface, callId, take, abandoned, kind, taskId, releasedCredits, note }`; read `GET /trailers/{jobId}/beats` after it. NOT metered and NOT confirmed: free. Requires `Idempotency-Key`. Scope: `artifact:publish` — the key that can start and spend on renders is the key that can let paid work in flight go.","security":[{"BearerApiKey":["artifact:publish"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TakeAbandonRequest"}}}},"responses":{"200":{"description":"Whether the take was let go, what it was waiting on, and what was given back.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TakeAbandonResult"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/TrailerStateInvalid"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/trailers/{jobId}/cancel-render":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Trailer job id."}],"post":{"operationId":"cancelTrailerRender","tags":["Trailer"],"summary":"Stop the trailer's render in flight (free; it gives back what was held).","description":"Stop the trailer's render in flight — its first pass, its finish, or a retake or repair still running. Nothing more is sent to a provider: its queued and running render jobs are cancelled, and a job already running stops at its next step. The writer's reservations for work that had not run are released (`released`, `releasedCredits`); every accepted take, selection, cut, finished take and master is kept (`keptTakes`). A shot already out at the provider is stopped and recorded on its beat (`abandoned`): not charged to the writer (the provider may still bill it, at our cost), and never resumed. It lands in `review` with what rendered, back at `draft` when nothing did (approving again quotes the same shots), back in `review` after a stopped finish, or as it was after a stopped retake. Free: no price and nothing to confirm. With nothing rendering it answers `stopped: false` and changes nothing. Answers `{ jobId, surface, stopped, previousStatus, status, cancelledJobs, abandoned, released, releasedCredits, keptTakes, note }`; read `GET /trailers/{jobId}/beats` after it. NOT metered and NOT confirmed. Requires `Idempotency-Key`. Scope: `artifact:publish` — the key that can start and spend on renders is the key that can stop them.","security":[{"BearerApiKey":["artifact:publish"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"200":{"description":"What the stop did: the status it lands in, what it gave back, what it abandoned and kept.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RenderCancelResult"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/TrailerStateInvalid"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/trailers/{jobId}/more-fixes":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Trailer job id."}],"post":{"operationId":"allowMoreTrailerFixes","tags":["Trailer"],"summary":"Let the trailer's automatic fixes try more (priced: one more allowance grant).","description":"The render approval carries an automatic-fix allowance: the engine watches the 480p preview and retakes the shots its review flags, inside that ceiling, until the preview is clean or it stops short (GET /trailers/{jobId}/beats `autoFix`: `state: stopped`, `stopReason`, `stoppedBecause`). When it stopped for want of credits or rounds, or because a flagged shot reached its tries, `autoFix.moreFixes` offers ONE MORE GRANT and `verdict.choices` carries `more_fixes`. METERED, TWO CALLS: without `confirmed` it PRICES the grant and reserves nothing (`quotedOnly: true`, `credits`, `costLabel`, `balanceAfter`, `sufficient`, `note`; when `sufficient` is false do not confirm); with `confirmed: true` it reserves it, raises the loop's caps (a round of fixes and a try per shot more) and the loop answers the same review again (`quotedOnly: false`, `queued: true`, `message`, `autoFix`). Only what the fixes draw is charged; the rest comes back when the loop stops. Refused with 409 `trailer_state_invalid` (`details.refusal.code`: `auto_fix_not_in_review`, `auto_fix_nothing_more` — still fixing, or nothing a new take or a text render would fix: finish anyway instead — `auto_fix_already_running`), 402 when the balance cannot hold the grant. Review only. Requires `Idempotency-Key`. Scope: `artifact:publish`.","security":[{"BearerApiKey":["artifact:publish"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MoreFixesRequest"}}}},"responses":{"200":{"description":"The price of one more grant, or the grant reserved and the loop running again.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MoreFixesAnswer"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/InsufficientCredits"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/TrailerStateInvalid"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/trailers/{jobId}/video-coverage":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Trailer job id."}],"get":{"operationId":"getTrailerVideoCoverage","tags":["Trailer"],"summary":"Read the trailer's video coverage.","description":"Returns the latest VIDEO COVERAGE report on the trailer: the stitched master (the 480p preview, then the finished master) watched with its sound and scored on its quality and its adherence to the shot list — lip sync, prompt adherence, visual fidelity, motion naturalness, identity continuity, text placement, audio sync — never its story. It says which master it watched and whether that is the master the trailer has now (`current`), the score against the bar (`gateThreshold`, with `vetoDimensions` that fail it on their own), every finding with its timecode located in code on beats, calls and shots and the remedy that fixes it, and whether it holds the paid finish (`blocksFinish`; then the finish is refused with `trailer_video_coverage_failed` unless the writer finishes anyway). It runs by itself after every stitch; a trailer with no report answers `status: \"missing\"`, never a 404. The same object is `videoCoverage` on GET /trailers/{jobId}/beats. Scope: `story:read`.","security":[{"BearerApiKey":["story:read"]}],"responses":{"200":{"description":"Trailer video coverage envelope.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VideoCoverageEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"generateTrailerVideoCoverage","tags":["Trailer"],"summary":"Cover the trailer's current master again (free).","description":"Watches the trailer's CURRENT stitched master again as a new report version. NOT metered and NOT confirmed: the watch costs the writer nothing, so there is no price and no `confirmed`. Coverage already runs by itself, idempotent per master, when the preview is stitched (review) and again when the finished master is (complete); call this to cover the master again (with an optional `focusPrompt`). A report already running on this master is returned as it is. Refused with 400 for a trailer with no stitched master, one that is not in review or complete, or when no provider can watch a video right now (that never holds the finish). Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VideoCoverageGenerateRequest"}}}},"responses":{"200":{"description":"The queued (or already running) video coverage envelope.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VideoCoverageEnvelope"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/trailers/{jobId}/storyboard/regenerate-text":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Trailer job id."}],"post":{"operationId":"regenerateTrailerText","tags":["Trailer"],"summary":"Price, then render, one trailer beat's text / motion-graphic layer.","description":"Renders one trailer beat's on-screen text / motion-graphic layer with its current caption + treatment, composited over the beat's cut or its poster. The finish composites every text layer at the delivery class; this is for seeing a caption before the finish. Metered — charges a single per-text credit (returned as `credits`). Poll GET /trailers/{jobId}/beats. TWO CALLS, PRICE FIRST: sent WITHOUT `confirmed` it prices the render and reserves nothing (`quotedOnly: true`); sent again with `confirmed: true` it reserves the single credit line and queues the render. The flag is optional rather than a required `true` because a field that must be true to validate cannot separate a confirmed call from an unconfirmed one, and it would force the price to be announced after the money moved. Preconditions run above the price: a beat missing from the cut, and a cut in a status where beats can no longer be edited, are refused with no quote at all. A confirmed call returns `reviewUrl` + a shareable per-beat `previewUrl`. Requires `Idempotency-Key`. Scope: `artifact:publish` — a read-only key can neither spend nor ask what spending would cost.","security":[{"BearerApiKey":["artifact:publish"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrailerBeatIndexRequest"}}}},"responses":{"200":{"description":"The price (no `confirmed`) or the queued render, plus a shareable beat preview link.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrailerTextRenderAnswer"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/trailers/{jobId}/storyboard/move-beat":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Trailer job id."}],"post":{"operationId":"moveTrailerBeat","tags":["Trailer"],"summary":"Move a trailer beat to another position.","description":"Moves one beat to another position in the cut; `toIndex` is where the beat ENDS UP. NOT metered — it costs no credits and generates nothing, so there is nothing to quote or confirm. It rewrites the order and carries each beat's rendered takes and cut with it. Returns the new `beatCount`, `movedTo`, and `staleBeats` — the beats whose FOLLOWING beat changed, so their seam direction is out of date; their footage stays playable. Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrailerMoveBeatRequest"}}}},"responses":{"200":{"description":"The reordered cut + the beats now out of date.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrailerBeatOrderAck"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/trailers/{jobId}/storyboard/insert-beat":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Trailer job id."}],"post":{"operationId":"insertTrailerBeat","tags":["Trailer"],"summary":"Insert a blank trailer beat at a seam.","description":"Inserts a new blank beat: `atIndex` is the position it takes, `afterBeatIndex` puts it just after that beat, and omitting both appends it to the end. NOT metered — the new beat arrives with no footage; direct it with `beats/refine`, then render it with `beats/render-clip` (priced). Returns `insertedBeatIndex`, the new `beatCount`, and `staleBeats`. Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrailerInsertBeatRequest"}}}},"responses":{"200":{"description":"The new beat's position + the beats now out of date.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrailerBeatOrderAck"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/trailers/{jobId}/storyboard/remove-beat":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Trailer job id."}],"post":{"operationId":"removeTrailerBeat","tags":["Trailer"],"summary":"Remove a trailer beat.","description":"Removes one beat from the cut, with everything already rendered on it. NOT metered — and it refunds nothing: what that beat bought is gone. The cut's last beat cannot be removed. Returns the new `beatCount` and `staleBeats`. Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrailerRemoveBeatRequest"}}}},"responses":{"200":{"description":"The shortened cut + the beats now out of date.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrailerBeatOrderAck"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/trailers/{jobId}/storyboard/takes":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Trailer job id."}],"get":{"operationId":"getTrailerBeatTakes","tags":["Trailer"],"summary":"Read the earlier cuts a trailer beat has played.","description":"Reads the cuts a beat played BEFORE the one it plays now — newest first, capped at `takesKept`. A beat's cut is assembled from its calls' selected takes, so each earlier cut stands for an earlier selection. NOT metered, and not a mutation: it hands back urls that already exist. Pass `beat` to narrow to one beat, or omit it to see which beats in the whole cut have anything to compare. Call this before `restore-take`, which needs an exact url. The per-call takes themselves are on GET /trailers/{jobId}/beats. Scope: `story:read`.","security":[{"BearerApiKey":["story:read"]}],"parameters":[{"name":"beat","in":"query","required":false,"schema":{"type":"integer","minimum":0},"description":"Zero-based beat to read. Omit for every beat in the cut."}],"responses":{"200":{"description":"Each beat's current cut and the earlier cuts behind it.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrailerBeatTakes"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/trailers/{jobId}/storyboard/restore-take":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Trailer job id."}],"post":{"operationId":"restoreTrailerTake","tags":["Trailer"],"summary":"Put an earlier cut of a beat back.","description":"Puts one of a beat's earlier cuts back and re-selects the takes it was assembled from, so the finish delivers exactly what was restored. NOT metered and NOT confirmed: it renders nothing, and it is reversible — the cut it displaces joins the beat's list, so the same call with the other url undoes it. `url` must be a cut THIS BEAT played (read them from `storyboard/takes`); a url the beat never held is refused with a 400 rather than trusted. A complete trailer goes back to review. Returns `{ success, clipUrl, selection }`. Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrailerRestoreTakeRequest"}}}},"responses":{"200":{"description":"The cut now playing, and the takes re-selected.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrailerRestoreTakeAck"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/TrailerStateInvalid"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/trailers/{jobId}/interludes":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Trailer job id."}],"post":{"operationId":"updateTrailerInterludes","tags":["Trailer"],"summary":"Replace the trailer's black interludes.","description":"Replaces the trailer's black interludes with the list given (`[]` removes them all; the beats read returns the current ones as `interludes`). A black interlude is a beat of pure black between two beats of the trailer (0.5 to 2 seconds), keyed by the beat it follows (`afterBeatIndex`, the beat's place from 0; at most one after any beat and never after the last), that may carry one sound (a breath, a bell) or one short line heard over the dark (at most 12 words), the narrator's or a character's own, always as a voice-over. It costs no render: the master draws the black and voices its line and its sound at no charge. Inserting, moving or removing a beat moves each black with the beat it follows. A whole list replaces the trailer's (an empty list removes them all); on a rendered trailer the master is stitched again with it, free. The whole list is validated: each follows a beat the plan has and never the last one, at most one after any beat, a line of at most 12 words, and a cast speaker who has a voice (400 otherwise). Free. On a draft plan the changed plan is queued for plan coverage again (`planCoverage`); in review or complete the master is re-assembled with the black, its lines and its sounds (`reassembling`). Refused while a render or finish pass runs (409). Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrailerInterludesUpdateRequest"}}}},"responses":{"200":{"description":"The trailer's interludes, as saved.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrailerInterludesUpdateAck"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/TrailerStateInvalid"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/trailers/{jobId}/closing-sequence":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Trailer job id."}],"post":{"operationId":"updateTrailerClosingSequence","tags":["Trailer"],"summary":"Set the trailer's closing sequence.","description":"Sets the trailer's closing sequence whole (`null` removes it; the beats read returns the current one as `closingSequence`, with each card's length and start, and `fadeOut`). A trailer's closing sequence ends its master, after the last beat: optionally a closing quote (`quote`: its words, quoted exactly, and where they are from, cited exactly), then \"Coming Soon\" (`comingSoon`, up to 40 characters; \"Coming Soon\" when left out), then the production credit (`productionCredit`; \"Sleeper Hit Studio\" when left out), each quiet on black, faded in and out and held long enough to read. It ADDS to the trailer's length: no beat is shortened for it. Across it the music (the score or the beds) fades out smoothly to silence on the master's last frame: what still sounds at the last beat's cut plays on under the cards. It is free: nothing is rendered for it, and a rendered trailer's master is stitched again with it. `enabled: false` keeps it without playing it. How its words look and move is directed separately (`closingElement` on the trailer's direct-text-motion call; read back as `textMotion`), and a save keeps those directions. Validated whole; an unknown field is refused by name (400). Free. On a draft plan it is saved on the plan; in review or complete the master is re-assembled with it (`reassembling`). Refused while a render or finish pass runs (409). Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrailerClosingSequenceUpdateRequest"}}}},"responses":{"200":{"description":"The trailer's closing sequence, as saved.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrailerClosingSequenceUpdateAck"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/TrailerStateInvalid"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/trailers/{jobId}/storyboard/direct-text-motion":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Trailer job id."}],"post":{"operationId":"directTrailerTextMotion","tags":["Trailer"],"summary":"Direct how a trailer beat's text (or a closing card) moves.","description":"Say in plain words how one beat's on-screen text should move — \"type it out letter by letter, then draw an underline beneath it\" — and it is compiled into the bounded motion spec that beat's text layer executes. NOT metered and NOT confirmed: it costs no credits and renders nothing, and re-directing REPLACES the treatment rather than adding to it, so there is nothing a confirmation would protect. Your own words are saved beside the compiled spec, so the next direction is not authored blind. Nothing MOVES on screen until the text layer is re-rendered with `storyboard/regenerate-text`, which IS metered. A beat with nothing to move is refused with a 409 before anything is compiled — a beat that is footage only, or one whose caption is still empty — and the refusal names the fix. With a `closingElement` (quote, comingSoon, productionCredit) instead of a `beatIndex` it directs one of the closing sequence's cards, or puts it back in its own type with `clear: true`; the master draws the card, so a rendered trailer is stitched again with it (free). A trailer's closing sequence's words are directed in the writer's own words through the trailer's direct-text-motion call with a `closingElement` instead of a `beatIndex`: `quote`, `comingSoon` or `productionCredit` (\"Coming Soon wide and slow, a faint vignette breathing behind it\"). The same director compiles the sentence into a motion-text spec (face, colour, size, motion and a quiet background) and saves it with the sentence on the closing sequence (`textMotion`). The words stay the closing's: a direction says how they look, never what they say. `clear: true` puts a card back in its own type. Free: an LLM call, no credits; a rendered trailer's master is stitched again with it, and nothing else renders. Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrailerDirectTextMotionRequest"}}}},"responses":{"200":{"description":"The direction now on the beat.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrailerDirectTextMotionAck"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/TrailerBeatHasNothingToMove"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/trailers/{jobId}/video-export":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Trailer job id."}],"post":{"operationId":"exportTrailerVideo","tags":["Trailer"],"summary":"Re-stitch the finished trailer master.","description":"Rebuilds the finished MP4 over the finished cuts and the narration and music ALREADY on the job. Generates nothing, so it is how a music bed or a narration track that landed after the finish is folded in. ONLY A COMPLETE TRAILER EXPORTS: a 480p preview master is for review, never the deliverable, so a trailer in review is refused with a 409 that says to finish it first. The answer carries `warnings`, one per flagged take the writer kept (a line not heard or in another character's voice, an opening off its plan). A queued or rendering export short-circuits rather than stacking. Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"200":{"description":"Master re-stitch queued; poll `renderedVideoStatus`.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrailerExportAck"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/TrailerStateInvalid"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/trailers/{jobId}/plan/approve":{"parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string"},"description":"Trailer job id."}],"post":{"operationId":"approveTrailerPlan","tags":["Trailer"],"summary":"Price, then render, a draft trailer's beat plan.","description":"Approves a draft trailer's beat plan into the render — the next step of EVERY trailer job, which stops at its plan (a story job's `progress.trailerJobId`, `progress.nextStep: approve_trailer_plan`): approving THIS job renders the cut that was scored, while creating a new plan re-runs the stochastic planner and renders an ungraded draw. TWO CALLS, PRICE FIRST: sent WITHOUT `confirmed` it runs the coverage gate, compiles and voices every footage beat (free to the writer) and prices the 480p render — a line per call at its voiced seconds and a line per location plate the renders make first — reserving nothing (`quotedOnly: true`, `lines`, `calls`, `warnings`); sent again with `confirmed: true` it reserves exactly the quoted calls and starts the render (`rendering`, then `review`). Every call renders at 480p with the cast's own recorded lines; a take that leaves a line unheard or says it in another character's voice, has lips out of step with its lines, or misses its opening framing is retaken once automatically at our cost. Refused before the price with the action that fixes it (`details.refusal`, `details.blocked`): an unscored or failing plan, a narrated cut with no narrator, a beat whose cast has no body figure or cast voice. A plan the gate refuses on the judge's scores alone says so (`details.overridable: true`, `details.overrideWhy`): the writer may render it anyway by sending `acknowledgePlanCoverage: { reason }` on the price and the confirm alike — only on their explicit choice. `details.overridable: false` cannot be rendered over (a blocking defect decided in code, or a missing, running, failed, stale or old-rubric report). The confirmed approval records the override on the report (the coverage read's `writerOverride`); the answer carries it as `planCoverageOverride`. Draft only. Requires `Idempotency-Key`. Scope: `story:write`. A trailer is priced by the second: 1 credit a second to render at 480p, plus 1 credit a second to finish at 720p (2 credits a second in all; a 1080p finish is 4 credits a second instead). Planning is free. The exact price is quoted per shot once the plan is ready, before anything renders; location plates and on-screen text are itemized in the same quotes.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrailerPlanApproveRequest"}}}},"responses":{"200":{"description":"The render price (no `confirmed`) or the queued render.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TrailerRenderAnswer"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/InsufficientCredits"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/TrailerStateInvalid"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/library/images":{"get":{"operationId":"listMyLibraryImages","tags":["Library"],"summary":"List your reusable My-Library images.","description":"Lists one page of the caller's reusable My-Library images (cached cast headshots, with the full-body figure when one exists), newest first: each image's `id`, the character it depicts and the script it came from. `total` is how many the whole library holds; pass `nextCursor` back as `cursor` for the next page (`null` on the last). A read of what the library holds; nothing renders from it directly. Scope: `story:read`.","security":[{"BearerApiKey":["story:read"]}],"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100},"description":"Images per page (default 48, max 100)."},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"},"description":"Opaque cursor: the `nextCursor` of the previous page."}],"responses":{"200":{"description":"Your reusable My-Library images.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MyLibraryImages"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/library/audio":{"get":{"operationId":"listMyLibraryAudio","tags":["Library"],"summary":"List your reusable My-Library audio.","description":"Lists one page of the durable music, sound effects, and soundscapes owned by the caller, newest first. Filter by `kind` or search labels/prompts with `query`. `total` is how many match across every page; pass `nextCursor` back as `cursor` (with the same filters) for the next page (`null` on the last). Scope: `story:read`.","security":[{"BearerApiKey":["story:read"]}],"parameters":[{"name":"kind","in":"query","required":false,"schema":{"type":"string","enum":["music","sfx","soundscape"]}},{"name":"query","in":"query","required":false,"schema":{"type":"string","maxLength":200}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100},"description":"Assets per page (default 48, max 100)."},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"},"description":"Opaque cursor: the `nextCursor` of the previous page."}],"responses":{"200":{"description":"Your reusable My-Library audio.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MyLibraryAudio"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"importMyLibraryAudio","tags":["Library"],"summary":"Import durable reusable audio.","description":"Banks owned or licensed audio as music, SFX, or a soundscape. Provide exactly one public `sourceUrl` or base64 audio payload. The server validates the container bytes, enforces a 50 MB decoded limit, fetches URLs with SSRF protections, and deduplicates by content hash. Requires `Idempotency-Key`. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MyLibraryAudioImportRequest"}}}},"responses":{"200":{"description":"The imported or deduplicated reusable audio asset.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MyLibraryAudioImportResponse"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/library/audio/{assetId}":{"parameters":[{"name":"assetId","in":"path","required":true,"schema":{"type":"string"},"description":"The id of a reusable My-Library audio asset, from GET /library/audio."}],"patch":{"operationId":"renameMyLibraryAudio","tags":["Library"],"summary":"Rename a reusable My-Library audio asset.","description":"Changes ONE reusable asset label (1 to 120 characters) and nothing else. Everything that uses the asset — a scene bed, a region, a timeline clip — keeps playing it; only the name changes. The same rename the Writers Lab sound picker, MCP `rename_my_library_audio` and `sleeperhit library audio rename` make. Free and naturally idempotent, so no `Idempotency-Key`. An unknown or another account asset is 404. Scope: `story:write`.","security":[{"BearerApiKey":["story:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MyLibraryAudioRenameRequest"}}}},"responses":{"200":{"description":"The renamed reusable audio asset.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MyLibraryAudioRenameResponse"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/artifacts/{artifactId}/performance":{"parameters":[{"$ref":"#/components/parameters/StoryArtifactId"}],"post":{"operationId":"updateArtifactPerformance","tags":["Artifacts"],"summary":"Refine the delivery of all of one character's lines on a table read artifact.","description":"Adjusts the performance — emotion, speed, volume, and cadence — across ALL of one character's lines on the live read backing a `table_read` artifact. Set `character` to a name, `\"NARRATOR\"` (all narration lines), or `\"ALL\"` (every voiced line). Speed and volume accept either an absolute value (`speed`, 1 the natural pace; `volume` 0.5–2.0) or a relative per-line multiplier (`speedFactor`/`volumeFactor`, e.g. `1.2` for +20%). Each line's speed is brought inside the range of the provider that speaks it (Cartesia 0.6–1.5, ElevenLabs 0.7–1.2, Deepgram 0.7–1.5), and a line whose voice takes no speed setting (OpenAI, Hume, Eleven v3, Cartesia before sonic-3, Deepgram Aura-1, a Cartesia professional clone) keeps its pace — `speedUnsupportedLines` counts them and `speedNote` says why. Volume clamps to 0.5–2.0. Provide at most one of each pair. There is no separate cadence field — express cadence as `speed` plus `pauseAfterMs` (breathing room after each line). To flatten delivery (\"less emotive\"), pass `emotions: [\"neutral\"]` with `overrideExisting: true`. No new revision — the artifact id and share URLs stay stable, like a voice recast. Durable media is invalidated; update the recording (POST /artifacts/{artifactId}/update-recording, free) to hear the change — only the lines it changed are spoken again.  Requires `Idempotency-Key`. Scope: `artifact:publish`.","security":[{"BearerApiKey":["artifact:publish"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PerformanceDirectiveRequest"}}}},"responses":{"200":{"description":"Performance directive applied.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PerformanceDirectiveResponse"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/ArtifactNotFound"},"409":{"$ref":"#/components/responses/ArtifactNotReady"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/artifacts/{artifactId}/music":{"parameters":[{"$ref":"#/components/parameters/StoryArtifactId"}],"post":{"operationId":"generateArtifactMusic","tags":["Artifacts"],"summary":"Generate adaptive soundtrack for a table read artifact.","description":"(Re)generates the per-scene adaptive soundtrack for the read backing this `table_read` artifact, or updates soundtrack direction for the whole screenplay, one scene, or one entry/line when directive fields are provided. No new revision — the artifact id and share URLs stay stable, like a voice recast. Optionally tune how much of the read carries soundtrack via `musicCoveragePercent` (a fraction 0..1 or a whole percent 0..100; clamped into [0,1]). For directive edits, send `{ scope?, mode?, sceneIndex?, entryIndex?, prompt?, summary?, enabled?, weight? }`, e.g. \"make scene 2 heavier bass\", \"mute the cold open\", or `mode: \"replace\"` to overwrite a stale palette when switching scoring styles. To change one scene's music direction field by field, send `{ sceneIndex, direction: { endTransition?, mix?, ducking?, bpm?, density?, brightness?, guidance?, temperature?, topK?, scale?, seed?, instruments?, summary?, enabled? } }`: the same save as the Table Read page's scene console, which drops the scene's rendered bed so the next render plays the new direction. GET returns each scene's direction as `music.sceneDirections`. Full regeneration is async + queued: returns `{ status: \"generating\" }`. Directive edits return `{ status: \"ready\" }`. Poll GET /artifacts/{artifactId}/music until `music.status` is `ready`.  Requires `Idempotency-Key`. Scope: `artifact:publish`.","security":[{"BearerApiKey":["artifact:publish"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MusicGenerateRequest"}}}},"responses":{"200":{"description":"Adaptive soundtrack generation dispatched or scene soundtrack direction updated.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MusicGenerateResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/ArtifactNotFound"},"409":{"$ref":"#/components/responses/ArtifactNotReady"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"get":{"operationId":"getArtifactMusic","tags":["Artifacts"],"summary":"Read adaptive soundtrack status for a table read artifact.","description":"Returns adaptive soundtrack readiness for the read backing this artifact. `music.status` is `none` (no active soundtrack, including intentional 0% coverage), `generating` (scoring is still in progress), or `ready` (at least one scene carries soundtrack).  Scope: `artifact:read`.","security":[{"BearerApiKey":["artifact:read"]}],"responses":{"200":{"description":"Adaptive soundtrack status envelope.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MusicStatusEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/ArtifactNotFound"},"409":{"$ref":"#/components/responses/ArtifactNotReady"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/artifacts/{artifactId}/sfx":{"parameters":[{"$ref":"#/components/parameters/StoryArtifactId"}],"post":{"operationId":"manageArtifactSfx","tags":["Artifacts"],"summary":"Manage line-level sound-effect cues for a table read artifact.","description":"Mutates timed sound-effect cues on the live read backing a `table_read` artifact. Use `{ op: \"add\", entryIndex, label, prompt, volume?, triggerOffset?, offsetMs?, generatedDurationS?, enabled? }` to add a cue. Use `{ op: \"update\", id, ...fields }` to retime, rename, mute, or regenerate a cue. Use `{ op: \"remove\", id }` to remove a cue. Adding a cue, or updating one with `regenerate: true`, generates its audio: while the project's gate is closed it is refused with 409 `project_precondition_failed` before anything is generated or saved. A retime, rename, mute or removal generates nothing and stays open. This does not create a new artifact revision and does not change share URLs.  Scope: `artifact:publish`. Idempotency-Key is required.","security":[{"BearerApiKey":["artifact:publish"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SfxManageRequest"}}}},"responses":{"200":{"description":"SFX mutation result.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SfxMutationResponse"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/ArtifactNotFound"},"409":{"$ref":"#/components/responses/ArtifactNotReady"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"get":{"operationId":"listArtifactSfx","tags":["Artifacts"],"summary":"List sound-effect cues for a table read artifact.","description":"Returns timed sound-effect cues on the read backing this artifact. Scope: `artifact:read`.","security":[{"BearerApiKey":["artifact:read"]}],"responses":{"200":{"description":"SFX cue list envelope.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SfxListResponse"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/ArtifactNotFound"},"409":{"$ref":"#/components/responses/ArtifactNotReady"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/artifacts/{artifactId}/location-plates":{"parameters":[{"$ref":"#/components/parameters/StoryArtifactId"}],"get":{"operationId":"getArtifactLocationPlates","tags":["Artifacts"],"summary":"Read the location plates of the script behind a table-read artifact.","description":"The empty rooms every video render places the cast into, as capped canon assets. Every room the script stores or walks through, each lighting variant of it (the same room by day and by night is ONE room with two variants), and per variant the master, medium and close plate in the landscape and portrait pools: `status` (`ready`, or `missing` — made when a video renders), `imageUrl`, `description` (the room as the plate shows it, written when the plate was accepted), the judge record `note` (advisory), the accepted `verdict`, any `remake` in flight, and `canRemake`. `duplicates` lists rooms stored under two spellings in one light; the read reports them and renames nothing. Plates are never approved. COSTS NOTHING and NEVER makes a plate. Scope: `artifact:read`.","security":[{"BearerApiKey":["artifact:read"]}],"responses":{"200":{"description":"Location plates envelope.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LocationPlatesEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/ArtifactNotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/artifacts/{artifactId}/location-plates/{plateKey}/remake":{"parameters":[{"$ref":"#/components/parameters/StoryArtifactId"},{"$ref":"#/components/parameters/LocationPlateKey"}],"post":{"operationId":"remakeArtifactLocationPlate","tags":["Artifacts"],"summary":"Remake one location plate (metered — TWO CALLS, price first).","description":"The writer's escape hatch for a bad accepted plate, not a review step. METERED: one still image per attempt, at most 3 attempts. Sent WITHOUT `confirmed` it PRICES the remake and makes nothing (`quotedOnly: true`, `credits`, `balanceAfter`, `sufficient`, and a `note` to show the writer; when `sufficient` is false the caller must not confirm); sent again with `confirmed: true` it holds one credit line per attempt and queues ONE capped round — the same cap and judge as every plate — which keeps the least defective attempt and replaces the stored plate as soon as the round ends. The writer pays only for the attempts made; there is no approval step and nothing waits on the writer. Preconditions run above the price: an unknown key is 404 `location_plate_not_found`; a plate not made yet, a remake already running, a missing source plate, or no image model bound is 409 `location_plate_precondition_failed`, with nothing priced. A confirmed call answers with a `chatToolJobId`; read the location plates until the plate's `remake` leaves `pending`. Scope: `artifact:publish`. Idempotency-Key is required.","security":[{"BearerApiKey":["artifact:publish"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LocationPlateRemakeRequest"}}}},"responses":{"200":{"description":"The price (no `confirmed`) or the queued remake.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LocationPlateRemakeEnvelope"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/InsufficientCredits"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/LocationPlateNotFound"},"409":{"$ref":"#/components/responses/LocationPlatePreconditionFailed"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/artifacts/{artifactId}/finalize":{"parameters":[{"$ref":"#/components/parameters/StoryArtifactId"}],"post":{"operationId":"finalizeArtifact","tags":["Artifacts"],"summary":"Durably render a table read artifact as an MP3 or MP4.","description":"Initiates a durable render of a `table_read` artifact alongside the live share.  `mode: \"audio\"` (default, free) queues a durable full-mix MP3 render (voices, adaptive soundtrack, and SFX) regardless of whether a live Pipecat/Daily recording exists. When a completed recording is present, the worker freezes that take as the voice layer; otherwise it synthesizes the voice track headlessly from TTS using the same producer the MP4 export uses. Poll GET /artifacts/{id}: `manifest.audio.finalize.status` progresses through `queued → rendering → uploading → complete` (or `failed` with `manifest.audio.finalize.error`). On `complete`, `manifest.audio.recordingUrl` (full-mix MP3 download URL) and `manifest.audio.recordingDurationMs` are populated.  `mode: \"video\"` (free) queues the Remotion MP4 export (same render as POST /artifacts/{id}/render-video). Poll GET /artifacts/{id}: `manifest.video.status` and `manifest.video.url` on completion.  Replay-safe: re-calling finalize while in-flight, or when complete and the recording still carries the read's current mix, returns the current status without re-charging credits (`finalize.mix: \"current\"`). A complete recording that the read has moved past since — its music, sound cues or voice track changed, in the app or through this API — is rendered again (free) under a new media revision; the old `manifest.audio.recordingUrl` keeps serving until the new render completes. A complete recording made before its mix was tracked replays with `finalize.mix: \"unknown\"`; pass `refresh: true` to render it again from the read as it plays now (the app's \"Update the recording\"). A recording that is current still replays free with `refresh: true`.  A finalize that would SPEAK a voice its provider has retired is refused before anything is charged or queued (409 `voice_retired`, `details.voices`): migrate those voices (POST /voice-migrations), or re-voice the character, then finalize again. A read whose voices-only track is current is mixed from it and speaks nothing, so it is never refused for its voices.  A read whose table read is not paid for yet — a Writing Lab read that was cast but never prepared — is refused before anything is charged or queued (409 `table_read_not_prepared`): prepare it (POST /scripts/{scriptUploadId}/table-read/prepare), then finalize. A read a Story API job made is paid by that job.  Requires `Idempotency-Key`. Scope: `artifact:publish` (billable + mutating).","security":[{"BearerApiKey":["artifact:publish"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"mode":{"type":"string","enum":["audio","video"],"description":"`audio` (default) queues a durable full-mix MP3 (voices + adaptive soundtrack + SFX); the worker uses a completed Pipecat/Daily recording when present, otherwise synthesizes the voice track from TTS. `video` queues the Remotion MP4 export."},"refresh":{"type":"boolean","description":"Audio only. `true` also re-renders a complete recording whose mix is unknown (`finalize.mix: \"unknown\"`, made before mixes were tracked). A recording already carrying the read's current mix replays free either way; one the read moved past re-renders without it."}}}}}},"responses":{"200":{"description":"Finalize accepted. For audio, poll GET /artifacts/{id} for `manifest.audio.finalize.status`. For video, poll `manifest.video.status`.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"type":"object","required":["artifactId","mode","finalize"],"additionalProperties":false,"properties":{"artifactId":{"type":"string"},"mode":{"type":"string","enum":["audio","video"]},"finalize":{"type":"object","additionalProperties":true,"description":"Current finalize state at the time of the call. For audio: `{ status }`, plus `recordingUrl` and `mix` (`current` | `unknown`) when a complete recording was returned. For video: render acknowledgement object."}}}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/InsufficientCredits"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/ArtifactNotFound"},"409":{"$ref":"#/components/responses/ArtifactNotReady"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/artifacts/{artifactId}/finalize/retry":{"parameters":[{"$ref":"#/components/parameters/StoryArtifactId"}],"post":{"operationId":"retryArtifactFinalize","tags":["Artifacts"],"summary":"Retry a failed or stalled table-read audio finalize in place.","description":"Reuses the existing artifact, media revision, and idempotent credit key; it never regenerates the screenplay or performance. Valid only when `manifest.audio.finalize.status` is `failed` or `stalled` (a render that stopped with nothing working on it). Requires `Idempotency-Key` and `artifact:publish`.","security":[{"BearerApiKey":["artifact:publish"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"200":{"description":"Finalize retry accepted.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ArtifactFinalizeAck"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/InsufficientCredits"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/ArtifactNotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/story-projects/{projectId}/publishing-series":{"parameters":[{"$ref":"#/components/parameters/ProjectId"}],"get":{"operationId":"listPublishingSeries","tags":["Publishing"],"summary":"List publishing series for a story project.","security":[{"BearerApiKey":["publishing:read"]}],"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Publishing series list.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublishingSeriesList"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"createPublishingSeries","tags":["Publishing"],"summary":"Create a project-owned publishing series.","description":"Creates the series setup surface: cadence, medium, format, destination metadata, and prompt direction. Audio series automatically get a first-party RSS destination.","security":[{"BearerApiKey":["publishing:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublishingSeriesCreateRequest"}}}},"responses":{"200":{"description":"Publishing series envelope.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublishingSeriesEnvelope"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/publishing-provider-configs":{"get":{"operationId":"listPublishingProviderConfigs","tags":["Publishing"],"summary":"List user-level publishing provider configuration.","security":[{"BearerApiKey":["publishing:connect"]}],"parameters":[{"name":"provider","in":"query","required":false,"schema":{"$ref":"#/components/schemas/PublishingProvider"}},{"name":"medium","in":"query","required":false,"schema":{"$ref":"#/components/schemas/PublishingMedium"}}],"responses":{"200":{"description":"Publishing provider config list.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublishingProviderConfigList"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"upsertPublishingProviderConfig","tags":["Publishing"],"summary":"Upsert publishing provider configuration.","description":"Stores provider configuration metadata and encrypted credential references. RSS v1 does not require OAuth or credentials.","security":[{"BearerApiKey":["publishing:connect"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublishingProviderConfigUpsertRequest"}}}},"responses":{"200":{"description":"Publishing provider config envelope.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublishingProviderConfigEnvelope"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/publishing-series/{seriesId}":{"parameters":[{"$ref":"#/components/parameters/PublishingSeriesId"}],"get":{"operationId":"getPublishingSeries","tags":["Publishing"],"summary":"Read one publishing series.","security":[{"BearerApiKey":["publishing:read"]}],"responses":{"200":{"description":"Publishing series envelope.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublishingSeriesEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/PublishingNotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"patch":{"operationId":"updatePublishingSeries","tags":["Publishing"],"summary":"Update series setup and cadence.","description":"Also grants or revokes the show's standing approval (`standingApprovalKeyId`). Granting — binding a key, or arming a cadence, which grants one — requires `userConfirmed: true`; revoking turns the cadence off. An armed cadence is re-validated on any edit to productionMode, medium, showConfig or cadence: recurring series are audio-only.","security":[{"BearerApiKey":["publishing:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublishingSeriesUpdateRequest"}}}},"responses":{"200":{"description":"Publishing series envelope.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublishingSeriesEnvelope"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/PublishingNotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/publishing-series/{seriesId}/generate-cover-art":{"parameters":[{"$ref":"#/components/parameters/PublishingSeriesId"}],"post":{"operationId":"generatePublishingSeriesCoverArt","tags":["Publishing"],"summary":"Generate podcast cover art for a series.","description":"Generates a 1536×1536 square podcast cover image from the series context (title, description, genre). The image is stored durably on R2 and the URL is returned. The URL is NOT automatically saved to the series — call PATCH /publishing-series/{seriesId} with `coverArtUrl` to persist it. Optional request body fields override the stored series values for this generation only, allowing callers to preview art from unsaved draft context.","security":[{"BearerApiKey":["publishing:write"]}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublishingCoverArtGenerateRequest"}}}},"responses":{"200":{"description":"Durable cover art URL.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublishingCoverArtGenerateResponse"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/PublishingNotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/publishing-series/{seriesId}/destinations":{"parameters":[{"$ref":"#/components/parameters/PublishingSeriesId"}],"get":{"operationId":"listPublishingDestinations","tags":["Publishing"],"summary":"List publishing destinations for a series.","security":[{"BearerApiKey":["publishing:read"]}],"responses":{"200":{"description":"Publishing destination list.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublishingDestinationList"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"upsertPublishingDestination","tags":["Publishing"],"summary":"Connect or update a publishing destination.","security":[{"BearerApiKey":["publishing:connect"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublishingDestinationUpsertRequest"}}}},"responses":{"200":{"description":"Publishing destination envelope.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublishingDestinationEnvelope"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/publishing-series/{seriesId}/releases":{"parameters":[{"$ref":"#/components/parameters/PublishingSeriesId"}],"get":{"operationId":"listPublishingReleases","tags":["Publishing"],"summary":"List promoted releases for a publishing series.","security":[{"BearerApiKey":["publishing:read"]}],"parameters":[{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["ready","scheduled","publishing","published","failed","canceled","stale"]}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Publishing release list.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublishingReleaseList"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"createPublishingRelease","tags":["Publishing"],"summary":"Promote a finalized artifact into a publishing release.","description":"Publishing does not create or refine MP3/MP4 assets. Pass a finalized table-read artifact id; the release binds the durable MP3 already stored on the artifact manifest. Requires `publishing:write`; passing `scheduledAt` also queues the publish job and additionally requires `publishing:publish` (a write-only key returns 403). Omit `scheduledAt` and use POST /publishing-releases/{releaseId}/schedule to keep create write-only.","security":[{"BearerApiKey":["publishing:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublishingReleaseCreateRequest"}}}},"responses":{"200":{"description":"Publishing release envelope.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublishingReleaseEnvelope"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"$ref":"#/components/responses/PublishingAssetNotReady"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/show-source-templates":{"get":{"operationId":"listShowSourceTemplates","tags":["Publishing"],"summary":"List interesting recurring sources.","description":"A short catalog of interesting recurring sources to suggest when the writer has no feed in mind: NASA, arXiv, Wikipedia's daily features, the Library of Congress, public-domain archives, the Federal Register, USGS, and a topic searched in the news. Each comes ready to save as one of `showConfig.sources`, with the cadence it suits, how much text its items carry (a headlines feed makes a thin episode, so it suits a weekly digest), and a note on who owns the material. Free. Save a template's `source` as one of `showConfig.sources` (PATCH /publishing-series/{seriesId}), changing what its `customize` note says, then read it with POST /publishing-series/{seriesId}/source-preview.","security":[{"BearerApiKey":["publishing:read"]}],"responses":{"200":{"description":"The catalog.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShowSourceCatalog"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/publishing-series/{seriesId}/quote":{"parameters":[{"$ref":"#/components/parameters/PublishingSeriesId"}],"get":{"operationId":"getShowQuote","tags":["Publishing"],"summary":"Quote a recurring show.","description":"One episode's quote (the pricing library's price for each paid step), the caps suggested from it — an episode at its quote + 25%, a month at the episodes the cadence makes × that — the caps the show holds, and what this calendar month's episodes have spent. Free. A show is armed only with both caps (`episodeCreditCap`, `monthlyCreditCap` on PATCH /publishing-series/{seriesId}, with `userConfirmed: true` after the creator agrees them); `caps.problem` says why the caps it holds cannot run it yet.","security":[{"BearerApiKey":["publishing:read"]}],"responses":{"200":{"description":"The show's quote, suggested caps, caps and month-to-date spend.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShowQuote"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/PublishingNotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/publishing-series/{seriesId}/source-preview":{"parameters":[{"$ref":"#/components/parameters/PublishingSeriesId"}],"post":{"operationId":"previewShowSources","tags":["Publishing"],"summary":"Preview a show's sources.","description":"Read the show's sources now, free, without making anything: what each would give an episode over the window (the item titles, authors and links, how much text), which items earlier episodes already covered, and which sources failed and why. Pass `sources` to try ones not saved yet. Each read goes through the guarded fetcher (private addresses refused, at most three redirects, 10 MB). Body: `{ sources?: ShowConfig.sources, lookbackDays?: 1-90 }`.","security":[{"BearerApiKey":["publishing:read"]}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"sources":{"type":"array","maxItems":10,"items":{"$ref":"#/components/schemas/ShowSource"}},"lookbackDays":{"type":"integer","minimum":1,"maximum":90}}}}}},"responses":{"200":{"description":"What the sources would give an episode.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShowSourcePreview"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/PublishingNotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/publishing-series/{seriesId}/episodes":{"parameters":[{"$ref":"#/components/parameters/PublishingSeriesId"}],"get":{"operationId":"listShowEpisodes","tags":["Publishing"],"summary":"List a show's episodes.","description":"Every episode the show set out to make, newest first: queued, producing, published, ready (made, not published: a preview, held for review, or not authorized), skipped (why: no new material, not runnable, not ready, cap reached) or failed (the code and sentence that ended it), with what it cost and the items it covered.","security":[{"BearerApiKey":["publishing:read"]}],"parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100}},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"The show's episodes, newest first.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShowEpisodeList"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/PublishingNotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"post":{"operationId":"produceShowEpisode","tags":["Publishing"],"summary":"Make a show episode now: a preview until the show is live.","description":"Make ONE episode now. Before the show is live it is a PREVIEW: made from the show's sources like any episode, priced by the same library, held for the creator to hear and never published — and a show is armed only after one is made. On a live show, `preview: false` makes an extra episode that goes out like any other. Two calls: without `userConfirmed` it quotes (nothing made); with `userConfirmed: true` it charges and queues. Refused with the arming rules (a recurring audio show with a source, a schedule and both caps), with 409 `project_precondition_failed` / `cast_precondition_failed` while the project cannot make an episode, and with 409 `show_cap_reached` when the month cannot pay for one. Requires `Idempotency-Key`.","security":[{"BearerApiKey":["publishing:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"preview":{"type":"boolean","description":"Default true. `false` only on a live show: an extra episode that goes out like any other."},"prompt":{"type":"string","maxLength":2000,"description":"What this episode is about, when its episode type needs one."},"episodeType":{"type":"string","description":"One of the show's `showConfig.episodeTypes`; defaults to its first scheduled one."},"userConfirmed":{"type":"boolean","enum":[true],"description":"Charges and queues the episode, after the user agrees to its quote. Without it the call only quotes."}}}}}},"responses":{"200":{"description":"Quoted, or queued.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShowEpisodeProduce"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/PublishingNotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/publishing-series/{seriesId}/analytics":{"parameters":[{"$ref":"#/components/parameters/PublishingSeriesId"}],"get":{"operationId":"listPublishingAnalyticsSnapshots","tags":["Publishing"],"summary":"List publishing analytics snapshots.","security":[{"BearerApiKey":["publishing:analytics"]}],"parameters":[{"name":"releaseId","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Publishing analytics snapshots.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublishingAnalyticsList"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/publishing-releases/{releaseId}":{"parameters":[{"$ref":"#/components/parameters/PublishingReleaseId"}],"get":{"operationId":"getPublishingRelease","tags":["Publishing"],"summary":"Read one promoted publishing release.","security":[{"BearerApiKey":["publishing:read"]}],"responses":{"200":{"description":"Publishing release envelope.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublishingReleaseEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/PublishingNotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"patch":{"operationId":"updatePublishingRelease","tags":["Publishing"],"summary":"Update release metadata.","description":"Requires `publishing:write`. Writing `scheduledAt` (setting or clearing the schedule) is a publish-state change and additionally requires `publishing:publish` (a write-only key returns 403); other metadata edits need only `publishing:write`.","security":[{"BearerApiKey":["publishing:write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublishingReleaseUpdateRequest"}}}},"responses":{"200":{"description":"Publishing release envelope.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublishingReleaseEnvelope"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/PublishingNotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/publishing-releases/{releaseId}/description/generate":{"parameters":[{"$ref":"#/components/parameters/PublishingReleaseId"}],"post":{"operationId":"generatePublishingReleaseDescription","tags":["Publishing"],"summary":"Generate and persist a podcast episode description.","description":"Requires `publishing:write`. Writes short, specific podcast episode copy from the promoted table-read transcript and persists it on the release. Accepts optional `direction` prompt text to steer tone and emphasis.","security":[{"BearerApiKey":["publishing:write"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"direction":{"type":"string","maxLength":2000,"description":"Optional prompt direction to steer the generated description."}}}}}},"responses":{"200":{"description":"Publishing release envelope.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublishingReleaseEnvelope"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/PublishingNotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/publishing-releases/{releaseId}/schedule":{"parameters":[{"$ref":"#/components/parameters/PublishingReleaseId"}],"post":{"operationId":"schedulePublishingRelease","tags":["Publishing"],"summary":"Schedule a promoted release.","description":"Scheduling IS publishing, only later and unattended — the feed update happens with nobody watching, so the human confirmation is taken now. Requires `userConfirmed: true` after the user approves this release and this exact datetime.","security":[{"BearerApiKey":["publishing:publish"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["scheduledAt","userConfirmed"],"properties":{"scheduledAt":{"type":"string","format":"date-time"},"userConfirmed":{"type":"boolean","enum":[true],"description":"Assert only after the user explicitly approves publishing this release at this datetime."}}}}}},"responses":{"200":{"description":"Publishing release envelope.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublishingReleaseEnvelope"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"$ref":"#/components/responses/PublishingAssetNotReady"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/publishing-releases/{releaseId}/publish":{"parameters":[{"$ref":"#/components/parameters/PublishingReleaseId"}],"post":{"operationId":"publishPublishingRelease","tags":["Publishing"],"summary":"Queue immediate release publishing.","description":"RSS v1 publishes by making the promoted item visible in the first-party feed. Provider-specific targets are recorded as skipped/needs_setup until their publisher implementation ships. Outward-facing and not retractable once aggregators fetch the feed, so it requires `userConfirmed: true` after the user approves this exact release — the same human boundary the season path enforces at `advance { action: \"publish\" }`. The one exception is the show's STANDING APPROVAL: the API key it is bound to may omit `userConfirmed` for an audio release created after the grant (see `standingApproval` on the series). Every other caller still needs it.","security":[{"BearerApiKey":["publishing:publish"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":[],"properties":{"userConfirmed":{"type":"boolean","enum":[true],"description":"Assert only after the user explicitly approves publishing this exact release."}}}}}},"responses":{"200":{"description":"Publishing release envelope.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublishingReleaseEnvelope"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"$ref":"#/components/responses/PublishingAssetNotReady"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/publishing-releases/{releaseId}/refresh-media":{"parameters":[{"$ref":"#/components/parameters/PublishingReleaseId"}],"post":{"operationId":"refreshPublishedReleaseMedia","tags":["Publishing"],"summary":"Refresh a published RSS episode enclosure from its source artifact.","description":"Explicit RSS repair operation for a published audio episode whose SAME table-read source artifact was re-finalized. Rebinds the existing MP3 asset and published RSS target to the artifact's latest finalized media while preserving release id, episode number, metadata, publish timestamps, and published status. Direct-provider targets are not changed because this operation does not republish to those providers. It rejects a different source artifact, non-audio releases, and media that is not ready.","security":[{"BearerApiKey":["publishing:publish"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublishingReleaseRefreshMediaRequest"}}}},"responses":{"200":{"description":"Publishing release envelope.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublishingReleaseEnvelope"}}}},"400":{"$ref":"#/components/responses/ValidationFailed"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"$ref":"#/components/responses/PublishingAssetNotReady"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/publishing-releases/{releaseId}/cancel":{"parameters":[{"$ref":"#/components/parameters/PublishingReleaseId"}],"post":{"operationId":"cancelPublishingRelease","tags":["Publishing"],"summary":"Cancel a promoted release.","description":"Marks a ready, scheduled, publishing, failed, or stale release as canceled. A published release is taken out of the feed with `POST /publishing-releases/{releaseId}/unpublish` instead.","security":[{"BearerApiKey":["publishing:publish"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"responses":{"200":{"description":"Publishing release envelope.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublishingReleaseEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"$ref":"#/components/responses/PublishingReleaseStateInvalid"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/publishing-releases/{releaseId}/unpublish":{"parameters":[{"$ref":"#/components/parameters/PublishingReleaseId"}],"post":{"operationId":"unpublishPublishingRelease","tags":["Publishing"],"summary":"Take a published episode back out of the feed.","description":"Moves a published release back to `ready` and its RSS target to `skipped`, so the episode leaves the podcast feed on the next fetch; apps that read the feed drop it on their next refresh, and copies listeners already downloaded stay. The audio, the RSS GUID (the release id) and `publishedAt` are kept: publishing it again restores the same episode under its original date. Refused (409) for a release that is not published; refused (400) for a season-run release or one still live on a platform this cannot reach (YouTube, TikTok). An already-unpublished release is returned unchanged.","security":[{"BearerApiKey":["publishing:publish"]}],"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["userConfirmed"],"additionalProperties":false,"properties":{"userConfirmed":{"type":"boolean","enum":[true],"description":"Required: the user explicitly agreed to take this episode out of the public feed. A standing approval does not cover unpublishing."}}}}}},"responses":{"200":{"description":"Publishing release envelope.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublishingReleaseEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"$ref":"#/components/responses/PublishingReleaseStateInvalid"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/publishing-releases/{releaseId}/targets":{"parameters":[{"$ref":"#/components/parameters/PublishingReleaseId"}],"get":{"operationId":"listPublishingReleaseTargets","tags":["Publishing"],"summary":"List provider targets for a release.","security":[{"BearerApiKey":["publishing:read"]}],"responses":{"200":{"description":"Publishing target list.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublishingTargetList"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}},"/publishing-feeds/{seriesSlug}.xml":{"parameters":[{"$ref":"#/components/parameters/PublishingSeriesSlug"}],"get":{"operationId":"getPublishingRssFeed","tags":["Publishing"],"summary":"Read the public RSS feed for a publishing series.","security":[],"responses":{"200":{"description":"RSS XML feed. A public show's feed is sent with `X-Robots-Tag: all`. An unlisted show's feed is served to whoever has the link, sent with `X-Robots-Tag: noindex, nofollow, noarchive`, and carries `<itunes:block>Yes</itunes:block>` and `<podcast:block>yes</podcast:block>` so podcast directories leave it out.","content":{"application/rss+xml":{"schema":{"type":"string"}}}},"404":{"description":"Series feed not found: missing, non-audio, archived, deleted, or a private show (a private show answers exactly as a missing one does). The body is a minimal RSS XML document, not a JSON error envelope.","content":{"application/rss+xml":{"schema":{"type":"string"}}}}}}},"/story-projects/{projectId}/sources/{sourceId}":{"parameters":[{"$ref":"#/components/parameters/ProjectId"},{"$ref":"#/components/parameters/SourceId"}],"get":{"operationId":"getStorySource","tags":["Sources"],"summary":"Read a source by id.","security":[{"BearerApiKey":["source:read"]}],"responses":{"200":{"description":"Source envelope.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StorySourceEnvelope"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}},"delete":{"operationId":"deleteStorySource","tags":["Sources"],"summary":"Soft-delete a source.","description":"The source is marked deleted, its extracted text preview is cleared, and it stops appearing in list responses.","security":[{"BearerApiKey":["source:write"]}],"responses":{"200":{"description":"Deletion ack.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeletionAck"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}}},"components":{"securitySchemes":{"BearerApiKey":{"type":"http","scheme":"bearer","bearerFormat":"Sleeper Hit API key (sh_<prefix>_<secret>)","description":"Production-only customer API key. Created and revoked at https://sleeperhit.studio/dashboard/api. Tokens are shown once; store them in your secret manager."}},"parameters":{"IdempotencyKey":{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","minLength":1,"maxLength":200},"description":"Customer-generated stable identifier for safe retries of idempotent POST requests."},"ProjectId":{"name":"projectId","in":"path","required":true,"schema":{"type":"string","pattern":"^[a-z0-9]{20,}$"},"description":"Story project id (cuid)."},"SeasonRunId":{"name":"runId","in":"path","required":true,"schema":{"type":"string","pattern":"^[a-z0-9]{20,}$"},"description":"Persistent story season run id (cuid)."},"SeasonRunEpisodeId":{"name":"episodeId","in":"path","required":true,"schema":{"type":"string","pattern":"^[a-z0-9]{20,}$"},"description":"Mapped episode id within a story season run (cuid)."},"MoodBoardFrameId":{"name":"frameId","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":64},"description":"Mood board frame id, as returned in `content.frames[].id`."},"SourceId":{"name":"sourceId","in":"path","required":true,"schema":{"type":"string","pattern":"^[a-z0-9]{20,}$"},"description":"Story source id (cuid)."},"StoryPlanId":{"name":"planId","in":"path","required":true,"schema":{"type":"string","pattern":"^[a-z0-9]{20,}$"},"description":"Story plan id (cuid)."},"StoryJobId":{"name":"jobId","in":"path","required":true,"schema":{"type":"string","pattern":"^[a-z0-9]{20,}$"},"description":"Story job id (cuid)."},"StoryArtifactId":{"name":"artifactId","in":"path","required":true,"schema":{"type":"string","pattern":"^[a-z0-9]{20,}$"},"description":"Story artifact id (cuid)."},"LocationPlateKey":{"name":"plateKey","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":300},"description":"A plate key exactly as the location-plates read returned it (`plates[].plateKey`, e.g. `INT_IVAN_S_ROOM__MEDIUM`), URL-encoded."},"PublishingSeriesId":{"name":"seriesId","in":"path","required":true,"schema":{"type":"string","pattern":"^[a-z0-9]{20,}$"},"description":"Publishing series id (cuid)."},"PublishingReleaseId":{"name":"releaseId","in":"path","required":true,"schema":{"type":"string","pattern":"^[a-z0-9]{20,}$"},"description":"Publishing release id (cuid)."},"PublishingSeriesSlug":{"name":"seriesSlug","in":"path","required":true,"schema":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]+$"},"description":"Public publishing series slug used in RSS feed URLs."}},"headers":{"XRequestId":{"description":"Per-request identifier. Echo this back when reporting integration issues.","schema":{"type":"string","format":"uuid"}},"RateLimitLimit":{"description":"Maximum requests permitted in the current window for this API key + IP + route family.","schema":{"type":"integer","minimum":1}},"RateLimitRemaining":{"description":"Requests remaining in the current window.","schema":{"type":"integer","minimum":0}},"RateLimitReset":{"description":"Seconds until the current rate-limit window resets.","schema":{"type":"integer","minimum":1}}},"responses":{"Unauthorized":{"description":"Missing, malformed, revoked, or expired API key.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"Forbidden":{"description":"API key is valid but lacks the scope, is blocked by IP allowlist, or the account is disabled.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"RateLimited":{"description":"Rate limit for this API key + IP + route family was exceeded.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"},"RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"},"Retry-After":{"description":"Seconds the client should wait before retrying.","schema":{"type":"integer","minimum":1}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"InternalError":{"description":"The API failed unexpectedly. Safe to retry with the same Idempotency-Key.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"ValidationFailed":{"description":"Request payload failed validation. The error message names the offending field.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"NotFound":{"description":"The named project or source does not exist or is owned by another account.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"SourceTooLarge":{"description":"Source content is larger than the documented inline limit.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"SourceTypeUnsupported":{"description":"Requested source type is not yet supported. See `/api/v1/capabilities.sources.types`.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"StoryPlanStateInvalid":{"description":"The plan is in a state that does not allow the requested operation.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"SeasonRunNotFound":{"description":"The season run does not exist, was deleted, or is owned by another account.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"SeasonRunStateInvalid":{"description":"The season run or episode is not at the required human-approved lifecycle state. Reload the run and follow nextAction.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"StoryJobNotFound":{"description":"The story job does not exist, was deleted, or is owned by another account.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"StoryJobStateInvalid":{"description":"The job is in a state that does not allow the requested operation (e.g. canceling a terminal job).","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"ArtifactNotFound":{"description":"The story artifact does not exist or is owned by another account.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"ArtifactNotReady":{"description":"The artifact is not in a state where this operation is valid yet (e.g. requesting an MP4 render before the share link exists).","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"ArtifactGenerationFailed":{"description":"The artifact adapter failed to produce a shareable output.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"PublishingNotFound":{"description":"The publishing series, destination, or release does not exist or is owned by another account.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"PublishingAssetNotReady":{"description":"The promoted source artifact does not have the required finalized durable asset yet. Finalize the artifact first, then promote or publish.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"PublishingReleaseStateInvalid":{"description":"The publishing release is in a state that does not allow the requested operation.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"MoodBoardFull":{"description":"The mood board already holds the maximum number of frames that can bind a render. Nothing was trimmed — drop a frame, then keep the new one.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"MoodBoardKeepConflict":{"description":"The frame was not kept, and nothing was trimmed: the board already holds the maximum number of frames that can bind a render (`mood_board_full` — drop a frame, then keep the new one), or the project has not passed premise coverage (`project_precondition_failed`, with `details.stage`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"StoryJobCreateConflict":{"description":"The job was refused and NO job row was created: the plan is not APPROVED (`story_plan_state_invalid`), the project has not reached the stage these artifacts need (`project_precondition_failed` — `details.stage` names it; finish that stage, do not retry), or a table read's cast is incomplete (`cast_precondition_failed` — `details.reason` is `no_cast_canon`, `cast_canon_collision` or `cast_canon_incomplete` for a read from a plan, `script_not_finalized` or `script_not_cast_ready` for an adopted `scriptUploadId`, `episode_cast_incomplete` for a resumed job's own screenplay; `details.missing` lists each member and what it lacks). Complete the cast, then call again.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"StoryJobInsufficientCredits":{"description":"Not enough Studio Credits to reserve this job. See `details.required` and `details.available`. `details.jobId` is null when the job was refused before it was created (the usual case — nothing is left behind), or the id of the one job this Idempotency-Key is bound to when a concurrent spend won the reservation; retrying the same key after topping up resumes that job.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"ProjectPreconditionFailed":{"description":"The project has not reached the development stage this call needs (`project_precondition_failed`), and nothing was created, queued or charged. `details` is ProjectPreconditionDetails: `stage` names the stage to finish, `missingFields` what it still lacks, `canPlan` / `canStartEpisode` what the project can do now. Finish the named stage — do not retry; the identical call refuses the same way until it is done.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"PitchDeckStateInvalid":{"description":"The pitch deck exists and the request is well-formed, but the deck, its chapter, its cast or its plates are not in a state that allows it (`pitch_deck_state_invalid`), and nothing was reserved: a plan approved outside draft, a take chosen, a chapter edited or rendered outside review/complete, an export before the finish, a finish while a call has no selected take or a footage chapter has not rendered, a text-only chapter asked for footage, a changed chapter asked for one call, a delivery class changed mid-pass, a render the compile blocked, or a finish while the master failed its video coverage (`pitch_deck_video_coverage_failed`, with `details.videoCoverage`). `details.refusal` is `{ code, action, fix }` (see PitchDeckRefusalDetails): `action` is what unblocks it — `recast_figure`, `add_signature_items`, `recast_voice` or `remake_location_plate` — and `fix` names the MCP tool and CLI command that take it; `details.blocked[]` lists every chapter a blocked compile could not render. The identical request succeeds once the deck is put right.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"PitchDeckSceneHasNothingToMove":{"description":"This chapter has no on-screen text for a motion direction to act on, and nothing was compiled or saved. Either the chapter is footage only (give it a caption, or set it to text/both) or its caption is still empty. A 409 rather than a 400 because the request body is fine — the same call succeeds once the chapter is put right — and the message says which of the two it is.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"TrailerStateInvalid":{"description":"The trailer exists and the request is well-formed, but the trailer, its beat, its cast or its plates are not in a state that allows it (`trailer_state_invalid`), and nothing was reserved: a plan approved outside draft, a take chosen or a beat rendered outside review/complete, an export before the finish, a finish while a call has no selected take, a narrated cut with no narrator, a render the compile blocked, or a finish while the master failed its video coverage (`trailer_video_coverage_failed`, with `details.videoCoverage`). `details.refusal` is `{ code, action, fix }` (see TrailerRefusalDetails): `action` is what unblocks it — `recast_figure`, `add_signature_items`, `recast_voice` or `remake_location_plate` — and `fix` names the MCP tool and CLI command that take it; `details.blocked[]` lists every beat a blocked compile could not render. The identical request succeeds once the trailer is put right.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"TrailerBeatHasNothingToMove":{"description":"This beat has no on-screen text for a motion direction to act on, and nothing was compiled or saved. Either the beat is footage only (give it a caption, or set it to text/both) or its caption is still empty. A 409 rather than a 400 because the request body is fine — the same call succeeds once the beat is put right — and the message says which of the two it is.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"CastPreconditionFailed":{"description":"Casting cannot run on this project yet, and nothing was quoted or drawn: nobody castable, nobody with a description to draw from, no passing screenplay coverage, or no image model bound (`cast_precondition_failed`, `details.reason` names which) — or the project itself has not reached casting (`project_precondition_failed`, `details.stage` names the stage to finish). The message says what to do.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"InsufficientCredits":{"description":"Not enough Studio Credits to reserve this job. See `details.required` and `details.available`.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"VoiceNotFound":{"description":"No voice with that id in your library, or another writer's cloned or designed voice, which is never yours to preview, cast, pin or migrate onto (`voice_not_found`). GET /voices lists the ones you own; a catalog voice can be previewed and assigned but not renamed or deleted.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"CharacterNotFound":{"description":"Nobody in the project's cast canon or Series Bible answers to that name (`character_not_found`), or the project is not yours (`project_not_found`). Nothing was changed or charged.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"CastPortraitChoiceRefused":{"description":"The headshot cannot be chosen, and nothing was pinned: `cast_portrait_casting_coverage_failed` when its casting coverage FAILED it (`details.castingCoverage`: the score, the bar, why, the headshot it recommends instead and any revised look; send `acknowledgeCastingCoverage: true` only once the writer has chosen it anyway), or `cast_precondition_failed` for a candidate not drawn yet.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"PortraitDesignNotFound":{"description":"No portrait design with that id in this project (`portrait_design_not_found`), or the project is not yours (`project_not_found`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"SeriesBibleVersionConflict":{"description":"The Series Bible changed since the version the edit was based on (`series_bible_version_conflict`; `details.expectedVersion`, `details.currentVersion`), so nothing was saved. Read it again and retry; `project_precondition_failed` when the edit touched the episode map before the project reached planning.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"VoiceDesignNotFound":{"description":"No cast voice design with that id in this project (`voice_design_not_found`), or the project is not yours (`project_not_found`).","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"VoiceConsentRequired":{"description":"A cloned voice has no consent on record from the person whose voice it is (`voice_consent_required`), so nothing was cast, spoken or charged. `details.voices` names each; have them confirm through POST /voices/{voiceId}/consent-link, then retry.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"ProjectNotRestorable":{"description":"The project cannot be restored (`project_not_restorable`): it is not deleted, its 30-day window has passed, or its purge has started. `message` says which. Its episodes are in the library as private scripts.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"AccountEmailMismatch":{"description":"`confirmEmail` is not the account's email address (`account_email_mismatch`), so no deletion was scheduled.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"AccountDeletionRefused":{"description":"The account cannot delete itself (`account_deletion_refused`): an admin account, a workshop host, or an organization owner (transfer or delete it first); or the purge already started, so the deletion can no longer be cancelled. `message` says which. Nothing was changed.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"VoiceConsentRevoked":{"description":"The consent for a cloned voice was withdrawn (`voice_consent_revoked`), so it is never used for anything new. `details.voices` names each; choose another voice.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"VoiceRetired":{"description":"A voice its provider has retired is in the way (`voice_retired`), so nothing was queued, charged or changed. `details.voices` names each one; migrate them (POST /voice-migrations), then retry.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"VoiceInUse":{"description":"The voice is still pinned in a cast canon or spoken in a read, so nothing was deleted (`voice_in_use`). `details.uses` lists `canon` people and `reads`; recast them first, or delete again with `confirmed: true`.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"CastVoiceChoiceRefused":{"description":"The candidate cannot be chosen, and nothing was cloned or pinned: `cast_voice_casting_coverage_failed` when its casting coverage FAILED it (`details.castingCoverage`: the score, the bar, why, the candidate it recommends instead and any revised brief; send `acknowledgeCastingCoverage: true` only once the writer has chosen it anyway), or `cast_voice_precondition_failed` for a take drawn before the designer changed.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"CastVoicePreconditionFailed":{"description":"The read or project is not ready for this cast voice operation, and nothing was priced or charged (`cast_voice_precondition_failed`): no description and no Series Bible voice line, a character with no lines, nothing to restore, no finished recording to re-voice, a voice the character already speaks with, a finalize already running, or a recording that cannot be cut line by line. The message says which and what to do; `project_precondition_failed` when the project gate holds spending.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"LocationPlateNotFound":{"description":"No artifact with that id, or the script behind it stores no location plate at that key (`location_plate_not_found`). Read the location plates for the keys it does store; a plate not made yet is made when a video renders.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"LocationPlatePreconditionFailed":{"description":"The plate cannot be remade yet, and nothing was priced or charged: it is not ready, a remake is already running for it, the plate it is cropped from is missing, or no image model is bound. The message says which; the identical call succeeds once the plate is put right.","headers":{"X-Request-Id":{"$ref":"#/components/headers/XRequestId"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"schemas":{"ScreenplayDraftStatus":{"type":["object","null"],"required":["id","status","completedScenes","totalScenes","act","totalActs","content","updatedAt"],"properties":{"id":{"type":"string"},"status":{"type":"string","description":"queued, writing, interrupted (a writing lease expired), failed, or complete."},"completedScenes":{"type":"integer","minimum":0},"totalScenes":{"type":"integer","minimum":1},"act":{"type":"integer","minimum":1},"totalActs":{"type":"integer","minimum":1},"error":{"type":["string","null"]},"content":{"type":"string","description":"The saved Fountain screenplay so far."},"updatedAt":{"type":"string","format":"date-time"},"coverage":{"type":["object","null"],"description":"Screenplay coverage started when drafting completed; null until then.","properties":{"id":{"type":"string"},"status":{"type":"string"},"progressDetail":{"type":["string","null"]},"overallScore":{"type":["number","null"]},"errorMessage":{"type":["string","null"]}}}}},"BackgroundActivity":{"type":"object","additionalProperties":false,"required":["id","label","state","retryCount","createdAt"],"properties":{"id":{"type":"string"},"label":{"type":"string"},"state":{"type":"string","enum":["created","active","retry","completed","failed","cancelled"],"description":"The task's outcome. For renders (StoryJob), coverage and queued chat tasks it is checked against the task's own record: a render whose StoryJob FAILED reads failed even though the queue finished it, and one the queue finished while its record is still running reads active."},"retryCount":{"type":"integer","minimum":0},"createdAt":{"type":"string","format":"date-time"},"detail":{"type":"string","description":"Safe display stage from durable job state, when available. Never a provider error or raw payload."},"contextLabel":{"type":"string","maxLength":160,"description":"Ownership-checked episode title and version, the source label for a source task, or what a render makes, when available. The project is projectName."},"projectId":{"type":"string","description":"The owned, live project the task works for, when it works for one: read from the source, plan, render, coverage report, queued chat task, deck or trailer job, or screenplay it works on."},"projectName":{"type":"string","maxLength":120,"description":"That project's name, present with projectId, so a list shown on one project's page can say which tasks belong to another."},"progress":{"type":"object","additionalProperties":false,"required":["completed","total","unit"],"properties":{"completed":{"type":"integer","minimum":0},"total":{"type":"integer","minimum":1},"unit":{"type":"string","enum":["assets","sections","chapters","beats"],"description":"assets for casting, sections for screenplay parsing, chapters or beats joined by a deck's or a trailer's assembly."}}},"queueState":{"type":"string","enum":["created","active","retry","completed","failed","cancelled"],"description":"The queue's own state, present only when the task's own record settled it otherwise."},"failureReason":{"type":"string","maxLength":500,"description":"Why the task failed, was cancelled or is retrying, from its own record (the same message the page that owns it shows), when the record gives one. Studio setup problems read as one plain statement. Never raw queue output."},"storyJobId":{"type":"string","description":"The owned story job a render works on."},"storyJobStatus":{"$ref":"#/components/schemas/StoryJobStatus"},"storyJobRunStale":{"type":"boolean","description":"Present with storyJobStatus: true when that story job is a RUNNING table read whose worker stopped answering (its `runStale`). POST /story-jobs/{jobId}/resume sends its run again from the last checkpoint at no new cost."},"chatToolJobId":{"type":"string","description":"The owned queued chat task, the id its chat tool answered with."},"toolName":{"type":"string","description":"The chat tool the queued task runs."},"toolJobStatus":{"type":"string","description":"The queued chat task's own status (pending, completed, failed)."},"nextAttemptAt":{"type":"string","format":"date-time","description":"Present while the queue is holding a queued or retrying task until a later time (a provider backoff, a retry delay, work scheduled for later): the earliest its next attempt can start. A task waiting for this time is scheduled, not stuck."},"providerRetryAttempt":{"type":"integer","minimum":1,"description":"Which provider-backoff retry this run is (the fifth is 5), when the task was rescheduled because its model provider was unavailable."}}},"ErrorCode":{"type":"string","enum":["authentication_required","invalid_api_key","api_key_revoked","api_key_expired","account_disabled","ip_not_allowed","insufficient_scope","rate_limited","idempotency_key_required","idempotency_conflict","validation_failed","project_not_found","source_not_found","source_type_unsupported","source_too_large","source_incomplete","source_fetch_failed","story_plan_not_found","story_plan_state_invalid","story_plan_failed","season_run_not_found","season_run_state_invalid","story_job_not_found","story_job_state_invalid","artifact_not_found","artifact_not_ready","artifact_generation_failed","publishing_series_not_found","publishing_destination_not_found","publishing_release_not_found","publishing_release_state_invalid","publishing_asset_not_ready","publishing_asset_stale","project_precondition_failed","spend_subject_unresolved","insufficient_credits","internal_error","trailer_not_found","trailer_state_invalid","pitch_deck_not_found","pitch_deck_state_invalid","pitch_deck_version_not_found","trailer_version_not_found","story_job_not_resumable","script_not_found","cast_precondition_failed","table_read_not_prepared","table_read_not_failed","voice_clone_not_found","mood_board_full","fal_account_locked","location_plate_not_found","location_plate_precondition_failed","voice_not_found","voice_design_not_found","voice_in_use","cast_voice_precondition_failed","script_in_use","script_state_invalid","plan_limit_reached","usage_limit_reached","cast_voice_casting_coverage_failed","cast_portrait_casting_coverage_failed","voice_retired","voice_consent_required","voice_consent_revoked","account_email_mismatch","account_deletion_refused","project_not_restorable","content_refused","safety_hold","safety_check_unavailable","likeness_consent_required","reference_image_not_owned","likeness_consent_not_found","character_not_found","portrait_design_not_found","series_bible_version_conflict","show_cap_reached","not_a_sign_in_key","not_found","method_not_allowed"],"description":"Stable machine-readable error code. Match on this rather than the human message."},"ErrorEnvelope":{"type":"object","required":["error"],"additionalProperties":false,"properties":{"error":{"type":"object","required":["code","message","requestId"],"additionalProperties":false,"properties":{"code":{"$ref":"#/components/schemas/ErrorCode"},"message":{"type":"string","description":"Human-readable error description. Subject to change; do not pattern-match."},"requestId":{"type":"string","format":"uuid","description":"Per-request identifier mirrored in the X-Request-Id response header."},"details":{"type":"object","additionalProperties":true,"description":"Machine-readable detail, when the error has any (e.g. `required`/`available` on insufficient_credits). A refused trailer render carries `refusal` and `blocked` — see TrailerRefusalDetails; a refused pitch-deck render the same — see PitchDeckRefusalDetails. A `project_precondition_failed` carries the stage the project has not finished — see ProjectPreconditionDetails."}}}}},"Scope":{"type":"string","enum":["story:read","story:write","source:read","source:write","artifact:read","artifact:publish","publishing:read","publishing:write","publishing:publish","publishing:connect","publishing:analytics","credits:read","webhook:write","account:read","account:write"],"description":"OAuth-style scope encoded on a customer API key."},"Capabilities":{"type":"object","required":["version","status","auth","developmentFlow","projects","seasonRuns","sources","artifacts","jobs","publishing","docs"],"additionalProperties":false,"properties":{"version":{"type":"string","const":"v1"},"status":{"type":"string","const":"foundation","description":"Lifecycle marker for the API surface. `foundation` means production-stable identity, auth, credits, and discovery — generation features are rolling out incrementally."},"auth":{"type":"object","required":["schemes","scopes","supportsIdempotency"],"properties":{"schemes":{"type":"array","items":{"type":"string","enum":["bearer_api_key"]}},"scopes":{"type":"array","items":{"$ref":"#/components/schemas/Scope"}},"supportsIdempotency":{"type":"boolean","const":true}}},"developmentFlow":{"$ref":"#/components/schemas/DevelopmentFlow"},"projects":{"type":"object","required":["available","crudScopes","documents"],"properties":{"available":{"type":"boolean"},"crudScopes":{"type":"object","required":["read","write"],"properties":{"read":{"$ref":"#/components/schemas/Scope"},"write":{"$ref":"#/components/schemas/Scope"}}},"documents":{"type":"object","required":["seriesBible"],"additionalProperties":false,"properties":{"seriesBible":{"type":"object","required":["available","scopes","promptSystem","sidecar","note"],"additionalProperties":false,"properties":{"available":{"type":"boolean"},"scopes":{"type":"object","required":["read","write","generate"],"additionalProperties":false,"properties":{"read":{"$ref":"#/components/schemas/Scope"},"write":{"$ref":"#/components/schemas/Scope"},"generate":{"$ref":"#/components/schemas/Scope"}}},"promptSystem":{"type":"string","const":"SidecarPrompt"},"sidecar":{"type":"string","const":"story_api_series_bible"},"note":{"type":"string"}}}}}}},"seasonRuns":{"type":"object","required":["available","scopes","statuses","episodeStatuses","coverageGate","humanApprovals","note"],"additionalProperties":false,"properties":{"available":{"type":"boolean"},"scopes":{"type":"object","required":["read","map","approve","create","advance"],"additionalProperties":false,"properties":{"read":{"$ref":"#/components/schemas/Scope"},"map":{"$ref":"#/components/schemas/Scope"},"approve":{"$ref":"#/components/schemas/Scope"},"create":{"type":"array","items":{"$ref":"#/components/schemas/Scope"}},"advance":{"type":"array","items":{"$ref":"#/components/schemas/Scope"}}}},"statuses":{"type":"array","items":{"$ref":"#/components/schemas/SeasonRunStatus"}},"episodeStatuses":{"type":"array","items":{"$ref":"#/components/schemas/SeasonRunEpisodeStatus"}},"coverageGate":{"$ref":"#/components/schemas/SeasonCoveragePolicy"},"humanApprovals":{"type":"array","items":{"type":"string"}},"note":{"type":"string"}}},"sources":{"type":"object","required":["types","retentionPolicies","note"],"properties":{"types":{"type":"array","items":{"type":"object","required":["type","availability"],"properties":{"type":{"type":"string","enum":["text","markdown","url","pdf"]},"availability":{"type":"string","enum":["available","planned"]},"maxInlineBytes":{"type":"integer","minimum":1},"note":{"type":"string"}}}},"retentionPolicies":{"type":"array","items":{"$ref":"#/components/schemas/RetentionPolicy"}},"note":{"type":"string"}}},"artifacts":{"type":"object","additionalProperties":{"type":"object","required":["outputs","availability"],"properties":{"outputs":{"type":"array","items":{"type":"string"}},"availability":{"type":"string","enum":["available","planned"]},"durationSeconds":{"type":"object","required":["min","max"],"properties":{"min":{"type":"integer","minimum":1},"max":{"type":"integer","minimum":1}}},"modes":{"type":"array","items":{"type":"string"},"description":"Supported generation modes for this artifact (e.g. table read `documentary | podcast | drama`)."},"narrationPolicies":{"type":"array","items":{"type":"string","enum":["auto","include","suppress"]},"description":"Supported table-read narrator policies. `auto` lets the plan infer; `include` requests a narrator; `suppress` makes the read speaker-only."},"optInOutputs":{"type":"array","items":{"type":"string"},"description":"Outputs that are never produced by default and must be requested explicitly (e.g. the table read `video` MP4 render)."},"price":{"type":"object","required":["currency","rate","summary"],"description":"A per-second price (a pitch deck's and a trailer's), read from the same rate the render and finish quotes charge. A pitch deck is priced by the second: 1 credit a second to render at 480p, plus 1 credit a second to finish at 720p (2 credits a second in all; a 1080p finish is 4 credits a second instead). Planning is free. The exact price is quoted per shot once the plan is ready, before anything renders; location plates and on-screen text are itemized in the same quotes. A trailer is priced by the second: 1 credit a second to render at 480p, plus 1 credit a second to finish at 720p (2 credits a second in all; a 1080p finish is 4 credits a second instead). Planning is free. The exact price is quoted per shot once the plan is ready, before anything renders; location plates and on-screen text are itemized in the same quotes.","properties":{"currency":{"type":"string","enum":["studio_credits"]},"rate":{"oneOf":[{"type":"object","required":["renderResolution","renderPerSecond","defaultDeliveryResolution","delivery"],"properties":{"renderResolution":{"type":"string","description":"Every call renders at this class, whatever the delivery class."},"renderPerSecond":{"type":"number","description":"Credits per rendered second."},"defaultDeliveryResolution":{"type":"string","enum":["720p","1080p"],"description":"The class the headline rate is stated at."},"delivery":{"type":"object","description":"Per delivery class: `finish` is the finish's credits per second, `total` a delivered second (render plus finish).","additionalProperties":{"type":"object","required":["finish","total"],"properties":{"finish":{"type":"number"},"total":{"type":"number"}}}}}},{"type":"null"}],"description":"Null while a finish rate is not set."},"summary":{"type":"string","description":"The price in the words the pricing page uses."}}},"note":{"type":"string"}}}},"jobs":{"type":"object","required":["available","statuses","artifactStatuses","scopes","note"],"properties":{"available":{"type":"boolean"},"statuses":{"type":"array","items":{"$ref":"#/components/schemas/StoryJobStatus"}},"artifactStatuses":{"type":"array","items":{"$ref":"#/components/schemas/StoryArtifactStatus"}},"scopes":{"type":"object","required":["create","read","cancel","artifactRead","renderVideo","refine","recastVoice","modifyVoice","finalize","scriptRead","coverageGenerate","coverageRead","musicGenerate","musicRead","sfxManage","sfxRead"],"properties":{"create":{"$ref":"#/components/schemas/Scope"},"read":{"$ref":"#/components/schemas/Scope"},"cancel":{"$ref":"#/components/schemas/Scope"},"artifactRead":{"$ref":"#/components/schemas/Scope"},"renderVideo":{"$ref":"#/components/schemas/Scope"},"refine":{"$ref":"#/components/schemas/Scope"},"recastVoice":{"$ref":"#/components/schemas/Scope"},"modifyVoice":{"$ref":"#/components/schemas/Scope"},"finalize":{"$ref":"#/components/schemas/Scope"},"scriptRead":{"$ref":"#/components/schemas/Scope"},"coverageGenerate":{"$ref":"#/components/schemas/Scope"},"coverageRead":{"$ref":"#/components/schemas/Scope"},"musicGenerate":{"$ref":"#/components/schemas/Scope"},"musicRead":{"$ref":"#/components/schemas/Scope"},"sfxManage":{"$ref":"#/components/schemas/Scope"},"sfxRead":{"$ref":"#/components/schemas/Scope"}}},"note":{"type":"string"}}},"publishing":{"type":"object","required":["available","media","providers","seriesStatuses","destinationStatuses","releaseStatuses","assetStatuses","targetStatuses","scopes","podcastV1","standingApproval","promptSystem","sidecars","note"],"properties":{"available":{"type":"boolean"},"media":{"type":"array","items":{"type":"string","enum":["audio","video","short_form","text","mixed"]}},"providers":{"type":"array","items":{"type":"string","enum":["rss","apple_podcasts","spotify","youtube","tiktok","instagram","linkedin","x","other"]}},"seriesStatuses":{"type":"array","items":{"type":"string","enum":["draft","active","paused","archived"]}},"destinationStatuses":{"type":"array","items":{"type":"string","enum":["draft","connected","needs_auth","needs_setup","disconnected","failed","archived"]}},"releaseStatuses":{"type":"array","items":{"type":"string","enum":["ready","scheduled","publishing","published","failed","canceled","stale"]}},"assetStatuses":{"type":"array","items":{"type":"string","enum":["ready","stale","failed"]}},"targetStatuses":{"type":"array","items":{"type":"string","enum":["pending","queued","publishing","published","failed","skipped","needs_setup","stale"]}},"scopes":{"type":"object","required":["read","write","publish","connect","analytics"],"properties":{"read":{"$ref":"#/components/schemas/Scope"},"write":{"$ref":"#/components/schemas/Scope"},"publish":{"$ref":"#/components/schemas/Scope"},"connect":{"$ref":"#/components/schemas/Scope"},"analytics":{"$ref":"#/components/schemas/Scope"}}},"podcastV1":{"type":"object","required":["available","feedRoute","assetSource","note"],"properties":{"available":{"type":"boolean"},"feedRoute":{"type":"string"},"assetSource":{"type":"string"},"note":{"type":"string"}}},"standingApproval":{"type":"object","required":["available","field","recurringMedia","note"],"properties":{"available":{"type":"boolean"},"field":{"type":"string"},"recurringMedia":{"type":"array","items":{"type":"string"}},"note":{"type":"string"}}},"promptSystem":{"type":"string","const":"SidecarPrompt"},"sidecars":{"type":"array","items":{"type":"string"}},"note":{"type":"string"}}},"docs":{"type":"object","required":["agentGuidance","openapi","examples","llms","llmsFull","docsSite","agentIndex","searchIndex","surfaceContract","cliCommands","mcpTools","sitemap","robots"],"properties":{"agentGuidance":{"$ref":"#/components/schemas/DocLink"},"openapi":{"$ref":"#/components/schemas/DocLink"},"examples":{"$ref":"#/components/schemas/DocLink"},"llms":{"$ref":"#/components/schemas/DocLink"},"llmsFull":{"$ref":"#/components/schemas/DocLink"},"docsSite":{"$ref":"#/components/schemas/DocLink"},"agentIndex":{"$ref":"#/components/schemas/DocLink"},"searchIndex":{"$ref":"#/components/schemas/DocLink"},"surfaceContract":{"$ref":"#/components/schemas/DocLink"},"cliCommands":{"$ref":"#/components/schemas/DocLink"},"mcpTools":{"$ref":"#/components/schemas/DocLink"},"sitemap":{"$ref":"#/components/schemas/DocLink"},"robots":{"$ref":"#/components/schemas/DocLink"}}}}},"DocLink":{"type":"object","required":["url","availability"],"additionalProperties":false,"properties":{"url":{"type":"string"},"availability":{"type":"string","enum":["available","planned"]}}},"DevelopmentFlow":{"type":"object","required":["warnings"],"additionalProperties":false,"properties":{"warnings":{"type":"array","items":{"type":"object","required":["id","stage","severity","blocking","title","guidance","recommendedActions","surfaces"],"additionalProperties":false,"properties":{"id":{"type":"string","enum":["series_bible_before_screenplay","project_stage_before_generation","mood_board_before_storyboard_rendering","coverage_round_after_screenplay"]},"stage":{"type":"string","enum":["before_screenplay_generation","before_planning_or_generation","before_storyboard_rendering","after_screenplay_exists"]},"severity":{"type":"string","const":"warning"},"blocking":{"type":"boolean"},"title":{"type":"string"},"guidance":{"type":"string"},"recommendedActions":{"type":"array","items":{"type":"string"}},"surfaces":{"type":"array","items":{"type":"string","enum":["ui","api","cli","mcp"]}}}}}}},"AgentGuidance":{"type":"object","required":["version","available","promptSystem","developmentFlow","source","guidance","mcp"],"additionalProperties":false,"properties":{"version":{"type":"integer","const":1},"available":{"type":"boolean"},"promptSystem":{"type":"string","const":"SidecarPrompt"},"developmentFlow":{"$ref":"#/components/schemas/DevelopmentFlow"},"source":{"type":"object","required":["feature","name","sidecarId","currentVersion","updatedAt","interpolatesSnippets"],"additionalProperties":false,"properties":{"feature":{"type":"string","const":"B2B_STORY_API"},"name":{"type":"string","const":"story_api_agent_guidance"},"sidecarId":{"type":["string","null"]},"currentVersion":{"type":["integer","null"]},"updatedAt":{"type":["string","null"],"format":"date-time"},"interpolatesSnippets":{"type":"boolean"}}},"guidance":{"anyOf":[{"type":"object","required":["format","content"],"additionalProperties":false,"properties":{"format":{"type":"string","const":"markdown"},"content":{"type":"string"}}},{"type":"null"}]},"mcp":{"type":"object","required":["available","promptSystem","source","metadata"],"additionalProperties":false,"properties":{"available":{"type":"boolean"},"promptSystem":{"type":"string","const":"SidecarPrompt"},"source":{"type":"object","required":["feature","name","sidecarId","currentVersion","updatedAt","interpolatesSnippets"],"additionalProperties":false,"properties":{"feature":{"type":"string","const":"B2B_STORY_API"},"name":{"type":"string","const":"story_api_mcp_metadata"},"sidecarId":{"type":["string","null"]},"currentVersion":{"type":["integer","null"]},"updatedAt":{"type":["string","null"],"format":"date-time"},"interpolatesSnippets":{"type":"boolean"}}},"metadata":{"anyOf":[{"type":"object","required":["version","serverInstructions","resources"],"additionalProperties":false,"properties":{"version":{"type":"integer","const":1},"serverInstructions":{"type":"string"},"resources":{"type":"object","additionalProperties":{"type":"object","additionalProperties":false,"properties":{"title":{"type":"string"},"description":{"type":"string"}}}}}},{"type":"null"}]},"message":{"type":"string"}}},"message":{"type":"string"}}},"CreditsSummary":{"type":"object","required":["credits","costs"],"additionalProperties":false,"properties":{"costs":{"type":"array","description":"What each operation costs, in the order the Billing page lists them — the same list the UI shows. A flat price has `per: null`; a rate names its unit in `per` and is quoted by the operation before anything is reserved.","items":{"$ref":"#/components/schemas/StudioCreditCost"}},"credits":{"type":"object","required":["balance","monthlyBucketAllocated","monthlyBucketUsed","monthlyBucketRemaining","perpetualBalance","cycleStartedAt","cycleExpiresAt"],"additionalProperties":false,"properties":{"balance":{"type":"integer","description":"Total Studio Credits available across monthly and perpetual buckets."},"monthlyBucketAllocated":{"type":"integer","minimum":0},"monthlyBucketUsed":{"type":"integer","minimum":0},"monthlyBucketRemaining":{"type":"integer","minimum":0},"perpetualBalance":{"type":"integer","minimum":0},"cycleStartedAt":{"oneOf":[{"type":"string","format":"date-time"},{"type":"null"}]},"cycleExpiresAt":{"oneOf":[{"type":"string","format":"date-time"},{"type":"null"}]}}}}},"StudioCreditCost":{"type":"object","required":["key","group","label","credits","per","note","ops"],"additionalProperties":false,"description":"One line of the price table: an action the writer pays for, by its unit — the same price on every surface. A pitch deck is priced by the second: 1 credit a second to render at 480p, plus 1 credit a second to finish at 720p (2 credits a second in all; a 1080p finish is 4 credits a second instead). Planning is free. The exact price is quoted per shot once the plan is ready, before anything renders; location plates and on-screen text are itemized in the same quotes. A trailer is priced by the second: 1 credit a second to render at 480p, plus 1 credit a second to finish at 720p (2 credits a second in all; a 1080p finish is 4 credits a second instead). Planning is free. The exact price is quoted per shot once the plan is ready, before anything renders; location plates and on-screen text are itemized in the same quotes.","properties":{"key":{"type":"string","enum":["TABLE_READ_SHORT","TABLE_READ_LONG","TABLE_READ_REFINE","TABLE_READ_REVOICE","IMAGE","VOICE","VOICE_CLONE_PRO","VIDEO_RENDER_480P","VIDEO_RENDER_720P","VIDEO_RENDER_1080P","VIDEO_FINISH_720P","VIDEO_FINISH_1080P","TAKE_REPAIR","TEXT_LAYER"],"description":"The price line."},"group":{"type":"string","enum":["Table reads","Pictures and voices","Video"],"description":"The heading the Billing page lists it under."},"label":{"type":"string"},"credits":{"type":"number","minimum":0,"description":"The price in Studio Credits per `per`."},"per":{"type":"string","description":"The unit one `credits` buys: `read`, `image`, `voice`, `second`, `layer`… An operation prices its units before anything is reserved."},"note":{"type":"string"},"ops":{"type":"array","items":{"type":"string","enum":["TABLE_READ_SHORT","TABLE_READ_LONG","TABLE_READ_REFINE","TABLE_READ_REVOICE","VISUALIZATION","VOICE_CLONE_PRO","PITCH_DECK","TRAILER","STORYBOARD"]},"description":"The ledger op keys (`opKey`) its credit transactions carry. An op can cover several lines: every cast picture and voice is booked `VISUALIZATION`, every line of a deck `PITCH_DECK`."}}},"RetentionPolicy":{"type":"string","enum":["EXTRACTED_ONLY","STORE_ORIGINAL","METADATA_ONLY"],"description":"How long the API persists source content. `EXTRACTED_ONLY` (default) stores the extracted text and metadata. `STORE_ORIGINAL` keeps the raw bytes too. `METADATA_ONLY` drops extracted text after the digest pass."},"SourceType":{"type":"string","enum":["text","markdown","url","pdf"],"description":"Source payload type. Inline `text` and `markdown` are accepted synchronously; `url` and `pdf` extract asynchronously through the source worker."},"SourceStatus":{"type":"string","enum":["PENDING","EXTRACTING","READY","FAILED"]},"SourceDigestStatus":{"type":"string","enum":["NOT_STARTED","PENDING","RUNNING","READY","FAILED","SKIPPED"],"description":"Lifecycle of the post-extraction LLM digest. `PENDING` with digestFailureCode `provider_capacity_backoff` means Sleeper Hit scheduled a delayed retry. `SKIPPED` is terminal (stop polling) and is not a fault; the underlying source remains usable. It means either the digest could not run (sidecar/provider missing or no extracted text), or, for a full-context producer item (`producer` set and `metadata.sourceContextMode: \"full\"`), that it is skipped by design (`digestFailureCode: digest_skipped`, `digestFailureMessage: \"Used in full by its episode plan.\"`) because its plan reads every character."},"StorySourceDigest":{"type":"object","required":["summary","thesis","claims","evidence","gaps","rightsNotes"],"additionalProperties":false,"description":"Structured digest of a single source. Always source-grounded; the model is instructed not to invent claims that the source does not make.","properties":{"summary":{"type":"string","description":"3-6 sentence plain-prose summary."},"thesis":{"type":"string","description":"Single sentence capturing the source's dominant point."},"claims":{"type":"array","maxItems":12,"items":{"type":"object","required":["statement","kind","attribution"],"properties":{"statement":{"type":"string"},"kind":{"type":"string","enum":["fact","estimate","opinion","forecast","quotation","definition"]},"attribution":{"oneOf":[{"type":"string"},{"type":"null"}]}}}},"evidence":{"type":"array","maxItems":20,"items":{"type":"object","required":["claimIndex","support"],"properties":{"claimIndex":{"type":"integer","minimum":0},"support":{"type":"string"}}}},"gaps":{"type":"array","maxItems":10,"items":{"type":"string"}},"rightsNotes":{"type":"array","maxItems":8,"items":{"type":"string"}}}},"StoryProject":{"type":"object","required":["id","name","description","metadata","seasonCadence","contentGuard","archivedAt","createdAt","updatedAt"],"additionalProperties":false,"properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"oneOf":[{"type":"string"},{"type":"null"}]},"metadata":{"oneOf":[{"type":"object","additionalProperties":true},{"type":"null"}]},"seasonCadence":{"oneOf":[{"type":"string","enum":["monthly"]},{"type":"null"}],"description":"How a new episode is filed into a season. `monthly`: the season named for the month (UTC) its source material was made (`originatedAt` on the source, else when the source was added, else when the episode was made), created after the last season when the month is new. Null: numbered seasons (the season the plan, its season run or its Series Bible entry names, else Season 1). Stored as `metadata.seasonCadence`."},"contentGuard":{"type":"boolean","description":"Keeps every script, plan and Series Bible written for the project suitable for ages 13 and up: no strong profanity, no graphic violence, no sexual content. On for a project started from ChatGPT; off otherwise unless set."},"archivedAt":{"oneOf":[{"type":"string","format":"date-time"},{"type":"null"}]},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"sourceCount":{"type":"integer","minimum":0}}},"StoryProjectEnvelope":{"type":"object","required":["project"],"additionalProperties":false,"properties":{"project":{"$ref":"#/components/schemas/StoryProject"}}},"ProjectWorkspaceGate":{"type":"object","required":["ready","stage","reason","missingFields","canPlan","canStartEpisode","fullCoverage","fullCoverageNotice"],"additionalProperties":false,"description":"Where the project stands in development — the readiness preflight. Read-only: reading it never queues or recovers coverage. A gated call refuses with 409 `project_precondition_failed` carrying the same `stage`, `missingFields`, `canPlan` and `canStartEpisode`, so this predicts it exactly. When `ready` is false, finish `stage` before planning, drafting, casting or creating jobs.","properties":{"ready":{"type":"boolean","description":"Every development stage is finished."},"stage":{"$ref":"#/components/schemas/ProjectWorkspaceStage"},"reason":{"oneOf":[{"type":"string"},{"type":"null"}],"description":"How to finish `stage`; null once `ready`."},"missingFields":{"type":"array","items":{"type":"string"},"description":"What the stage still lacks, when the gate can name it. Often empty: a coverage stage is finished by an act, not a field."},"canPlan":{"type":"boolean","description":"Plans, casting and the episode map are allowed now."},"canStartEpisode":{"type":"boolean","description":"Episode drafting, story jobs for project media, and publishing are allowed now."},"fullCoverage":{"type":"string","enum":["required","advisory"],"description":"`required`: full Series Bible coverage must pass before episodes, project media and publishing. `advisory`: the project is a recurring show (a live publishing series that runs a recurring cadence or holds a standing approval), so premise coverage alone opens it and full coverage is shown but blocks nothing."},"fullCoverageNotice":{"oneOf":[{"type":"string"},{"type":"null"}],"description":"For a recurring show: \"Full coverage advisory (recurring show): …\" plus where full coverage stands. Null for every other project."}}},"ProjectTableReadReadiness":{"type":"object","required":["ready","audioOnly","reason","narratorVoice","members"],"additionalProperties":false,"description":"Whether a table_read story job can start FROM A PLAN right now — the canon-cast rule a job refuses with as 409 `cast_precondition_failed`, read without starting anything. The project's cast canon must cover every character: a voice for everyone and, unless the project is audio-only (Series Bible `productionMedium: \"audio\"`), a headshot, body shot and character sheet. A narrated read also needs a narrator voice. The job's own `voiceMap` can supply voices the canon lacks, and once the job has written its screenplay any speaker the cast does not cover fails it before any audio, releasing its credits.","properties":{"ready":{"type":"boolean","description":"Every cast canon character is cast for this medium, so a speaker-only read can start from a plan."},"audioOnly":{"type":"boolean","description":"The project is audio-only: members need a voice and nothing else."},"reason":{"type":"string","description":"What to finish, or that the cast is complete."},"narratorVoice":{"type":"boolean","description":"The canon carries a NARRATOR voice, so a narrated read can start too. Otherwise pass a NARRATOR voice in the job's voiceMap or keep the read speaker-only."},"members":{"type":"array","description":"Each cast canon member and what it still lacks.","items":{"type":"object","required":["name","narrator","ready","missing"],"additionalProperties":false,"properties":{"name":{"type":"string"},"narrator":{"type":"boolean"},"ready":{"type":"boolean"},"missing":{"type":"array","items":{"type":"string","enum":["face","body","sheet","voice"]}}}}}}},"StoryProjectDetail":{"type":"object","required":["id","name","description","metadata","seasonCadence","contentGuard","archivedAt","createdAt","updatedAt","workspaceGate","tableReadReadiness"],"additionalProperties":false,"description":"A project as `getStoryProject` returns it: the project fields plus its `workspaceGate` and `tableReadReadiness`.","properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"oneOf":[{"type":"string"},{"type":"null"}]},"metadata":{"oneOf":[{"type":"object","additionalProperties":true},{"type":"null"}]},"seasonCadence":{"oneOf":[{"type":"string","enum":["monthly"]},{"type":"null"}],"description":"How a new episode is filed into a season. `monthly`: the season named for the month (UTC) its source material was made (`originatedAt` on the source, else when the source was added, else when the episode was made), created after the last season when the month is new. Null: numbered seasons (the season the plan, its season run or its Series Bible entry names, else Season 1). Stored as `metadata.seasonCadence`."},"contentGuard":{"type":"boolean","description":"Keeps every script, plan and Series Bible written for the project suitable for ages 13 and up: no strong profanity, no graphic violence, no sexual content. On for a project started from ChatGPT; off otherwise unless set."},"archivedAt":{"oneOf":[{"type":"string","format":"date-time"},{"type":"null"}]},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"sourceCount":{"type":"integer","minimum":0},"workspaceGate":{"$ref":"#/components/schemas/ProjectWorkspaceGate"},"tableReadReadiness":{"$ref":"#/components/schemas/ProjectTableReadReadiness"}}},"StoryProjectDetailEnvelope":{"type":"object","required":["project"],"additionalProperties":false,"properties":{"project":{"$ref":"#/components/schemas/StoryProjectDetail"}}},"StoryProjectList":{"type":"object","required":["projects","nextCursor"],"additionalProperties":false,"properties":{"projects":{"type":"array","items":{"$ref":"#/components/schemas/StoryProject"}},"nextCursor":{"oneOf":[{"type":"string"},{"type":"null"}]}}},"StoryProjectCreate":{"type":"object","required":["name"],"additionalProperties":false,"properties":{"name":{"type":"string","minLength":1,"maxLength":200},"description":{"type":"string","maxLength":4000},"metadata":{"type":"object","additionalProperties":true,"description":"Your own JSON for cross-reference, with keys the platform reads: `coverageCalibrations` (up to 5 names, e.g. `[\"comedy\"]`) opts this project's screenplay coverage into the series coverage calibrations an admin has written (a name with no active calibration is ignored); `seasonCadence` (`\"monthly\"`) files new episodes by month; `seasons` is the project's season list. A malformed value is refused."},"contentGuard":{"type":"boolean","description":"Keep everything written for the project suitable for ages 13 and up. Default false."}}},"StoryProjectFromScript":{"type":"object","additionalProperties":false,"description":"Exactly one of `scriptId` or `scriptUploadId`.","properties":{"scriptId":{"type":"string","minLength":1,"maxLength":200,"description":"The script id, or the script id shown in its Writers Lab URL."},"scriptUploadId":{"type":"string","minLength":1,"maxLength":200,"description":"Any version (script upload) of the script."}}},"StoryProjectFromScriptEnvelope":{"type":"object","required":["project","scriptId","created","workspacePath"],"additionalProperties":false,"properties":{"project":{"$ref":"#/components/schemas/StoryProject"},"scriptId":{"type":"string","description":"The wrapped script."},"created":{"type":"boolean","description":"True when this call made a new project; false when the script was already in one, or rejoined the live project that already wrapped it."},"workspacePath":{"type":"string","description":"The project Overview in the Writers Lab, relative to the site origin."}}},"ScriptOverrides":{"type":"object","additionalProperties":false,"required":["scriptUploadId","overrides","soundscapeDefault"],"properties":{"scriptUploadId":{"type":"string"},"overrides":{"type":"object","additionalProperties":false,"required":["avatar","theater","soundscape","pitchDeck","locations"],"properties":{"avatar":{"type":["string","null"],"description":"Episode Avatar Override. Optional script-specific exception layered over the Series Bible visual canon for character portraits. Null: none is set, so the Series Bible visual canon applies."},"theater":{"type":["string","null"],"description":"Episode Theater Override. Optional script-specific exception layered over the Series Bible visual canon for theater presentation. Null: none is set, so the Series Bible visual canon applies."},"soundscape":{"type":["string","null"],"description":"Soundscape Style. Global sonic direction fed directly into adaptive scene music and beat cue generation. Null: none is set, so the studio default (`soundscapeDefault`) applies."},"pitchDeck":{"type":["string","null"],"description":"Episode Pitch Deck Override. Optional script-specific exception layered over the Series Bible visual canon for pitch deck generation. Null: none is set, so the Series Bible visual canon applies."},"locations":{"type":["string","null"],"description":"Episode Locations Override. Optional script-specific exception layered over the Series Bible visual canon for this episode's location plates. Null: none is set, so the Series Bible visual canon applies."}}},"soundscapeDefault":{"type":["string","null"],"description":"What a null `soundscape` falls back to: the studio's default soundscape style, or null when none is configured."}}},"ScriptOverridesPatch":{"type":"object","additionalProperties":false,"minProperties":1,"description":"Only the overrides to change. Text sets one; null or blank text clears it; keys left out are untouched.","properties":{"avatar":{"type":["string","null"],"maxLength":8000,"description":"Episode Avatar Override. Optional script-specific exception layered over the Series Bible visual canon for character portraits."},"theater":{"type":["string","null"],"maxLength":8000,"description":"Episode Theater Override. Optional script-specific exception layered over the Series Bible visual canon for theater presentation."},"soundscape":{"type":["string","null"],"maxLength":8000,"description":"Soundscape Style. Global sonic direction fed directly into adaptive scene music and beat cue generation."},"pitchDeck":{"type":["string","null"],"maxLength":8000,"description":"Episode Pitch Deck Override. Optional script-specific exception layered over the Series Bible visual canon for pitch deck generation."},"locations":{"type":["string","null"],"maxLength":8000,"description":"Episode Locations Override. Optional script-specific exception layered over the Series Bible visual canon for this episode's location plates."}}},"ScriptLineTuning":{"type":"object","additionalProperties":false,"required":["scriptUploadId","entryIndex","audits","versions","line"],"properties":{"scriptUploadId":{"type":"string"},"entryIndex":{"type":"integer","minimum":0},"audits":{"type":"array","items":{"type":"object","additionalProperties":true},"description":"The Slug conversation's rolling audit of this line, newest last."},"versions":{"type":"array","items":{"type":"object","additionalProperties":true},"description":"The edit snapshots taken before each change, newest first."},"line":{"type":["object","null"],"additionalProperties":false,"description":"The line's current editable state, or null when the index is past the end.","required":["text","ttsText","parenthetical","sceneHeading","emotion","pauseAfterMs","actingDirection"],"properties":{"text":{"type":"string"},"ttsText":{"type":["string","null"]},"parenthetical":{"type":["string","null"]},"sceneHeading":{"type":["string","null"]},"emotion":{"type":["object","null"],"additionalProperties":false,"description":"The delivery. `speed` is display-only: only re-voicing the line changes it.","required":["emotions","speed"],"properties":{"emotions":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":3},"speed":{"type":"number","description":"How fast the line is spoken — set only by re-voicing the line (`revoice_line`)."},"volume":{"type":"number","minimum":0.5,"maximum":2}}},"pauseAfterMs":{"type":["integer","null"],"minimum":0,"maximum":5000},"actingDirection":{"type":["string","null"]}}}}},"ScriptLineEdit":{"type":"object","additionalProperties":false,"minProperties":1,"description":"Only the line fields to change; keys left out are untouched. There is deliberately no speed and no acting-direction field — re-voice the line (POST /artifacts/{artifactId}/revoice-line) to change either; `actingDirection` here is refused with 400 `validation_failed`.","properties":{"text":{"type":"string"},"ttsText":{"type":"string"},"parenthetical":{"type":"string"},"sceneHeading":{"type":"string"},"emotion":{"type":"object","additionalProperties":false,"required":["emotions"],"properties":{"emotions":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":3},"volume":{"type":"number","minimum":0.5,"maximum":2}}},"pauseAfterMs":{"type":"integer","minimum":0,"maximum":5000,"description":"Milliseconds to pause after the line before the next. The one pause field."}}},"ScriptShareDescriptor":{"type":"object","additionalProperties":false,"description":"The playback a share describes. Every field is optional; a create with none is the whole-read theater share the dialog's default is.","properties":{"mode":{"type":"string","enum":["theater","audio"],"description":"`theater`: an MP4 link that plays the read. `audio`: a scoped full-mix MP3. Default `theater`."},"source":{"type":"string","enum":["recording","adhoc"],"description":"Where the recording comes from: the job's recording, or an ad-hoc performance. Default `recording`."},"scope":{"type":"string","enum":["whole","scene","entries","conversation"],"description":"What the share plays: the whole read, one scene, a band of entries, or one conversation. Default `whole`."},"sceneIndex":{"type":"integer","minimum":0,"description":"For `scope: scene`: which scene."},"conversationId":{"type":"string","minLength":1,"description":"For `scope: conversation`: which conversation."},"startEntry":{"type":"integer","minimum":0,"description":"For `scope: entries`: the first entry index."},"endEntry":{"type":"integer","minimum":0,"description":"For `scope: entries`: the last entry index."},"withCredits":{"type":"boolean","description":"Bake the opening title + closing credits into a whole-episode theater MP4."}}},"ScriptShareLink":{"type":"object","additionalProperties":false,"required":["id","audience","descriptor","mode","active","includeOnProfile","expiresAt","renderedVideoStatus","renderedVideoUrl","renderedVideoDurationMs","renderedVideoError","renderedAudioStatus","renderedAudioUrl","renderedAudioDurationMs","renderedAudioError"],"properties":{"id":{"type":"string","description":"The share link id."},"audience":{"type":"string","enum":["public","private_review","team"]},"descriptor":{"$ref":"#/components/schemas/ScriptShareDescriptor"},"mode":{"type":"string","enum":["theater","audio"]},"active":{"type":"boolean"},"includeOnProfile":{"type":"boolean"},"expiresAt":{"type":["string","null"],"description":"When the link stops working, or null for no expiry."},"renderedVideoStatus":{"type":"string","description":"The theater MP4's render state: idle, needs_recording, queued, rendering, complete, stale or failed. A render that stopped with no job behind it reads `failed` (or `stale` over an earlier MP4) with `renderedVideoError` saying so: ask for it again."},"renderedVideoUrl":{"type":["string","null"]},"renderedVideoDurationMs":{"type":["integer","null"]},"renderedVideoError":{"type":["string","null"]},"renderedAudioStatus":{"type":"string","description":"The scoped MP3's render state. A render that stopped with no job behind it reads `failed` (or `stale` over an earlier MP3) with `renderedAudioError` saying so: share the read again to render it."},"renderedAudioUrl":{"type":["string","null"]},"renderedAudioDurationMs":{"type":["integer","null"]},"renderedAudioError":{"type":["string","null"]}}},"ScriptShareState":{"type":"object","additionalProperties":false,"required":["scriptUploadId","jobId","shareUrl","share"],"description":"A script version's table-read share state: the active link if the read is shared, else null.","properties":{"scriptUploadId":{"type":"string"},"jobId":{"type":["string","null"],"description":"The table read the share resolves to (the script's latest read), null when there is none."},"shareUrl":{"type":["string","null"],"description":"The public, signed watch/listen URL, or null when there is no active share."},"share":{"oneOf":[{"$ref":"#/components/schemas/ScriptShareLink"},{"type":"null"}],"description":"The share link's state, or null when the read is not shared."}}},"CreateScriptShareRequest":{"type":"object","additionalProperties":false,"description":"The share options to create with. Every field is optional; an empty body is the public, whole-read theater share for the studio's 30-day window.","properties":{"audience":{"type":"string","enum":["public","private_review","team"],"description":"Who can use the link. Default `public`."},"includeOnProfile":{"type":"boolean","description":"Show the share on the writer's public profile (only honored for a public share). Default false."},"descriptor":{"$ref":"#/components/schemas/ScriptShareDescriptor"},"expiresInDays":{"type":"integer","minimum":1,"maximum":365,"description":"How long the link works, in days. Default 30."}}},"CreateScriptShareResult":{"type":"object","additionalProperties":false,"required":["scriptUploadId","jobId","shareUrl","share","dashboardPath","dashboardUrl"],"properties":{"scriptUploadId":{"type":"string"},"jobId":{"type":"string"},"shareUrl":{"type":"string","description":"The public, signed watch/listen URL."},"share":{"$ref":"#/components/schemas/ScriptShareLink"},"dashboardPath":{"type":"string","description":"The Writers Lab path the owner opens to manage the share."},"dashboardUrl":{"type":"string"}}},"StoryProjectPatch":{"type":"object","additionalProperties":false,"properties":{"name":{"type":"string","minLength":1,"maxLength":200},"description":{"oneOf":[{"type":"string","maxLength":4000},{"type":"null"}]},"metadata":{"oneOf":[{"type":"object","additionalProperties":true},{"type":"null"}],"description":"Your own JSON for cross-reference, MERGED into what is stored: a key you send is set, a key sent as null is removed, a key left out is kept; null removes every key you can set. Keys the platform reads: `coverageCalibrations` (up to 5 names, e.g. `[\"comedy\"]`) opts this project's screenplay coverage into the series coverage calibrations an admin has written (a name with no active calibration is ignored); `seasonCadence` (`\"monthly\"`) files new episodes by month. A malformed value is refused. `seasons` is the project's season list and is refused here (POST /story-projects/{projectId}/seasons names one)."},"archived":{"type":"boolean"},"seasonCadence":{"oneOf":[{"type":"string","enum":["monthly"]},{"type":"null"}],"description":"Set (`\"monthly\"`) or clear (null) how new episodes are filed into seasons — the same as `metadata.seasonCadence`, keeping every other metadata key. Not combined with `metadata`. Only episodes made afterwards are filed by it."},"contentGuard":{"type":"boolean","description":"Turn the ages-13-and-up content guard on or off for everything written for the project from now on."}}},"SeriesBibleEpisode":{"type":"object","required":["id","label","title","summary","status"],"additionalProperties":false,"properties":{"id":{"type":"string"},"label":{"type":"string","description":"Episode/installment label such as \"Episode 1\" or \"Pilot\"."},"title":{"type":"string"},"summary":{"type":"string"},"status":{"type":"string","description":"Planning state, e.g. planned, drafted, table read ready, complete."}}},"SeriesBibleCharacter":{"type":"object","required":["id","name","role","want","wound","arc","voice","relationships","appearance","wardrobe","movement"],"additionalProperties":false,"properties":{"id":{"type":"string"},"name":{"type":"string"},"role":{"type":"string"},"want":{"type":"string"},"wound":{"type":"string"},"arc":{"type":"string"},"voice":{"type":"string"},"relationships":{"type":"string"},"appearance":{"type":"string","description":"Binding visual identity details that supplement the canonical avatar and model sheet."},"wardrobe":{"type":"string","description":"Canonical wardrobe silhouette, materials, colors, and permitted changes."},"movement":{"type":"string","description":"Posture, gait, gesture vocabulary, and recurring physical behavior."}}},"SeriesBibleEpisodePatch":{"type":"object","additionalProperties":false,"description":"Partial Series Bible episode item accepted by PATCH; the server fills missing fields with normalized defaults.","properties":{"id":{"type":"string"},"label":{"type":"string"},"title":{"type":"string"},"summary":{"type":"string"},"status":{"type":"string"}}},"SeriesBibleCharacterPatch":{"type":"object","additionalProperties":false,"description":"Partial Series Bible character item accepted by PATCH; the server fills missing fields with normalized defaults.","properties":{"id":{"type":"string"},"name":{"type":"string"},"role":{"type":"string"},"want":{"type":"string"},"wound":{"type":"string"},"arc":{"type":"string"},"voice":{"type":"string"},"relationships":{"type":"string"},"appearance":{"type":"string"},"wardrobe":{"type":"string"},"movement":{"type":"string"}}},"SeriesBibleTypography":{"type":"object","required":["direction","titleFace","titleColor","titleCase","titleWeight","bodyFace","bodyColor"],"additionalProperties":false,"description":"Type identity inherited by every trailer, pitch deck, and episode rendered for this project.","properties":{"direction":{"type":"string","description":"Prose direction for how the type should feel and behave on screen. Read by prompts, not by the compositor."},"titleFace":{"type":"string","enum":["","chunky_sans","grotesque_sans","condensed_sans","classical_serif","typewriter"],"description":"Face for titles, credits, and on-screen text. Empty string means the renderer default."},"titleColor":{"type":"string","maxLength":9,"description":"Hex colour such as \"#8A0303\". Empty string means the renderer default."},"titleCase":{"type":"string","enum":["","upper","as_written"],"description":"Whether a title card forces capitals or preserves the copy as written."},"titleWeight":{"type":"integer","minimum":0,"maximum":900,"description":"CSS weight 100-900. 0 means unset, and the renderer picks the weight the chosen face exists for."},"bodyFace":{"type":"string","enum":["","chunky_sans","grotesque_sans","condensed_sans","classical_serif","typewriter"],"description":"Face for subtitles, lower thirds, and supporting copy."},"bodyColor":{"type":"string","maxLength":9,"description":"Hex colour for body copy. Empty string means the renderer default."}}},"SeriesBibleTypographyPatch":{"type":"object","additionalProperties":false,"description":"Partial typography block accepted by PATCH. Keys you send are merged over the saved block; keys you omit are preserved.","properties":{"direction":{"type":"string"},"titleFace":{"type":"string","enum":["","chunky_sans","grotesque_sans","condensed_sans","classical_serif","typewriter"]},"titleColor":{"type":"string","maxLength":9},"titleCase":{"type":"string","enum":["","upper","as_written"]},"titleWeight":{"type":"integer","minimum":0,"maximum":900},"bodyFace":{"type":"string","enum":["","chunky_sans","grotesque_sans","condensed_sans","classical_serif","typewriter"]},"bodyColor":{"type":"string","maxLength":9}}},"SeriesBibleContent":{"type":"object","required":["logline","format","genre","audience","tone","premise","seasonArc","themes","productionMedium","characterReferencePolicy","visualStyle","characterDesign","visualTruths","productionDesign","colorPalette","lightingRules","cameraLanguage","directorStyle","typography","locationTruths","audioStyle","writingStyle","worldRules","canonTimeline","openQuestions","notes","episodes","characters"],"additionalProperties":false,"description":"Project-level canon and continuity shared by UI, API, CLI, and MCP.","properties":{"logline":{"type":"string"},"format":{"type":"string"},"genre":{"type":"string"},"audience":{"type":"string"},"tone":{"type":"string"},"premise":{"type":"string"},"seasonArc":{"type":"string"},"themes":{"type":"string"},"productionMedium":{"type":"string","enum":["live_action","two_d_animation","three_d_animation","stop_motion","claymation","anime","documentary","hybrid","audio","unknown"],"description":"Binding production medium. Image and video rendering require a value other than unknown."},"characterReferencePolicy":{"type":"string","enum":["identity_only","identity_and_style","model_sheet_reference","ignore_style_use_identity"],"description":"How canonical portrait, body, and character-sheet assets constrain generated production images."},"visualStyle":{"type":"string"},"characterDesign":{"type":"string","description":"Project-wide portrait and body-design language used with canonical cast assets."},"visualTruths":{"type":"string","description":"Non-negotiable visual facts and forbidden drift for every generated image."},"productionDesign":{"type":"string","description":"Sets, props, textures, materials, signage, and recurring production-design vocabulary."},"colorPalette":{"type":"string","description":"Canonical color, saturation, contrast, and accent-color rules."},"lightingRules":{"type":"string","description":"Motivated light sources, time-of-day logic, exposure, contrast, and atmosphere."},"cameraLanguage":{"type":"string","description":"Canonical shot sizes, lenses, heights, movements, speeds, tracking rules, and forbidden camera behavior."},"directorStyle":{"type":"string","enum":["","malick","fincher","coen","villeneuve","tarantino","wes_anderson"],"description":"Optional camera-grammar layer applied to generated shots. Empty string means no director layer."},"typography":{"$ref":"#/components/schemas/SeriesBibleTypography"},"locationTruths":{"type":"string","description":"Stable location geography, entrances, sightlines, recurring zones, and location-specific facts."},"audioStyle":{"type":"string"},"writingStyle":{"type":"string"},"worldRules":{"type":"string"},"canonTimeline":{"type":"string"},"openQuestions":{"type":"string"},"notes":{"type":"string"},"episodes":{"type":"array","maxItems":100,"items":{"$ref":"#/components/schemas/SeriesBibleEpisode"}},"characters":{"type":"array","maxItems":200,"items":{"$ref":"#/components/schemas/SeriesBibleCharacter"}}}},"SeriesBibleDocument":{"type":"object","required":["id","projectId","kind","title","content","readiness","generatedFrom","currentVersion","createdAt","updatedAt","renderContext"],"additionalProperties":false,"properties":{"id":{"type":"string"},"projectId":{"type":"string"},"kind":{"type":"string","const":"series_bible"},"title":{"oneOf":[{"type":"string"},{"type":"null"}]},"content":{"$ref":"#/components/schemas/SeriesBibleContent"},"readiness":{"type":"object","required":["development","moodBoard"],"additionalProperties":false,"properties":{"development":{"type":"object","required":["ready","completedFields","totalFields","missingFields"],"additionalProperties":false,"properties":{"ready":{"type":"boolean"},"completedFields":{"type":"integer","minimum":0},"totalFields":{"type":"integer","minimum":0},"missingFields":{"type":"array","items":{"type":"string"}}}},"moodBoard":{"type":"object","required":["ready","missingFields"],"additionalProperties":false,"properties":{"ready":{"type":"boolean"},"missingFields":{"type":"array","items":{"type":"string"}}}}}},"generatedFrom":{"oneOf":[{"type":"object","additionalProperties":true},{"type":"null"}]},"currentVersion":{"type":"integer","minimum":0},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"renderContext":{"$ref":"#/components/schemas/SeriesRenderContext"}}},"SeriesRenderContext":{"type":"object","description":"What renders derive FROM the Series Bible and keep, per Bible version. Read-only: it is derived, never written through the Bible.","required":["periodNegatives"],"additionalProperties":false,"properties":{"periodNegatives":{"type":"object","description":"This series' period world, derived from the Bible's period, place and tone once per Bible version: the world as it is (`indoors`, `outdoors`), which every reference render (decks, trailers and their re-renders) is told after the series canon, and the anachronisms it never has (`negatives`), which every take is checked for and which are never sent to the video model as a list. Empty for a contemporary series.","required":["status","derivedFromBibleVersion","world","indoors","outdoors","negatives","derivedAt"],"additionalProperties":false,"properties":{"status":{"type":"string","enum":["current","stale","not_derived","no_bible"],"description":"current: derived from this Bible version, sent by every render. stale: derived from an earlier version; the next render derives it again first. not_derived: no render has derived it yet; the first one will. no_bible: no Series Bible, nothing excluded."},"derivedFromBibleVersion":{"oneOf":[{"type":"integer","minimum":1},{"type":"null"}]},"world":{"type":"string","description":"When and where the Bible sets this world, as the list was derived from it (e.g. \"1870s provincial Russia\")."},"indoors":{"type":"string","maxLength":320,"description":"The world as it is indoors, in positive words — what lights, heats and furnishes its rooms. What every render of an interior is told. Empty for a contemporary series."},"outdoors":{"type":"string","maxLength":320,"description":"The world as it is outdoors, in positive words — its streets, roads, transport and skyline. What every render of an exterior is told. Empty for a contemporary series."},"negatives":{"type":"array","maxItems":12,"items":{"type":"string","maxLength":60},"description":"What this world never has: what every take is checked for. Never sent to the video model."},"derivedAt":{"oneOf":[{"type":"string","format":"date-time"},{"type":"null"}]}}}}},"SeriesBibleEnvelope":{"type":"object","required":["document"],"additionalProperties":false,"properties":{"document":{"$ref":"#/components/schemas/SeriesBibleDocument"}}},"SeriesBibleGatePosition":{"type":"object","required":["stage","ready","canPlan","canStartEpisode","canGenerateProjectMedia","reason"],"additionalProperties":false,"description":"The project's workspace gate at one moment.","properties":{"stage":{"$ref":"#/components/schemas/ProjectWorkspaceStage"},"ready":{"type":"boolean"},"canPlan":{"type":"boolean","description":"Planning seasons and casting at project level."},"canStartEpisode":{"type":"boolean"},"canGenerateProjectMedia":{"type":"boolean","description":"Paid work on every episode already written (coverage, casting, table reads, videos, decks, trailers, publishing) and project media."},"reason":{"type":"string","nullable":true,"description":"What to do next when the gate is closed; null when it is open."}}},"SeriesBibleGateChange":{"type":"object","required":["before","after","staleCoverage"],"additionalProperties":false,"description":"Every save returns `gateChange` { before, after, staleCoverage }: what it did to the project's gate. Appending (episodes for a new season, a new character) never re-locks anything. An edit to canon a coverage report scored puts that report out of date (`staleCoverage`) and re-locks what it opened: after full coverage, paid work on every episode already written (coverage, casting, table reads, videos, decks, trailers, publishing) until full coverage passes again. When `after.stage` comes before `before.stage`, tell the user.","properties":{"before":{"$ref":"#/components/schemas/SeriesBibleGatePosition"},"after":{"$ref":"#/components/schemas/SeriesBibleGatePosition"},"staleCoverage":{"type":"array","items":{"type":"string","enum":["premise","full"]},"description":"The coverage reports that spoke for the Bible before this save and no longer do. Empty for an append or a visual edit."}}},"SeriesBibleSaveResult":{"type":"object","required":["document","gateChange"],"additionalProperties":false,"properties":{"document":{"$ref":"#/components/schemas/SeriesBibleDocument"},"gateChange":{"$ref":"#/components/schemas/SeriesBibleGateChange"}}},"SeriesBibleContentPatch":{"type":"object","additionalProperties":false,"description":"Partial Series Bible content payload accepted by PATCH; the server fills missing fields with their normalized defaults.","properties":{"logline":{"type":"string"},"format":{"type":"string"},"genre":{"type":"string"},"audience":{"type":"string"},"tone":{"type":"string"},"premise":{"type":"string"},"seasonArc":{"type":"string"},"themes":{"type":"string"},"productionMedium":{"type":"string","enum":["live_action","two_d_animation","three_d_animation","stop_motion","claymation","anime","documentary","hybrid","audio","unknown"]},"characterReferencePolicy":{"type":"string","enum":["identity_only","identity_and_style","model_sheet_reference","ignore_style_use_identity"]},"visualStyle":{"type":"string"},"characterDesign":{"type":"string"},"visualTruths":{"type":"string"},"productionDesign":{"type":"string"},"colorPalette":{"type":"string"},"lightingRules":{"type":"string"},"cameraLanguage":{"type":"string"},"directorStyle":{"type":"string","enum":["","malick","fincher","coen","villeneuve","tarantino","wes_anderson"]},"typography":{"$ref":"#/components/schemas/SeriesBibleTypographyPatch"},"locationTruths":{"type":"string"},"audioStyle":{"type":"string"},"writingStyle":{"type":"string"},"worldRules":{"type":"string"},"canonTimeline":{"type":"string"},"openQuestions":{"type":"string"},"notes":{"type":"string"},"episodes":{"type":"array","maxItems":100,"items":{"$ref":"#/components/schemas/SeriesBibleEpisodePatch"}},"characters":{"type":"array","maxItems":200,"items":{"$ref":"#/components/schemas/SeriesBibleCharacterPatch"}}}},"SeriesBiblePatch":{"type":"object","required":["content"],"additionalProperties":false,"properties":{"title":{"type":"string","maxLength":200},"content":{"$ref":"#/components/schemas/SeriesBibleContentPatch"},"arrayMode":{"type":"string","enum":["merge","replace"],"description":"How episodes[]/characters[] reconcile with the stored arrays. Defaults to \"merge\" (upsert by id, unsent items survive). Send \"replace\" with a whole-document save so items you removed disappear."}}},"SeriesBibleGenerateRequest":{"type":"object","additionalProperties":false,"properties":{"instructions":{"type":"string","maxLength":8000}}},"SeriesBibleCoverageDimension":{"type":"object","required":["score","finding","evidence","nextMove"],"additionalProperties":false,"properties":{"score":{"type":"number","minimum":0,"maximum":10},"finding":{"type":"string"},"evidence":{"type":"array","items":{"type":"string"}},"nextMove":{"type":"string"}}},"SeriesBibleCoverageReport":{"type":"object","required":["overallScore","verdict","oneLineDiagnosis","audiencePromise","thematicDifferentiator","showingStrategy","dimensions","audioBlueprint","strengths","risks","priorityFixes","exemplarComparisons"],"additionalProperties":false,"properties":{"overallScore":{"type":"number","minimum":0,"maximum":10},"verdict":{"type":"string","enum":["pass","revise","rebuild"]},"oneLineDiagnosis":{"type":"string"},"audiencePromise":{"type":"string"},"thematicDifferentiator":{"type":"string"},"showingStrategy":{"type":"string"},"dimensions":{"type":"object","required":["premiseAndHook","thematicEngine","serialEngine","episodeArchitecture","characterAndAccess","evidenceAndEthics","showingNotTelling","audioGrammar","audienceAndMarket"],"additionalProperties":false,"properties":{"premiseAndHook":{"$ref":"#/components/schemas/SeriesBibleCoverageDimension"},"thematicEngine":{"$ref":"#/components/schemas/SeriesBibleCoverageDimension"},"serialEngine":{"$ref":"#/components/schemas/SeriesBibleCoverageDimension"},"episodeArchitecture":{"$ref":"#/components/schemas/SeriesBibleCoverageDimension"},"characterAndAccess":{"$ref":"#/components/schemas/SeriesBibleCoverageDimension"},"evidenceAndEthics":{"$ref":"#/components/schemas/SeriesBibleCoverageDimension"},"showingNotTelling":{"$ref":"#/components/schemas/SeriesBibleCoverageDimension"},"audioGrammar":{"$ref":"#/components/schemas/SeriesBibleCoverageDimension"},"audienceAndMarket":{"$ref":"#/components/schemas/SeriesBibleCoverageDimension"}}},"audioBlueprint":{"type":"object","required":["narration","scenes","archive","atmosphere","music","sfx","transitions"],"additionalProperties":false,"properties":{"narration":{"type":"string"},"scenes":{"type":"string"},"archive":{"type":"string"},"atmosphere":{"type":"string"},"music":{"type":"string"},"sfx":{"type":"string"},"transitions":{"type":"string"}}},"strengths":{"type":"array","items":{"type":"string"}},"risks":{"type":"array","items":{"type":"string"}},"priorityFixes":{"type":"array","items":{"type":"object","required":["priority","fix","why","acceptanceTest"],"additionalProperties":false,"properties":{"priority":{"type":"integer","minimum":1},"fix":{"type":"string"},"why":{"type":"string"},"acceptanceTest":{"type":"string"}}}},"exemplarComparisons":{"type":"array","items":{"type":"object","required":["exemplarTitle","craftLesson","application"],"additionalProperties":false,"properties":{"exemplarTitle":{"type":"string"},"craftLesson":{"type":"string"},"application":{"type":"string"}}}}}},"SeriesBibleCoverageStatus":{"type":"object","required":["status","current","stale","gatePassed","gateThreshold","reportId","documentVersion","reportVersion","overallScore","progressDetail","failureMessage","report","exemplarIds","createdAt","updatedAt"],"additionalProperties":false,"properties":{"status":{"type":"string","enum":["missing","generating","complete","failed"]},"current":{"type":"boolean","description":"True while every field this report scored reads the same in the Bible now. Premise reports score the story fields and each principal character's want, wound and arc; full reports also score style, world, timeline, notes, the episode map and each principal's name, role, voice and relationships. Visual canon and characters added after the report never make it out of date."},"stale":{"type":"boolean","description":"The inverse of current: a field this report scored has changed since."},"gatePassed":{"type":"boolean"},"gateThreshold":{"type":"number"},"reportId":{"oneOf":[{"type":"string"},{"type":"null"}]},"documentVersion":{"type":"integer","minimum":0},"reportVersion":{"oneOf":[{"type":"integer","minimum":1},{"type":"null"}]},"overallScore":{"oneOf":[{"type":"number","minimum":0,"maximum":10},{"type":"null"}]},"progressDetail":{"oneOf":[{"type":"string"},{"type":"null"}]},"failureMessage":{"oneOf":[{"type":"string"},{"type":"null"}]},"report":{"oneOf":[{"$ref":"#/components/schemas/SeriesBibleCoverageReport"},{"type":"null"}]},"exemplarIds":{"type":"array","items":{"type":"string"}},"createdAt":{"oneOf":[{"type":"string","format":"date-time"},{"type":"null"}]},"updatedAt":{"oneOf":[{"type":"string","format":"date-time"},{"type":"null"}]}}},"SeriesBibleCoverageEnvelope":{"type":"object","required":["coverage"],"additionalProperties":false,"properties":{"coverage":{"$ref":"#/components/schemas/SeriesBibleCoverageStatus"}}},"SeriesBibleCoverageGenerateRequest":{"type":"object","additionalProperties":false,"properties":{"focusPrompt":{"type":"string","maxLength":5000},"phase":{"type":"string","enum":["premise","full"],"description":"Coverage checkpoint. Defaults to `premise`, which unlocks planning and the optional Mood board. Ask for `full` only after the episode map exists; it gates episode work and new project media on its 7.0 score. Anything else is a 400."}}},"OutlineCoverageDimension":{"type":"object","required":["score","finding","evidence","nextMove"],"additionalProperties":false,"properties":{"score":{"type":"number","minimum":0,"maximum":10},"finding":{"type":"string"},"evidence":{"type":"array","items":{"type":"string"}},"nextMove":{"type":"string"}}},"OutlineCoverageReport":{"type":"object","required":["overallScore","verdict","oneLineDiagnosis","structuralSummary","dimensions","episodeNotes","strengths","risks","priorityFixes"],"additionalProperties":false,"properties":{"overallScore":{"type":"number","minimum":0,"maximum":10},"verdict":{"type":"string","enum":["pass","revise","rebuild"]},"oneLineDiagnosis":{"type":"string"},"structuralSummary":{"type":"string"},"dimensions":{"type":"object","required":["structuralIntegrity","causalChain","escalation","characterAgency","setupAndPayoff","episodeDistinctness","seasonMomentum"],"additionalProperties":false,"properties":{"structuralIntegrity":{"$ref":"#/components/schemas/OutlineCoverageDimension"},"causalChain":{"$ref":"#/components/schemas/OutlineCoverageDimension"},"escalation":{"$ref":"#/components/schemas/OutlineCoverageDimension"},"characterAgency":{"$ref":"#/components/schemas/OutlineCoverageDimension"},"setupAndPayoff":{"$ref":"#/components/schemas/OutlineCoverageDimension"},"episodeDistinctness":{"$ref":"#/components/schemas/OutlineCoverageDimension"},"seasonMomentum":{"$ref":"#/components/schemas/OutlineCoverageDimension"}}},"episodeNotes":{"type":"array","items":{"type":"object","required":["bibleEpisodeId","episodeLabel","score","finding","nextMove"],"additionalProperties":false,"properties":{"bibleEpisodeId":{"type":"string"},"episodeLabel":{"type":"string"},"score":{"type":"number","minimum":0,"maximum":10},"finding":{"type":"string"},"nextMove":{"type":"string"}}}},"strengths":{"type":"array","items":{"type":"string"}},"risks":{"type":"array","items":{"type":"string"}},"priorityFixes":{"type":"array","items":{"type":"object","required":["priority","fix","why","acceptanceTest"],"additionalProperties":false,"properties":{"priority":{"type":"integer","minimum":1,"maximum":8},"fix":{"type":"string"},"why":{"type":"string"},"acceptanceTest":{"type":"string"}}}}}},"OutlineCoverageStatus":{"type":"object","required":["status","current","stale","reportId","documentVersion","reportVersion","overallScore","progressDetail","failureMessage","report","unoutlinedEpisodeIds","readiness","createdAt","updatedAt"],"additionalProperties":false,"properties":{"status":{"type":"string","enum":["missing","generating","complete","failed"]},"current":{"type":"boolean"},"stale":{"type":"boolean"},"reportId":{"oneOf":[{"type":"string"},{"type":"null"}]},"documentVersion":{"type":"integer","minimum":0},"reportVersion":{"oneOf":[{"type":"integer","minimum":1},{"type":"null"}]},"overallScore":{"oneOf":[{"type":"number","minimum":0,"maximum":10},{"type":"null"}]},"progressDetail":{"oneOf":[{"type":"string"},{"type":"null"}]},"failureMessage":{"oneOf":[{"type":"string"},{"type":"null"}]},"report":{"oneOf":[{"$ref":"#/components/schemas/OutlineCoverageReport"},{"type":"null"}]},"unoutlinedEpisodeIds":{"type":"array","items":{"type":"string"},"description":"Bible episode ids that had no outline when this report was produced."},"readiness":{"$ref":"#/components/schemas/OutlineCoverageReadiness"},"createdAt":{"oneOf":[{"type":"string","format":"date-time"},{"type":"null"}]},"updatedAt":{"oneOf":[{"type":"string","format":"date-time"},{"type":"null"}]}}},"OutlineSceneCard":{"type":"object","additionalProperties":false,"properties":{"title":{"type":"string","maxLength":500,"description":"The scene or beat, in a few words."},"body":{"type":"string","maxLength":20000,"description":"What happens in it."}}},"EpisodeOutlineInput":{"type":"object","additionalProperties":false,"description":"An episode outline: the Outline tab's fields, every one optional. It replaces the episode's outline. A structure that is not three acts goes in `titleNotes` whole.","properties":{"titleNotes":{"type":"string","maxLength":20000,"description":"What the episode is about, or the whole outline act by act or scene by scene when it does not fit three acts."},"actOne":{"type":"object","additionalProperties":false,"properties":{"incitingIncident":{"type":"string","maxLength":20000},"plotPointOne":{"type":"string","maxLength":20000},"notecards":{"type":"array","maxItems":60,"items":{"$ref":"#/components/schemas/OutlineSceneCard"}}}},"actTwo":{"type":"object","additionalProperties":false,"properties":{"midpoint":{"type":"string","maxLength":20000},"plotPointTwo":{"type":"string","maxLength":20000},"notecards":{"type":"array","maxItems":60,"items":{"$ref":"#/components/schemas/OutlineSceneCard"}}}},"actThree":{"type":"object","additionalProperties":false,"properties":{"climax":{"type":"string","maxLength":20000},"denouement":{"type":"string","maxLength":20000},"notecards":{"type":"array","maxItems":60,"items":{"$ref":"#/components/schemas/OutlineSceneCard"}}}}}},"SaveEpisodeOutlineRequest":{"type":"object","required":["episodeId","outline"],"additionalProperties":false,"properties":{"episodeId":{"type":"string","minLength":1,"maxLength":200,"description":"A season-map episode id, as the outline map lists it."},"outline":{"$ref":"#/components/schemas/EpisodeOutlineInput"}}},"OutlineMapEpisode":{"type":"object","required":["episodeId","label","title","source","outline","words"],"additionalProperties":false,"properties":{"episodeId":{"type":"string"},"label":{"type":"string"},"title":{"type":"string"},"source":{"oneOf":[{"type":"string","enum":["outline_map","season_map"]},{"type":"null"}],"description":"`outline_map`: written for the episode. `season_map`: its season-map note, standing in because it lays the episode out act by act or scene by scene. Null: no outline."},"outline":{"oneOf":[{"type":"object","additionalProperties":true,"description":"The outline document (the Outline tab's fields, with scene cards)."},{"type":"null"}]},"words":{"type":"integer","minimum":0}}},"OutlineMap":{"type":"object","required":["projectId","currentVersion","episodes","progress","words","readiness","updatedAt"],"additionalProperties":false,"properties":{"projectId":{"type":"string"},"currentVersion":{"type":"integer","minimum":0,"description":"The outline map's revision; it moves on every saved outline."},"episodes":{"type":"array","items":{"$ref":"#/components/schemas/OutlineMapEpisode"},"description":"Every planned episode, in season-map order."},"progress":{"type":"object","required":["total","outlined","missing","complete"],"additionalProperties":false,"properties":{"total":{"type":"integer","minimum":0},"outlined":{"type":"integer","minimum":0},"missing":{"type":"array","items":{"type":"string"}},"complete":{"type":"boolean"}}},"words":{"type":"integer","minimum":0},"readiness":{"$ref":"#/components/schemas/OutlineCoverageReadiness"},"updatedAt":{"oneOf":[{"type":"string","format":"date-time"},{"type":"null"}]}}},"OutlineMapEnvelope":{"type":"object","required":["outlineMap"],"additionalProperties":false,"properties":{"outlineMap":{"$ref":"#/components/schemas/OutlineMap"}}},"OutlineCoverageReadiness":{"type":"object","required":["plannedEpisodes","outlinedEpisodes","canScore","reason"],"additionalProperties":false,"description":"What can be scored right now, read off the outlines GET /story-projects/{projectId}/outline-map returns. An episode is outlined once an outline is written for it (its Outline tab, Slug, or PATCH outline-map), or — with nothing written — when its season-map note lays it out act by act or scene by scene. A season-map synopsis or a written screenplay is not an outline. When `canScore` is false, a generate call refuses with 400 `validation_failed` carrying `reason` word for word, so read this first instead of queueing.","properties":{"plannedEpisodes":{"type":"integer","minimum":0,"description":"Episodes in the Series Bible's season map."},"outlinedEpisodes":{"type":"integer","minimum":0,"description":"Planned episodes with an outline: written for the episode, or its season-map note when that lays out acts or scenes."},"canScore":{"type":"boolean","description":"True when at least one planned episode is outlined, so a report can be queued."},"reason":{"oneOf":[{"type":"string"},{"type":"null"}],"description":"Why a report cannot be queued yet; null when `canScore` is true."}}},"OutlineCoverageEnvelope":{"type":"object","required":["coverage"],"additionalProperties":false,"properties":{"coverage":{"$ref":"#/components/schemas/OutlineCoverageStatus"}}},"OutlineCoverageGenerateRequest":{"type":"object","additionalProperties":false,"properties":{"focusPrompt":{"type":"string","maxLength":5000}}},"SeasonRunStatus":{"type":"string","enum":["mapping","approved","running","blocked","complete"],"description":"Durable season-run lifecycle. `mapping` is editable and unapproved; `approved`/`running` have a locked map; `blocked` requires human intervention; `complete` means every episode published."},"SeasonRunEpisodeStatus":{"type":"string","enum":["mapping","ready_to_plan","planning","plan_review","generating","coverage","needs_revision","coverage_passed","finalizing","ready_to_publish","publishing","published","blocked"],"description":"Durable state for one mapped episode. The runner deliberately stops at `plan_review`, `needs_revision`, and `ready_to_publish` for a human decision."},"SeasonCoverageRecommendation":{"type":"string","enum":["RECOMMEND","CONSIDER"],"description":"Coverage recommendations permitted to pass the season gate. `PASS` and lower recommendations fail closed."},"SeasonCoveragePolicy":{"type":"object","required":["minimumOverallScore","minimumCategoryScore","allowedRecommendations","maxRevisionAttempts"],"additionalProperties":false,"description":"Strict, deterministic coverage gate evaluated before durable MP3 finalization. Score and recommendation defaults are hard safety floors: callers may demand higher scores or fewer allowed recommendations, but may not weaken overall below 8/10, any required category below 7/10, or admit PASS. Revision attempts default to three, while callers may choose any positive integer when they want to keep spending credits on approved revisions.","properties":{"minimumOverallScore":{"type":"integer","minimum":8,"maximum":10,"default":8},"minimumCategoryScore":{"type":"integer","minimum":7,"maximum":10,"default":7},"allowedRecommendations":{"type":"array","minItems":1,"maxItems":2,"default":["RECOMMEND","CONSIDER"],"items":{"$ref":"#/components/schemas/SeasonCoverageRecommendation"}},"maxRevisionAttempts":{"type":"integer","minimum":1,"default":3}}},"SeasonAudienceIntent":{"type":"string","enum":["INTERNAL","EXTERNAL"],"default":"INTERNAL","description":"Who this season's finished work is for. INTERNAL stays inside the company; EXTERNAL leaves it. Fixed when the season is created and never editable afterwards — escalating a season after material is already in it would retroactively re-license work nobody reviewed for the outside. To take a season public, create a new EXTERNAL season and re-curate its sources."},"SeasonAudienceOriginRule":{"type":"object","required":["providerSlug"],"additionalProperties":false,"description":"Which places inside one connected product may feed this season. An empty allowOrigins means \"nothing from this product\", never \"everything\".","properties":{"providerSlug":{"type":"string","minLength":1,"maxLength":64,"description":"Connected product slug, e.g. slack, notion, github."},"allowOrigins":{"type":"array","maxItems":500,"items":{"type":"string","minLength":1,"maxLength":200},"description":"Stable upstream origin ids — channel id, owner/repo, space key. Required and non-empty for EXTERNAL seasons."},"denyOrigins":{"type":"array","maxItems":500,"items":{"type":"string","minLength":1,"maxLength":200},"description":"Never pulled, whatever the intent or the allow list says."},"disabled":{"type":"boolean","default":false,"description":"Switch the whole product off for this season without discarding its lists."}}},"SeasonAudiencePolicy":{"type":"object","additionalProperties":false,"description":"Per-season narrowing on top of the audience intent. Deny rules apply to both intents and always beat allow rules. Items marked confidential are blocked on every season by a built-in list a policy may add to but never subtract from. Nothing that passes this becomes part of the season on its own — a person still has to keep it.","properties":{"version":{"type":"integer","enum":[1],"default":1},"integrations":{"type":"array","maxItems":50,"items":{"$ref":"#/components/schemas/SeasonAudienceOriginRule"},"description":"At most one rule per connected product; a duplicate is rejected rather than merged."},"content":{"type":"object","additionalProperties":false,"properties":{"denyKeywords":{"type":"array","maxItems":200,"items":{"type":"string","minLength":2,"maxLength":200},"description":"Blocked anywhere in an item's title, text, link, or labels. Matched on word boundaries."},"denyPatterns":{"type":"array","maxItems":100,"items":{"type":"string","minLength":2,"maxLength":200},"description":"Glob patterns, not regular expressions: * matches any run, ? matches one character, at most ten wildcards."},"extraConfidentialMarkers":{"type":"array","maxItems":100,"items":{"type":"string","minLength":2,"maxLength":200},"description":"Added on top of the always-on confidentiality markers. There is no way to remove a built-in marker."}}},"note":{"type":"string","maxLength":2000,"description":"Free text echoed back in the plain-language summary the reviewer reads."}}},"SeasonCoverageScoreSnapshot":{"type":"object","required":["overallScore","recommendation","categories"],"additionalProperties":false,"properties":{"overallScore":{"oneOf":[{"type":"integer","minimum":1,"maximum":10},{"type":"null"}]},"recommendation":{"oneOf":[{"type":"string"},{"type":"null"}]},"categories":{"type":"object","required":["premise","structure","characters","dialogue","pacing","marketFit"],"additionalProperties":false,"properties":{"premise":{"oneOf":[{"type":"integer","minimum":1,"maximum":10},{"type":"null"}]},"structure":{"oneOf":[{"type":"integer","minimum":1,"maximum":10},{"type":"null"}]},"characters":{"oneOf":[{"type":"integer","minimum":1,"maximum":10},{"type":"null"}]},"dialogue":{"oneOf":[{"type":"integer","minimum":1,"maximum":10},{"type":"null"}]},"pacing":{"oneOf":[{"type":"integer","minimum":1,"maximum":10},{"type":"null"}]},"marketFit":{"oneOf":[{"type":"integer","minimum":1,"maximum":10},{"type":"null"}]}}}}},"SeasonCoverageGateResult":{"type":"object","required":["passed","failureReasons"],"additionalProperties":false,"properties":{"passed":{"type":"boolean"},"failureReasons":{"type":"array","items":{"type":"string"}}}},"SeasonCoverageAttempt":{"type":"object","required":["id","attemptNumber","artifactId","artifactVersion","coverageReportId","coveragePolicy","scoreSnapshot","passed","failureReasons","revisionInstruction","revisionApprovedAt","createdAt"],"additionalProperties":false,"description":"Immutable audit record for one coverage evaluation. A failed attempt can carry the exact revision instruction a human later approved.","properties":{"id":{"type":"string"},"attemptNumber":{"type":"integer","minimum":1},"artifactId":{"type":"string"},"artifactVersion":{"type":"integer","minimum":1},"coverageReportId":{"type":"string"},"coveragePolicy":{"$ref":"#/components/schemas/SeasonCoveragePolicy"},"scoreSnapshot":{"$ref":"#/components/schemas/SeasonCoverageScoreSnapshot"},"passed":{"type":"boolean"},"failureReasons":{"type":"array","items":{"type":"string"}},"revisionInstruction":{"oneOf":[{"type":"string"},{"type":"null"}]},"revisionApprovedAt":{"oneOf":[{"type":"string","format":"date-time"},{"type":"null"}]},"createdAt":{"type":"string","format":"date-time"}}},"SeasonRunEpisode":{"type":"object","required":["id","bibleEpisodeId","position","episodeNumber","title","summary","objective","audience","tone","targetDurationMinutes","notes","confirmed","briefVersion","status","currentPlanId","planAttemptCount","currentJobId","currentArtifactId","currentArtifactVersion","currentCoverageReportId","currentReleaseId","coverageAttemptCount","pendingReplanInstruction","revisionAttemptCount","coverageSnapshot","gateResult","productionReadiness","blockerCode","lastError","coverageAttempts","createdAt","updatedAt"],"additionalProperties":false,"properties":{"id":{"type":"string"},"bibleEpisodeId":{"type":"string"},"position":{"type":"integer","minimum":0},"episodeNumber":{"type":"integer","minimum":1,"maximum":999},"title":{"type":"string"},"summary":{"type":"string"},"objective":{"type":"string"},"audience":{"oneOf":[{"type":"string"},{"type":"null"}]},"tone":{"oneOf":[{"type":"string"},{"type":"null"}]},"targetDurationMinutes":{"type":"integer","minimum":1},"notes":{"oneOf":[{"type":"string"},{"type":"null"}],"description":"What the writer must know before this episode is written, one point per line."},"confirmed":{"type":"boolean","description":"True only after the episode brief was discussed and explicitly confirmed during season mapping."},"briefVersion":{"type":"integer","minimum":1},"status":{"$ref":"#/components/schemas/SeasonRunEpisodeStatus"},"currentPlanId":{"oneOf":[{"type":"string"},{"type":"null"}]},"planAttemptCount":{"type":"integer","minimum":0,"description":"Durable count of planning attempts for this episode, including replaced failed, rejected, or superseded plans."},"currentJobId":{"oneOf":[{"type":"string"},{"type":"null"}]},"currentArtifactId":{"oneOf":[{"type":"string"},{"type":"null"}]},"currentArtifactVersion":{"oneOf":[{"type":"integer","minimum":1},{"type":"null"}]},"currentCoverageReportId":{"oneOf":[{"type":"string"},{"type":"null"}]},"currentReleaseId":{"oneOf":[{"type":"string"},{"type":"null"}]},"coverageAttemptCount":{"type":"integer","minimum":0},"pendingReplanInstruction":{"oneOf":[{"type":"string"},{"type":"null"}],"description":"One-shot correction brief waiting to be copied into the next replacement StoryPlan. It is cleared only after that plan is durably attached."},"revisionAttemptCount":{"type":"integer","minimum":0},"coverageSnapshot":{"oneOf":[{"$ref":"#/components/schemas/SeasonCoverageScoreSnapshot"},{"type":"null"}]},"gateResult":{"oneOf":[{"$ref":"#/components/schemas/SeasonCoverageGateResult"},{"type":"null"}]},"productionReadiness":{"oneOf":[{"type":"object","required":["status","retryable","seasonRunEpisodeId","artifactId","artifactVersion","requirements","blockers"],"additionalProperties":false,"properties":{"status":{"type":"string","enum":["blocked"]},"retryable":{"type":"boolean","enum":[true]},"seasonRunEpisodeId":{"type":"string"},"artifactId":{"type":"string"},"artifactVersion":{"type":"integer","minimum":1},"requirements":{"type":"object","required":["musicRequired","endMusicRequired","sfxRequired","voiceEffectsRequired"],"additionalProperties":false,"properties":{"musicRequired":{"type":"boolean"},"endMusicRequired":{"type":"boolean"},"sfxRequired":{"type":"integer","minimum":0},"voiceEffectsRequired":{"type":"integer","minimum":0}}},"blockers":{"type":"array","items":{"type":"object","required":["code","message"],"additionalProperties":false,"properties":{"code":{"type":"string"},"message":{"type":"string"},"sceneIndex":{"type":"integer","minimum":0},"label":{"type":"string"},"modificationId":{"type":"string"}}}}}},{"type":"null"}],"description":"Durable retryable production blockers. Present across reconnects until music, SFX, and voice effects are ready and advance re-evaluates the gate."},"blockerCode":{"oneOf":[{"type":"string"},{"type":"null"}],"description":"Durable blocker classification used to distinguish guarded retryable stages from terminal human intervention."},"lastError":{"oneOf":[{"type":"string"},{"type":"null"}]},"coverageAttempts":{"type":"array","items":{"$ref":"#/components/schemas/SeasonCoverageAttempt"}},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"SeasonRunNextAction":{"type":"object","required":["type","message"],"additionalProperties":false,"description":"Agent-oriented next durable action. Treat `map_episode`, `approve_map`, `approve_plan`, `replan`, `retry_coverage`, `revise`, `repair_production`, and `publish` as human conversation stops, never as permission to act automatically.","properties":{"type":{"type":"string","enum":["map_episode","approve_map","approve_plan","replan","retry_coverage","revise","repair_production","publish","resolve_blocker","advance","complete"]},"episodeId":{"type":"string"},"message":{"type":"string"},"reportId":{"type":"string"},"planId":{"type":"string"},"jobId":{"type":"string"},"artifactId":{"type":"string"},"releaseId":{"type":"string"},"proposedRevisionInstruction":{"type":"string"},"requirements":{"type":"object","additionalProperties":true},"blockers":{"type":"array","items":{"type":"object","additionalProperties":true}},"blockerCode":{"type":"string"}}},"SeasonRun":{"type":"object","required":["id","projectId","publishingSeriesId","publishingSeries","title","seasonNumber","status","coveragePolicy","audienceIntent","audiencePolicy","mapVersion","seriesBibleVersion","currentVersion","approvedAt","completedAt","lastError","progress","episodes","nextAction","createdAt","updatedAt"],"additionalProperties":false,"properties":{"id":{"type":"string"},"projectId":{"type":"string"},"publishingSeriesId":{"oneOf":[{"type":"string"},{"type":"null"}]},"publishingSeries":{"oneOf":[{"type":"object","required":["id","title","slug","status"],"additionalProperties":false,"properties":{"id":{"type":"string"},"title":{"type":"string"},"slug":{"type":"string"},"status":{"type":"string"}}},{"type":"null"}]},"title":{"type":"string"},"seasonNumber":{"type":"integer","minimum":1,"maximum":999},"status":{"$ref":"#/components/schemas/SeasonRunStatus"},"coveragePolicy":{"$ref":"#/components/schemas/SeasonCoveragePolicy"},"audienceIntent":{"$ref":"#/components/schemas/SeasonAudienceIntent"},"audiencePolicy":{"oneOf":[{"$ref":"#/components/schemas/SeasonAudiencePolicy"},{"type":"null"}],"description":"Null when no per-season narrowing was configured. An EXTERNAL season with no policy pulls nothing in — the empty policy is default-deny on that side."},"mapVersion":{"type":"integer","minimum":1,"description":"Optimistic concurrency version for conversational map edits."},"seriesBibleVersion":{"oneOf":[{"type":"integer","minimum":1},{"type":"null"}]},"currentVersion":{"type":"integer","minimum":1},"approvedAt":{"oneOf":[{"type":"string","format":"date-time"},{"type":"null"}]},"completedAt":{"oneOf":[{"type":"string","format":"date-time"},{"type":"null"}]},"lastError":{"oneOf":[{"type":"string"},{"type":"null"}]},"progress":{"type":"object","required":["total","coveragePassed","published"],"additionalProperties":false,"properties":{"total":{"type":"integer","minimum":0},"coveragePassed":{"type":"integer","minimum":0},"published":{"type":"integer","minimum":0}}},"episodes":{"type":"array","items":{"$ref":"#/components/schemas/SeasonRunEpisode"}},"nextAction":{"$ref":"#/components/schemas/SeasonRunNextAction"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"SeasonRunEnvelope":{"type":"object","required":["run"],"additionalProperties":false,"properties":{"run":{"$ref":"#/components/schemas/SeasonRun"}}},"SeasonRunList":{"type":"object","required":["runs"],"additionalProperties":false,"properties":{"runs":{"type":"array","items":{"$ref":"#/components/schemas/SeasonRun"}}}},"SeasonRunCreateRequest":{"type":"object","additionalProperties":false,"description":"Create a persistent season run from the current Series Bible episode map. Agents should first inspect document.readiness.development and complete an audio-appropriate Bible; the runtime minimum is intentionally the saved episode map because the full development profile includes visual/camera requirements. The imported episodes remain unconfirmed until discussed and saved one by one. Durable identity defaults to project + season number + Series Bible version; runKey selects an explicit domain identity. Every replay must use the same immutable create inputs. HTTP Idempotency-Key remains transport replay protection.","properties":{"publishingSeriesId":{"type":"string","description":"Existing audio publishing series in this project. Omit to reuse the first audio series or create one automatically."},"title":{"type":"string","minLength":1,"maxLength":200},"seasonNumber":{"type":"integer","minimum":1,"maximum":999,"default":1},"runKey":{"type":"string","minLength":1,"maxLength":200,"description":"Optional durable domain identity. Reusing a runKey with different title, season, Bible version, policy, audience intent, audience policy, or explicit series is rejected."},"coveragePolicy":{"$ref":"#/components/schemas/SeasonCoveragePolicy"},"audienceIntent":{"$ref":"#/components/schemas/SeasonAudienceIntent"},"audiencePolicy":{"$ref":"#/components/schemas/SeasonAudiencePolicy"}}},"SeasonRunEpisodeMapPatch":{"type":"object","additionalProperties":false,"minProperties":1,"properties":{"title":{"type":"string","minLength":1,"maxLength":1000},"summary":{"type":"string","minLength":20,"maxLength":50000},"audience":{"oneOf":[{"type":"string","minLength":1,"maxLength":240},{"type":"null"}]},"tone":{"oneOf":[{"type":"string","minLength":1,"maxLength":160},{"type":"null"}]},"targetDurationMinutes":{"type":"integer","minimum":1},"objective":{"type":"string","minLength":10,"maxLength":10000,"description":"What the episode has to achieve. Its plan reads the first 400 characters."},"notes":{"oneOf":[{"type":"string","maxLength":20000},{"type":"null"}],"description":"What the writer must know before this episode is written, one point per line. Each line becomes one of the plan's must-know items (220 characters each, 11 lines at most beside the objective)."},"episodeNumber":{"type":"integer","minimum":1,"maximum":999},"confirmed":{"type":"boolean","description":"Set true only after the user agrees to this complete episode brief."}}},"SeasonRunEpisodeUpdateRequest":{"type":"object","required":["expectedMapVersion","patch"],"additionalProperties":false,"properties":{"expectedMapVersion":{"type":"integer","minimum":1,"description":"Required optimistic-concurrency version from the latest SeasonRun response. A mismatch returns season_run_state_invalid; reload before retrying."},"patch":{"$ref":"#/components/schemas/SeasonRunEpisodeMapPatch"},"userConfirmed":{"type":"boolean","description":"Required and true when patch.confirmed=true, after the user explicitly agrees to this episode brief."}}},"SeasonRunApprovalRequest":{"type":"object","required":["expectedMapVersion","userConfirmed"],"additionalProperties":false,"properties":{"expectedMapVersion":{"type":"integer","minimum":1,"description":"Required mapVersion from the exact SeasonRun response the user reviewed. A stale value returns season_run_state_invalid; reload and review before retrying."},"userConfirmed":{"type":"boolean","enum":[true],"description":"Assert only after the user explicitly approves the complete map and persisted coverage policy."}}},"SeasonRunAdvanceRequest":{"type":"object","additionalProperties":false,"properties":{"episodeId":{"type":"string","description":"Episode to advance. Omit to select the first unpublished episode. Earlier episodes must publish first."},"action":{"type":"string","enum":["advance","replan","retry_coverage","revise","publish"],"default":"advance","description":"`advance` performs one idempotent reconciliation/queue step. `replan`, `retry_coverage`, `revise`, and `publish` are explicit human-approved actions valid only at matching stop states. replan replaces a terminal failed/rejected/superseded episode plan; retry_coverage requeues the same failed current CoverageReport id/version without creating a report. Omit this field to default to advance; any other supplied value returns validation_failed."},"instruction":{"type":"string","minLength":1,"maxLength":5000,"description":"Required with action=revise unless screenplay is supplied. Optional with action=replan to supply the exact correction brief; when omitted, a rejected plan reuses its persisted rejection reason. The replacement StoryPlan retains the instruction in its creativeBrief."},"screenplay":{"type":"string","minLength":1,"maxLength":500000,"description":"For action=revise only: exact replacement screenplay. Bypasses the AI rewrite while retaining versioning, parsing, casting, scoring, and fresh coverage."},"userConfirmed":{"type":"boolean","description":"Required and true for action=replan, action=retry_coverage, action=revise, or action=publish after the user explicitly approves that exact action."}}},"ArtifactScriptReplacementRequest":{"type":"object","required":["screenplay"],"additionalProperties":false,"properties":{"screenplay":{"type":"string","minLength":1,"maxLength":500000,"description":"Complete screenplay content to version without an AI rewrite."},"instruction":{"type":"string","minLength":1,"maxLength":5000,"description":"Optional revision audit note. Defaults to an exact-replacement note."},"narrationPolicy":{"type":"string","enum":["include","suppress"],"description":"Optional narration posture override. Omit to preserve the artifact posture."},"deferMusic":{"type":"boolean","description":"When true, skip automatic soundtrack planning/rendering while preserving the normal SFX detection/bake dispatch. Attach exact clips through the music surface and verify SFX readiness before finalizing."},"deferAudioRender":{"type":"boolean","description":"When true, skip the automatic durable-MP3 render when the replaced read reaches READY. Use this when the operator runs their own post-production and will call POST /artifacts/{artifactId}/finalize later; without it the read is synthesised twice, once for a mix they discard."}}},"CastCanonCharacter":{"type":"object","required":["name"],"additionalProperties":false,"description":"One canon person. Every key the store holds is here, and a save refuses any other key.","properties":{"name":{"type":"string","minLength":1,"maxLength":200,"description":"The name a human reads (\"Ivan Karamazov\"). A save naming a person by any of their names or aliases updates that person."},"aliases":{"type":"array","maxItems":20,"items":{"type":"string","minLength":1,"maxLength":200},"description":"Every other name this person answers to, the script cue included (\"IVAN\"). Declaring the cue here is what makes a script character and a canon person one identity; the cast read warns when one name reaches two people."},"id":{"type":"string","minLength":1,"maxLength":64,"description":"Stable identity assigned on save. Send it with a new `name` to rename a person instead of creating a stranger."},"avatarUrl":{"type":"string","maxLength":2000,"description":"Canonical portrait; https only. Seeded before generation, so the pipeline never renders a replacement."},"avatarPrompt":{"type":"string","maxLength":2000},"bodyFigureUrl":{"type":"string","maxLength":2000,"description":"Canonical full-body figure; https only."},"characterSheetUrl":{"type":"string","maxLength":2000,"description":"Canonical turnaround/model sheet; https only."},"voiceId":{"type":"string","maxLength":200,"description":"The pinned voice every episode and every video uses for this person."},"voiceName":{"type":"string","maxLength":200},"voiceProvider":{"type":"string","maxLength":60},"voiceGender":{"type":"string","maxLength":40},"signatureItems":{"type":"array","maxItems":8,"items":{"type":"string","minLength":1,"maxLength":120},"description":"What only this person wears or carries (\"round wire spectacles\"), at most 8 items of at most 120 characters. A video with two or more people states who owns each item, so it cannot move onto another person; such a video needs signature items for every person in it. The list you send replaces the saved one; an empty list clears it."},"lookDirection":{"type":"string","minLength":1,"maxLength":2000,"description":"How this person should look on camera, in the writer's words (\"22, a merchant's kept woman, striking but not modern, natural skin, period dress\"). Every headshot, portrait candidate, body shot and sheet drawn for them reads it, and it outranks the Series Bible's appearance. `PATCH /story-projects/{projectId}/cast-look` sets or clears it alone."},"age":{"type":"string","minLength":1,"maxLength":100,"description":"The age this person is played at (\"22\"), outranking the Series Bible and an episode roster."}}},"TakeAbandonRequest":{"type":"object","required":["callId","take"],"additionalProperties":false,"properties":{"callId":{"type":"string","minLength":1,"description":"The call whose take to let go (its `callId` from the chapters or beats read)."},"take":{"type":"integer","minimum":1,"description":"The take number (from the call's `takes`)."}}},"TakeAbandonResult":{"type":"object","required":["jobId","surface","callId","take","abandoned","kind","taskId","releasedCredits","note"],"additionalProperties":false,"properties":{"jobId":{"type":"string"},"surface":{"type":"string","enum":["pitch_deck","trailer"]},"callId":{"type":"string"},"take":{"type":"integer","minimum":1},"abandoned":{"type":"boolean","description":"False when the take was not out at a provider: nothing changed."},"kind":{"type":["string","null"],"enum":["render","repair",null],"description":"`render`: a new take out at the video provider; `repair`: a repair out at its host. Null when nothing was let go."},"taskId":{"type":["string","null"],"description":"The provider task it was waiting on, when one had been sent. Any bill for it is ours, never the writer's."},"releasedCredits":{"type":"integer","minimum":0,"description":"The writer's credits given back for it (0 when the take was ours, or nothing was held)."},"note":{"type":"string","description":"Read it to the writer."}}},"RenderCancelResult":{"type":"object","required":["jobId","surface","stopped","previousStatus","status","cancelledJobs","abandoned","released","releasedCredits","keptTakes","note"],"additionalProperties":false,"properties":{"jobId":{"type":"string"},"surface":{"type":"string","enum":["pitch_deck","trailer"]},"stopped":{"type":"boolean","description":"False when nothing was rendering: nothing was changed."},"previousStatus":{"type":"string"},"status":{"type":"string","description":"Where it is now: `review` with what rendered, `draft` with its plan, or as it was."},"cancelledJobs":{"type":"integer","minimum":0,"description":"Render queue jobs cancelled (queued, retrying, or running when the stop was written)."},"abandoned":{"type":"array","description":"Work that was out at a provider: stopped and recorded; never charged to the writer, never resumed.","items":{"type":"object","required":["callId","take","kind","taskId"],"additionalProperties":false,"properties":{"callId":{"type":"string"},"take":{"type":"integer","minimum":1},"kind":{"type":"string","enum":["render","finish"]},"taskId":{"type":["string","null"]}}}},"released":{"type":"array","description":"The writer's reservations given back, by line.","items":{"type":"object","required":["idempotencyKey","credits"],"additionalProperties":false,"properties":{"idempotencyKey":{"type":"string"},"credits":{"type":"integer","minimum":0}}}},"releasedCredits":{"type":"integer","minimum":0},"keptTakes":{"type":"integer","minimum":0,"description":"Accepted takes kept, untouched."},"note":{"type":"string","description":"Read it to the writer."}}},"VideoDeleteResult":{"type":"object","description":"A deleted pitch deck or trailer. Deleting it first stops anything still rendering or finishing, and every credit held for work that had not run comes back to your balance (`releasedCredits`).","required":["jobId","success","releasedCredits","note"],"additionalProperties":false,"properties":{"jobId":{"type":"string"},"success":{"type":"boolean","enum":[true]},"releasedCredits":{"type":"integer","minimum":0,"description":"Credits that were held for work that had not run (its render, plates, finish, text renders and automatic-fix allowance), given back to the writer's balance."},"note":{"type":"string","description":"Read it to the writer: what was deleted and the credits that came back."}}},"CastCanonContent":{"type":"object","additionalProperties":false,"properties":{"characters":{"type":"array","maxItems":200,"items":{"$ref":"#/components/schemas/CastCanonCharacter"}}}},"CastCanonEnvelope":{"type":"object","required":["canon","identity"],"additionalProperties":false,"properties":{"identity":{"$ref":"#/components/schemas/CastIdentityReport"},"canon":{"type":"object","required":["projectId","content","currentVersion","updatedAt"],"additionalProperties":false,"properties":{"projectId":{"type":"string"},"content":{"$ref":"#/components/schemas/CastCanonContent"},"currentVersion":{"type":"integer","minimum":0},"updatedAt":{"type":"string","format":"date-time"}}}}},"CastCanonPatch":{"type":"object","required":["content"],"additionalProperties":false,"properties":{"content":{"$ref":"#/components/schemas/CastCanonContent"},"arrayMode":{"type":"string","enum":["merge","replace"],"description":"`characters` merge strategy: merge by any name or alias a person answers to, or by `id` (default), or replace the whole roster."}}},"CastIdentityReport":{"type":"object","required":["duplicates","noBibleAppearance","unresolvedBibleNames"],"additionalProperties":false,"description":"Identity warnings on a cast read. Read-only: nothing is merged, generated or charged.","properties":{"duplicates":{"type":"array","description":"One name that reaches two or more canon people (a key two people claim, or a person with no name of their own beside another, like FYODOR beside Fyodor Karamazov), or two people sharing one body figure. Fix it by keeping one person and listing the other names as aliases.","items":{"type":"object","required":["kind","character","people","figureUrl","message"],"additionalProperties":false,"properties":{"kind":{"type":"string","enum":["one_character_two_people","shared_figure"]},"character":{"type":["string","null"],"description":"The name that reaches more than one person; null for a shared figure."},"people":{"type":"array","items":{"type":"string"},"description":"The people involved, by display name."},"figureUrl":{"type":["string","null"],"description":"The shared body figure; null for one_character_two_people."},"message":{"type":"string"}}}},"noBibleAppearance":{"type":"array","description":"Characters with no appearance or wardrobe in the Series Bible: a video would describe them by name only.","items":{"type":"object","required":["character","message"],"additionalProperties":false,"properties":{"character":{"type":"string"},"message":{"type":"string"}}}},"unresolvedBibleNames":{"type":"array","description":"Series Bible names that no cast canon person answers to by name or alias (\"Foma Stepanovich\" beside the canon's FOMA), each with the canon people it probably is. A voice or body shot asked for under the Bible name is not found. Fix it by adding the Bible name to that person's `aliases` on the cast canon; nothing is resolved automatically.","items":{"type":"object","required":["bibleName","candidates","message"],"additionalProperties":false,"properties":{"bibleName":{"type":"string","description":"The Series Bible's name for the character."},"candidates":{"type":"array","items":{"type":"string"},"description":"Canon people (display names) one of whose names is wholly contained in the Bible name."},"message":{"type":"string"}}}}}},"EpisodeCharacterDirectionRequest":{"type":"object","required":["scriptUploadId","character"],"additionalProperties":false,"description":"Provide at least one direction field. Narrator direction must not include image prompts, signature items or an identity answer. When the cast canon cannot tell who the character is, nothing is saved: the 409 `cast_precondition_failed` carries `details.reason` (`cast_identity_unresolved` or `cast_identity_declaration_refused`), `details.character`, `details.candidates` and `details.suggestedName`; ask the writer and send `sameAs` or `newPersonName` with their answer.","properties":{"scriptUploadId":{"type":"string","minLength":1},"character":{"type":"string","minLength":1,"maxLength":200},"avatarPrompt":{"type":"string","minLength":1,"maxLength":2000},"figurePrompt":{"type":"string","minLength":1,"maxLength":2000},"age":{"type":"string","maxLength":2000},"gender":{"type":"string","maxLength":2000},"personality":{"type":"string","maxLength":2000},"speechStyle":{"type":"string","maxLength":2000},"backstory":{"type":"string","maxLength":2000},"relationships":{"type":"string","maxLength":2000},"signatureItems":{"type":"array","maxItems":8,"items":{"type":"string","minLength":1,"maxLength":120},"description":"What only this person wears or carries (\"round wire spectacles\"). Saved on the project cast canon for every episode, not on this episode; replaces the saved list, and an empty list clears it."},"sameAs":{"type":"string","minLength":1,"maxLength":200,"description":"The writer says this character IS this cast canon person (a candidate id or name: \"IVAN is Ivan Karamazov\"). The script name becomes their alias, and a stray canon row named by the script name is folded into them, their art standing. Send only on the writer's answer. Not with `newPersonName`."},"newPersonName":{"type":"string","minLength":1,"maxLength":200,"description":"The writer says this character is someone else: saved in the cast canon under this full name, with the script name as an alias. Not with `sameAs`."}}},"EpisodeCharacterDirectionResult":{"type":"object","required":["saved","character","direction","generated"],"properties":{"saved":{"const":true},"character":{"type":"string"},"direction":{"type":"object","properties":{"signatureItems":{"type":"array","items":{"type":"string"}}},"additionalProperties":{"type":"string"}},"generated":{"const":false},"canonPerson":{"type":"object","required":["id","name","aliases"],"description":"The cast canon person written to, present when signature items or an identity answer were saved.","properties":{"id":{"type":["string","null"]},"name":{"type":"string"},"aliases":{"type":"array","items":{"type":"string"}}}},"declared":{"type":"object","required":["kind","absorbed","releasedFrom"],"description":"What the writer's `sameAs` / `newPersonName` answer changed.","properties":{"kind":{"type":"string","enum":["same_as","new_person"]},"absorbed":{"type":"array","items":{"type":"string"},"description":"Stray canon rows folded into the person; their names are now its aliases."},"releasedFrom":{"type":"array","items":{"type":"string"},"description":"People who listed the script name as an alias and no longer do."}}}}},"GenerateProjectCastRequest":{"type":"object","additionalProperties":false,"description":"Pass scriptUploadId for complete paid episode casting. Omit it for project Bible visual casting.","properties":{"scriptUploadId":{"type":"string","description":"Which finalized episode to cast. Omit for project-level Bible visuals."},"characters":{"type":"array","maxItems":200,"items":{"type":"string","minLength":1,"maxLength":200},"description":"Limit to these character names (case-insensitive). Omit to cast everybody castable. Episode casting gives NARRATOR a voice but no images."},"force":{"type":"boolean","description":"Re-draw Face, Body and Sheet even where art already exists (only `images`, when given). This REPLACES pictures the writer may have chosen."},"images":{"type":"array","minItems":1,"maxItems":3,"uniqueItems":true,"items":{"type":"string","enum":["face","body","sheet"]},"description":"Narrow the cast to these pictures and no voices: `face` (the headshot), `body` (the body shot), `sheet` (the character sheet). With `force` they are drawn again: the Cast page's Redraw, a body shot or sheet drawn from the person's chosen headshot and checked against it. Priced per picture; two calls (the quote's `quoteId` confirms). With scriptUploadId it narrows that episode's cast. WITHOUT scriptUploadId it redraws people on the project cast canon, including people no episode names: `body` and `sheet` only (a headshot is redrawn as candidates, POST /cast-portraits/design; `face` is a 400), the chosen headshot is kept, and each picture is written onto the cast canon (a ProjectCastRedrawQuote, then a ProjectCastRedrawQueued; read the cast until `redraw` is complete). A body shot or sheet for a person with no headshot is refused (409 `cast_precondition_failed`, `details.reason: \"cast_face_missing\"`)."},"quoteId":{"type":"string","minLength":1,"description":"Required when confirming episode casting or a picture redraw; returned by its quote."},"confirmed":{"type":"boolean","description":"Omit (or false) to get the price and the plan WITHOUT drawing anything. Pass true only after the writer has seen that plan and said go."}}},"ProjectCastMember":{"type":"object","required":["character","face","reused"],"additionalProperties":false,"properties":{"character":{"type":"string"},"face":{"oneOf":[{"type":"string"},{"type":"null"}],"description":"The headshot, or null when it could not be drawn."},"reused":{"type":"boolean","description":"True when the Face came from an already-rendered portrait rather than a fresh render."},"error":{"type":"string","description":"Present only when this character has no Face; says why."},"artChecks":{"type":"array","description":"The body shot and sheet drawn in this call, each checked against the face: the same person, age, build and wardrobe. A drawing that did not match was drawn once more at our cost (`retaken`) and the closer kept. Never blocks the cast.","items":{"type":"object","required":["slot","status","retaken","scores","mismatches","note"],"additionalProperties":false,"properties":{"slot":{"type":"string","enum":["body","sheet"]},"status":{"type":"string","enum":["match","mismatch","unavailable","skipped"],"description":"`mismatch`: neither draw matched (the closer was kept); `unavailable`: could not be checked; `skipped`: no face to compare with."},"retaken":{"type":"boolean"},"scores":{"oneOf":[{"type":"null"},{"type":"object","required":["samePerson","ageAndBuild","wardrobe"],"additionalProperties":false,"properties":{"samePerson":{"type":"number"},"ageAndBuild":{"type":"number"},"wardrobe":{"type":"number"}}}]},"mismatches":{"type":"array","items":{"type":"string"}},"note":{"type":"string"}}}}}},"ProjectCastQuote":{"type":"object","required":["projectId","scriptUploadId","quotedOnly","credits","balanceAfter","sufficient","costLabel","characters","drawing","alreadyDrawn","faces","images","note"],"additionalProperties":false,"description":"The answer to a call WITHOUT `confirmed`: what would happen, what it costs, what it would leave (`balanceAfter` / `sufficient`), and the fact that it has not happened.","properties":{"projectId":{"type":"string"},"scriptUploadId":{"type":["string","null"],"description":"The named episode, or null for project Bible visuals."},"quoteId":{"type":"string","description":"Episode quote identity required on confirmation."},"voices":{"type":"integer","minimum":0},"quotedOnly":{"type":"boolean","enum":[true]},"credits":{"type":"integer","minimum":0,"description":"Studio Credits required for the quoted casting work."},"balanceAfter":{"type":"integer","description":"Quote only. Studio Credits the writer would have left once this is spent: the balance available now (as Billing shows it) minus the price. Negative means they are that many short."},"sufficient":{"type":"boolean","description":"Quote only. True when the available balance covers the price. False means the writer must top up first (the note names the Billing link) — do not confirm; the reserve would refuse."},"costLabel":{"type":"string"},"characters":{"type":"array","items":{"type":"string"},"description":"Everyone in scope for this call."},"drawing":{"type":"array","items":{"type":"string"},"description":"Who a confirmed call would draw."},"alreadyDrawn":{"type":"array","items":{"type":"string"},"description":"Who already has a Face and would be left alone."},"faces":{"type":"integer","minimum":0},"images":{"type":"integer","minimum":0,"description":"Every picture the run commissions: each Face, plus the Body and Sheet that follow it."},"note":{"type":"string","description":"The same sentence to show the writer, in their terms."}}},"ProjectCastResult":{"type":"object","required":["projectId","scriptUploadId","quotedOnly","members","faces","bodyAndSheet","note"],"additionalProperties":false,"description":"The answer to a call WITH `confirmed: true`.","properties":{"projectId":{"oneOf":[{"type":"string"},{"type":"null"}]},"scriptUploadId":{"type":["string","null"]},"quotedOnly":{"type":"boolean","enum":[false]},"members":{"type":"array","items":{"$ref":"#/components/schemas/ProjectCastMember"}},"faces":{"type":"object","required":["generated","reused","failed"],"additionalProperties":false,"properties":{"generated":{"type":"integer","minimum":0},"reused":{"type":"integer","minimum":0},"failed":{"type":"integer","minimum":0}}},"bodyAndSheet":{"type":"string","enum":["incomplete","complete"],"description":"Whether all required body shots and character sheets finished successfully."},"note":{"type":"string"}}},"ProjectCastEnvelope":{"type":"object","required":["cast"],"additionalProperties":false,"properties":{"cast":{"oneOf":[{"$ref":"#/components/schemas/ProjectCastQuote"},{"$ref":"#/components/schemas/ProjectCastResult"},{"$ref":"#/components/schemas/EpisodeCastQueuedResult"},{"$ref":"#/components/schemas/ProjectCastRedrawQuote"},{"$ref":"#/components/schemas/ProjectCastRedrawQueued"}],"description":"Branch on `quotedOnly`: true means nothing was drawn and this is the price."}}},"EpisodeCastQueuedResult":{"type":"object","required":["projectId","scriptUploadId","quotedOnly","jobId","status","credits","note"],"properties":{"projectId":{"type":"string"},"scriptUploadId":{"type":"string"},"quotedOnly":{"type":"boolean","enum":[false]},"jobId":{"type":"string"},"status":{"type":"string","enum":["queued","running","retrying","complete","failed","cancelled"]},"progressDetail":{"type":["string","null"]},"error":{"type":["string","null"]},"credits":{"type":"integer","minimum":0},"note":{"type":"string"},"pictures":{"type":"array","items":{"$ref":"#/components/schemas/CastJobPicture"},"description":"The pictures this job draws."}}},"ProjectCastRedrawQuote":{"type":"object","required":["projectId","scriptUploadId","quotedOnly","quoteId","credits","balanceAfter","sufficient","costLabel","characters","drawing","alreadyDrawn","faces","images","pictures","note"],"additionalProperties":false,"description":"A project-canon picture redraw (`images` without scriptUploadId), priced and not drawn: confirm with `quoteId`.","properties":{"projectId":{"type":"string"},"scriptUploadId":{"type":"null"},"quotedOnly":{"type":"boolean","enum":[true]},"quoteId":{"type":"string","description":"Required on the confirmed call. A cast that changed since (a headshot, a picture, the drawing model) refuses it: 409 `cast_quote_changed`."},"credits":{"type":"integer","minimum":0,"description":"Studio Credits, per picture at the episode cast rate."},"balanceAfter":{"type":"integer","description":"Quote only. Studio Credits the writer would have left once this is spent: the balance available now (as Billing shows it) minus the price. Negative means they are that many short."},"sufficient":{"type":"boolean","description":"Quote only. True when the available balance covers the price. False means the writer must top up first (the note names the Billing link) — do not confirm; the reserve would refuse."},"costLabel":{"type":"string"},"characters":{"type":"array","items":{"type":"string"},"description":"Everyone in scope, by their canon name."},"drawing":{"type":"array","items":{"type":"string"}},"alreadyDrawn":{"type":"array","items":{"type":"string"},"description":"Who already has those pictures and would be left alone (without `force`)."},"faces":{"type":"integer","enum":[0],"description":"A redraw never draws a headshot."},"images":{"type":"integer","minimum":0},"pictures":{"type":"array","items":{"$ref":"#/components/schemas/CastJobPicture"}},"note":{"type":"string"}}},"ProjectCastRedrawQueued":{"type":"object","required":["projectId","scriptUploadId","quotedOnly","jobId","status","progressDetail","error","credits","pictures","note"],"additionalProperties":false,"description":"A confirmed project-canon picture redraw: reserved and queued. Read the cast (GET without scriptUploadId) until `redraw.status` is complete.","properties":{"projectId":{"type":"string"},"scriptUploadId":{"type":"null"},"quotedOnly":{"type":"boolean","enum":[false]},"jobId":{"type":"string","description":"Empty when there was nothing to redraw."},"status":{"type":"string","enum":["queued","running","complete","failed"]},"progressDetail":{"type":["string","null"]},"error":{"type":["string","null"]},"credits":{"type":"integer","minimum":0},"pictures":{"type":"array","items":{"$ref":"#/components/schemas/CastJobPicture"}},"note":{"type":"string"}}},"ProjectCastRedrawJob":{"type":"object","required":["jobId","status","credits","pictures","progressDetail","error","createdAt"],"description":"The project's newest picture redraw, picture by picture. A picture is charged only once the cast canon holds it.","properties":{"jobId":{"type":"string"},"status":{"type":"string","enum":["queued","running","complete","failed"]},"credits":{"type":"integer","minimum":0,"description":"What the pictures drawn so far were charged."},"pictures":{"type":"array","items":{"type":"object","required":["character","image","castPersonId","status","url","check","error"],"properties":{"character":{"type":"string"},"image":{"type":"string","enum":["body","sheet"]},"castPersonId":{"type":["string","null"]},"status":{"type":"string","enum":["queued","drawn","failed"]},"url":{"type":["string","null"],"description":"The new picture, once the cast canon holds it."},"check":{"type":["object","null"],"description":"What the check against the headshot found (slot, status, retaken, scores, mismatches, note)."},"error":{"type":["string","null"]}}}},"progressDetail":{"type":["string","null"]},"error":{"type":["string","null"]},"createdAt":{"type":"string"}}},"CastJobPicture":{"type":"object","required":["character","image"],"additionalProperties":false,"properties":{"character":{"type":"string","description":"The cast member's cue."},"image":{"type":"string","enum":["face","body","sheet"]}}},"ReadVoiceBehindCanon":{"type":"object","required":["character","person","canonVoice","readVoice"],"additionalProperties":false,"description":"A character the episode's finished read still speaks with another voice than their cast canon voice: the Cast page plays `canonVoice`, the read plays `readVoice`. POST /artifacts/{artifactId}/revoice with this `character` (and no voice) makes them agree; priced, two calls, the old recording stays restorable.","properties":{"character":{"type":"string","description":"The name the read speaks them under (its cue)."},"person":{"type":"string","description":"The cast canon person."},"canonVoice":{"type":"object","required":["voiceId","voiceName"],"properties":{"voiceId":{"type":"string"},"voiceName":{"type":["string","null"]}}},"readVoice":{"type":"object","required":["voiceId","voiceName"],"properties":{"voiceId":{"type":"string"},"voiceName":{"type":["string","null"]}}}}},"EpisodeReadVoices":{"type":"object","required":["artifactId","restorable","behindCanon"],"description":"The episode's finished table read as the Cast page sees it.","properties":{"artifactId":{"type":["string","null"],"description":"The finished read (a `table_read` artifact) a re-voice speaks into; null while the episode has none."},"restorable":{"type":"array","items":{"$ref":"#/components/schemas/RestorableVoice"},"description":"Each character's newest voice change a restore would undo."},"behindCanon":{"type":"array","items":{"$ref":"#/components/schemas/ReadVoiceBehindCanon"}}}},"ProjectCastReadEnvelope":{"type":"object","required":["cast"],"properties":{"cast":{"type":"object","required":["projectId","people","drawn","total"],"properties":{"projectId":{"type":"string"},"people":{"type":"array","items":{"type":"object"}},"drawn":{"type":"integer"},"total":{"type":"integer"},"redraw":{"oneOf":[{"$ref":"#/components/schemas/ProjectCastRedrawJob"},{"type":"null"}],"description":"The project's newest picture redraw of cast canon people (`images` without scriptUploadId), or null."},"episode":{"type":["object","null"],"description":"Returned when scriptUploadId is supplied. Reads saved state without starting preparation or voice playback.","properties":{"job":{"type":["object","null"],"description":"Newest durable casting job or null; `pictures` names what it draws, and a finished redraw's `progressDetail` says what each check against the headshot found."},"readiness":{"$ref":"#/components/schemas/EpisodeCastingReadiness"},"read":{"$ref":"#/components/schemas/EpisodeReadVoices"}}}}}}},"EpisodeCastingReadiness":{"type":"object","required":["ready","castReady","parseReady","members","missingCount","reason"],"properties":{"ready":{"type":"boolean"},"castReady":{"type":"boolean"},"parseReady":{"type":"boolean"},"missingCount":{"type":"integer","minimum":0},"reason":{"type":"string"},"members":{"type":"array","items":{"$ref":"#/components/schemas/EpisodeCastMember"}}}},"EpisodeCastMember":{"type":"object","required":["id","name","displayName","aliases","narrator","face","body","sheet","voiceId","voiceName","voiceProvider","voiceGender","voiceSnippets","signatureItems","missing","ready"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"displayName":{"type":"string"},"aliases":{"type":"array","items":{"type":"string"}},"narrator":{"type":"boolean"},"face":{"type":["string","null"]},"body":{"type":["string","null"]},"sheet":{"type":["string","null"]},"voiceId":{"type":["string","null"]},"voiceName":{"type":["string","null"]},"voiceProvider":{"type":["string","null"]},"voiceGender":{"type":["string","null"],"description":"Gender of the assigned saved voice; never inferred from the character appearance."},"voiceSnippets":{"type":"array","maxItems":3,"items":{"type":"string","maxLength":200},"description":"Actual short spoken lines from this episode for this character, resolved through cue aliases. Narrator snippets contain narration only. Empty when parsed lines are unavailable; reading them does not generate audio."},"signatureItems":{"type":"array","maxItems":8,"items":{"type":"string"},"description":"The person's canon signature items (what only they wear or carry). Empty for the narrator."},"missing":{"type":"array","items":{"type":"string","enum":["face","body","sheet","voice"]}},"ready":{"type":"boolean"}}},"MoodBoardFrame":{"type":"object","required":["id","url"],"additionalProperties":false,"properties":{"id":{"type":"string","maxLength":64,"description":"Stable id for this frame; keeping an id that is already on the board replaces that entry."},"url":{"type":"string","maxLength":2000,"description":"Where the picture lives; https only. Frames are re-hosted, because a provider URL goes dead overnight."},"r2Key":{"type":"string","maxLength":500},"prompt":{"type":"string","maxLength":4000,"description":"What was asked for. A resumed conversation reads these to know the look it is continuing."},"note":{"type":"string","maxLength":1000,"description":"The writer's own words about why this frame is right. Never generated."},"modelId":{"type":"string","maxLength":200},"providerType":{"type":"string","maxLength":60},"seed":{"type":"integer"},"aspectRatio":{"type":"string","maxLength":20},"keptAt":{"type":"string","maxLength":40,"description":"Set by the server when the frame is kept."}}},"MoodBoardContent":{"type":"object","additionalProperties":false,"properties":{"frames":{"type":"array","maxItems":4,"items":{"$ref":"#/components/schemas/MoodBoardFrame"}},"updatedAt":{"type":"string","maxLength":40}}},"MoodBoardCandidateFrame":{"type":"object","required":["id","url"],"additionalProperties":false,"properties":{"id":{"type":"string","maxLength":64},"url":{"type":"string","maxLength":2000},"prompt":{"type":"string","maxLength":4000},"modelId":{"type":"string","maxLength":200},"aspectRatio":{"type":"string","maxLength":20}}},"MoodBoardRenderRecord":{"type":"object","required":["chatToolJobId","status","count","error"],"additionalProperties":false,"properties":{"chatToolJobId":{"type":"string"},"status":{"type":"string","description":"pending | completed | failed."},"count":{"type":"integer","nullable":true,"description":"How many frames the batch asked for."},"error":{"type":"string","nullable":true}}},"MoodBoardEnvelope":{"type":"object","required":["moodBoard"],"additionalProperties":false,"properties":{"moodBoard":{"type":"object","required":["projectId","content","currentVersion","updatedAt"],"additionalProperties":false,"properties":{"projectId":{"type":"string"},"content":{"$ref":"#/components/schemas/MoodBoardContent"},"currentVersion":{"type":"integer","minimum":0},"updatedAt":{"type":"string","format":"date-time"}}},"candidates":{"type":"array","items":{"$ref":"#/components/schemas/MoodBoardCandidateFrame"}},"renders":{"type":"array","items":{"$ref":"#/components/schemas/MoodBoardRenderRecord"}}}},"MoodBoardRenderRequest":{"type":"object","required":["brief"],"additionalProperties":false,"properties":{"brief":{"type":"string","minLength":1,"maxLength":2000,"description":"What this batch should show, in the writer's terms — a place, a light, a texture, a weather."},"count":{"type":"integer","minimum":1,"maximum":4,"description":"How many candidates to draw. Defaults to 2; the cap is the board's cap. EACH ONE COSTS."},"aspectRatio":{"type":"string","maxLength":20,"description":"Frame shape, e.g. \"16:9\". Defaults to the show's widescreen."},"confirmed":{"type":"boolean","description":"Omit (or false) to PRICE the batch and render nothing. True only after the writer has seen that price and said go."}}},"MoodBoardRenderEnvelope":{"type":"object","required":["quotedOnly","projectId","count","brief","modelId","credits"],"properties":{"quotedOnly":{"type":"boolean","description":"true when this call only PRICED the batch: nothing reserved, nothing queued."},"projectId":{"type":"string"},"count":{"type":"integer","minimum":1,"maximum":4},"brief":{"type":"string"},"modelId":{"type":"string","description":"The bound image model the frames would come from."},"credits":{"type":"integer","minimum":0},"costLabel":{"type":"string","nullable":true,"description":"Quote only."},"balanceAfter":{"type":"integer","description":"Quote only. Studio Credits the writer would have left once this is spent: the balance available now (as Billing shows it) minus the price. Negative means they are that many short."},"sufficient":{"type":"boolean","description":"Quote only. True when the available balance covers the price. False means the writer must top up first (the note names the Billing link) — do not confirm; the reserve would refuse."},"note":{"type":"string","description":"Quote only — the price, what it would leave (or how short the writer is, with the top-up link), and that unkept frames still cost. Show it to the writer."},"queued":{"type":"boolean","description":"Confirmed calls only."},"chatToolJobId":{"type":"string","description":"Confirmed calls only. Poll getProjectMoodBoard until this batch leaves `renders` and its frames appear in `candidates`."},"message":{"type":"string","description":"Confirmed calls only."}}},"LocationPlate":{"type":"object","required":["plateKey","rung","pool","status","label","imageUrl","description","judgement","verdict","note","remake","canRemake","croppedInto"],"properties":{"plateKey":{"type":"string","description":"The key a remake takes."},"rung":{"type":"string","enum":["wide","medium","close"]},"pool":{"type":"string","enum":["landscape","portrait"]},"status":{"type":"string","description":"`ready`, `draft`, `failed`, or `missing` — made when a video renders; a read never makes one."},"label":{"type":"string","nullable":true},"imageUrl":{"type":"string","nullable":true},"description":{"type":"string","nullable":true,"description":"The room as this plate shows it, in one sentence, written when it was accepted. The render declares the plate with it and stages against it."},"judgement":{"type":"object","nullable":true,"additionalProperties":true,"description":"The capped round's record: `acceptance`, `retries`, `maxRetries`, `note`, `pendingRound`. Advisory, never a gate."},"verdict":{"type":"object","nullable":true,"additionalProperties":true,"description":"The accepted attempt's verdict (`singleFrame`, `humanFigures`, `framing`, `reason`). Advisory."},"note":{"type":"string","description":"One line for the writer."},"remake":{"type":"object","nullable":true,"additionalProperties":true,"description":"The latest remake: `chatToolJobId`, `status` (pending | completed | failed), `requestedAt`, `note`, `error`."},"canRemake":{"type":"boolean","description":"True when a priced remake can start now. There is no approve action."},"croppedInto":{"type":"array","items":{"type":"string"},"description":"The ready plates of this room made from crops of this one (its medium, close and vertical plates down the crop chain). A remake replaces this plate only; these keep the room as it is now."}}},"LocationPlatesEnvelope":{"type":"object","required":["scriptUploadId","rooms","duplicates","counts","remake","note"],"properties":{"artifactId":{"type":"string"},"scriptUploadId":{"type":"string"},"rooms":{"type":"array","items":{"type":"object","required":["room","label","variants"],"properties":{"room":{"type":"string","description":"The normalized room, without its light."},"label":{"type":"string"},"variants":{"type":"array","items":{"type":"object","required":["key","lighting","label","exterior","storedKeys","description","plates"],"properties":{"key":{"type":"string","description":"The room in one light, e.g. `INT_KARAMAZOV_HOUSE_ENTRANCE_HALL@NIGHT`. DAY is the default and is not written."},"lighting":{"type":"string","enum":["DAY","NIGHT","EVENING","MORNING"]},"label":{"type":"string"},"exterior":{"type":"boolean"},"storedKeys":{"type":"array","items":{"type":"string"},"description":"The keys this room-in-this-light is stored under; more than one is a duplicate."},"description":{"type":"string","nullable":true},"plates":{"type":"array","items":{"$ref":"#/components/schemas/LocationPlate"}}}}}}}},"duplicates":{"type":"array","items":{"type":"object","required":["key","storedKeys","note"],"properties":{"key":{"type":"string"},"storedKeys":{"type":"array","items":{"type":"string"}},"note":{"type":"string"}}}},"counts":{"type":"object","required":["rooms","variants","plates","ready","missing","duplicates"],"properties":{"rooms":{"type":"integer","minimum":0},"variants":{"type":"integer","minimum":0},"plates":{"type":"integer","minimum":0},"ready":{"type":"integer","minimum":0},"missing":{"type":"integer","minimum":0},"duplicates":{"type":"integer","minimum":0}}},"remake":{"type":"object","required":["creditsPerAttempt","maxAttempts","maxCredits"],"properties":{"creditsPerAttempt":{"type":"integer","minimum":0},"maxAttempts":{"type":"integer","minimum":1},"maxCredits":{"type":"integer","minimum":0}}},"note":{"type":"string"}}},"LocationPlateRemakeRequest":{"type":"object","additionalProperties":false,"properties":{"confirmed":{"type":"boolean","description":"Omit (or false) to PRICE the remake and make nothing. True only after the writer has seen that price and said go."}}},"LocationPlateRemakeEnvelope":{"type":"object","required":["quotedOnly","scriptUploadId","plateKey","maxAttempts","credits"],"properties":{"quotedOnly":{"type":"boolean","description":"true when this call only PRICED the remake: nothing reserved, nothing queued."},"artifactId":{"type":"string"},"scriptUploadId":{"type":"string"},"plateKey":{"type":"string"},"maxAttempts":{"type":"integer","minimum":1,"description":"The most attempts the capped round can take."},"creditsPerAttempt":{"type":"integer","minimum":0,"description":"Quote only."},"credits":{"type":"integer","minimum":0,"description":"The most the remake can cost: every attempt the cap allows. Only the attempts made are charged."},"costLabel":{"type":"string","nullable":true,"description":"Quote only."},"balanceAfter":{"type":"integer","description":"Quote only. Studio Credits the writer would have left once this is spent: the balance available now (as Billing shows it) minus the price. Negative means they are that many short."},"sufficient":{"type":"boolean","description":"Quote only. True when the available balance covers the price. False means the writer must top up first (the note names the Billing link) — do not confirm; the reserve would refuse."},"note":{"type":"string","description":"Quote only — the price, what it would leave (or how short the writer is, with the top-up link), and that only the attempts made are charged. Show it to the writer."},"caveat":{"type":"string","description":"Quote only — the remake's own sentence: the price covers every attempt the cap allows, only the attempts made are charged, the new plate replaces the old one with no approval step, and which plates cropped from this one it leaves as they are."},"croppedInto":{"type":"array","items":{"type":"string"},"description":"Quote only — the ready plates made from crops of this one. A remake leaves them as they are, so they can differ from the new plate until they are remade too."},"queued":{"type":"boolean","description":"Confirmed calls only."},"chatToolJobId":{"type":"string","description":"Confirmed calls only. Read the location plates until this plate's `remake` leaves `pending`."},"message":{"type":"string","description":"Confirmed calls only."}}},"MoodBoardFrameKeepRequest":{"type":"object","required":["frame"],"additionalProperties":false,"properties":{"frame":{"$ref":"#/components/schemas/MoodBoardFrame"}}},"StorySource":{"type":"object","required":["id","projectId","type","label","uri","status","retentionPolicy","byteSize","tokenEstimate","contentHash","extractedTextPreview","extractedObjectKey","rawObjectKey","failureCode","failureMessage","producer","externalId","metadata","originatedAt","digest","digestStatus","digestFailureCode","digestFailureMessage","digestedAt","createdAt","updatedAt","extractedAt"],"additionalProperties":false,"properties":{"id":{"type":"string"},"projectId":{"type":"string"},"type":{"type":"string","enum":["TEXT","MARKDOWN","URL","PDF"]},"label":{"oneOf":[{"type":"string"},{"type":"null"}]},"uri":{"oneOf":[{"type":"string"},{"type":"null"}]},"status":{"$ref":"#/components/schemas/SourceStatus"},"retentionPolicy":{"$ref":"#/components/schemas/RetentionPolicy"},"byteSize":{"oneOf":[{"type":"integer","minimum":0},{"type":"null"}]},"tokenEstimate":{"oneOf":[{"type":"integer","minimum":0},{"type":"null"}]},"contentHash":{"oneOf":[{"type":"string"},{"type":"null"}]},"extractedTextPreview":{"oneOf":[{"type":"string"},{"type":"null"}]},"extractedObjectKey":{"oneOf":[{"type":"string"},{"type":"null"}]},"rawObjectKey":{"oneOf":[{"type":"string"},{"type":"null"}]},"failureCode":{"oneOf":[{"type":"string"},{"type":"null"}]},"failureMessage":{"oneOf":[{"type":"string"},{"type":"null"}]},"producer":{"oneOf":[{"type":"string"},{"type":"null"}],"description":"Who made this item (`hackernewsradio`, or `integration:<integrationId>` for an item kept from a connected product); null for material a person attached. Producer items are episode material, not canon: Bible generation never reads them, and a season pins one only when it was curated into that season."},"externalId":{"oneOf":[{"type":"string"},{"type":"null"}],"description":"The producer's id for this item. Null once the source is deleted or FAILED, which frees the item to be sent again."},"metadata":{"oneOf":[{"type":"object","additionalProperties":true},{"type":"null"}]},"originatedAt":{"oneOf":[{"type":"string","format":"date-time"},{"type":"null"}],"description":"When the source material itself was made (`metadata.originatedAt`), e.g. a thread's posting time; null when the producer did not say. A project with `seasonCadence: \"monthly\"` files the episode made from this source under that month, falling back to `createdAt`."},"digest":{"oneOf":[{"$ref":"#/components/schemas/StorySourceDigest"},{"type":"null"}]},"digestStatus":{"$ref":"#/components/schemas/SourceDigestStatus"},"digestFailureCode":{"oneOf":[{"type":"string"},{"type":"null"}]},"digestFailureMessage":{"oneOf":[{"type":"string"},{"type":"null"}]},"digestedAt":{"oneOf":[{"type":"string","format":"date-time"},{"type":"null"}]},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"extractedAt":{"oneOf":[{"type":"string","format":"date-time"},{"type":"null"}]}}},"StorySourceEnvelope":{"type":"object","required":["source"],"additionalProperties":false,"properties":{"source":{"$ref":"#/components/schemas/StorySource"}}},"StorySourceCreateResult":{"type":"object","required":["source","deduplicated"],"additionalProperties":false,"properties":{"source":{"$ref":"#/components/schemas/StorySource"},"deduplicated":{"type":"boolean","description":"True when the project already held a live source for this `producer` + `externalId`: that source is returned and nothing new is stored."}}},"StorySourceList":{"type":"object","required":["sources","nextCursor"],"additionalProperties":false,"properties":{"sources":{"type":"array","items":{"$ref":"#/components/schemas/StorySource"}},"nextCursor":{"oneOf":[{"type":"string"},{"type":"null"}]}}},"StorySourceCreate":{"type":"object","required":["type"],"additionalProperties":false,"description":"Inline `text` and `markdown` require `content`. `url` and `pdf` require `uri` and extract asynchronously — poll the source until `status: READY` or `FAILED`. PDF sources require the upstream Content-Type to be `application/pdf`; `url` sources require a text-y Content-Type.","properties":{"type":{"$ref":"#/components/schemas/SourceType"},"content":{"type":"string","description":"Inline payload for `text` and `markdown` types. Max 4,000,000 bytes (UTF-8). Retained text is normally capped at 1,500,000 characters."},"uri":{"type":"string","format":"uri","description":"URL for `url` and `pdf` sources. Must be http or https; SSRF-blocked addresses (private, link-local, cloud metadata, multicast) are rejected."},"label":{"type":"string","maxLength":200},"retentionPolicy":{"$ref":"#/components/schemas/RetentionPolicy"},"producer":{"type":"string","maxLength":64,"pattern":"^[a-z0-9][a-z0-9._-]*$","description":"For a program that sends one source per item it covers: the producer's name (e.g. `hackernewsradio`). Send with `externalId`, or neither. The `integration:` prefix is reserved. A producer's items are episode material, not canon: Bible generation never reads them, and a season pins one only when it was curated into that season (items sent here never are). A full-context producer item is not digested: `digestStatus: SKIPPED` (\"Used in full by its episode plan.\") is terminal and by design."},"externalId":{"type":"string","minLength":1,"maxLength":256,"description":"The producer's id for this item (for `hackernewsradio`, the numeric Hacker News story id). A project holds one live source per `producer` + `externalId`: a repeat returns that source with `deduplicated: true`, including one still `PENDING`/`EXTRACTING`. Delete the source to send the item again (and delete one that never becomes READY)."},"originatedAt":{"type":"string","format":"date-time","description":"When the source material itself was made, not when it is attached: a thread's posting time, an article's publication time. ISO 8601; at most a day in the future. Stored as `metadata.originatedAt` and returned as `originatedAt`. A project with `seasonCadence: \"monthly\"` files the episode made from this source under that month (UTC); without it, the source's `createdAt` is used."},"metadata":{"type":"object","additionalProperties":true,"description":"Freeform source metadata. Reserved policy: `sourceContextMode: \"full\"` makes both StoryPlan generation and final artifact writing consume the complete hash-verified retained text. Full mode requires EXTRACTED_ONLY or STORE_ORIGINAL retention and fails instead of clipping above 1,500,000 characters. `sourceProducer` is refused: send the top-level `producer` and `externalId`. `originatedAt` here is held to the same rule as the top-level field, and must agree with it when both are sent."}}},"DeletionAck":{"type":"object","required":["id","deletedAt"],"additionalProperties":false,"properties":{"id":{"type":"string"},"deletedAt":{"type":"string","format":"date-time"}}},"StoryPlanStatus":{"type":"string","enum":["PENDING","RUNNING","REQUIRES_APPROVAL","READY","APPROVED","REJECTED","FAILED","SUPERSEDED"]},"StoryArtifactType":{"type":"string","enum":["pitch_deck","table_read","trailer"]},"StoryArtifactRequest":{"type":"object","required":["type"],"additionalProperties":false,"properties":{"type":{"$ref":"#/components/schemas/StoryArtifactType"},"mode":{"type":"string","maxLength":80},"deckMode":{"type":"string","enum":["pitch","showcase"],"description":"For pitch_deck: what the deck is for. \"pitch\" (default) tells the approved story to a buyer and is judged as a pitch. \"showcase\" is an epic sizzle of the show's world, shown as its story: built on its intent (what the show is about, the one question it leaves open, an arc that builds to a turn), carried by a narrator and the characters' own lines, with world chapters (vistas, crowds, life, scale) that carry the theme interleaved with chapters of the characters in that world doing things; it is judged as a showcase, not as a pitch of the approved story. Every deck, in either mode, is planned from its intent. Set when the deck is planned; it never changes after."},"showcaseCharacters":{"type":"array","minItems":1,"maxItems":12,"items":{"type":"string","minLength":1,"maxLength":120},"description":"For a showcase deck only: the characters it must feature, by their exact canon names, in the order they first appear (at most 12). Each is on camera in at least one chapter, among the world chapters. Omit to let the planner choose from the cast."},"shotDensity":{"type":"string","enum":["held","standard","dense"],"description":"For pitch_deck and trailer: how many angles (cuts) each rendered shot of a chapter or beat carries. Cuts inside a rendered shot are free, so density never changes the price, the plan or a line. \"held\" lets one long camera move carry a shot (1-2 angles); \"standard\" (default) cuts 2-3 angles into each shot where its seconds allow; \"dense\" cuts 3-5. No angle is planned under about 1.5 seconds, and a spoken line keeps its own seconds. Set when the deck or trailer is planned."},"durationSeconds":{"type":"integer","minimum":5,"description":"Target duration in seconds. Video artifacts (trailer, pitch_deck) are planned at CUT density, not minimum-call density: round(target / mean) units of 4-30s each, with a ~5s mean trailer beat and a ~9s mean pitch-deck chapter (one render call, at most 10s) — so a 90s trailer plans about 18 beats and a 180s deck about 20 chapters. Each type carries its own ceiling: trailer 90s, pitch_deck 180s. Audio runtimes (table_read) are not unit-rendered and may run longer."},"aspectRatio":{"type":"string","pattern":"^\\d+:\\d+$","description":"For a trailer, the shape the whole cut renders at: 16:9 (default), 9:16 or 1:1. Its shots, text composites and stitched master all follow it."},"videoModelResolution":{"type":"string","enum":["480p","720p","1080p"],"description":"For a trailer, the class the finished cut is DELIVERED at: 720p or 1080p (default 720p; 480p delivers the default). Every shot renders at 480p first; the takes you keep are upscaled to this class when the trailer is finished, never re-rendered. Every class renders its first pass at 480p, finished by upscaling: about $0.068/s at 480p, $0.078/s at 720p and $0.088/s at 1080p (the 480p first pass plus $0.01/s or $0.02/s to upscale; confirmed rates), so a 90-second cut comes to about $6.12, $7.02 or $7.92."},"includePdf":{"type":"boolean","description":"The deck's companion PDF, laid out from poster frames of the finished takes. Off unless true: decks ship without a PDF by default; render_pitch_deck_pdf renders one afterwards."},"narrationPolicy":{"type":"string","enum":["auto","include","suppress"],"description":"For table_read: auto lets the planner decide; include requests a narrator; suppress makes the read speaker-only. This is a user/plan choice, not tied to mode."},"deferMusic":{"type":"boolean","description":"For table_read: skip automatic soundtrack planning/rendering (SFX detection is unaffected). Use when you supply your own beds — attach them through POST /artifacts/{id}/music after the artifact exists. The manifest reports audio.adaptiveMusic=false with audio.musicFailure=null, since no music was requested rather than attempted and failed."},"deferSfx":{"type":"boolean","description":"For table_read: never SYNTHESISE a sound effect. Cues are still detected and placed, but each may only resolve to an effect the SFX bank already holds; anything with no match is left silent for you to fill via POST /artifacts/{id}/sfx. Use when the show has a fixed sound palette, so a one-off effect is never rendered, banked, or billed."},"deferAudioRender":{"type":"boolean","description":"For table_read: skip the automatic durable-MP3 render when the read reaches READY. Use when you run your own post-production — without it the whole read is synthesised once on ready, for a mix you then invalidate, and again on your finalize. You must POST /artifacts/{artifactId}/finalize yourself; until then `audio.recordingUrl` stays null and share players show a preparing state."},"coverageTarget":{"type":"number","minimum":1,"maximum":10,"description":"For table_read: minimum development-coverage score (out of 10) the screenplay must reach BEFORE the read is performed. Defaults to 8. Below the bar the script is rewritten against the coverage notes and re-scored, so parse/casting/voices/soundtrack/mix are never spent on a draft that did not clear it. Progress is reported as stage `coverage_gate`."},"coverageMaxRewrites":{"type":"integer","minimum":0,"maximum":6,"description":"For table_read: how many coverage-driven rewrites to attempt before performing the best draft. Defaults to 3. Set 0 to score without rewriting. Bounded because coverage scores are not monotonic — a rewrite can come back lower — so the best draft is kept rather than the last."},"punchUp":{"type":"boolean","description":"For table_read on a job that writes its screenplay: true runs a punch-up pass over the finished draft before it is scored or performed, one scene at a time. Every exchange is made to land, bits are topped, the register is brought up to what the notes and the Series Bible ask for (swearing included when they ask for it), and speeches, explained jokes and dead air are cut, at the same length or shorter. A scene keeps its punch-up only if every verbatim quote survives, it adds no neverSay term, it keeps its slugline, it adds no speaker and it grows by no more than 15%. A scene that only ran long is asked once to cut back to its length; any other failure, or a second miss, and the written scene stands. Default false. Not combined with scriptUploadId."},"neverSay":{"type":"array","minItems":1,"maxItems":20,"items":{"type":"string","minLength":1,"maxLength":40},"description":"For table_read with punchUp: up to 20 words or phrases (each up to 40 characters) the punch-up must never introduce, matched as whole words, ignoring case. These are a show's hard lines, e.g. [\"goddamn\", \"Jesus\", \"Christ\"]. A punched scene that adds one keeps its written version instead. Requires punchUp."},"firstLineClean":{"type":"boolean","description":"For table_read with punchUp: true keeps the episode's first spoken line clean. If the punched opening scene's first spoken line swears and the written one did not, that scene keeps its written version (no retry). The writer's own first line is never changed. Requires punchUp."},"scopeKind":{"type":"string","enum":["episode","season","project"],"description":"For pitch_deck and trailer: \"episode\" (default, one installment), \"season\" (a whole season), or \"project\" (the whole project across all episodes)."},"projectId":{"type":"string","description":"For season/project pitch decks and trailers: the StoryProject id (defaults to the plan's project)."},"seasonNumber":{"type":"integer","minimum":1,"description":"For season pitch decks and trailers: which season number."},"voiceMap":{"type":"object","minProperties":1,"maxProperties":100,"propertyNames":{"type":"string","minLength":1,"maxLength":200},"description":"For table_read: a complete character-to-voice map applied before generation to skip automatic voice assignment. Use saved voice IDs from GET /voices. Each value requires voiceId; voiceName, gender, and provider are optional hints and authoritative saved-voice metadata wins.","additionalProperties":{"type":"object","required":["voiceId"],"additionalProperties":false,"properties":{"voiceId":{"type":"string","minLength":1,"maxLength":200},"voiceName":{"type":"string","minLength":1,"maxLength":200},"gender":{"type":"string","minLength":1,"maxLength":80},"provider":{"type":"string","enum":["cartesia","elevenlabs","openai","deepgram","hume"]}}}},"scriptUploadId":{"type":"string","minLength":1,"maxLength":200,"description":"For table_read on a job: perform this finalized, cast-ready screenplay version (a ScriptUpload id in the plan's project) as written, instead of writing a new one from the plan. Its episode cast must be complete — every voice and, unless the project is audio-only, every headshot, body shot and sheet — or the job is refused before anything is created with 409 `cast_precondition_failed` (`details.reason`: `script_not_finalized` or `script_not_cast_ready`); a screenplay outside the project is 404 `script_not_found`. No coverage rewrite runs and no second coverage report is queued. Priced by its length the way Prepare Table Read prices a read (the table-read rate for 60 pages or fewer, or for over 60); the job's quote shows which. Charged once per screenplay version, both ways: a screenplay whose read is already paid for (prepared in the Writing Lab with POST /scripts/{scriptUploadId}/table-read/prepare, or held by another job) is quoted at 0 for the read and nothing is reserved, and a read the job holds is one a later prepare charges nothing for. Not combined with voiceMap, notes, durationSeconds, coverageTarget, coverageMaxRewrites, punchUp or neverSay."},"notes":{"type":"string","maxLength":5000}}},"StoryPlanTarget":{"type":"object","required":["audience","objective","outcome"],"additionalProperties":false,"properties":{"audience":{"type":"string","maxLength":200},"objective":{"type":"string","maxLength":400},"outcome":{"type":"string","maxLength":400},"tone":{"type":"string","maxLength":120},"industry":{"type":"string","maxLength":120},"distributionContext":{"type":"string","maxLength":300}}},"StoryPlanStyleConstraints":{"type":"object","additionalProperties":false,"properties":{"preferredVisualStyle":{"type":"string","maxLength":400},"forbiddenVisuals":{"type":"array","maxItems":10,"items":{"type":"string","maxLength":160}},"voicePreference":{"type":"string","maxLength":160},"musicPolicy":{"type":"string","maxLength":300},"brandSafety":{"type":"array","maxItems":10,"items":{"type":"string","maxLength":160}}}},"StoryPlanCreativeBrief":{"type":"object","additionalProperties":false,"description":"Optional Storyteller/script brief used before table-read planning. For table_read plans, capture the audience, genre, page target, cast, music, and SFX basics here so the generated scriptBlueprint can be reviewed before generation.","properties":{"projectFormat":{"type":"string","enum":["standalone","feature","tv_series","limited_series","short_form_series","audio_series","other"]},"installmentLabel":{"type":"string","maxLength":160},"installmentNumber":{"type":"integer","minimum":1,"maximum":999},"seriesContext":{"type":"string","maxLength":1200},"genre":{"type":"string","maxLength":160},"audience":{"type":"string","maxLength":240},"writingStyle":{"type":"string","maxLength":600},"comps":{"type":"array","maxItems":8,"items":{"type":"string","maxLength":160}},"pageTarget":{"type":"integer","minimum":1},"castNotes":{"type":"string","maxLength":1000},"musicStyle":{"type":"string","maxLength":500},"sfxPolicy":{"type":"string","maxLength":500},"replanInstruction":{"type":"string","minLength":1,"maxLength":5000,"description":"Exact correction brief carried forward from a terminal prior planning attempt."},"mustKnowBeforeWriting":{"type":"array","maxItems":12,"items":{"type":"string","maxLength":220}}}},"StoryPlanCreate":{"type":"object","required":["target","artifactRequests"],"additionalProperties":false,"description":"Inputs for a new StoryPlan. `sourceIds` are validated as belonging to the project and having status READY. `autoApprove: true` is only allowed for non-table-read plans; table_read plans require review/approval of the generated scriptBlueprint.","properties":{"title":{"type":"string","maxLength":200},"target":{"$ref":"#/components/schemas/StoryPlanTarget"},"artifactRequests":{"type":"array","minItems":1,"maxItems":6,"items":{"$ref":"#/components/schemas/StoryArtifactRequest"}},"styleConstraints":{"$ref":"#/components/schemas/StoryPlanStyleConstraints"},"creativeBrief":{"$ref":"#/components/schemas/StoryPlanCreativeBrief"},"sourceIds":{"type":"array","maxItems":20,"items":{"type":"string"}},"autoApprove":{"type":"boolean"}}},"StoryPlanApprovalRequest":{"type":"object","required":[],"additionalProperties":false,"description":"Mandatory human confirmation for approving the exact generated StoryPlan the user reviewed. The one exception: the API key that holds a show's standing approval may omit it for a table-read plan in that show's project created after the grant (see PublishingStandingApproval). Any other caller that omits it gets 400 `validation_failed`.","properties":{"userConfirmed":{"type":"boolean","enum":[true],"description":"Assert only after the user explicitly reviews and approves this exact StoryPlan."}}},"StoryPlanQuoteLineItem":{"type":"object","required":["kind","label","credits"],"additionalProperties":false,"properties":{"kind":{"type":"string","enum":["planning","source_digest","artifact","addon"]},"label":{"type":"string"},"artifactType":{"$ref":"#/components/schemas/StoryArtifactType"},"opKey":{"oneOf":[{"type":"string"},{"type":"null"}]},"credits":{"type":"integer","minimum":0},"note":{"type":"string"}}},"StoryPlanQuote":{"type":"object","required":["currency","total","lineItems","notes","computedAt"],"additionalProperties":false,"properties":{"currency":{"type":"string","const":"studio_credits"},"total":{"type":"integer","minimum":0},"lineItems":{"type":"array","items":{"$ref":"#/components/schemas/StoryPlanQuoteLineItem"}},"notes":{"type":"array","items":{"type":"string"},"description":"When `balanceAfter` is present and the price is above zero, the first note is the price and what it would leave (or how short the writer is, with the top-up link)."},"computedAt":{"type":"string","format":"date-time"},"balanceAfter":{"type":"integer","description":"StoryPlan reads only, and only while the plan is still a price before a spend: status READY or REQUIRES_APPROVAL, or APPROVED with no live StoryJob (none, or only FAILED/CANCELED ones), read by a caller holding `credits:read`. Absent otherwise: once create_job exists it has reserved `total`, so the plan is paid for; PENDING/RUNNING quotes are preliminary; REJECTED/SUPERSEDED/FAILED plans will not be bought; and never on a StoryJob's recorded quote. A live balance, never stored. Studio Credits the writer would have left once `total` is spent: the balance available now (as Billing shows it) minus `total`. Negative means they are that many short."},"sufficient":{"type":"boolean","description":"Present exactly when `balanceAfter` is. True when the available balance covers `total`. False means the writer must top up before creating the job; say so."}}},"StoryPlanBeatRole":{"type":"string","enum":["hook","context","conflict","proof","turn","payoff","cta"]},"StoryPlanBeat":{"type":"object","required":["id","role","title"],"additionalProperties":false,"properties":{"id":{"type":"string","maxLength":80},"role":{"$ref":"#/components/schemas/StoryPlanBeatRole"},"title":{"type":"string","maxLength":200},"narration":{"type":"string","maxLength":1200},"visualIntent":{"type":"string","maxLength":800},"evidenceIds":{"type":"array","maxItems":10,"items":{"type":"string","maxLength":120}},"estimatedSeconds":{"type":"integer","minimum":1,"description":"Planned beat duration. Long-form audio beats may exceed 120 seconds; table-read beats must sum to the requested table-read duration."}}},"StoryPlanScriptBlueprint":{"type":"object","required":["pageTarget","genre","audience","writingStyle","outline","cast","approvalSummary"],"additionalProperties":false,"description":"Generated for table_read plans and intended for review before approving the plan. The job runner requires this field for table_read generation.","properties":{"projectFormat":{"type":"string","enum":["standalone","feature","tv_series","limited_series","short_form_series","audio_series","other"]},"installmentLabel":{"type":"string"},"pageTarget":{"type":"integer","minimum":1},"genre":{"type":"string"},"audience":{"type":"string"},"writingStyle":{"type":"string"},"narrationPolicy":{"type":"string","enum":["auto","include","suppress"],"description":"Narration posture for this read. `suppress` means no NARRATOR voice/cue; `include` means a narrator is expected; `auto` lets the plan infer from the format and cast."},"outline":{"type":"array","minItems":1,"items":{"type":"object","required":["sceneId","heading","dramaticPurpose","storyAction"],"properties":{"sceneId":{"type":"string"},"heading":{"type":"string"},"pageBudget":{"type":"number","minimum":0.25,"multipleOf":0.25,"description":"Pages allocated to the scene, in quarter pages. The budgets always sum to `pageTarget`: the planner's proportions are rescaled onto it, so an outline with more scenes than pages gets fractional scenes rather than a refusal. Long-form audio scenes are not capped at 30 pages."},"dramaticPurpose":{"type":"string"},"storyAction":{"type":"string"},"sourceEvidenceIds":{"type":"array","maxItems":10,"items":{"type":"string"}}}}},"cast":{"type":"array","minItems":1,"items":{"type":"object","required":["name","role","function"],"properties":{"name":{"type":"string"},"role":{"type":"string"},"function":{"type":"string"},"voiceDirection":{"type":"string"}}}},"audio":{"type":"object","properties":{"musicStyle":{"type":"string"},"sfxMoments":{"type":"array","items":{"type":"object","required":["sceneId","label","description"],"properties":{"sceneId":{"type":"string"},"label":{"type":"string"},"description":{"type":"string"}}}}}},"plannedNextInstallments":{"type":"array","maxItems":10,"items":{"type":"object","required":["label","premise"],"properties":{"label":{"type":"string"},"premise":{"type":"string"}}}},"approvalSummary":{"type":"string"}}},"StoryPlanEnvelope":{"type":"object","required":["version","projectId","title","target","sourceDigest","thesis","journey","visualSystem","artifacts"],"additionalProperties":false,"description":"Canonical source-grounded plan. Returned inside `plan.plan` on a StoryPlanEnvelopeResponse.","properties":{"version":{"type":"integer","const":1},"projectId":{"type":"string"},"title":{"type":"string","maxLength":200},"target":{"$ref":"#/components/schemas/StoryPlanTarget"},"sourceDigest":{"type":"object","required":["summary","sourceIds","evidence","gaps","rightsNotes"],"properties":{"summary":{"type":"string"},"sourceIds":{"type":"array","items":{"type":"string"},"maxItems":20},"evidence":{"type":"array","maxItems":40,"items":{"type":"object","required":["sourceId","claim","support","confidence"],"properties":{"sourceId":{"type":"string"},"claim":{"type":"string"},"support":{"type":"string"},"confidence":{"type":"string","enum":["low","medium","high"]}}}},"gaps":{"type":"array","maxItems":15,"items":{"type":"string"}},"rightsNotes":{"type":"array","maxItems":10,"items":{"type":"string"}}}},"thesis":{"type":"object","required":["oneLine","supportingClaims","avoidClaims"],"properties":{"oneLine":{"type":"string","maxLength":300},"supportingClaims":{"type":"array","maxItems":10,"items":{"type":"string"}},"avoidClaims":{"type":"array","maxItems":10,"items":{"type":"string"}}}},"journey":{"type":"object","required":["beats"],"properties":{"beats":{"type":"array","minItems":3,"description":"Requested-work-driven journey beats; long-form audio plans are not truncated to a fixed beat count.","items":{"$ref":"#/components/schemas/StoryPlanBeat"}}}},"visualSystem":{"type":"object","required":["style"],"properties":{"style":{"type":"string"},"palette":{"type":"array","maxItems":8,"items":{"type":"string"}},"typography":{"type":"string"},"aspectRatios":{"type":"array","maxItems":6,"items":{"type":"string","pattern":"^\\d+:\\d+$"}},"continuityNotes":{"type":"string"},"forbiddenVisuals":{"type":"array","maxItems":10,"items":{"type":"string"}}}},"audioSystem":{"type":"object","additionalProperties":false,"properties":{"voice":{"type":"string"},"music":{"type":"string"},"sfxPolicy":{"type":"string"}}},"scriptBlueprint":{"$ref":"#/components/schemas/StoryPlanScriptBlueprint"},"artifacts":{"type":"array","minItems":1,"maxItems":6,"items":{"type":"object","required":["type","renderPlan"],"properties":{"type":{"$ref":"#/components/schemas/StoryArtifactType"},"mode":{"type":"string","maxLength":80},"durationSeconds":{"type":"integer","minimum":5,"description":"Target duration in seconds. Video artifacts (trailer, pitch_deck) are planned at CUT density, not minimum-call density: round(target / mean) units of 4-30s each, with a ~5s mean trailer beat and a ~9s mean pitch-deck chapter (one render call, at most 10s) — so a 90s trailer plans about 18 beats and a 180s deck about 20 chapters. Each type carries its own ceiling: trailer 90s, pitch_deck 180s. Audio runtimes (table_read) are not unit-rendered and may run longer."},"aspectRatio":{"type":"string","pattern":"^\\d+:\\d+$"},"renderPlan":{"type":"object","additionalProperties":true}}}}}},"StoryPlan":{"type":"object","required":["id","projectId","status","title","target","artifactRequests","styleConstraints","creativeBrief","sourceIds","autoApprove","plan","quote","modelUsed","providerUsed","failureCode","failureMessage","approvedAt","rejectedAt","rejectionReason","generatedAt","currentVersion","createdAt","updatedAt"],"additionalProperties":false,"properties":{"id":{"type":"string"},"projectId":{"type":"string"},"status":{"$ref":"#/components/schemas/StoryPlanStatus"},"title":{"oneOf":[{"type":"string"},{"type":"null"}]},"target":{"$ref":"#/components/schemas/StoryPlanTarget"},"artifactRequests":{"type":"array","items":{"$ref":"#/components/schemas/StoryArtifactRequest"}},"styleConstraints":{"oneOf":[{"$ref":"#/components/schemas/StoryPlanStyleConstraints"},{"type":"null"}]},"creativeBrief":{"oneOf":[{"$ref":"#/components/schemas/StoryPlanCreativeBrief"},{"type":"null"}]},"sourceIds":{"type":"array","items":{"type":"string"}},"autoApprove":{"type":"boolean"},"plan":{"oneOf":[{"$ref":"#/components/schemas/StoryPlanEnvelope"},{"type":"null"}]},"quote":{"oneOf":[{"$ref":"#/components/schemas/StoryPlanQuote"},{"type":"null"}]},"modelUsed":{"oneOf":[{"type":"string"},{"type":"null"}]},"providerUsed":{"oneOf":[{"type":"string"},{"type":"null"}]},"failureCode":{"oneOf":[{"type":"string"},{"type":"null"}]},"failureMessage":{"oneOf":[{"type":"string"},{"type":"null"}]},"approvedAt":{"oneOf":[{"type":"string","format":"date-time"},{"type":"null"}]},"approvedVia":{"oneOf":[{"type":"string","enum":["user_confirmed","standing_approval"]},{"type":"null"}],"description":"How the plan was approved: a person's `userConfirmed`, or a show's standing approval standing in for them. Null for a plan an app flow approved by construction. A job from a `standing_approval` plan may request only `table_read`."},"approvedUnderSeriesId":{"oneOf":[{"type":"string"},{"type":"null"}],"description":"The show whose standing approval covered the approval (only when `approvedVia` is `standing_approval`)."},"rejectedAt":{"oneOf":[{"type":"string","format":"date-time"},{"type":"null"}]},"rejectionReason":{"oneOf":[{"type":"string"},{"type":"null"}]},"generatedAt":{"oneOf":[{"type":"string","format":"date-time"},{"type":"null"}]},"currentVersion":{"type":"integer","minimum":1},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"StoryPlanEnvelopeResponse":{"type":"object","required":["plan"],"additionalProperties":false,"properties":{"plan":{"$ref":"#/components/schemas/StoryPlan"}}},"StoryPlanList":{"type":"object","required":["plans","nextCursor"],"additionalProperties":false,"properties":{"plans":{"type":"array","items":{"$ref":"#/components/schemas/StoryPlan"}},"nextCursor":{"oneOf":[{"type":"string"},{"type":"null"}]}}},"StoryJobStatus":{"type":"string","enum":["PENDING","RESERVED","RUNNING","PARTIAL","READY","FAILED","CANCELED"],"description":"Lifecycle of a queued artifact-generation job. `RESERVED` means credits are held; when progress.stage is `provider_backoff`, Sleeper Hit has scheduled a delayed retry and clients should continue polling. `PARTIAL` means at least one artifact is READY while others are still running; `READY` is fully complete. Cancelable while `PENDING` or `RESERVED`, or `FAILED` before it ever started (canceling that one means it can no longer be resumed); a `RUNNING` job, and a `FAILED` job that had started generating, cannot be canceled. `CANCELED` is final."},"StoryArtifactStatus":{"type":"string","enum":["PENDING","RUNNING","READY","FAILED"],"description":"Lifecycle of an individual artifact within a job. `READY` means the manifest URLs are populated and the artifact is consumable."},"StoryArtifact":{"type":"object","required":["id","storyJobId","type","status","manifest","tableReadShareLinkId","creditLineItemIds","sourcePlanRevision","currentVersion","refineCount","failureCode","failureMessage","createdAt","updatedAt"],"additionalProperties":false,"description":"A single generated output of a story job. For `table_read`, the `manifest` carries the public share token, theater URL, on-demand live-audio start URL, refine state, and finalize state. The artifact id and share URLs are stable across refines. Fetch a prior revision with GET /artifacts/{id}?revision=N.","properties":{"id":{"type":"string"},"storyJobId":{"type":"string"},"type":{"type":"string","enum":["pitch_deck","table_read","trailer","production_video"],"description":"`production_video` is a dialogue video made before storyboarding was retired (2026-10-09). Those artifacts are still read and published; no new ones are made."},"status":{"$ref":"#/components/schemas/StoryArtifactStatus"},"manifest":{"oneOf":[{"$ref":"#/components/schemas/TableReadArtifactManifest"},{"type":"object","additionalProperties":true},{"type":"null"}]},"tableReadShareLinkId":{"oneOf":[{"type":"string"},{"type":"null"}]},"creditLineItemIds":{"type":"array","items":{"type":"string"}},"sourcePlanRevision":{"oneOf":[{"type":"integer","minimum":1},{"type":"null"}]},"currentVersion":{"type":"integer","minimum":1,"description":"Current manifest revision. v1 is the initial generation; each refine increments. Use ?revision=N on GET /artifacts/{id} to fetch a prior version."},"refineCount":{"type":"integer","minimum":0,"description":"Number of times this artifact has been refined. 0 for the initial generation."},"failureCode":{"oneOf":[{"type":"string"},{"type":"null"}]},"failureMessage":{"oneOf":[{"type":"string"},{"type":"null"}]},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"TableReadArtifactManifest":{"type":"object","description":"Manifest for a `table_read` artifact. The share token authorizes the returned URLs (not the customer API key), so the consumer can embed or hand them to end-users. The artifact id, share token, and all player URLs remain STABLE across refine operations. The MP4 `video` output and the durable MP3 `audio.finalize` are opt-in.","properties":{"type":{"type":"string","const":"table_read"},"status":{"type":"string","enum":["ready"]},"shareToken":{"type":"string","description":"Expiring JWT share token. Public but unguessable (\"unlisted\"). Stable across refines."},"theaterUrl":{"type":"string","format":"uri","description":"Framed visual theater-mode page (`/share/table-read/{token}`) with attribution chrome. Stable across refines."},"theaterFullscreenUrl":{"type":"string","format":"uri","description":"Immersive, chrome-free fullscreen theater (same in-app theater stage). Ideal for embeds/kiosk. Stable across refines."},"script":{"type":"object","description":"About the delivered screenplay.","properties":{"lengthNote":{"oneOf":[{"type":"object","additionalProperties":true},{"type":"null"}],"description":"Why the delivered screenplay sits outside the approved page band, when it does (an estimate from word count, not layout). Null on a clean run."},"punchUp":{"oneOf":[{"type":"object","required":["scenes","kept","reverted"],"properties":{"scenes":{"type":"integer","minimum":0,"description":"Scenes the punch-up pass looked at."},"kept":{"type":"integer","minimum":0,"description":"Scenes that kept their punch-up."},"reverted":{"type":"array","description":"Scenes that kept their written version, with why (a verbatim quote dropped, a neverSay term introduced, the slugline changed, a speaker added, grown past the limit, or the call failed). A scene that ran long and missed again on its one retry lists both, the second prefixed \"after one retry:\".","items":{"type":"object","required":["scene","reasons"],"properties":{"scene":{"type":"integer","minimum":1},"reasons":{"type":"array","items":{"type":"string"}}}}},"retried":{"type":"integer","minimum":0,"description":"Scenes that only ran long and were asked once to cut back to their length; `kept` counts any that then passed."},"skippedReason":{"type":"string","description":"Set when the pass did not run at all; the written screenplay was performed."},"modelId":{"type":"string"}}},{"type":"null"}],"description":"What the punch-up pass did to the screenplay, when the request set `punchUp: true`. Null when it did not."}}},"audio":{"type":"object","description":"Audio output section. `liveUrl` opens the audio player (serves the durable MP3 once available). The durable MP3 is auto-rendered on ready — UNLESS the artifact request set `deferAudioRender`, or this is a season-run episode, in which case it is rendered only when you POST /artifacts/{id}/finalize.","properties":{"mode":{"type":"string","enum":["durable_recording","live_on_demand"],"description":"`durable_recording` — auto-rendered MP3; `audio.recordingUrl` is populated once complete. (`live_on_demand` is a legacy value for older artifacts.)"},"liveUrl":{"type":"string","format":"uri","description":"Minimal-chrome, audio-only player page (`/share/table-read-audio/{token}`). Serves the durable auto-rendered MP3; shows a \"Preparing…\" state until the render is complete."},"recordingUrl":{"oneOf":[{"type":"string","format":"uri"},{"type":"null"}],"description":"Populated after a successful audio finalize (auto-render on ready, or explicit POST /artifacts/{id}/finalize). `null` until finalize is complete — and stays `null` indefinitely when `deferAudioRender` was set and you have not finalized yet."},"recordingDurationMs":{"oneOf":[{"type":"integer","minimum":0},{"type":"null"}],"description":"Duration of the finalized MP3 recording in milliseconds. Populated alongside `recordingUrl` on finalize completion."},"voiceMap":{"type":"object","additionalProperties":true,"description":"Current cast: character name → { voiceId, voiceName, gender, provider }. Recast a single character in place with POST /artifacts/{id}/voice (scope `artifact:publish`)."},"modifications":{"type":"array","description":"Voice modification records applied to entry ranges on this read — one per POST /artifacts/{id}/voice-modification, newest last per range. This is where a dispatched modification is polled to completion, and the ONLY place a render failure explains itself.","items":{"$ref":"#/components/schemas/ArtifactVoiceModification"}},"finalize":{"type":"object","description":"State of the durable MP3 finalize. The render is triggered automatically when the table read is READY. Poll GET /artifacts/{id} until `status` is `complete`, `failed` or `stalled`.","properties":{"status":{"type":"string","enum":["not_finalized","needs_recording","queued","synthesizing","stitching","rendering","uploading","stalled","complete","failed"],"description":"`needs_recording` means no completed Pipecat/Daily recording is ready to freeze. `stalled` means the render stopped with nothing working on it (no progress for ten minutes and no job queued or running): POST /artifacts/{id}/finalize/retry renders it again. Otherwise poll GET /artifacts/{id} until `complete`, `failed` or `stalled`."},"finalizeUrl":{"oneOf":[{"type":"string","format":"uri"},{"type":"null"}],"description":"Internal staging URL used during the finalize pipeline. Not for end-user consumption; use `recordingUrl` on completion."},"error":{"oneOf":[{"type":"string"},{"type":"null"}],"description":"Human-readable description of the finalize failure when `status` is `failed` or `stalled`."}}}}},"video":{"type":"object","description":"Video output section. Status reflects real render state once the Remotion MP4 export has been triggered.","properties":{"status":{"type":"string","enum":["not_rendered","queued","rendering","complete","stale","needs_recording","failed"],"description":"Render pipeline stage. `not_rendered` until POST /artifacts/{id}/render-video or POST /artifacts/{id}/finalize?mode=video is called. `complete` = a current MP4 is available; `stale` = the read changed after the last render (re-render to refresh); `needs_recording` = finalize audio first."},"renderUrl":{"type":"string","description":"POST to trigger the opt-in Remotion MP4 render via the render-video endpoint (free)."},"url":{"oneOf":[{"type":"string","format":"uri"},{"type":"null"}],"description":"Durable MP4 URL. Populated when `status` is `complete` (or `stale`)."}}},"expiresAt":{"oneOf":[{"type":"string","format":"date-time"},{"type":"null"}]}}},"StoryArtifactEnvelope":{"type":"object","required":["artifact"],"additionalProperties":false,"properties":{"artifact":{"$ref":"#/components/schemas/StoryArtifact"}}},"ArtifactFinalizeAck":{"type":"object","required":["artifactId","mode","finalize"],"additionalProperties":false,"properties":{"artifactId":{"type":"string"},"mode":{"type":"string","enum":["audio"]},"finalize":{"type":"object","required":["status"],"additionalProperties":true,"properties":{"status":{"type":"string","enum":["queued","synthesizing","stitching","rendering","uploading","complete"]},"recordingUrl":{"type":"string","format":"uri"}}}}},"StoryArtifactListResponse":{"type":"object","required":["artifacts"],"additionalProperties":false,"properties":{"artifacts":{"type":"array","items":{"$ref":"#/components/schemas/StoryArtifact"}}}},"ArtifactScriptEntry":{"type":"object","required":["entryIndex","character","text","isNarration","sceneHeading","parenthetical","page","overlap","concurrentSpeechMs","overlapNote"],"additionalProperties":false,"properties":{"entryIndex":{"type":"integer","minimum":0},"character":{"type":"string"},"text":{"type":"string"},"isNarration":{"type":"boolean"},"sceneHeading":{"oneOf":[{"type":"string"},{"type":"null"}]},"parenthetical":{"oneOf":[{"type":"string"},{"type":"null"}]},"page":{"oneOf":[{"type":"integer","minimum":1},{"type":"null"}]},"overlap":{"description":"Simultaneous dialogue: how the line meets the one before it. overlap = comes in before it stops; reaction = tucked into its pause, quieter; simultaneous = starts with it; unison = said with it, or, with speakers, a group line (ALL, BOTH, \"A & B\") every member says at once. Null for a plain turn.","oneOf":[{"type":"object","required":["kind"],"additionalProperties":false,"properties":{"kind":{"type":"string","enum":["overlap","simultaneous","unison","reaction"]},"overlapMs":{"type":"integer","minimum":0},"speakers":{"type":"array","items":{"type":"string"}}}},{"type":"null"}]},"concurrentSpeechMs":{"oneOf":[{"type":"integer","minimum":0},{"type":"null"}],"description":"Read-only. On an overlapping line of a finalized read: milliseconds both voices actually sound together in the recording (on a group line, every member), measured from each clip's speech rather than its file edges. Null when the line does not overlap or no measured recording exists yet."},"overlapNote":{"description":"Read-only. A marker the guards dropped, adjusted or only warned about, with why: from the script (first-entry, previous-not-performable, same-character, narration, group-unresolved, scene-over-budget) or from the render (too-short, no-frame-format, voices-at-once, no-pause). message is the plain sentence the app shows. Null when nothing happened to the marker.","oneOf":[{"type":"object","required":["requested","outcome","reason","message"],"additionalProperties":false,"properties":{"requested":{"type":"string","enum":["overlap","simultaneous","unison","reaction"]},"outcome":{"type":"string","enum":["dropped","downgraded","warned"]},"reason":{"type":"string","enum":["first-entry","previous-not-performable","same-character","narration","group-unresolved","scene-over-budget","too-short","no-frame-format","voices-at-once","no-pause"]},"detail":{"type":"string"},"message":{"type":"string"}}},{"type":"null"}]}}},"ArtifactScriptContent":{"type":"object","required":["artifactId","scriptUploadId","title","scope","pageCount","totalEntries","pages","scenes","characters","selection"],"additionalProperties":false,"description":"Readable script-content view for a table_read artifact. Entries are the same parsed backing entries used by theater playback, music, SFX, and coverage.","properties":{"artifactId":{"type":"string"},"scriptUploadId":{"type":"string"},"title":{"oneOf":[{"type":"string"},{"type":"null"}]},"scope":{"type":"string","enum":["all","page","scene","character","range"]},"pageCount":{"type":"integer","minimum":0},"totalEntries":{"type":"integer","minimum":0},"pages":{"type":"array","items":{"type":"object","required":["pageNumber","startEntry","endEntry","wordCount"],"properties":{"pageNumber":{"type":"integer","minimum":1},"startEntry":{"type":"integer","minimum":0},"endEntry":{"type":"integer","minimum":-1},"wordCount":{"type":"integer","minimum":0}}}},"scenes":{"type":"array","items":{"type":"object","required":["sceneIndex","heading","startEntry","endEntry"],"properties":{"sceneIndex":{"type":"integer","minimum":0},"heading":{"type":"string"},"startEntry":{"type":"integer","minimum":0},"endEntry":{"type":"integer","minimum":0}}}},"characters":{"type":"array","items":{"type":"object","required":["character","lineCount","wordCount"],"properties":{"character":{"type":"string"},"lineCount":{"type":"integer","minimum":0},"wordCount":{"type":"integer","minimum":0}}}},"selection":{"type":"object","required":["startEntry","endEntry","entries"],"properties":{"pageNumber":{"type":"integer","minimum":1},"sceneIndex":{"type":"integer","minimum":0},"character":{"type":"string"},"startEntry":{"type":"integer","minimum":0},"endEntry":{"type":"integer","minimum":-1},"entries":{"type":"array","items":{"$ref":"#/components/schemas/ArtifactScriptEntry"}}}}}},"ArtifactScriptContentEnvelope":{"type":"object","required":["script"],"additionalProperties":false,"properties":{"script":{"$ref":"#/components/schemas/ArtifactScriptContent"}}},"StoryJob":{"type":"object","required":["id","projectId","storyPlanId","status","artifactRequests","quote","creditReservationId","progress","modelUsed","failureCode","failureMessage","usableArtifactId","failureClass","retryable","runStale","currentVersion","startedAt","completedAt","artifacts","createdAt","updatedAt"],"additionalProperties":false,"description":"A queued artifact-generation job against an APPROVED StoryPlan. Holds the reserved credit quote, durable progress/checkpoint state, and generated artifacts. Provider-capacity recovery may move RUNNING back to RESERVED with progress.stage=provider_backoff until the core-managed retry wakes it.","properties":{"id":{"type":"string"},"projectId":{"type":"string"},"storyPlanId":{"type":"string"},"status":{"$ref":"#/components/schemas/StoryJobStatus"},"artifactRequests":{"type":"array","items":{"$ref":"#/components/schemas/StoryArtifactRequest"}},"quote":{"oneOf":[{"$ref":"#/components/schemas/StoryPlanQuote"},{"type":"null"}]},"creditReservationId":{"oneOf":[{"type":"string"},{"type":"null"}]},"progress":{"oneOf":[{"type":"object","additionalProperties":true},{"type":"null"}]},"modelUsed":{"oneOf":[{"type":"string"},{"type":"null"}]},"failureCode":{"oneOf":[{"type":"string"},{"type":"null"}]},"failureMessage":{"oneOf":[{"type":"string"},{"type":"null"}]},"usableArtifactId":{"oneOf":[{"type":"string"},{"type":"null"}],"description":"The artifact you can act on, or null — the single \"is there something to collect?\" answer. Do not rebuild it from status + artifacts[] + failureMessage: a job can be FAILED with a complete read (only its planned soundtrack missed) or PARTIAL with a READY artifact, and reconstructing that has cost callers finished, paid-for work."},"failureClass":{"oneOf":[{"type":"string","enum":["quota","provider","contract","transient","internal"]},{"type":"null"}],"description":"What KIND of failure this was. `quota` = balance/billing ceiling (needs a top-up or rebind). `provider` = upstream model provider throttling (needs its cooldown). `contract` = we rejected your request deterministically; the identical request fails identically. `transient` = timeout/reset, worth retrying as-is. `internal` = unclassified defect on our side. Branch on this rather than pattern-matching failureMessage, which is prose and will change. Null unless the job is `FAILED` or `PARTIAL`: a `CANCELED` job keeps the failureCode/failureMessage it had before it was canceled, but it has no failure class."},"retryable":{"oneOf":[{"type":"boolean"},{"type":"null"}],"description":"Whether repeating the SAME request could plausibly succeed later. False for contract (deterministic), quota, and provider (both need an external change first, so an immediate retry only adds load). Always false for a `CANCELED` job: cancel is final and resume refuses it. Null when there is no failure."},"runStale":{"type":"boolean","description":"True when the job is a RUNNING table read whose worker stopped answering: no heartbeat for five minutes, because its server died or was replaced mid-run. POST /story-jobs/{jobId}/resume sends its run again from the last checkpoint at no new cost (action `generation_requeued`); Sleeper Hit also sends it again on its own when its workers restart. False for every other job, including a RUNNING one that is still working: keep polling it."},"currentVersion":{"type":"integer","minimum":1},"startedAt":{"oneOf":[{"type":"string","format":"date-time"},{"type":"null"}]},"completedAt":{"oneOf":[{"type":"string","format":"date-time"},{"type":"null"}]},"artifacts":{"type":"array","items":{"$ref":"#/components/schemas/StoryArtifact"}},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"StoryJobCreate":{"type":"object","required":["storyPlanId"],"additionalProperties":false,"description":"Inputs for a new story job. The plan must be `APPROVED`. `artifactRequests` defaults to the plan's own requests when omitted; if supplied, it overrides them (max 6). A table read performs the project's cast: from a plan it starts only when the cast canon covers every character (GET /story-projects/{projectId} → `project.tableReadReadiness`), and a single table_read request may add a voiceMap using saved IDs from GET /voices for voices the canon lacks, or set `scriptUploadId` to perform a finalized, cast-ready screenplay as written. `projectId`, if supplied, must match the plan's project.","properties":{"storyPlanId":{"type":"string","pattern":"^[a-z0-9]{20,}$"},"projectId":{"type":"string","pattern":"^[a-z0-9]{20,}$"},"artifactRequests":{"type":"array","minItems":1,"maxItems":6,"items":{"$ref":"#/components/schemas/StoryArtifactRequest"},"description":"Narrows or refines the approved plan's requests (e.g. a table read's `voiceMap` or `narrationPolicy`). It may not add an artifact type the approved plan does not contain — that work was never quoted or approved (400 `validation_failed`, `details.reason: \"artifact_type_not_in_plan\"`); make and approve a plan for it. A plan approved under a show's standing approval runs `table_read` only."},"callbackUrl":{"type":"string","format":"uri","description":"Public https endpoint POSTed once when this job reaches a terminal state (READY, PARTIAL, FAILED, CANCELED). Removes the need to poll a run that legitimately takes 9-31 minutes. The body is a StoryJobCallback. Credentials, loopback, and private/link-local addresses are rejected. Delivery retries with backoff on 5xx/408/429 and is not retried on other 4xx."},"callbackSecret":{"type":"string","minLength":16,"description":"Your own HMAC-SHA256 key (min 16 chars). Deliveries carry X-SleeperHit-Timestamp and X-SleeperHit-Signature = hex(HMAC(secret, `{timestamp}.{rawBody}`)); the timestamp is signed so a captured delivery cannot be replayed. Requires callbackUrl. Grants no access to this API."}}},"StoryJobCallback":{"type":"object","required":["event","storyJobId","status","usableArtifactId","failureCode","failureMessage","failureClass","retryable","completedAt"],"additionalProperties":false,"description":"Body POSTed to a job's callbackUrl when it reaches a terminal state.","properties":{"event":{"type":"string","const":"story_job.completed"},"storyJobId":{"type":"string"},"status":{"type":"string","enum":["READY","PARTIAL","FAILED","CANCELED"]},"usableArtifactId":{"oneOf":[{"type":"string"},{"type":"null"}],"description":"The artifact you can act on, or null. Resolved server-side so you never reconstruct \"do I have something usable?\" from status + artifacts[] + failureMessage. Non-null is possible on a FAILED job whose read completed before a later stage failed, and on PARTIAL."},"failureCode":{"oneOf":[{"type":"string"},{"type":"null"}]},"failureMessage":{"oneOf":[{"type":"string"},{"type":"null"}]},"failureClass":{"oneOf":[{"type":"string","enum":["quota","provider","contract","transient","internal"]},{"type":"null"}],"description":"What KIND of failure this was. `quota` = balance/billing ceiling (needs a top-up or rebind). `provider` = upstream model provider throttling (needs its cooldown). `contract` = we rejected your request deterministically; the identical request fails identically. `transient` = timeout/reset, worth retrying as-is. `internal` = unclassified defect on our side. Branch on this rather than pattern-matching failureMessage, which is prose and will change. Null unless the job is `FAILED` or `PARTIAL`: a `CANCELED` job keeps the failureCode/failureMessage it had before it was canceled, but it has no failure class."},"retryable":{"oneOf":[{"type":"boolean"},{"type":"null"}],"description":"Whether repeating the SAME request could plausibly succeed later. False for contract (deterministic), quota, and provider (both need an external change first, so an immediate retry only adds load). Always false for a `CANCELED` job: cancel is final and resume refuses it. Null when there is no failure."},"completedAt":{"oneOf":[{"type":"string","format":"date-time"},{"type":"null"}]}}},"ProviderChainLink":{"type":"object","required":["label","modelId","healthy","cooldownUntil","failureStreak","unreachableReason"],"additionalProperties":false,"properties":{"label":{"type":"string"},"modelId":{"type":"string"},"healthy":{"type":"boolean","description":"Active, its stored API key readable by this server, and not currently cooling down. A key that cannot be decrypted fails every request at this link, so the link is never healthy."},"cooldownUntil":{"oneOf":[{"type":"string","format":"date-time"},{"type":"null"}]},"failureStreak":{"type":"integer","minimum":0},"unreachableReason":{"oneOf":[{"type":"string"},{"type":"null"}],"description":"Why this link cannot serve a request, or null when it can: the provider row is inactive; it is an image, video or music model, which cannot answer a text call; its API key cannot be read (names the fix: re-enter the key, or fix the server's AI_ENCRYPTION_KEY when no stored key can be read); or it is cooling down until a stated time."}}},"GenerationHealthEntry":{"type":"object","required":["sidecar","healthy","reason","chain","cyclic"],"additionalProperties":false,"properties":{"sidecar":{"type":"string"},"healthy":{"type":"boolean","description":"False when this sidecar can reach no healthy provider — including when it is bound to none at all. Also false when a request would reach a link whose stored API key this server cannot decrypt, even with a healthy fallback behind it: the request fails at that link and no fallback is tried, so the sidecar is unreachable until the key is fixed. (A link cooling down is routed past before its key is read.)"},"reason":{"oneOf":[{"type":"string"},{"type":"null"}],"description":"Null when the bound provider itself is healthy. Otherwise one line: the link every request fails at and why (an unreadable API key names its fix: re-enter the key, or fix the server's AI_ENCRYPTION_KEY), each link that cannot serve and why, or, when a fallback serves, the bound provider's problem and the fallback that serves instead."},"chain":{"type":"array","items":{"$ref":"#/components/schemas/ProviderChainLink"},"description":"The resolved fallback chain, in order."},"cyclic":{"type":"boolean","description":"True when the chain loops back on itself; a configuration error worth fixing before it parks jobs."}}},"GenerationHealthEnvelope":{"type":"object","required":["health"],"additionalProperties":false,"properties":{"health":{"type":"object","required":["healthy","checkedAt","sidecars","misboundSidecars"],"additionalProperties":false,"properties":{"healthy":{"type":"boolean","description":"True only when every critical sidecar can reach a healthy provider. Without ?probe this is derived from provider flags and recorded cooldowns, so a provider that has not been TRIED since it died still counts as healthy — its failure streak is zero precisely because nothing has failed against it yet. With ?probe=true it is replaced by what the providers actually answered."},"checkedAt":{"type":"string","format":"date-time"},"sidecars":{"type":"array","items":{"$ref":"#/components/schemas/GenerationHealthEntry"}},"misboundSidecars":{"type":"array","description":"Every active text sidecar, critical or not, bound to an image, video or music provider. Such a sidecar cannot generate at all: its text call fails before reaching the provider. A critical one also reads unhealthy in `sidecars`; any other is reported only here. Empty when every text sidecar is on a text model.","items":{"type":"object","required":["sidecar","feature","provider","providerType","modelId","reason"],"additionalProperties":false,"properties":{"sidecar":{"type":"string"},"feature":{"type":"string"},"provider":{"type":"string","description":"The bound provider row's label."},"providerType":{"type":"string"},"modelId":{"type":"string"},"reason":{"type":"string","description":"What is wrong and the fix: bind a text model."}}}}}},"bindingProbes":{"type":"array","description":"Present only when ?probe=true. Per critical sidecar, a real (small) call against EVERY link of its fallback chain, in order, stopping at the first link that answers. Probing only the bound provider answered a different question from the one this endpoint asks: the link that decides whether a request can be served is usually not the first one. When probes run they also override `health.healthy`, because an answer from the provider outranks a recorded flag.","items":{"type":"object","required":["sidecar","usable","servedBy","links","guidance"],"additionalProperties":false,"properties":{"sidecar":{"type":"string"},"usable":{"type":"boolean","description":"True when SOME link in the chain answered — the honest form of \"can this sidecar generate right now\"."},"servedBy":{"oneOf":[{"type":"string"},{"type":"null"}],"description":"Label of the link that actually answered, or null when none did. When this is NOT the primary, the pipeline is running on a different model than the one bound — a quality variable, not a no-op."},"links":{"type":"array","description":"Each link probed, in chain order, ending at the one that answered. A chain that serves requests from its third link shows all three.","items":{"type":"object","required":["label","modelId","verdict"],"additionalProperties":false,"properties":{"label":{"type":"string"},"modelId":{"type":"string"},"verdict":{"type":"object","additionalProperties":true,"description":"usable:true, or usable:false with reason `unreachable` (could not be called at all), `rejects_call_shape` (refuses the sampling parameters we send — bind a param-shim variant), `unusable_output` (answers, but with nothing the pipeline can use), or `api_key_undecryptable` (its stored API key cannot be decrypted by this server, so it was not called; `detail` names the fix: re-enter the key, or fix the server's AI_ENCRYPTION_KEY)."}}}},"guidance":{"type":"string","description":"One line on what to do: usable, served by a named fallback, or nothing in the chain can serve a request."}}}}}},"StoryJobEnvelope":{"type":"object","required":["job"],"additionalProperties":false,"properties":{"job":{"$ref":"#/components/schemas/StoryJob"}}},"StoryJobResumeResponse":{"type":"object","required":["action","artifactIds","job"],"additionalProperties":false,"properties":{"action":{"type":"string","enum":["already_active","already_complete","generation_requeued","finalize_requeued"]},"artifactIds":{"type":"array","items":{"type":"string"}},"job":{"$ref":"#/components/schemas/StoryJob"}}},"StoryJobList":{"type":"object","required":["jobs","nextCursor"],"additionalProperties":false,"properties":{"jobs":{"type":"array","items":{"$ref":"#/components/schemas/StoryJob"}},"nextCursor":{"oneOf":[{"type":"string"},{"type":"null"}]}}},"CoverageStatus":{"type":"string","enum":["generating","complete","failed"],"description":"Lifecycle of an async coverage report. Poll GET /artifacts/{id}/coverage until `complete` or `failed`."},"CoverageDispatchAck":{"type":"object","required":["artifactId","reportId","status","version"],"additionalProperties":false,"description":"Acknowledgement that a coverage report has been queued. Poll GET /artifacts/{artifactId}/coverage with the returned `reportId`.","properties":{"artifactId":{"type":"string"},"reportId":{"type":"string"},"status":{"$ref":"#/components/schemas/CoverageStatus"},"version":{"type":"integer","minimum":1,"description":"Sequential coverage version for the backing script (increments per report)."}}},"CoverageReport":{"type":"object","required":["id","artifactId","status","version","focusPrompt","progressDetail","failureMessage","overallScore","gateThreshold","gatePassed","judging","judgingIncomplete","summary","report","createdAt"],"additionalProperties":false,"description":"A screenplay-rubric report from the shared coverage lifecycle for the screenplay backing a table read artifact. `report` carries the structured screenplay payload only when `status: complete`. `overallScore` is the GATED score: a first judgment within `judging.nearGateBand` of the bar is followed by another, two that disagree on the bar by a third, and the score is the MEDIAN of the judgments (`judging`); a clear pass or fail is one judgment. Read `summary` first.","properties":{"id":{"type":"string"},"artifactId":{"type":"string"},"status":{"$ref":"#/components/schemas/CoverageStatus"},"version":{"type":"integer","minimum":1},"focusPrompt":{"oneOf":[{"type":"string"},{"type":"null"}],"description":"The optional focus lens supplied at generation time."},"progressDetail":{"oneOf":[{"type":"string"},{"type":"null"}],"description":"Human-readable progress while `status: generating` (including \"reading the script again\" while a near-bar score is judged again)."},"failureMessage":{"oneOf":[{"type":"string"},{"type":"null"}],"description":"Populated when `status: failed`."},"overallScore":{"oneOf":[{"type":"number","minimum":0,"maximum":10},{"type":"null"}],"description":"The gated score: near the bar, the MEDIAN of the judgments (`judging.median`); a clear pass or fail is one judgment. `null` unless `status: complete`."},"gateThreshold":{"oneOf":[{"type":"number","minimum":0,"maximum":10},{"type":"null"}],"description":"The bar the report was judged against (a coverage gate's target, a season's minimum, else the default 8). `null` unless `status: complete`."},"gatePassed":{"oneOf":[{"type":"boolean"},{"type":"null"}],"description":"Whether `overallScore` clears `gateThreshold`. `null` unless `status: complete`."},"judging":{"oneOf":[{"$ref":"#/components/schemas/ScriptCoverageJudging"},{"type":"null"}],"description":"How the score was reached. `null` unless `status: complete`, or for a report written before judgments were recorded."},"judgingIncomplete":{"oneOf":[{"type":"object","required":["judgments","scores","maxJudgments","passMark"],"additionalProperties":false,"properties":{"judgments":{"type":"integer","minimum":1,"description":"The judgments that did run."},"scores":{"type":"array","items":{"type":"number"},"description":"Their scores, in order."},"maxJudgments":{"type":"integer","minimum":1},"passMark":{"type":"number","description":"The bar the script sat near."}}},{"type":"null"}],"description":"A script near the bar whose median could not be completed: the further judgments it needed could not run, even queued again. The report is `failed` and is NEITHER a pass nor a fail (\"Coverage couldn't finish judging — Score again\"); generate coverage again. Null otherwise."},"summary":{"oneOf":[{"type":"string"},{"type":"null"}],"description":"The gated score in one line, e.g. \"8/10 against the 8 bar — the median of 3 judgments (7 · 8 · 8)\"; for an incomplete judging (`judgingIncomplete`), its sentence. `null` otherwise unless `status: complete`."},"report":{"oneOf":[{"type":"object","additionalProperties":true},{"type":"null"}],"description":"Structured coverage payload (logline, premise, structure, characters, dialogue, pacing, marketFit, overallScore, topFixes, strengths, comparable, recommendation, judging): the report of the judgment that gave the median, whose notes are the ones to revise against. `null` unless `status: complete`."},"createdAt":{"type":"string","format":"date-time"}}},"ScriptCoverageJudging":{"type":"object","required":["judgments","scores","median","medianOf","medianJudgment","medianWhy","passMark","passed","stoppedBecause","nearGateBand","maxJudgments","extraSpendUsd","extraSpendCapUsd","fingerprint","reusedFrom"],"additionalProperties":false,"description":"HOW A SCRIPT COVERAGE SCORE WAS REACHED. One judgment is noisy near the bar, so a first judgment within `nearGateBand` of `passMark` is judged again, up to `maxJudgments` in all, stopping as soon as a majority sit on one side of the bar; the score is the MEDIAN of `scores` (an even count reads the lower middle, so it is always a score one judgment gave, and `report` is that judgment's). A report the platform queues by itself (a table-read job's coverage gate and first-pass report, a season run) on a script whose text, rubric and Series Bible are identical to an earlier report's (`fingerprint`) reuses that report's judgments (`reusedFrom`) instead of paying for them again; a report a writer requests is always judged afresh.","properties":{"judgments":{"type":"array","items":{"$ref":"#/components/schemas/ScriptCoverageJudgment"},"description":"Every judgment, in order — including any reused from an earlier report."},"scores":{"type":"array","items":{"type":"number"},"description":"The scores the median was taken over, in judgment order."},"median":{"oneOf":[{"type":"number"},{"type":"null"}],"description":"The gated score (`overallScore`). Null when no judgment gave a score."},"medianOf":{"type":"integer","minimum":0,"description":"How many scores the median was taken over."},"medianJudgment":{"oneOf":[{"type":"integer","minimum":1},{"type":"null"}],"description":"The judgment that gave the median; its report is the one shown."},"medianWhy":{"type":"string","description":"Why that judgment is the median, in a sentence (empty for one plain judgment)."},"passMark":{"type":"number","description":"The bar it was judged against."},"passed":{"type":"boolean"},"stoppedBecause":{"type":"string","enum":["decisive","agreement","max_judgments","spend_cap","judgment_failed"],"description":"`decisive`: one judgment, clear of the bar by the band; `agreement`: enough judgments sit on one side that another could not move the median across it; `max_judgments`; `spend_cap`: another was wanted but the script's extra-judgment spend cap was reached; `judgment_failed`: another was wanted but could not run."},"nearGateBand":{"type":"number","description":"Within this many points of the bar, one judgment is not trusted on its own."},"maxJudgments":{"type":"integer","minimum":1,"description":"The most judgments one script takes."},"extraSpendUsd":{"type":"number","minimum":0,"description":"What the judgments beyond the first cost, in USD."},"extraSpendCapUsd":{"type":"number","minimum":0,"description":"The most the judgments beyond the first may cost for one script, in USD."},"fingerprint":{"oneOf":[{"type":"string"},{"type":"null"}],"description":"Hash of what was judged: the script, the rubric, the Series Bible and the model."},"reusedFrom":{"oneOf":[{"type":"object","required":["reportId","reportVersion"],"additionalProperties":false,"properties":{"reportId":{"type":"string"},"reportVersion":{"oneOf":[{"type":"integer"},{"type":"null"}]}}},{"type":"null"}],"description":"The earlier report of the identical script whose judgments were reused, or null."}}},"ScriptCoverageJudgment":{"type":"object","required":["judgment","reason","reportId","reused","model","judgedAt","overallScore","recommendation","costUsd","priced"],"additionalProperties":false,"description":"One judgment of the script.","properties":{"judgment":{"type":"integer","minimum":1,"description":"1, 2, 3 — in the order taken."},"reason":{"type":"string","enum":["first","near_bar","disagreement"],"description":"Why it was taken: the first; the judgments so far sat near the bar; or they sat on opposite sides of it."},"reportId":{"type":"string","description":"The report it was paid for (an earlier one when `reused`)."},"reused":{"type":"boolean","description":"True when an earlier report of the identical script took it, and it was reused here."},"model":{"type":"string"},"judgedAt":{"type":"string","format":"date-time"},"overallScore":{"oneOf":[{"type":"number"},{"type":"null"}],"description":"The overall score this judgment gave."},"recommendation":{"oneOf":[{"type":"string"},{"type":"null"}]},"costUsd":{"type":"number","minimum":0,"description":"What it cost on the usage ledger, in USD."},"priced":{"type":"boolean","description":"False when the model has no price on file and `costUsd` is the flat estimate."}}},"CoverageReportEnvelope":{"type":"object","required":["coverage"],"additionalProperties":false,"description":"Coverage read envelope. `coverage` is `null` when no coverage has been requested for the artifact yet.","properties":{"coverage":{"oneOf":[{"$ref":"#/components/schemas/CoverageReport"},{"type":"null"}]}}},"MusicStatus":{"type":"string","enum":["none","generating","ready"],"description":"Adaptive soundtrack readiness for a table read. `none` — no active soundtrack is present (including intentional 0% coverage); `generating` — scoring is still in progress; `ready` — at least one scene carries soundtrack."},"MusicGenerateRequest":{"type":"object","additionalProperties":false,"description":"Soundtrack tuning. Omit the body entirely to (re)generate scene music (defined-clip scripts render durable per-scene beds; realtime scripts regenerate adaptive directions). Provide `musicMode` to switch modes, `sceneIndex` + `clip` to edit one defined clip, `regenerateScenes` to re-render specific clips, or `scope`/`sceneIndex`/`entryIndex` with directive fields for realtime soundtrack direction.","properties":{"musicMode":{"type":"string","enum":["defined_clips"],"description":"Switch the script-level music mode. `defined_clips` (the only mode) plays durable per-scene music clips and auto-queues a baseline coverage pass."},"clip":{"type":"object","additionalProperties":false,"description":"Defined-clip edit for the scene at `sceneIndex`. Editing `prompt` or `vocals` marks the clip pending (re-render via `regenerateScenes`); `disabled`/`volume`/`playMode` keep a ready clip playable. Vocals are an explicit opt-in — clips render instrumental otherwise.","properties":{"prompt":{"type":"string","maxLength":600,"description":"Music prompt for this scene — instrumentation, texture, tempo."},"vocals":{"type":"boolean","description":"Allow vocals in this scene's clip. Defaults false; only set true when the writer explicitly asks for vocals."},"disabled":{"type":"boolean","description":"Mute this scene's clip without deleting the prompt/audio."},"volume":{"type":"number","minimum":0,"maximum":1,"description":"Playback volume override (default 0.25)."},"fadeInMs":{"type":"integer","minimum":0,"maximum":20000,"description":"Music fade-in for this clip, ms (default ~1500). 0 disables the fade-in."},"fadeOutMs":{"type":"integer","minimum":0,"maximum":20000,"description":"Music fade-out for this clip, ms (default ~2500). 0 disables the fade-out."},"playMode":{"type":"string","enum":["once","loop"],"description":"Playback mode. `once` (default) plays the natural ~30-40s bed a single time; `loop` repeats it to fill the whole scene."},"soundUrl":{"type":"string","format":"uri","description":"Banked/library audio URL on files.sleeperhit.studio. Supplying it adopts the clip as ready without rendering new music."},"durationMs":{"type":"number","minimum":0,"description":"Measured duration of banked `soundUrl` audio in milliseconds, used for play-once placement and fades."},"anchor":{"type":"string","enum":["start","end"],"description":"For play-once clips in longer scenes, align the clip to the scene start (default) or scene end (outro themes)."},"postRollMs":{"type":"integer","minimum":0,"description":"Music-only tail after the final spoken line, in milliseconds. A positive value automatically selects play-once and end anchoring; render extension applies only to the final scene."}}},"regenerateScenes":{"type":"array","items":{"type":"integer","minimum":0},"minItems":1,"description":"Zero-based scene indexes whose defined clips should re-render with their current prompts."},"soundtrackEnabled":{"type":"boolean","description":"The read's \"Scene Soundtrack Active\" master switch. `false` silences every scene's music everywhere (live playback, the MP3 and the MP4); banked clips and coverage are kept. Used alone. Free — it never waits on the project gate."},"musicCoveragePercent":{"type":"number","description":"Soundscape coverage: how much of the read carries music. Accepts a fraction (0..1) or a whole percent (0..100); clamped into [0,1]. The same setting as the in-app coverage slider: it keeps the key scenes (opening, finale, the most intense) and silences the rest, saves the percent, and queues clips for kept scenes that have none. 0 silences every scene; banked clips are kept."},"scope":{"type":"string","enum":["screenplay","scene","entry"],"description":"Directive scope. Defaults to `scene` when `sceneIndex` is supplied, `entry` when `entryIndex` is supplied, otherwise `screenplay`."},"mode":{"type":"string","enum":["merge","replace"],"description":"Directive mode. Defaults to `merge`, which appends to the existing weighted prompt palette. Use `replace` to overwrite the targeted palette, e.g. when switching the whole read to a new scoring style."},"sceneIndex":{"type":"integer","minimum":0,"description":"Zero-based scene index to update. Required for `scope: \"scene\"`; optional context for entry-level updates."},"entryIndex":{"type":"integer","minimum":0,"description":"Zero-based parsed script entry index for line/beat-level soundtrack steering. Required for `scope: \"entry\"`."},"prompt":{"type":"string","maxLength":240,"description":"Optional adaptive soundtrack prompt, e.g. \"a single piano tone, held, then released\" or \"heavier bass pulse, tense low strings\"."},"summary":{"type":"string","maxLength":400,"description":"Optional plain-language soundtrack summary."},"enabled":{"type":"boolean","description":"Optional soundtrack toggle. `false` mutes soundtrack for this scope; `true` turns it back on."},"weight":{"type":"number","minimum":0,"maximum":2,"description":"Optional prompt weight; defaults to 1 and is clamped by the API."},"direction":{"$ref":"#/components/schemas/SceneMusicDirectionUpdate","description":"With `sceneIndex`: change that scene's music direction field by field (end transition, mix, ducking, tempo, instruments and the rest), the same save as the Table Read page's scene console. Used with `sceneIndex` only."}}},"MusicGenerateResponse":{"type":"object","required":["artifactId","status"],"additionalProperties":false,"description":"Acknowledgement that adaptive soundtrack (re)generation has been dispatched, or that one scene direction was updated.","properties":{"artifactId":{"type":"string"},"status":{"type":"string","enum":["generating","ready","ok"]},"musicMode":{"type":"string","enum":["defined_clips"],"description":"Present when the music mode was switched."},"soundtrackEnabled":{"type":"boolean","description":"Present when the scene soundtrack master switch was set: its value now."},"sceneIndexes":{"type":"array","items":{"type":"integer","minimum":0},"description":"Present when specific defined clips were queued to re-render."},"clip":{"type":"object","additionalProperties":true,"description":"Present when one scene's defined clip was edited — the updated clip record."},"coveragePercent":{"oneOf":[{"type":"number"},{"type":"null"}],"description":"The clamped coverage percent applied (null when none was supplied and none was previously set)."},"totalScenes":{"type":"integer","description":"Number of scenes the read was decomposed into."},"scope":{"type":"string","enum":["screenplay","scene","entry"],"description":"Present when a soundtrack directive update was applied."},"sceneIndex":{"type":"integer","minimum":0,"description":"Present when a scene-level or resolved entry-level soundtrack update was applied."},"entryIndex":{"type":"integer","minimum":0,"description":"Present when an entry-level soundtrack update was applied."},"directive":{"type":"object","additionalProperties":true,"description":"Hidden soundtrack directive that was stored for future adaptive-scene regeneration."},"adaptiveSceneDirection":{"type":"object","additionalProperties":true,"description":"Present when a concrete scene direction was updated. This is the live theater adaptive scene direction."},"sceneDirection":{"oneOf":[{"$ref":"#/components/schemas/SceneMusicDirection"},{"type":"null"}],"description":"Present when a scene's music `direction` was changed: the scene's direction as it now stands."}}},"MusicStatusEnvelope":{"type":"object","required":["music"],"additionalProperties":false,"description":"Adaptive soundtrack status envelope for a table read artifact.","properties":{"music":{"type":"object","required":["artifactId","musicMode","status","soundtrackEnabled","scenesWithMusic","totalScenes","coveragePercent","missingClipSceneIndexes","definedClips","sceneDirections"],"additionalProperties":false,"properties":{"artifactId":{"type":"string"},"musicMode":{"type":"string","enum":["defined_clips"],"description":"Script-level music mode. defined_clips = durable per-scene music beds (the only mode)."},"status":{"$ref":"#/components/schemas/MusicStatus"},"soundtrackEnabled":{"type":"boolean","description":"The read's \"Scene Soundtrack Active\" master switch. false: no scene plays music, `scenesWithMusic` is 0 and `status` is `none`."},"scenesWithMusic":{"type":"integer","description":"Scenes whose music the read plays. Defined clips: the soundtrack is on, the scene is on (not silenced by soundscape coverage), its clip is ready, and the clip is not muted."},"totalScenes":{"type":"integer"},"coveragePercent":{"oneOf":[{"type":"number"},{"type":"null"}],"description":"The soundscape coverage applied to the read (0..1); null when none was applied."},"missingClipSceneIndexes":{"type":"array","items":{"type":"integer","minimum":0},"description":"Scenes coverage keeps on that have no clip to play and none being made (never generated, or failed). Re-render them with `regenerateScenes`."},"definedClips":{"type":"array","description":"Per-scene durable clips (empty for realtime scripts with no clips). Vocals are an explicit per-scene opt-in; clips render instrumental otherwise.","items":{"type":"object","required":["sceneIndex","heading","prompt","vocals","status","soundUrl","error","disabled","fadeInMs","fadeOutMs","playMode","durationMs","anchor","postRollMs","volume","sceneEnabled","playing"],"additionalProperties":false,"properties":{"sceneIndex":{"type":"integer","minimum":0},"heading":{"type":"string"},"prompt":{"type":"string"},"vocals":{"type":"boolean"},"status":{"type":"string","enum":["pending","rendering","ready","failed"]},"soundUrl":{"oneOf":[{"type":"string"},{"type":"null"}]},"error":{"oneOf":[{"type":"string"},{"type":"null"}]},"disabled":{"type":"boolean"},"fadeInMs":{"oneOf":[{"type":"integer"},{"type":"null"}],"description":"Clip fade-in, ms (null → default fade applies)."},"fadeOutMs":{"oneOf":[{"type":"integer"},{"type":"null"}],"description":"Clip fade-out, ms (null → default fade applies)."},"playMode":{"type":"string","enum":["once","loop"],"description":"Playback mode: `once` (default) plays the bed a single time; `loop` repeats it to fill the scene."},"durationMs":{"oneOf":[{"type":"integer","minimum":0},{"type":"null"}],"description":"Measured natural duration of the clip, in milliseconds, when known."},"anchor":{"oneOf":[{"type":"string","enum":["start","end"]},{"type":"null"}],"description":"Play-once placement inside a longer scene. `end` makes an outro finish at the scene boundary; null defaults to start."},"postRollMs":{"oneOf":[{"type":"integer","minimum":0},{"type":"null"}],"description":"Music-only tail after the final spoken line, in milliseconds."},"volume":{"type":"number","minimum":0,"maximum":1,"description":"The level this scene's bed plays at (the default 0.25 when none was set)."},"sceneEnabled":{"type":"boolean","description":"The scene's music switch. false when soundscape coverage, or a per-scene mute, silences the scene."},"playing":{"type":"boolean","description":"The read plays this clip: the soundtrack and the scene are on, the clip is ready, and the clip is not muted."}}}},"sceneDirections":{"type":"array","description":"Each scene's music direction (only scenes that have one), as the Table Read page's scene console sets it. Change one with `sceneIndex` + `direction` on POST.","items":{"$ref":"#/components/schemas/SceneMusicDirection"}}}}}},"SceneMusicDirection":{"type":"object","required":["sceneIndex","heading","enabled","endTransition","summary","instruments","mix","ducking","bpm","density","brightness","guidance","temperature","topK","scale","seed"],"additionalProperties":false,"description":"One scene's music direction.","properties":{"sceneIndex":{"type":"integer","minimum":0},"heading":{"type":"string"},"enabled":{"type":"boolean","description":"The scene's music is on."},"endTransition":{"type":"string","enum":["fadeOut","cut"],"description":"How the music ends at the scene's boundary: `fadeOut` eases it out, `cut` stops it hard."},"summary":{"oneOf":[{"type":"string"},{"type":"null"}],"description":"One line on the music's mood."},"instruments":{"type":"array","items":{"type":"object","required":["text","weight"],"additionalProperties":false,"properties":{"text":{"type":"string"},"weight":{"type":"number"}}}},"mix":{"oneOf":[{"type":"number"},{"type":"null"}],"description":"Music level."},"ducking":{"oneOf":[{"type":"number"},{"type":"null"}],"description":"How far the music drops under dialogue."},"bpm":{"oneOf":[{"type":"number"},{"type":"null"}]},"density":{"oneOf":[{"type":"number"},{"type":"null"}]},"brightness":{"oneOf":[{"type":"number"},{"type":"null"}]},"guidance":{"oneOf":[{"type":"number"},{"type":"null"}]},"temperature":{"oneOf":[{"type":"number"},{"type":"null"}]},"topK":{"oneOf":[{"type":"number"},{"type":"null"}]},"scale":{"oneOf":[{"type":"string"},{"type":"null"}]},"seed":{"oneOf":[{"type":"number"},{"type":"null"}]}}},"SceneMusicDirectionUpdate":{"type":"object","additionalProperties":false,"minProperties":1,"description":"A scene's music direction, field by field: fields left out are kept, `instruments` replaces the palette. Saving drops the scene's rendered bed so the next render plays the new direction. Bounds: bpm 60–180, mix 0–1, ducking 0.1–1, topK 1–100, at most 5 instruments.","properties":{"enabled":{"type":"boolean","description":"The scene's music on or off."},"endTransition":{"type":"string","enum":["fadeOut","cut"],"description":"`fadeOut` eases the music out at the scene's end; `cut` stops it hard."},"summary":{"type":"string","maxLength":400},"instruments":{"type":"array","maxItems":5,"items":{"type":"object","required":["text","weight"],"additionalProperties":false,"properties":{"text":{"type":"string","minLength":1,"maxLength":240,"description":"An instrument, timbre or atmosphere, e.g. \"Cello\", \"Ominous Drone\"."},"weight":{"type":"number","minimum":0,"maximum":2}}}},"mix":{"type":"number","minimum":0,"maximum":1},"ducking":{"type":"number","minimum":0.1,"maximum":1},"bpm":{"type":"number","minimum":60,"maximum":180},"density":{"type":"number","minimum":0,"maximum":1},"brightness":{"type":"number","minimum":0,"maximum":1},"guidance":{"type":"number","minimum":0,"maximum":6},"temperature":{"type":"number","minimum":0,"maximum":3},"topK":{"type":"integer","minimum":1,"maximum":100},"scale":{"type":"string","enum":["SCALE_UNSPECIFIED","C_MAJOR_A_MINOR","D_FLAT_MAJOR_B_FLAT_MINOR","D_MAJOR_B_MINOR","E_FLAT_MAJOR_C_MINOR","E_MAJOR_D_FLAT_MINOR","F_MAJOR_D_MINOR","G_FLAT_MAJOR_E_FLAT_MINOR","G_MAJOR_E_MINOR","A_FLAT_MAJOR_F_MINOR","A_MAJOR_G_FLAT_MINOR","B_FLAT_MAJOR_G_MINOR","B_MAJOR_A_FLAT_MINOR"]},"seed":{"type":"integer"}}},"TableReadVisualStyle":{"type":"object","additionalProperties":false,"description":"A theater look: gradient, colors, filter, animation, lighting, texture and intensity.","properties":{"bgGradient":{"type":"string"},"ambientColor":{"type":"string"},"textColor":{"type":"string"},"accentColor":{"type":"string"},"shadowColor":{"type":"string"},"filter":{"type":"string"},"animation":{"type":"string"},"lighting":{"type":"string"},"texture":{"type":"string"},"intensity":{"type":"number","minimum":0,"maximum":1},"textRevealStyle":{"type":"string","enum":["fade","drift","spotlight"],"description":"How on-screen text appears."}}},"TableReadFxCue":{"type":"object","required":["entryIndex","kind","origin"],"additionalProperties":false,"description":"One atmospheric, non-text FX cue on a line. At most 24 are kept per scene; out-of-range values are clamped.","properties":{"id":{"type":"string"},"entryIndex":{"type":"integer","minimum":0,"description":"The line the effect fires on."},"kind":{"type":"string","enum":["light-flash","hot-white","strobe","pulse","color-wash","texture-shift","fabric-tear","blood-splatter","lightning","weather","darkness","shroud","ember","vignette-pulse"]},"origin":{"type":"string","enum":["tl","tr","bl","br","top","bottom","left","right","edges","full"],"description":"The corner or edge it comes from, every edge, or the full frame."},"color":{"type":"string","description":"Hex tint, e.g. \"#ef4444\"."},"intensity":{"type":"number","minimum":0,"maximum":1},"durationMs":{"type":"number","description":"How long it plays (150–8000 ms)."},"delayMs":{"type":"number","description":"Delay after the line before it starts (0–6000 ms)."},"easing":{"type":"string","enum":["ease-in","ease-out","ease-in-out","linear"]}}},"TableReadTheaterScene":{"type":"object","required":["sceneIndex","heading","mood","summary","textRevealStyle","visualStyle","transitionType","transitionDurationMs","transitionPrompt","backdrop","fxCues","motionBeatCount"],"additionalProperties":false,"description":"One scene's theater.","properties":{"sceneIndex":{"type":"integer","minimum":0},"heading":{"type":"string"},"mood":{"oneOf":[{"type":"string"},{"type":"null"}]},"summary":{"oneOf":[{"type":"string"},{"type":"null"}]},"textRevealStyle":{"oneOf":[{"type":"string","enum":["fade","drift","spotlight"]},{"type":"null"}]},"visualStyle":{"type":"object","additionalProperties":true},"transitionType":{"oneOf":[{"type":"string","enum":["crossfade","spotlight","curtain","drift","hard-cut"]},{"type":"null"}]},"transitionDurationMs":{"oneOf":[{"type":"number"},{"type":"null"}]},"transitionPrompt":{"oneOf":[{"type":"string"},{"type":"null"}]},"backdrop":{"type":"object","required":["prompt","mediaPreference","status","imageUrl","videoUrl","error"],"additionalProperties":false,"properties":{"prompt":{"oneOf":[{"type":"string"},{"type":"null"}]},"mediaPreference":{"oneOf":[{"type":"string","enum":["image","video"]},{"type":"null"}]},"status":{"type":"string","enum":["idle","queued","ready","failed"]},"imageUrl":{"oneOf":[{"type":"string"},{"type":"null"}]},"videoUrl":{"oneOf":[{"type":"string"},{"type":"null"}]},"error":{"oneOf":[{"type":"string"},{"type":"null"}]}}},"fxCues":{"type":"array","items":{"$ref":"#/components/schemas/TableReadFxCue"}},"motionBeatCount":{"type":"integer","minimum":0,"description":"Motion graphics directed on this scene (POST /artifacts/{artifactId}/theater-motion)."}}},"TableReadTheaterResponse":{"type":"object","required":["artifactId","presentation","styleSets","scenes"],"additionalProperties":false,"description":"The whole read's theater presentation, its style sets, and every scene's theater.","properties":{"artifactId":{"type":"string"},"presentation":{"type":"object","additionalProperties":true,"description":"Every presentation field resolved to what plays: theaterModeDefault, backgroundAnimationEnabled, adaptiveMusicEnabled, adaptiveStyleEnabled, activeStyleSetId, fullscreenChrome, globalSceneTheme, scriptPaneTheme, fontTheme, titleScreen, styleSets, nounImagery."},"styleSets":{"type":"array","items":{"type":"object","required":["id","name","description","source"],"additionalProperties":false,"properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"oneOf":[{"type":"string"},{"type":"null"}]},"source":{"type":"string","enum":["preset","manual","slug"]}}}},"scenes":{"type":"array","items":{"$ref":"#/components/schemas/TableReadTheaterScene"}}}},"TableReadTheaterUpdateRequest":{"type":"object","additionalProperties":false,"minProperties":1,"description":"A partial update of the whole read's presentation. Keys left out are untouched; theme objects merge field by field.","properties":{"theaterModeDefault":{"type":"boolean","description":"Open the read in theater mode by default."},"backgroundAnimationEnabled":{"type":"boolean","description":"Moving backdrops on or off."},"adaptiveStyleEnabled":{"type":"boolean","description":"Scene visuals (each scene's own motifs, overlay and accents) on or off."},"applyStyleSetId":{"type":"string","description":"Apply a built-in or saved style set (GET lists them) and mark it active."},"styleSets":{"type":"array","description":"Replaces the saved custom style sets.","items":{"type":"object","required":["id","name"],"additionalProperties":true,"properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":"string"}}}},"fullscreenChrome":{"type":"object","additionalProperties":false,"properties":{"autoHideEnabled":{"type":"boolean"},"idleDelayMs":{"type":"number","minimum":300,"maximum":8000},"hoverRevealHeight":{"type":"number","minimum":12,"maximum":72}}},"globalSceneTheme":{"type":"object","additionalProperties":false,"properties":{"mediaPreference":{"type":"string","enum":["image","video"]},"transitionType":{"type":"string","enum":["crossfade","spotlight","curtain","drift","hard-cut"]},"transitionDurationMs":{"type":"number","minimum":150,"maximum":4000},"transitionEasing":{"type":"string"},"artDirection":{"type":"string"},"visualStyle":{"$ref":"#/components/schemas/TableReadVisualStyle"}}},"scriptPaneTheme":{"type":"object","additionalProperties":false,"properties":{"layoutMode":{"type":"string","enum":["glass_panel","stage_captions","cinematic_dialogue"]},"dialoguePresentation":{"type":"string","enum":["bubbles","captions","floating"]},"bubbleStyle":{"type":"string","enum":["soft","glass","outline"]},"paneOpacity":{"type":"number","minimum":0.12,"maximum":1},"paneBlurPx":{"type":"number","minimum":0,"maximum":36},"paneBorderOpacity":{"type":"number","minimum":0,"maximum":1},"paneShadowStrength":{"type":"number","minimum":0,"maximum":1},"isTransparent":{"type":"boolean"},"backgroundGradient":{"type":"string"}}},"fontTheme":{"type":"object","additionalProperties":false,"properties":{"headingFontId":{"type":"string"},"dialogueFontId":{"type":"string"},"headingScale":{"type":"number","minimum":0.8,"maximum":1.5},"dialogueScale":{"type":"number","minimum":0.8,"maximum":1.4},"headingWeight":{"type":"string"},"dialogueWeight":{"type":"string"},"letterSpacingEm":{"type":"number","minimum":0,"maximum":0.1},"lineHeight":{"type":"number","minimum":1,"maximum":2}}},"titleScreen":{"type":"object","additionalProperties":false,"properties":{"layout":{"type":"string","enum":["center","stage_left","lower_third"]},"titleOverride":{"type":"string"},"subtitle":{"type":"string"}}},"nounImagery":{"type":"object","required":["displayMode"],"additionalProperties":false,"properties":{"displayMode":{"type":"string","enum":["insets","replace_backdrop","off"]}}}}},"TableReadTheaterUpdateResponse":{"type":"object","required":["artifactId","presentation"],"additionalProperties":false,"properties":{"artifactId":{"type":"string"},"presentation":{"type":"object","additionalProperties":true,"description":"The whole read's presentation as it now stands."}}},"TableReadTheaterSceneRequest":{"type":"object","required":["sceneIndex"],"additionalProperties":false,"description":"One scene's theater. Fields left out are kept.","properties":{"sceneIndex":{"type":"integer","minimum":0},"textRevealStyle":{"type":"string","enum":["fade","drift","spotlight"]},"summary":{"type":"string","maxLength":2000,"description":"A short note on the scene's theater treatment."},"backdropPrompt":{"type":"string","maxLength":4000,"description":"The backdrop prompt: the setting only, no characters, no on-screen text."},"mediaPreference":{"type":"string","enum":["image","video"]},"transitionType":{"type":"string","enum":["crossfade","spotlight","curtain","drift","hard-cut"]},"transitionDurationMs":{"type":"number","minimum":150,"maximum":4000},"transitionPrompt":{"type":"string","maxLength":2000,"description":"How the scene arrives from the one before it."},"visualStyle":{"$ref":"#/components/schemas/TableReadVisualStyle"},"fxCues":{"type":"array","items":{"$ref":"#/components/schemas/TableReadFxCue"},"description":"Replaces the scene's FX cues; [] clears them."}}},"TableReadTheaterSceneResponse":{"type":"object","required":["artifactId","scene"],"additionalProperties":false,"properties":{"artifactId":{"type":"string"},"scene":{"type":"object","required":["sceneIndex","heading","textRevealStyle","backdropPrompt","transitionType","transitionDurationMs","fxCues"],"additionalProperties":false,"properties":{"sceneIndex":{"type":"integer","minimum":0},"heading":{"type":"string"},"textRevealStyle":{"oneOf":[{"type":"string"},{"type":"null"}]},"backdropPrompt":{"oneOf":[{"type":"string"},{"type":"null"}]},"transitionType":{"oneOf":[{"type":"string"},{"type":"null"}]},"transitionDurationMs":{"oneOf":[{"type":"number"},{"type":"null"}]},"fxCues":{"oneOf":[{"type":"array","items":{"$ref":"#/components/schemas/TableReadFxCue"}},{"type":"null"}]}}}}},"TableReadFxCuesRequest":{"type":"object","required":["sceneIndex"],"additionalProperties":false,"properties":{"sceneIndex":{"type":"integer","minimum":0}}},"TableReadFxCuesResponse":{"type":"object","required":["artifactId","sceneIndex","fxCues"],"additionalProperties":false,"properties":{"artifactId":{"type":"string"},"sceneIndex":{"type":"integer","minimum":0},"fxCues":{"type":"array","items":{"$ref":"#/components/schemas/TableReadFxCue"}}}},"TableReadBackdropPromptsRequest":{"type":"object","additionalProperties":false,"properties":{"sceneIndexes":{"type":"array","items":{"type":"integer","minimum":0},"description":"Only these scenes. Omit for every scene."},"force":{"type":"boolean","description":"Write over a prompt a scene has."}}},"TableReadBackdropPromptsResponse":{"type":"object","required":["artifactId","written","skipped","totalScenes"],"additionalProperties":false,"properties":{"artifactId":{"type":"string"},"written":{"type":"array","items":{"type":"object","required":["sceneIndex","heading","prompt"],"additionalProperties":false,"properties":{"sceneIndex":{"type":"integer","minimum":0},"heading":{"type":"string"},"prompt":{"type":"string"}}}},"skipped":{"type":"array","items":{"type":"integer","minimum":0},"description":"Scenes that kept the prompt they had, or got none."},"totalScenes":{"type":"integer","minimum":0}}},"TableReadNounImageryResponse":{"type":"object","required":["artifactId","displayMode","annotatedLines","counts","nouns"],"additionalProperties":false,"properties":{"artifactId":{"type":"string"},"displayMode":{"type":"string","enum":["insets","replace_backdrop","off"],"description":"How spoken-noun images appear: `insets` over the backdrop, `replace_backdrop`, or `off`. Set it with POST /artifacts/{artifactId}/theater `nounImagery`."},"annotatedLines":{"type":"integer","minimum":0,"description":"Lines that name at least one depictable noun."},"counts":{"type":"object","required":["ready","queued","failed"],"additionalProperties":false,"properties":{"ready":{"type":"integer"},"queued":{"type":"integer"},"failed":{"type":"integer"}}},"nouns":{"type":"array","items":{"type":"object","required":["normalizedNoun","displayLabel","status","imageUrl","error","lastGeneratedAt"],"additionalProperties":false,"properties":{"normalizedNoun":{"type":"string","description":"The noun's key (lowercase, no leading article): pass it as `noun.normalizedNoun` to draw it again."},"displayLabel":{"type":"string"},"status":{"type":"string","enum":["queued","ready","failed"]},"imageUrl":{"oneOf":[{"type":"string"},{"type":"null"}]},"error":{"oneOf":[{"type":"string"},{"type":"null"}]},"lastGeneratedAt":{"oneOf":[{"type":"string"},{"type":"null"}]}}}}}},"TableReadNounImageryRequest":{"type":"object","additionalProperties":false,"description":"Empty (or `force: true` to draw every noun again) builds the library; `noun` draws one noun again.","properties":{"force":{"type":"boolean"},"noun":{"type":"object","required":["normalizedNoun"],"additionalProperties":false,"properties":{"normalizedNoun":{"type":"string","minLength":1,"maxLength":200},"displayLabel":{"type":"string","maxLength":200},"kind":{"type":"string","enum":["object","location","entity"],"description":"What the noun is (it decides the framing). Default object."}}}}},"TableReadNounImageryQueued":{"type":"object","required":["artifactId","status","chatToolJobId"],"additionalProperties":false,"properties":{"artifactId":{"type":"string"},"status":{"type":"string","enum":["queued"]},"chatToolJobId":{"type":"string"},"force":{"type":"boolean","description":"Present when the library was queued."},"normalizedNoun":{"type":"string","description":"Present when one noun was queued."}}},"TableReadSoundscape":{"type":"object","required":["kind","id","sceneIndex","label","startEntry","endEntry","soundUrl","libraryAssetId","volume","fadeInMs","fadeOutMs","playMode","isDraft"],"additionalProperties":false,"description":"One soundscape: a scene's own bed, or a custom region across lines.","properties":{"kind":{"type":"string","enum":["scene_bed","region"]},"id":{"type":"string","description":"The scene index for a bed, the region id for a region."},"sceneIndex":{"oneOf":[{"type":"integer","minimum":0},{"type":"null"}]},"label":{"type":"string"},"startEntry":{"type":"integer","minimum":0},"endEntry":{"type":"integer","minimum":0},"soundUrl":{"oneOf":[{"type":"string"},{"type":"null"}]},"libraryAssetId":{"oneOf":[{"type":"string"},{"type":"null"}]},"volume":{"oneOf":[{"type":"number"},{"type":"null"}]},"fadeInMs":{"oneOf":[{"type":"number"},{"type":"null"}]},"fadeOutMs":{"oneOf":[{"type":"number"},{"type":"null"}]},"playMode":{"oneOf":[{"type":"string","enum":["loop","stretch"]},{"type":"null"}]},"isDraft":{"type":"boolean","description":"A region not yet given a sound."}}},"TableReadSoundscapesResponse":{"type":"object","required":["artifactId","soundscapes"],"additionalProperties":false,"properties":{"artifactId":{"type":"string"},"soundscapes":{"type":"array","items":{"$ref":"#/components/schemas/TableReadSoundscape"}}}},"TableReadSoundscapeRequest":{"type":"object","required":["op"],"additionalProperties":false,"description":"One soundscape edit by `op`. `update_bed` needs `sceneIndex`; `add_region` needs `startEntry` and `endEntry`; `update_region` needs `regionId`; `attach` needs `target` (`scene_bed` with `sceneIndex`, or `region` with `regionId`) and `assetId`; `remove` needs `target` and its `sceneIndex` or `regionId`.","properties":{"op":{"type":"string","enum":["update_bed","add_region","update_region","attach","remove"]},"target":{"type":"string","enum":["scene_bed","region"]},"sceneIndex":{"type":"integer","minimum":0},"regionId":{"type":"string"},"assetId":{"type":"string","description":"A library audio asset (GET /library/audio)."},"startEntry":{"type":"integer","minimum":0},"endEntry":{"type":"integer","minimum":0},"startEntryOffset":{"type":"integer","minimum":0,"description":"A scene bed: lines after the scene's first line it starts."},"endEntryOffset":{"type":"integer","minimum":0,"description":"A scene bed: lines before the scene's last line it ends."},"label":{"type":"string","maxLength":200},"volume":{"type":"number","minimum":0,"maximum":1},"fadeInMs":{"type":"integer","minimum":0,"maximum":20000},"fadeOutMs":{"type":"integer","minimum":0,"maximum":20000},"playMode":{"type":"string","enum":["loop","stretch"]}}},"TableReadSoundscapeResponse":{"type":"object","required":["artifactId","op","target","id","soundscape"],"additionalProperties":false,"properties":{"artifactId":{"type":"string"},"op":{"type":"string","enum":["update_bed","add_region","update_region","attach","remove"]},"target":{"type":"string","enum":["scene_bed","region"]},"id":{"type":"string"},"soundscape":{"oneOf":[{"$ref":"#/components/schemas/TableReadSoundscape"},{"type":"null"}],"description":"The bed or region as it now stands (null for a removed region)."}}},"TableReadAvatarVideoRequest":{"type":"object","required":["character"],"additionalProperties":false,"properties":{"character":{"type":"string","minLength":1,"maxLength":200,"description":"The character whose portrait comes alive (never the narrator)."},"description":{"type":"string","maxLength":600,"description":"Optional direction for the motion."}}},"TableReadAvatarVideoResponse":{"type":"object","required":["artifactId","character","status","chatToolJobId"],"additionalProperties":false,"properties":{"artifactId":{"type":"string"},"character":{"type":"string"},"status":{"type":"string","enum":["queued"]},"chatToolJobId":{"type":"string"}}},"VoiceModificationRequest":{"type":"object","required":["startEntryIndex","endEntryIndex"],"additionalProperties":false,"description":"Apply a voice effect to a contiguous range of dialogue entries. The autotune recipe is optional and partial — any omitted field falls back to the proven defaults (key D, scale minpent, strength 1.0, smooth 1, reverb chapel).","properties":{"startEntryIndex":{"type":"integer","minimum":0,"description":"First dialogue-entry index in the range (inclusive)."},"endEntryIndex":{"type":"integer","minimum":0,"description":"Last dialogue-entry index in the range (inclusive)."},"effect":{"type":"string","enum":["autotune"],"default":"autotune","description":"Voice effect to apply. Defaults to autotune (the only effect today)."},"params":{"type":"object","additionalProperties":false,"description":"Partial autotune recipe; omitted fields fall back to the defaults.","properties":{"key":{"type":"string","enum":["C","C#","D","D#","E","F","F#","G","G#","A","A#","B"],"default":"D"},"scale":{"type":"string","enum":["major","minor","majpent","minpent","chromatic"],"default":"minpent"},"strength":{"type":"number","minimum":0,"maximum":1,"default":1,"description":"Pitch-correction strength, 0 (off) to 1 (full snap)."},"smooth":{"type":"integer","minimum":1,"default":1,"description":"Pitch-glide smoothing; higher = slower glide."},"reverb":{"type":"string","enum":["none","light","chapel"],"default":"chapel"}}}}},"VoiceModificationResponse":{"type":"object","required":["artifactId","modificationId","status"],"additionalProperties":false,"description":"Acknowledgement that a voice modification has been dispatched. Poll GET /artifacts/{artifactId} and read the matching record on `manifest.audio.modifications`: the rendered audio is projected onto entries once its `status` reaches `ready`, and its `error` carries the reason when it reaches `failed`.","properties":{"artifactId":{"type":"string"},"modificationId":{"type":"string"},"status":{"type":"string","enum":["queued","rendering","ready","failed"],"description":"Lifecycle of the modification record."}}},"ArtifactVoiceModification":{"type":"object","required":["id","type","startEntryIndex","endEntryIndex","status","createdAt","updatedAt"],"additionalProperties":true,"description":"One voice modification applied to a contiguous entry range, as it appears on `manifest.audio.modifications`. Renders are asynchronous and queued: poll GET /artifacts/{id} until `status` is `ready` or `failed`. More than one record can exist for the same range (a retry appends); the newest by `updatedAt` is authoritative.","properties":{"id":{"type":"string","description":"Matches the `modificationId` returned when the modification was dispatched."},"type":{"type":"string","enum":["autotune","performance-booth"],"description":"Effect applied, or `performance-booth` for a speech-to-speech override."},"startEntryIndex":{"type":"integer","minimum":0},"endEntryIndex":{"type":"integer","minimum":0},"status":{"type":"string","enum":["queued","rendering","ready","failed"],"description":"Lifecycle of this record. `failed` is terminal for this record; re-dispatch the range to retry."},"error":{"oneOf":[{"type":"string"},{"type":"null"}],"description":"Human-readable reason the render failed when `status` is `failed` — including upstream TTS provider errors such as an exhausted credit balance. Read this rather than inferring a cause from a poll timeout: a consumer that tracks only `status` cannot tell a provider quota cliff from a slow render."},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time","description":"Use this to pick the newest record when a range has more than one."}}},"SfxCue":{"type":"object","required":["id","label","entryIndex","soundUrl","isDraft"],"additionalProperties":true,"description":"Timed sound-effect cue on the read backing a table-read artifact.","properties":{"id":{"type":"string"},"label":{"type":"string"},"entryIndex":{"type":"integer","minimum":0},"prompt":{"oneOf":[{"type":"string"},{"type":"null"}],"description":"Sound-effect generation prompt when available."},"soundUrl":{"oneOf":[{"type":"string","format":"uri"},{"type":"null"}]},"volume":{"oneOf":[{"type":"number","minimum":0,"maximum":1},{"type":"null"}]},"triggerOffset":{"type":"string","enum":["before","with","after"],"description":"Where the cue fires relative to the target line."},"offsetMs":{"type":"integer","description":"Millisecond timing nudge relative to the trigger placement."},"generatedDurationS":{"oneOf":[{"type":"number","minimum":0.5,"maximum":10},{"type":"null"}]},"isDisabled":{"type":"boolean","description":"True when the cue is muted/excluded from playback and finalized renders."},"enabled":{"type":"boolean","description":"False when the cue is muted/excluded from playback and finalized renders."},"isDraft":{"type":"boolean","description":"True when sound generation has not produced a reusable sound URL yet."}}},"SfxMutationResponse":{"type":"object","additionalProperties":false,"description":"Result of adding, updating, or removing a sound-effect cue.","properties":{"artifactId":{"type":"string"},"cue":{"$ref":"#/components/schemas/SfxCue"},"removed":{"type":"boolean"},"id":{"type":"string"},"label":{"type":"string"}}},"SfxListResponse":{"type":"object","required":["sfx"],"additionalProperties":false,"properties":{"sfx":{"type":"object","required":["artifactId","cues"],"additionalProperties":false,"properties":{"artifactId":{"type":"string"},"cues":{"type":"array","items":{"$ref":"#/components/schemas/SfxCue"}}}}}},"PerformanceDirectiveRequest":{"type":"object","required":["character"],"additionalProperties":false,"description":"Refine the delivery of all of one character's lines. Target a name, \"NARRATOR\" (all narration), or \"ALL\" (every voiced line). Provide at least one of emotions/speed/speedFactor/volume/volumeFactor/pauseAfterMs, and at most one of each speed/volume pair.","properties":{"character":{"type":"string","minLength":1,"description":"Character name, \"NARRATOR\", or \"ALL\"."},"emotions":{"type":"array","items":{"type":"string"},"description":"1–3 emotion tags applied to the matched lines; use [\"neutral\"] to flatten delivery."},"speed":{"type":"number","minimum":0.6,"maximum":1.5,"description":"Absolute speed (1 is the voice's natural pace). Each line is brought inside the range of the provider that speaks it (Cartesia 0.6–1.5, ElevenLabs 0.7–1.2, Deepgram 0.7–1.5); a line whose voice takes no speed setting keeps its pace (`speedUnsupportedLines`). Mutually exclusive with speedFactor."},"speedFactor":{"type":"number","exclusiveMinimum":0,"description":"Relative per-line speed multiplier (1.2 = +20%); each line is brought inside its own provider's range, and a voice with no speed setting keeps its pace. Mutually exclusive with speed."},"volume":{"type":"number","minimum":0.5,"maximum":2,"description":"Absolute volume slider. Mutually exclusive with volumeFactor."},"volumeFactor":{"type":"number","exclusiveMinimum":0,"description":"Relative per-line volume multiplier; each line clamps to 0.5–2.0. Mutually exclusive with volume."},"pauseAfterMs":{"type":"integer","minimum":0,"maximum":5000,"description":"Pause after each matched line, in ms (cadence/breathing room)."},"overrideExisting":{"type":"boolean","description":"When true, also overwrite lines that already carry a non-neutral emotion (needed to flatten). Default false."},"instruction":{"type":"string","description":"Optional natural-language note recorded in the per-line audit trail."}}},"StudioVoiceListResponse":{"type":"object","required":["voices"],"additionalProperties":false,"description":"The caller's reusable studio voices — designed and cloned — with provider and source tagging.","properties":{"voices":{"type":"array","items":{"type":"object","required":["id","voiceId","name","provider","sourceKind","isCloned"],"additionalProperties":true,"properties":{"id":{"type":"string","description":"Studio voice record id."},"voiceId":{"type":"string","description":"Provider voice id — use with cast/voice surfaces alongside `provider`."},"name":{"type":"string"},"description":{"type":"string"},"gender":{"type":"string","nullable":true},"age":{"type":"string","nullable":true},"provider":{"type":"string","enum":["cartesia","elevenlabs","openai","deepgram","hume"],"description":"TTS provider that owns the voice."},"sourceKind":{"type":"string","description":"designed (text-description voice design) | cloned (recorded sample) | cloned_pro (professional clone)."},"isCloned":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"retiresAt":{"type":["string","null"],"format":"date-time","description":"When the voice's provider retires its voices, if it is retiring. Migrate it before then (POST /voice-migrations)."},"retired":{"type":"boolean","description":"True once `retiresAt` has passed: the voice can no longer speak, and is migrated rather than cast."},"consent":{"$ref":"#/components/schemas/StudioVoiceConsent"}}}}}},"StudioVoiceConsent":{"type":"object","required":["status","consentId","recordedAt","subjectType","subjectName","revokedAt"],"additionalProperties":false,"description":"The consent of the person whose voice a CLONE is. A clone speaks only while `status` is `recorded`; `required` (cloned before consent records began) is refused with 409 `voice_consent_required` until its subject confirms through POST /voices/{voiceId}/consent-link; `revoked` is final (409 `voice_consent_revoked`). A designed voice is `not_needed`.","properties":{"status":{"type":"string","enum":["recorded","required","revoked","not_needed"]},"consentId":{"type":["string","null"]},"recordedAt":{"type":["string","null"],"format":"date-time"},"subjectType":{"type":["string","null"],"enum":["self","living_third_party","estate",null],"description":"Whose voice it is: the account holder, another living person, or a deceased person approved by their estate."},"subjectName":{"type":["string","null"]},"revokedAt":{"type":["string","null"],"format":"date-time"}}},"AccountExportStatus":{"type":"object","required":["state","exportId","requestedAt","builtAt","expiresAt","sizeBytes","download","contents","error","keepDays"],"additionalProperties":false,"properties":{"state":{"type":"string","enum":["none","queued","building","ready","failed","expired"]},"exportId":{"type":["string","null"]},"requestedAt":{"type":["string","null"],"format":"date-time"},"builtAt":{"type":["string","null"],"format":"date-time"},"expiresAt":{"type":["string","null"],"format":"date-time","description":"When the archive is deleted."},"sizeBytes":{"type":["integer","null"]},"download":{"type":["object","null"],"required":["url","expiresAt"],"additionalProperties":false,"properties":{"url":{"type":"string","format":"uri","description":"A signed link; it stops working at `expiresAt`. Read the status again for a new one."},"expiresAt":{"type":"string","format":"date-time"}}},"contents":{"type":["object","null"],"additionalProperties":{"type":"integer"}},"error":{"type":["string","null"]},"keepDays":{"type":"integer"}}},"ProjectDeletionResult":{"type":"object","required":["id","deletedAt","restorableUntil","episodesAtPurge"],"additionalProperties":false,"properties":{"id":{"type":"string"},"deletedAt":{"type":"string","format":"date-time"},"restorableUntil":{"type":"string","format":"date-time","description":"Restore before this, or the project is purged."},"episodesAtPurge":{"type":"integer","description":"Episodes that go with the project: private during the window, restored with it or deleted at the purge."}}},"DeletedStoryProject":{"type":"object","required":["id","name","description","deletedAt","restorableUntil","episodesAtPurge"],"additionalProperties":false,"properties":{"id":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"deletedAt":{"type":"string","format":"date-time"},"restorableUntil":{"type":"string","format":"date-time"},"episodesAtPurge":{"type":"integer"}}},"DeletedStoryProjectList":{"type":"object","required":["projects"],"additionalProperties":false,"properties":{"projects":{"type":"array","items":{"$ref":"#/components/schemas/DeletedStoryProject"}}}},"ProjectRestoreResult":{"type":"object","required":["id","name","episodesRestored","episodesSkipped"],"additionalProperties":false,"properties":{"id":{"type":"string"},"name":{"type":"string"},"episodesRestored":{"type":"integer"},"episodesSkipped":{"type":"integer","description":"Episodes not put back: deleted since, or moved to another project."}}},"AccountDeletionRequest":{"type":"object","required":["confirmEmail"],"additionalProperties":false,"properties":{"confirmEmail":{"type":"string","minLength":3,"description":"The account's email address, to confirm whose account is being deleted."}}},"AccountDeletionTopUp":{"type":"object","required":["purchasedAt","credits","usd","refundableUntil"],"additionalProperties":false,"properties":{"purchasedAt":{"type":"string","format":"date-time"},"credits":{"type":"integer"},"usd":{"type":["number","null"]},"refundableUntil":{"type":"string","format":"date-time","description":"The end of the 14-day refund window for unused top-ups."}}},"AccountDeletionStatus":{"type":"object","required":["state","requestedAt","scheduledFor","blockedReason","canRequest","refusal","credits","deletes","keeps","exportNotice","uploadsNotice","graceDays"],"additionalProperties":false,"description":"Where the account's deletion stands, with the credit balance and what deleting removes.","properties":{"state":{"type":"string","enum":["none","scheduled","purging","blocked"]},"requestedAt":{"type":["string","null"],"format":"date-time"},"scheduledFor":{"type":["string","null"],"format":"date-time","description":"When the purge runs; cancellable until then."},"blockedReason":{"type":["string","null"],"description":"Why a purge stopped and needs a person (state `blocked`)."},"canRequest":{"type":"boolean"},"refusal":{"type":["string","null"],"description":"Why this account cannot delete itself, when it cannot."},"credits":{"type":"object","required":["balance","refundableTopUps","notice"],"additionalProperties":false,"properties":{"balance":{"type":"integer"},"refundableTopUps":{"type":"array","items":{"$ref":"#/components/schemas/AccountDeletionTopUp"}},"notice":{"type":"string","description":"The balance, that unused credits are forfeited at the purge, and a refund prompt for any top-up still refundable. Show it before the confirm."}}},"deletes":{"type":"array","items":{"type":"string"}},"keeps":{"type":"string"},"exportNotice":{"type":"string"},"uploadsNotice":{"type":"string"},"graceDays":{"type":"integer"}}},"StudioLikenessConsent":{"type":"object","required":["id","subjectName","subjectType","recordedAt","revokedAt","status"],"additionalProperties":false,"description":"One likeness approval: a real person's agreement to pictures and videos of them for this account (legal gap G6c2).","properties":{"id":{"type":"string"},"subjectName":{"type":"string","description":"Whose likeness it is."},"subjectType":{"type":"string","enum":["self","living_third_party","estate"],"description":"`self`: the account holder; `living_third_party`: the person approved; `estate`: approved by a recently deceased person's estate."},"recordedAt":{"type":"string","format":"date-time"},"revokedAt":{"oneOf":[{"type":"string","format":"date-time"},{"type":"null"}]},"status":{"type":"string","enum":["active","revoked"]}}},"LikenessConsentList":{"type":"object","required":["consents"],"additionalProperties":false,"properties":{"consents":{"type":"array","items":{"$ref":"#/components/schemas/StudioLikenessConsent"}}}},"LikenessConsentLinkResponse":{"type":"object","required":["consentUrl","subjectName","expiresAt"],"additionalProperties":false,"description":"A 7-day, single-use browser link the person named (or their estate) opens to approve. The link is the auth; hand it to them.","properties":{"consentUrl":{"type":"string","format":"uri"},"subjectName":{"type":"string"},"expiresAt":{"type":"string","format":"date-time"}}},"VoiceConsentLinkResponse":{"type":"object","required":["voiceId","consentUrl","expiresAt"],"additionalProperties":false,"description":"A 7-day browser link the person whose voice a clone is opens to confirm their consent (they read a statement aloud and agree). The link is the auth; hand it to them.","properties":{"voiceId":{"type":"string"},"consentUrl":{"type":"string","format":"uri"},"expiresAt":{"type":"string","format":"date-time"}}},"VoiceConsentRevokeResponse":{"type":"object","required":["voiceId","name","revoked","revokedAt","providerDeletionBy","consent"],"additionalProperties":false,"description":"A withdrawn consent: the voice is refused for anything new from now on, and its copy at the provider is deleted by `providerDeletionBy`. Work already published stays up.","properties":{"voiceId":{"type":"string"},"name":{"type":"string"},"revoked":{"type":"integer","description":"How many consent records were withdrawn (0 when none was active)."},"revokedAt":{"type":"string","format":"date-time"},"providerDeletionBy":{"type":["string","null"],"format":"date-time"},"consent":{"$ref":"#/components/schemas/StudioVoiceConsent"}}},"RestorableVoice":{"type":"object","required":["character","historyId","at","previousVoiceId","previousVoiceName","currentVoiceName","recordingRecoverable"],"additionalProperties":false,"description":"A recast that POST /artifacts/{artifactId}/voice/restore can undo.","properties":{"character":{"type":"string"},"historyId":{"type":"string"},"at":{"type":"string","format":"date-time"},"previousVoiceId":{"type":["string","null"],"description":"The voice a restore puts back; null when the character had none."},"previousVoiceName":{"type":["string","null"]},"currentVoiceName":{"type":["string","null"]},"recordingRecoverable":{"type":"boolean","description":"A restore also brings back the recording the read had before the recast."}}},"VoiceRestoreRequest":{"type":"object","additionalProperties":false,"properties":{"character":{"type":"string","description":"The character to restore. Omitted: the newest recast of any character."}}},"VoiceRestoreResponse":{"type":"object","required":["artifactId","character","voice","voiceMap","recordingRestored","finishedMixRestored","videoRestored","canonRestored","restore"],"additionalProperties":false,"properties":{"artifactId":{"type":"string"},"character":{"type":"string"},"voice":{"oneOf":[{"type":"null"},{"type":"object","additionalProperties":true}],"description":"The voice the character speaks with now; null when it had none before the recast."},"voiceMap":{"type":"object","additionalProperties":true},"recordingRestored":{"type":"boolean","description":"The read's voices-only track came back."},"finishedMixRestored":{"type":"boolean","description":"The finished MP3 came back; no new finalize is needed."},"videoRestored":{"type":"boolean","description":"The rendered video came back (only while the share link still holds that render)."},"canonRestored":{"type":"boolean","description":"The cast canon voice came back (only while the canon still held the voice the recast pinned)."},"restore":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/RestorableVoice"}],"description":"Restoring again returns to the voice this restore replaced."}}},"LineReviewItem":{"type":"object","additionalProperties":false,"required":["id","jobId","entryIndex","character","lineText","note"],"properties":{"id":{"type":"string"},"jobId":{"type":"string"},"entryIndex":{"type":"integer","minimum":0,"description":"Which line: its index in the read's parsed screenplay."},"character":{"type":["string","null"],"description":"Who speaks the line; null for narration."},"lineText":{"type":"string","description":"The line as it stood when it was flagged."},"note":{"type":["string","null"],"description":"The writer's note on why it needs review."},"createdAt":{"type":"string"}}},"LineReviewList":{"type":"object","additionalProperties":false,"required":["artifactId","status","reviewItems"],"properties":{"artifactId":{"type":"string"},"status":{"type":["string","null"],"description":"The read's status, for polling."},"reviewItems":{"type":"array","items":{"$ref":"#/components/schemas/LineReviewItem"},"description":"The flagged lines and their notes."}}},"LineReviewFlag":{"type":"object","additionalProperties":false,"required":["entryIndex","lineText"],"description":"One line to flag for review. One flag per line: flagging it again replaces its note.","properties":{"entryIndex":{"type":"integer","minimum":0,"description":"Which line: its index in the read's parsed screenplay."},"lineText":{"type":"string","minLength":1,"description":"The line's text as it stands."},"character":{"type":["string","null"],"description":"Who speaks the line; null or omitted for narration."},"note":{"type":["string","null"],"description":"The writer's note on why it needs review."}}},"LineReviewFlagResult":{"type":"object","additionalProperties":false,"required":["artifactId","reviewItem"],"properties":{"artifactId":{"type":"string"},"reviewItem":{"$ref":"#/components/schemas/LineReviewItem"}}},"RefineArtifactRequest":{"type":"object","required":["instruction"],"additionalProperties":false,"properties":{"instruction":{"type":"string","minLength":1,"maxLength":5000,"description":"Free-form revision instruction (e.g. \"rename the host to Dana and drop the narrator; make the skeptic warmer\")."},"scope":{"type":"string","enum":["auto","screenplay","plan"],"description":"`auto` (default) lets the model self-route. `screenplay` rewrites cast + screenplay only. `plan` re-grounds against sources first."},"narrationPolicy":{"type":"string","enum":["include","suppress"],"description":"Optional narration-posture override for this revision. Omit to keep the read's current posture."},"confirmed":{"type":"boolean","description":"Omit (or false) to get the price without spending anything. true reserves exactly the quoted TABLE_READ_REFINE and creates the refine job, only after the writer has seen the price."}}},"RefineArtifactEnvelope":{"type":"object","required":["quotedOnly"],"properties":{"quotedOnly":{"type":"boolean","description":"true when this call only PRICED the revision. false (or absent) when it created the job."},"credits":{"type":"integer","minimum":0,"description":"Quote only: the TABLE_READ_REFINE price."},"costLabel":{"type":["string","null"],"description":"Quote only."},"lines":{"type":"array","description":"Quote only: the lines the confirmed call would reserve.","items":{"type":"object","properties":{"label":{"type":"string"},"credits":{"type":"integer","minimum":0}}}},"balanceAfter":{"type":"integer","description":"Quote only. Studio Credits the writer would have left once this is spent: the balance available now (as Billing shows it) minus the price. Negative means they are that many short."},"sufficient":{"type":"boolean","description":"Quote only. True when the available balance covers the price. False means the writer must top up first (the note names the Billing link) — do not confirm; the reserve would refuse."},"note":{"type":"string","description":"Quote only: the price and what it would leave, and that nothing has been spent. Show it to the writer."},"storyJob":{"$ref":"#/components/schemas/StoryJob","description":"Confirmed calls only: the refine job to poll."}}},"RevoiceRequest":{"type":"object","required":["character"],"additionalProperties":false,"properties":{"character":{"type":"string","minLength":1,"description":"The character to re-voice: canon name, alias or script cue."},"voiceId":{"type":"string","description":"The new voice. Omitted: the cast canon voice for this person."},"voiceName":{"type":"string"},"provider":{"type":"string"},"gender":{"type":"string"},"confirmed":{"type":"boolean","description":"Omit (or false) to get the price without spending anything. true reserves and queues the re-voice, only after the writer has seen the price."}}},"RevoiceEnvelope":{"type":"object","required":["quotedOnly","artifactId","character","voice","previousVoice","lines","chars","keptLines","resynthesizedLines","credits","note"],"properties":{"quotedOnly":{"type":"boolean","description":"true when this call only PRICED the re-voice."},"artifactId":{"type":"string"},"character":{"type":"string"},"voice":{"type":"object","properties":{"voiceId":{"type":"string"},"voiceName":{"type":"string"},"provider":{"type":["string","null"]}}},"previousVoice":{"type":"object","properties":{"voiceId":{"type":["string","null"]},"voiceName":{"type":["string","null"]}}},"lines":{"type":"integer","minimum":1,"description":"The character's lines, spoken again."},"chars":{"type":"integer","minimum":0},"keptLines":{"type":"integer","minimum":0,"description":"Lines kept as they are in the current recording."},"resynthesizedLines":{"type":"integer","minimum":0,"description":"Every line spoken again."},"credits":{"type":"integer","minimum":0},"costLabel":{"type":["string","null"],"description":"Quote only."},"balanceAfter":{"type":"integer","description":"Quote only. Studio Credits the writer would have left once this is spent: the balance available now (as Billing shows it) minus the price. Negative means they are that many short."},"sufficient":{"type":"boolean","description":"Quote only. True when the available balance covers the price. False means the writer must top up first (the note names the Billing link) — do not confirm; the reserve would refuse."},"note":{"type":"string","description":"The price and what it would leave (quote), or what happens next (confirmed). Show it to the writer."},"status":{"type":"string","enum":["queued"],"description":"Confirmed calls only. Poll GET /artifacts/{artifactId} until `manifest.audio.finalize.status` is complete."},"restore":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/RestorableVoice"}],"description":"Confirmed calls only: the previous voice and recording, restorable."}}},"RevoiceLineRequest":{"type":"object","required":["entryIndex"],"additionalProperties":false,"description":"Give `speed`, `actingDirection`, or both: the line is spoken once.","properties":{"entryIndex":{"type":"integer","minimum":0,"description":"The line: its index in the read (GET /artifacts/{artifactId}/script)."},"speed":{"type":"number","exclusiveMinimum":0,"description":"A new speed. 1 is the voice's natural pace; 1.15 is 15% faster. Brought inside the line's provider's range (Cartesia 0.6–1.5, ElevenLabs 0.7–1.2, Deepgram 0.7–1.5). Omit to keep the line's speed."},"actingDirection":{"type":["string","null"],"maxLength":500,"description":"A new acting direction: how the line is delivered, in the writer's words (\"quiet, low-key storytelling tone\"). Reaches OpenAI voices as `instructions` and ElevenLabs voices on Eleven v3 or later as a leading audio tag. An empty string or null takes it off. Omit to keep the line's direction."},"confirmed":{"type":"boolean","description":"Omit (or false) to get the quote (free: 0 credits) without changing anything. true does it, only after the writer has seen the quote and said go."}}},"RevoiceLineEnvelope":{"type":"object","required":["quotedOnly","artifactId","entryIndex","character","text","voice","changes","speed","requestedSpeed","clamped","previousSpeed","range","actingDirection","previousActingDirection","direction","mode","keptLines","credits","note"],"properties":{"quotedOnly":{"type":"boolean","description":"true when this call only QUOTED it."},"artifactId":{"type":["string","null"]},"entryIndex":{"type":"integer","minimum":0},"character":{"type":"string","description":"Who speaks the line: its character cue, or NARRATOR."},"text":{"type":"string","description":"The words spoken."},"voice":{"type":"object","properties":{"voiceId":{"type":"string"},"voiceName":{"type":["string","null"]},"provider":{"type":"string"}}},"changes":{"type":"array","items":{"type":"string","enum":["speed","actingDirection"]},"description":"What this changes: the line's speed, its acting direction, or both."},"speed":{"type":"number","description":"The speed the line is spoken at: the one asked for, inside the provider's range (its own, when the speed does not change)."},"requestedSpeed":{"type":"number"},"clamped":{"type":"boolean","description":"true when the speed asked for was outside the provider's range."},"previousSpeed":{"type":"number","description":"The speed the line has now. Setting it back to this brings its current take back."},"range":{"oneOf":[{"type":"null"},{"type":"object","properties":{"min":{"type":"number"},"max":{"type":"number"},"parameter":{"type":"string","description":"The provider's request field the speed is sent in."}}}],"description":"The provider's speed range. Null: what speaks the line has no speed setting (only when the speed does not change)."},"actingDirection":{"type":["string","null"],"description":"The acting direction the line is spoken with (its own, when the direction does not change). Null: none."},"previousActingDirection":{"type":["string","null"],"description":"The direction the line has now. Setting it back to this brings its current take back."},"direction":{"oneOf":[{"type":"null"},{"type":"object","properties":{"channel":{"type":"string","enum":["instructions","audio_tag","emotion"]},"parameter":{"type":"string","description":"Where the provider reads the direction."}}}],"description":"How the direction reaches the voice: a written channel (`instructions`, `audio_tag`) or, on Cartesia sonic-3 and newer, one `generation_config.emotion` mapped from the direction's words. Null: what speaks the line reads no direction (only when the direction does not change)."},"mode":{"type":"string","enum":["revoice","restore_take","save"],"description":"All free. `revoice`: spoken again. `restore_take`: a take it had with this speed and direction comes back. `save`: no recording yet, the speed and direction are saved."},"keptLines":{"type":"integer","minimum":0,"description":"Lines kept as they are in the current recording."},"credits":{"type":"integer","minimum":0,"maximum":0,"description":"Always 0: a line's speed and direction are free."},"costLabel":{"type":["string","null"],"description":"Quote only."},"balanceAfter":{"type":"integer","description":"Quote only. Studio Credits the writer would have left once this is spent: the balance available now (as Billing shows it) minus the price. Negative means they are that many short."},"sufficient":{"type":"boolean","description":"Quote only. True when the available balance covers the price. False means the writer must top up first (the note names the Billing link) — do not confirm; the reserve would refuse."},"note":{"type":"string","description":"What it would do, and that it is free (quote), or what happens next (confirmed). Show it to the writer."},"status":{"type":"string","enum":["queued","saved"],"description":"Confirmed calls only. `queued`: poll GET /artifacts/{artifactId} until `manifest.audio.finalize.status` is complete. `saved`: the speed and direction are saved for the read's recording."}}},"UpdateRecordingRequest":{"type":"object","additionalProperties":false,"properties":{"confirmed":{"type":"boolean","description":"Omit (or false) to get the quote (free: 0 credits) without changing anything. true does it, only after the writer has seen the quote and said go."}}},"UpdateRecordingEnvelope":{"type":"object","required":["quotedOnly","artifactId","mode","mix","changedLines","untrackedLines","respokenLines","pauseChanges","removedLines","keptLines","spliceable","lines","credits","note"],"properties":{"quotedOnly":{"type":"boolean","description":"true when this call only QUOTED it."},"artifactId":{"type":"string"},"mode":{"type":"string","enum":["respeak","restitch","remix","record","current"],"description":"All free. `respeak`: changed lines spoken again, the rest kept. `restitch`: only pauses moved or lines were taken out, nothing spoken. `remix`: only the music or sound moved. `record`: no recording yet. `current`: nothing to update."},"mix":{"type":["string","null"],"enum":["current","stale","unknown",null],"description":"Whether the finished recording carries the read as it is now (its lines and its mix). Null: no finished recording yet."},"changedLines":{"type":"integer","minimum":0,"description":"Lines whose words, voice or delivery (emotion, speed, volume, acting direction) changed since the recording, or that are new."},"untrackedLines":{"type":"integer","minimum":0,"description":"Lines recorded before the read kept track of line changes: spoken again too."},"respokenLines":{"type":"integer","minimum":0,"description":"Lines spoken again in all."},"pauseChanges":{"type":"integer","minimum":0,"description":"Lines whose only change is the pause after them: the track is stitched again, nothing is spoken."},"removedLines":{"type":"integer","minimum":0,"description":"Lines the read no longer speaks: stitched out."},"keptLines":{"type":"integer","minimum":0,"description":"Lines kept from the recording byte for byte."},"spliceable":{"type":"boolean","description":"false when the recording has lines said over each other (mixed into one track), so no line can be cut from it and every line is made again."},"lines":{"type":"array","description":"The lines that change, in read order (the first 25).","items":{"type":"object","required":["entryIndex","character","text","change"],"properties":{"entryIndex":{"type":"integer","minimum":0},"character":{"type":"string","description":"Who says it: the character cue, or NARRATOR."},"text":{"type":"string","description":"Its words (up to 120 characters)."},"change":{"type":"string","enum":["respeak","pause","removed"]}}}},"credits":{"type":"integer","minimum":0,"maximum":0,"description":"Always 0: updating the recording is free."},"costLabel":{"type":["string","null"],"description":"Quote only."},"balanceAfter":{"type":"integer","description":"Quote only. Studio Credits the writer would have left once this is spent: the balance available now (as Billing shows it) minus the price. Negative means they are that many short."},"sufficient":{"type":"boolean","description":"Quote only. True when the available balance covers the price. False means the writer must top up first (the note names the Billing link) — do not confirm; the reserve would refuse."},"note":{"type":"string","description":"What it would do, and that it is free (quote), or what happens next (confirmed). Show it to the writer."},"status":{"type":"string","enum":["queued","complete"],"description":"Confirmed calls only. `queued`: poll GET /artifacts/{artifactId} until `manifest.audio.finalize.status` is complete. `complete`: the recording already was the read."}}},"PrepareTableReadRequest":{"type":"object","additionalProperties":false,"properties":{"confirmed":{"type":"boolean","description":"Omit (or false) to get the price without spending anything. true charges the read once for this screenplay version and adds its sound effects and music — only after the writer has seen the price."}}},"RetryCreativeAnalysisResult":{"type":"object","additionalProperties":false,"required":["success","jobId"],"properties":{"success":{"type":"boolean","description":"Always true on a 200 — a read that is not failed is refused with 409 instead."},"jobId":{"type":"string","description":"The read whose creative analysis is retrying."}}},"RemoveCharacterRequest":{"type":"object","additionalProperties":false,"required":["character"],"properties":{"character":{"type":"string","minLength":1,"description":"The character's name (or cue) as it appears in the read. A name, not an id."}}},"RemoveCharacterResult":{"type":"object","additionalProperties":false,"required":["removed","character"],"properties":{"removed":{"type":"boolean","description":"Always true — removing a character the read no longer has is a no-op that still answers true."},"character":{"type":"string","description":"The character that was removed."}}},"TableReadPrepareEnvelope":{"type":"object","required":["status","quotedOnly","scriptUploadId","tableReadJobId","opKey","label","pageUsage","credits","note"],"properties":{"status":{"type":"string","enum":["quoted","queued","prepared","preparing"],"description":"`quoted`: priced, nothing spent. `queued`: charged (or an earlier attempt's charge used) and its media queued. `prepared`: already prepared for this screenplay version, on any surface — nothing charged. `preparing`: its media is being made now — nothing charged."},"quotedOnly":{"type":"boolean","description":"true when this call only PRICED the preparation."},"scriptUploadId":{"type":"string"},"tableReadJobId":{"type":["string","null"],"description":"The read it prepares. Null on a quote for a version that has no read yet; the confirmed call makes one."},"opKey":{"type":"string","enum":["TABLE_READ_SHORT","TABLE_READ_LONG"],"description":"The table-read price it is charged at, by the screenplay's pages (GET /credits lists both)."},"label":{"type":"string","description":"The price list's name for it."},"pageUsage":{"type":"integer","minimum":0,"description":"The pages it is priced by (0 when the screenplay's length was never measured)."},"credits":{"type":"integer","minimum":0,"description":"The price (quote), what was charged (queued), or 0."},"charged":{"type":"boolean","description":"Not on a quote. true when this call charged the credit; false when nothing was charged."},"costLabel":{"type":["string","null"],"description":"Quote only."},"balanceAfter":{"type":"integer","description":"Quote only. Studio Credits the writer would have left once this is spent: the balance available now (as Billing shows it) minus the price. Negative means they are that many short."},"sufficient":{"type":"boolean","description":"Quote only. True when the available balance covers the price. False means the writer must top up first (the note names the Billing link) — do not confirm; the reserve would refuse."},"note":{"type":"string","description":"The price and what it would leave (quote), or what happened. Show it to the writer."}}},"CastVoiceDesignRequest":{"type":"object","required":["character"],"additionalProperties":false,"properties":{"character":{"type":"string","minLength":1,"description":"The cast member: canon name, alias or script cue."},"description":{"type":"string","maxLength":1000,"description":"How the voice should sound. Omitted: the Series Bible's voice line for this person."},"candidates":{"type":"integer","minimum":1,"maximum":4,"description":"Candidate voices to draw (default 3). Each is charged."},"previewLines":{"type":"integer","minimum":0,"maximum":3,"description":"More of the character's lines each take speaks after the design line (default 2). When the lines are too short for a take to clone a voice from, more of their lines are added."},"sampleText":{"type":"string","maxLength":500,"description":"A line of your own to design on, for a person with no lines in the screenplays yet."},"gender":{"type":"string"},"age":{"type":"string"},"confirmed":{"type":"boolean","description":"Omit (or false) to get the price without spending anything. true draws the candidates, only after the writer has seen the price."}}},"CastVoiceDesignPreview":{"type":"object","required":["text","url","scriptUploadId","entryIndex"],"properties":{"text":{"type":"string","description":"The words the take says: the design line, then the preview lines."},"url":{"type":"string","description":"Playable MP3 of the take."},"scriptUploadId":{"type":["string","null"],"description":"The screenplay the line is from; null for `sampleText`."},"entryIndex":{"type":["integer","null"]}}},"CastVoiceDesign":{"type":"object","required":["designId","projectId","character","description","createdAt","candidates","chosen"],"properties":{"designId":{"type":"string"},"projectId":{"type":"string"},"character":{"type":"string"},"description":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"candidates":{"type":"array","items":{"type":"object","required":["index","label","previewId","voiceId","provider","previews"],"properties":{"index":{"type":"integer","minimum":1,"description":"The number POST .../cast-voice/choose takes."},"label":{"type":"string"},"previewId":{"type":["string","null"],"description":"The take's preview id at the voice designer (ElevenLabs Voice Design); null for a candidate drawn before the designer changed, which cannot be chosen."},"voiceId":{"type":["string","null"],"description":"The voice the candidate became once chosen (cloned from its take); null until it is chosen."},"provider":{"type":"string","enum":["cartesia","elevenlabs","openai","deepgram","hume"],"description":"The provider that speaks the candidate once chosen: every designed voice is cloned into it."},"previews":{"type":"array","description":"The candidate's take.","items":{"$ref":"#/components/schemas/CastVoiceDesignPreview"}}}}},"chosen":{"oneOf":[{"type":"null"},{"type":"object","required":["index","voiceId"],"properties":{"index":{"type":"integer"},"voiceId":{"type":["string","null"],"description":"Null for a design chosen before the designer changed."}}}]},"coverage":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/CastingCoverage"}],"description":"The design's casting coverage: the ranking, every candidate's scores and flags, and the recommendation (null when the read did not attach it)."}}},"CastingCandidateCoverage":{"type":"object","additionalProperties":true,"required":["candidate","rank","score","passed","recommended","gateReasons","flags","dimensions"],"properties":{"candidate":{"type":"integer","minimum":1,"description":"The number POST .../cast-voice/choose takes."},"rank":{"type":["number","null"],"description":"Median place across the runs (1 = best)."},"ranks":{"type":"array","items":{"type":"integer"}},"score":{"type":["number","null"],"description":"The weighted score the bar reads (0–10)."},"passed":{"type":"boolean"},"recommended":{"type":"boolean"},"gateReasons":{"type":"array","items":{"type":"string"}},"flags":{"type":"array","items":{"type":"string"},"description":"Short flags to lead with: failures first, then warnings."},"dimensions":{"type":"object","additionalProperties":{"type":"object","properties":{"score":{"type":["number","null"]},"scores":{"type":"array","items":{"type":"number"}},"note":{"type":"string"}}},"description":"emblematicFit, truthfulness, periodRegister, accentHonesty, artifacts, wordAccuracy, distinctness (null: not judged)."},"transcript":{"type":["string","null"],"description":"What the take was heard to say, transcribed without the script."},"wordAccuracy":{"type":["number","null"],"description":"Share of the scripted words heard as written (0–1)."},"misheard":{"type":"array","items":{"type":"string"}},"heardAs":{"oneOf":[{"type":"null"},{"type":"object","properties":{"gender":{"type":"string"},"age":{"type":"string"},"accent":{"type":"string"}}}]},"strengths":{"type":"string"},"problems":{"type":"array","items":{"type":"object","properties":{"atSeconds":{"type":"number"},"note":{"type":"string"}}}},"closestCastVoice":{"oneOf":[{"type":"null"},{"type":"object","properties":{"character":{"type":"string"},"note":{"type":"string"}}}]},"speakerSimilarity":{"type":"array","description":"MEASURED speaker-embedding similarity (cosine 0–1) to every voice it was compared with, closest first (empty: embeddings unavailable).","items":{"type":"object","properties":{"character":{"type":"string"},"source":{"type":"string","enum":["chosen","recommended"]},"similarity":{"type":"number"}}}},"soundsLike":{"oneOf":[{"type":"null"},{"type":"object","properties":{"character":{"type":"string"},"similarity":{"type":"number"}}}],"description":"A chosen voice of another character this candidate measures as the same speaker as (at or above `distinctnessThreshold`): it fails on its own."},"excerpt":{"oneOf":[{"type":"null"},{"type":"object","properties":{"startSeconds":{"type":"number"},"endSeconds":{"type":"number"}}}],"description":"The strongest 15–20 s of the take, on sentence boundaries."}}},"CastingCoverage":{"type":"object","additionalProperties":false,"required":["status","kind","projectId","designId","character","keyCharacter","available","gateThreshold","vetoDimensions","distinctnessThreshold","recommended","cast","failingCandidates","summary","trigger","reportId","reportVersion","rubricVersion","model","progressDetail","failureMessage","report","createdAt","updatedAt"],"description":"CASTING COVERAGE of a cast voice design: a model listened to every candidate take blind, beside the voices already cast in the project, and scored each against the brief and the Series Bible. It runs by itself, free to the writer, when a key character's design is drawn. Choosing a candidate in `failingCandidates` is refused unless the writer chooses it anyway; a report still running, one that could not run, or an unavailable listener never holds a choice.","properties":{"status":{"type":"string","enum":["missing","generating","complete","failed"]},"kind":{"type":"string","enum":["voice"]},"projectId":{"type":"string"},"designId":{"type":"string"},"character":{"type":"string"},"keyCharacter":{"type":"boolean","description":"A principal (by the Series Bible or by line count): judged by itself when a design is drawn."},"available":{"type":"boolean","description":"False when no provider can listen right now."},"gateThreshold":{"type":"number"},"vetoDimensions":{"type":"array","items":{"type":"string"}},"distinctnessThreshold":{"type":"number","description":"Speaker-embedding similarity at or above which two voices count as the same speaker (`casting_coverage_policy`, 0.88 by default)."},"recommended":{"type":["integer","null"],"description":"The best candidate that cleared the bar, on its own."},"cast":{"description":"The design's place in the CAST PLAN: across every open design of the project, one candidate each so that no two — and none against a chosen voice — measure as the same speaker. `pick` is the candidate the set takes for this design; `conflicts` the better ones it passed over and who they sound like. Null without a complete, measured report.","oneOf":[{"type":"null"},{"type":"object","properties":{"pick":{"type":["integer","null"]},"feasible":{"type":"boolean","description":"True only when `pick` keeps every voice apart AND clears its own bar. False: no valid set — design again; `note` names the colliding pair."},"conflicts":{"type":"array","items":{"type":"object","properties":{"candidate":{"type":"integer"},"character":{"type":"string"},"similarity":{"type":"number"}}}},"designs":{"type":"integer"},"note":{"type":"string"}}}]},"failingCandidates":{"type":"array","items":{"type":"integer"},"description":"Choosing one needs `acknowledgeCastingCoverage: true`."},"summary":{"type":"string","description":"One sentence to lead with."},"trigger":{"type":["string","null"],"enum":["auto","manual",null]},"reportId":{"type":["string","null"]},"reportVersion":{"type":["integer","null"]},"rubricVersion":{"type":["integer","null"]},"model":{"type":["string","null"]},"progressDetail":{"type":["string","null"]},"failureMessage":{"type":["string","null"]},"report":{"oneOf":[{"type":"null"},{"type":"object","additionalProperties":true,"required":["kind","candidates","ranking","recommended","runnerUp","close","allFail","redesign","verdict","references","judging"],"properties":{"kind":{"type":"string","enum":["voice"]},"candidates":{"type":"array","items":{"$ref":"#/components/schemas/CastingCandidateCoverage"}},"ranking":{"type":"array","items":{"type":"integer"},"description":"Every candidate, best first."},"recommended":{"type":["integer","null"]},"runnerUp":{"type":["integer","null"]},"close":{"type":"boolean","description":"The runner-up sat close to the recommendation: hear both."},"allFail":{"type":"boolean"},"redesign":{"oneOf":[{"type":"null"},{"type":"object","properties":{"why":{"type":"string"},"revisedBrief":{"type":"string","description":"Design again from this (POST .../cast-voice/design with `description`)."}}}]},"verdict":{"type":"string"},"references":{"type":"array","items":{"type":"object","properties":{"character":{"type":"string"},"source":{"type":"string","enum":["chosen","recommended"]},"designId":{"type":"string"},"candidate":{"type":"integer"}}}},"judging":{"type":"object","additionalProperties":true,"description":"How it was reached: each run (its order, ranking, scores and cost), why it stopped, the extra-run spend, the fingerprint, and what was carried."}}}]},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"}}},"CastingCoverageResponse":{"type":"object","required":["coverage"],"additionalProperties":false,"properties":{"coverage":{"$ref":"#/components/schemas/CastingCoverage"}}},"CastingCoverageGenerateRequest":{"type":"object","additionalProperties":false,"properties":{"focusPrompt":{"type":"string","maxLength":2000,"description":"Optional: what to listen for hardest this time."}}},"CastVoiceDesignEnvelope":{"type":"object","required":["quotedOnly","credits","note"],"description":"Quote: the price and the plan (`description`, `descriptionSource`, `candidates` as a count, `designLine`, `previewLines`). Confirmed: a CastVoiceDesign (`candidates` as the drawn list) with `credits` charged.","properties":{"quotedOnly":{"type":"boolean"},"credits":{"type":"integer","minimum":0},"costLabel":{"type":["string","null"],"description":"Quote only."},"balanceAfter":{"type":"integer","description":"Quote only. Studio Credits the writer would have left once this is spent: the balance available now (as Billing shows it) minus the price. Negative means they are that many short."},"sufficient":{"type":"boolean","description":"Quote only. True when the available balance covers the price. False means the writer must top up first (the note names the Billing link) — do not confirm; the reserve would refuse."},"note":{"type":"string","description":"Show it to the writer. The quote says every candidate drawn is charged, chosen or not."},"character":{"type":"string"},"description":{"type":"string"},"descriptionSource":{"type":"string","enum":["request","series_bible"],"description":"Quote only."},"designLine":{"description":"Quote only: the line every take opens on. Null for a person with no lines in the screenplays and no `sampleText`: each take speaks a passage the designer writes for the description.","oneOf":[{"type":"null"},{"type":"object","properties":{"text":{"type":"string"},"scriptUploadId":{"type":["string","null"]},"entryIndex":{"type":["integer","null"]}}}]},"previewLines":{"type":"array","description":"Quote only: the further lines every take speaks after the design line.","items":{"type":"object","properties":{"text":{"type":"string"},"scriptUploadId":{"type":"string"},"entryIndex":{"type":"integer"}}}},"candidates":{"description":"Quote: how many will be drawn. Confirmed: the drawn candidates.","oneOf":[{"type":"integer"},{"type":"array","items":{"type":"object","additionalProperties":true}}]},"designId":{"type":"string","description":"Confirmed only."},"projectId":{"type":"string","description":"Confirmed only."},"createdAt":{"type":"string","format":"date-time","description":"Confirmed only."},"chosen":{"description":"Confirmed only (null until one is chosen).","oneOf":[{"type":"null"},{"type":"object","additionalProperties":true}]}}},"CastVoiceChooseRequest":{"type":"object","required":["designId","candidate"],"additionalProperties":false,"properties":{"designId":{"type":"string","minLength":1},"candidate":{"type":"integer","minimum":1,"description":"1-based candidate number from the design."},"voiceName":{"type":"string","maxLength":100,"description":"Name in your voice library. Default \"<project> - <character>\"."},"acknowledgeCastingCoverage":{"type":"boolean","description":"Choose a candidate its casting coverage FAILED (otherwise refused with 409 `cast_voice_casting_coverage_failed`, `details.castingCoverage` carrying the score, the bar, why and the candidate it recommends). Send true ONLY after the writer has heard it and chosen it anyway — never on your own."}}},"CastVoiceChooseResponse":{"type":"object","required":["projectId","designId","character","voice","reads","note"],"additionalProperties":false,"properties":{"projectId":{"type":"string"},"designId":{"type":"string"},"character":{"type":"string"},"voice":{"type":"object","required":["voiceId","voiceName","provider","gender"],"properties":{"voiceId":{"type":"string"},"voiceName":{"type":"string"},"provider":{"type":"string"},"gender":{"type":["string","null"]}}},"reads":{"type":"array","description":"Finished reads that still speak this character with another voice. Re-voice each with POST /artifacts/{artifactId}/revoice.","items":{"type":"object","required":["artifactId","title","voiceId","voiceName"],"properties":{"artifactId":{"type":"string"},"title":{"type":["string","null"]},"voiceId":{"type":["string","null"]},"voiceName":{"type":["string","null"]}}}},"castingCoverageWarning":{"type":["string","null"],"description":"What casting coverage said about this choice: still running, could not run, another candidate recommended, or chosen past a failure."},"note":{"type":"string"}}},"CastVoiceSetRequest":{"type":"object","required":["character","voiceId"],"additionalProperties":false,"properties":{"character":{"type":"string","minLength":1,"maxLength":200,"description":"The person: canon name, alias or script cue. Anyone on the project cast, in an episode or not; a Series Bible character the canon does not hold yet is added."},"voiceId":{"type":"string","minLength":1,"maxLength":200,"description":"The voice to pin, by its id at its provider (GET /voices lists yours)."},"voiceName":{"type":"string","maxLength":200,"description":"The name the voice is shown under. Default: your voice library's name for it."},"provider":{"type":"string","enum":["cartesia","elevenlabs","openai","deepgram","hume"],"description":"The provider that speaks the voice. Default: the library's, else the configured one. A provider retiring its voices is refused."},"gender":{"type":"string","maxLength":40}}},"CastVoiceSetResponse":{"type":"object","required":["projectId","character","voice","previous","changed","reads","note"],"additionalProperties":false,"properties":{"projectId":{"type":"string"},"character":{"type":"string","description":"The cast canon person the voice is pinned on."},"voice":{"type":"object","required":["voiceId","voiceName","provider","gender"],"properties":{"voiceId":{"type":"string"},"voiceName":{"type":"string"},"provider":{"type":"string"},"gender":{"type":["string","null"]}}},"previous":{"description":"The canon voice this replaced (null: they had none, or nothing changed). Set it again to go back.","oneOf":[{"type":"null"},{"type":"object","required":["voiceId"],"properties":{"voiceId":{"type":"string"},"voiceName":{"type":["string","null"]},"provider":{"type":["string","null"]},"gender":{"type":["string","null"]}}}]},"changed":{"type":"boolean","description":"False when the person already had this voice: nothing was written."},"reads":{"type":"array","description":"Finished reads that still speak this person with another voice. Re-voice each with POST /artifacts/{artifactId}/revoice (priced).","items":{"type":"object","required":["artifactId","title","voiceId","voiceName"],"properties":{"artifactId":{"type":"string"},"title":{"type":["string","null"]},"voiceId":{"type":["string","null"]},"voiceName":{"type":["string","null"]}}}},"note":{"type":"string"}}},"CastVoiceDesignListResponse":{"type":"object","required":["designs"],"additionalProperties":false,"properties":{"designs":{"type":"array","items":{"$ref":"#/components/schemas/CastVoiceDesign"}}}},"CastPortraitDesignRequest":{"type":"object","required":["character"],"additionalProperties":false,"properties":{"character":{"type":"string","minLength":1,"maxLength":200,"description":"The cast member: canon name, alias, script cue or Series Bible name."},"direction":{"type":"string","maxLength":2000,"description":"How they should look, in the writer's words. Omitted: their look direction on the cast canon, else the Series Bible alone."},"age":{"type":"string","maxLength":100,"description":"The age they are played at. Omitted: their cast canon age."},"candidates":{"type":"integer","minimum":1,"maximum":4,"description":"Headshot candidates to draw (default 3). Each is charged."},"keepFace":{"type":"boolean","description":"Restyle the person KEEPING THEIR FACE: each candidate is their current headshot on the cast canon, edited to the look (`direction`, else their saved look) on the model that keeps a face, so the hair, grooming, wardrobe or styling change and the person stays. Same price per candidate, judged by casting coverage and chosen like any headshot. Refused (409 `cast_precondition_failed`, reason `cast_face_missing`) when they have no headshot yet. Omit (or false) for new faces."},"confirmed":{"type":"boolean","description":"Omit (or false) to get the price without spending anything. true reserves and queues the draw, only after the writer has seen the price."}}},"CastPortraitCandidate":{"type":"object","required":["candidateId","index","status","imageUrl","error","drawnAt"],"additionalProperties":false,"properties":{"candidateId":{"type":"string","description":"Stable across reads (`<designId>-c<index>`): what a judge scores."},"index":{"type":"integer","minimum":1,"description":"The number POST .../cast-portraits/choose takes."},"status":{"type":"string","enum":["drawn","failed"]},"imageUrl":{"type":["string","null"],"description":"The headshot. Null for a candidate that could not be drawn (not charged)."},"error":{"type":["string","null"]},"drawnAt":{"type":["string","null"],"format":"date-time"}}},"CastPortraitDesign":{"type":"object","required":["designId","projectId","castPersonId","character","direction","directionSource","age","status","error","candidateCount","candidates","chosen","credits","createdAt","updatedAt","restyledFrom"],"additionalProperties":false,"properties":{"designId":{"type":"string"},"projectId":{"type":"string"},"castPersonId":{"type":["string","null"],"description":"The cast canon person's stable id, when the canon knew them."},"character":{"type":"string"},"direction":{"type":"string","description":"The look the candidates were drawn from."},"directionSource":{"type":"string","enum":["request","cast_canon","series_bible"]},"age":{"type":["string","null"]},"status":{"type":"string","enum":["queued","drawing","complete","failed"]},"error":{"type":["string","null"]},"candidateCount":{"type":"integer","minimum":1,"description":"How many candidates were bought."},"candidates":{"type":"array","items":{"$ref":"#/components/schemas/CastPortraitCandidate"}},"chosen":{"oneOf":[{"type":"null"},{"type":"object","required":["index","candidateId","imageUrl"],"properties":{"index":{"type":"integer"},"candidateId":{"type":"string"},"imageUrl":{"type":"string"}}}]},"credits":{"type":"integer","minimum":0,"description":"Charged for the candidates drawn."},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"coverage":{"oneOf":[{"type":"null"},{"$ref":"#/components/schemas/CastingPortraitCoverage"}],"description":"The design's casting coverage (null when the read did not attach it)."},"restyledFrom":{"type":["string","null"],"description":"A restyle that keeps the face (`keepFace`): the headshot each candidate was edited from. Null for candidates drawn as new faces."}}},"CastPortraitDesignEnvelope":{"type":"object","required":["quotedOnly","credits","note","character","direction","directionSource","candidates"],"description":"Quote: the price and the plan, nothing drawn. Confirmed: `designId` and `status: queued` — poll GET .../cast-portraits/designs?designId= until `status` is `complete`.","properties":{"quotedOnly":{"type":"boolean"},"queued":{"type":"boolean","description":"Confirmed only."},"credits":{"type":"integer","minimum":0},"costLabel":{"type":["string","null"],"description":"Quote only."},"balanceAfter":{"type":"integer","description":"Quote only. Studio Credits the writer would have left once this is spent: the balance available now (as Billing shows it) minus the price. Negative means they are that many short."},"sufficient":{"type":"boolean","description":"Quote only. True when the available balance covers the price. False means the writer must top up first (the note names the Billing link) — do not confirm; the reserve would refuse."},"note":{"type":"string","description":"Show it to the writer. The quote says every candidate drawn is charged, chosen or not."},"character":{"type":"string"},"castPersonId":{"type":["string","null"]},"direction":{"type":"string"},"directionSource":{"type":"string","enum":["request","cast_canon","series_bible"]},"age":{"type":["string","null"]},"candidates":{"type":"integer","description":"How many candidates will be (or are being) drawn."},"restyledFrom":{"type":["string","null"],"description":"A restyle that keeps the face (`keepFace`): the current headshot each candidate is edited from. Null for new faces."},"designId":{"type":"string","description":"Confirmed only."},"status":{"type":"string","enum":["queued","drawing","complete","failed"],"description":"Confirmed only."}}},"CastPortraitChooseRequest":{"type":"object","required":["designId","candidate"],"additionalProperties":false,"properties":{"designId":{"type":"string","minLength":1},"candidate":{"type":"integer","minimum":1,"description":"1-based candidate number from the design."},"acknowledgeCastingCoverage":{"type":"boolean","description":"Choose a headshot its casting coverage FAILED (otherwise 409 `cast_portrait_casting_coverage_failed`, `details.castingCoverage`). Send true ONLY after the writer has seen the failure and chosen it anyway."}}},"CastingPortraitCoverage":{"type":"object","additionalProperties":false,"required":["status","kind","projectId","designId","character","keyCharacter","available","gateThreshold","vetoDimensions","recommended","failingCandidates","summary","trigger","reportId","reportVersion","rubricVersion","model","progressDetail","failureMessage","report","createdAt","updatedAt"],"description":"PORTRAIT CASTING COVERAGE of a portrait design: a model looked at every drawn headshot blind, beside the faces already chosen for the cast, and scored each on emblematic fit, period truth, naturalness (not generated) and distinctness. Choosing a headshot in `failingCandidates` is refused unless the writer chooses it anyway.","properties":{"status":{"type":"string","enum":["missing","generating","complete","failed"]},"kind":{"type":"string","enum":["portrait"]},"projectId":{"type":"string"},"designId":{"type":"string"},"character":{"type":"string"},"keyCharacter":{"type":"boolean"},"available":{"type":"boolean"},"gateThreshold":{"type":"number"},"vetoDimensions":{"type":"array","items":{"type":"string"}},"recommended":{"type":["integer","null"]},"failingCandidates":{"type":"array","items":{"type":"integer"}},"summary":{"type":"string"},"trigger":{"type":["string","null"],"enum":["auto","manual",null]},"reportId":{"type":["string","null"]},"reportVersion":{"type":["integer","null"]},"rubricVersion":{"type":["integer","null"]},"model":{"type":["string","null"]},"progressDetail":{"type":["string","null"]},"failureMessage":{"type":["string","null"]},"report":{"oneOf":[{"type":"null"},{"type":"object","additionalProperties":true,"description":"The ranking, every headshot (`candidate`, `candidateId`, `imageUrl`, `score`, `passed`, `flags`, `dimensions`, `problems`, `closestCastFace`), `recommended`, `runnerUp`, `close`, `allFail`, `redesign` ({ why, revisedDirection }), `verdict`, `references` and `judging`."}]},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":["string","null"],"format":"date-time"}}},"CastingPortraitCoverageResponse":{"type":"object","required":["coverage"],"additionalProperties":false,"properties":{"coverage":{"$ref":"#/components/schemas/CastingPortraitCoverage"}}},"CastPortraitChooseResponse":{"type":"object","required":["projectId","designId","character","candidate","cleared","previous","note"],"additionalProperties":false,"properties":{"projectId":{"type":"string"},"designId":{"type":"string"},"character":{"type":"string"},"candidate":{"type":"object","required":["index","candidateId","imageUrl"],"properties":{"index":{"type":"integer"},"candidateId":{"type":"string"},"imageUrl":{"type":"string"}}},"cleared":{"type":"array","items":{"type":"string","enum":["body","sheet"]},"description":"Canon pictures drawn from the previous face, cleared by the choice: draw them again with POST /story-projects/{projectId}/cast for this character."},"castingCoverageWarning":{"type":["string","null"],"description":"What casting coverage said about this choice: still running, could not run, another recommended, or chosen past a failure."},"previous":{"type":"object","required":["face","body","sheet"],"properties":{"face":{"type":["string","null"]},"body":{"type":["string","null"]},"sheet":{"type":["string","null"]}}},"note":{"type":"string"}}},"CastPortraitDesignListResponse":{"type":"object","required":["designs"],"additionalProperties":false,"properties":{"designs":{"type":"array","items":{"$ref":"#/components/schemas/CastPortraitDesign"}}}},"CastLookEntry":{"type":"object","required":["name","id","aliases","lookDirection","age","inCanon","bible"],"additionalProperties":false,"properties":{"name":{"type":"string"},"id":{"type":["string","null"],"description":"The cast canon person's stable id; null for a Series Bible character the canon does not hold yet."},"aliases":{"type":"array","items":{"type":"string"}},"lookDirection":{"type":["string","null"]},"age":{"type":["string","null"]},"inCanon":{"type":"boolean"},"bible":{"oneOf":[{"type":"null"},{"type":"object","required":["appearance","wardrobe"],"properties":{"appearance":{"type":["string","null"]},"wardrobe":{"type":["string","null"]}}}],"description":"What the Series Bible says they look like; the look outranks it where they differ."}}},"CastLookRead":{"type":"object","required":["projectId","people"],"additionalProperties":false,"properties":{"projectId":{"type":"string"},"people":{"type":"array","items":{"$ref":"#/components/schemas/CastLookEntry"}}}},"CastLookPatch":{"type":"object","required":["character"],"additionalProperties":false,"properties":{"character":{"type":"string","minLength":1,"maxLength":200,"description":"The person, by canon name, alias, cue or Series Bible name."},"lookDirection":{"type":["string","null"],"maxLength":2000,"description":"How they should look on camera. null or \"\" clears it; omitted leaves it alone."},"age":{"type":["string","null"],"maxLength":100,"description":"The age they are played at. null or \"\" clears it; omitted leaves it alone."}}},"CastLookSaved":{"type":"object","required":["projectId","person","note"],"additionalProperties":false,"properties":{"projectId":{"type":"string"},"person":{"$ref":"#/components/schemas/CastLookEntry"},"note":{"type":"string"}}},"SeriesBibleCharacterUpdateRequest":{"type":"object","required":["character","content"],"additionalProperties":false,"properties":{"character":{"type":"string","minLength":1,"description":"The character's id, or any name the Bible or the cast canon knows them by."},"content":{"type":"object","additionalProperties":false,"description":"The fields to change; every other field and every other character stay as stored. `name` renames.","properties":{"name":{"type":"string"},"role":{"type":"string"},"want":{"type":"string"},"wound":{"type":"string"},"arc":{"type":"string"},"voice":{"type":"string"},"relationships":{"type":"string"},"appearance":{"type":"string"},"wardrobe":{"type":"string"},"movement":{"type":"string"}}},"expectedVersion":{"type":"integer","minimum":0,"description":"Compare-and-swap: the Bible `currentVersion` you read. Omitted: edit whatever is saved now."},"create":{"type":"boolean","description":"Add the character when nobody answers to the name."}}},"SeriesBibleCharacterResult":{"type":"object","required":["projectId","currentVersion","character"],"additionalProperties":false,"properties":{"projectId":{"type":"string"},"currentVersion":{"type":"integer","minimum":0,"description":"The Bible's version; send it as `expectedVersion` to edit safely."},"character":{"$ref":"#/components/schemas/SeriesBibleCharacter"},"created":{"type":"boolean"},"gateChange":{"allOf":[{"$ref":"#/components/schemas/SeriesBibleGateChange"}],"description":"On an edit: what it did to the project's gate."}}},"VoicePreviewRequest":{"type":"object","required":["voiceId"],"additionalProperties":false,"properties":{"voiceId":{"type":"string","minLength":1},"provider":{"type":"string","enum":["cartesia","elevenlabs","openai","deepgram","hume"],"description":"The voice's provider. Your own voices know theirs; otherwise the configured table-read provider."},"text":{"type":"string","maxLength":500,"description":"The words to hear. Omitted: `character`'s first line in the read `artifactId`."},"artifactId":{"type":"string"},"character":{"type":"string"}}},"VoicePreviewResponse":{"type":"object","required":["preview"],"additionalProperties":false,"properties":{"preview":{"type":"object","required":["voiceId","provider","text","url","cached","durationSeconds"],"properties":{"voiceId":{"type":"string"},"provider":{"type":"string"},"text":{"type":"string"},"url":{"type":"string","description":"Playable audio."},"cached":{"type":"boolean","description":"true when these words in this voice had been voiced before, so nothing was bought."},"durationSeconds":{"type":["number","null"]}}}}},"StudioVoiceRenameRequest":{"type":"object","required":["name"],"additionalProperties":false,"properties":{"name":{"type":"string","minLength":1,"maxLength":100}}},"StudioVoiceRenameResponse":{"type":"object","required":["voice","canonRenamed"],"additionalProperties":false,"properties":{"voice":{"type":"object","additionalProperties":true,"description":"The voice as GET /voices lists it."},"canonRenamed":{"type":"integer","minimum":0,"description":"Cast canon people pinned to this voice, now carrying the new name."}}},"VoiceUses":{"type":"object","required":["canon","reads"],"properties":{"canon":{"type":"array","items":{"type":"object","required":["projectId","projectName","character"],"properties":{"projectId":{"type":"string"},"projectName":{"type":"string"},"character":{"type":"string"}}}},"reads":{"type":"array","items":{"type":"object","required":["jobId","artifactId","characters"],"properties":{"jobId":{"type":"string"},"artifactId":{"type":["string","null"]},"characters":{"type":"array","items":{"type":"string"}}}}}}},"VoiceMigrationRequest":{"type":"object","required":["fromVoiceId","toVoiceId"],"additionalProperties":false,"properties":{"fromVoiceId":{"type":"string","minLength":1,"description":"The voice being replaced: its provider voice id (or library `id`)."},"toVoiceId":{"type":"string","minLength":1,"description":"The replacement: one of your library voices (GET /voices), or a catalog voice with `toProvider`."},"toProvider":{"type":"string","enum":["cartesia","elevenlabs","openai","deepgram","hume"],"description":"The replacement's provider, for a catalog voice. A library voice knows its own; otherwise the configured table-read provider."},"toVoiceName":{"type":"string","maxLength":100,"description":"The name the canon and reads show; the library voice's own name otherwise."},"projectId":{"type":"string","description":"Only this project's uses. Omitted: every use you have."},"character":{"type":"string","description":"Only this character of `projectId`, under any name the canon knows them by."},"confirmed":{"type":"boolean","description":"true migrates and records it. Omitted: answers with what would change and changes nothing."}}},"VoiceMigrationVoice":{"type":"object","required":["voiceId","voiceName","provider","retiresAt"],"properties":{"voiceId":{"type":"string"},"voiceName":{"type":["string","null"]},"provider":{"type":["string","null"]},"retiresAt":{"type":["string","null"],"format":"date-time","description":"When the voice's provider retires its voices, if it is retiring."}}},"VoiceMigrationChange":{"type":"object","required":["kind","previous","next"],"additionalProperties":true,"description":"One re-pointed use: `canon` (a cast canon person: projectId, projectName, character), `read` (a read with no finished recording: jobId, artifactId, title, character) or `trailer` / `pitch_deck` (its pinned narrator: jobId, title). `previous` is what it held; `next` what it holds now.","properties":{"kind":{"type":"string","enum":["canon","read","trailer","pitch_deck"]},"projectId":{"type":"string"},"projectName":{"type":"string"},"jobId":{"type":"string"},"artifactId":{"type":["string","null"]},"title":{"type":["string","null"]},"character":{"type":"string"},"previous":{"type":"object","additionalProperties":true},"next":{"type":"object","additionalProperties":true}}},"VoiceMigrationSkippedRead":{"type":"object","required":["jobId","artifactId","title","characters","reason"],"description":"A read left on the old voice because it has a finished recording, which is kept. Re-voice its characters (POST /artifacts/{artifactId}/revoice, priced) to finalize it again on the new voice.","properties":{"jobId":{"type":"string"},"artifactId":{"type":["string","null"]},"title":{"type":["string","null"]},"characters":{"type":"array","items":{"type":"string"}},"reason":{"type":"string"}}},"VoiceMigrationRecord":{"type":"object","required":["migrationId","createdAt","from","to","scope","changes","skipped","undoneAt","undo"],"additionalProperties":true,"properties":{"migrationId":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"from":{"$ref":"#/components/schemas/VoiceMigrationVoice"},"to":{"$ref":"#/components/schemas/VoiceMigrationVoice"},"scope":{"type":"object","properties":{"projectId":{"type":["string","null"]},"character":{"type":["string","null"]}}},"changes":{"type":"array","items":{"$ref":"#/components/schemas/VoiceMigrationChange"}},"skipped":{"type":"array","items":{"$ref":"#/components/schemas/VoiceMigrationSkippedRead"}},"undoneAt":{"type":["string","null"],"format":"date-time"},"undo":{"oneOf":[{"type":"null"},{"type":"object","required":["at","restored","kept"],"properties":{"at":{"type":"string","format":"date-time"},"restored":{"type":"array","items":{"$ref":"#/components/schemas/VoiceMigrationChange"}},"kept":{"type":"array","items":{"type":"object","properties":{"change":{"$ref":"#/components/schemas/VoiceMigrationChange"},"reason":{"type":"string"}}}}}}]}}},"VoiceMigrationEnvelope":{"type":"object","required":["applied","from","to","scope","changes","skipped","note"],"additionalProperties":true,"description":"Without `confirmed`: `applied: false` and what would change (nothing was written). With `confirmed: true`: `applied: true` and the recorded migration (`migrationId`, `createdAt`, `undoneAt`, `undo`).","properties":{"applied":{"type":"boolean"},"migrationId":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"from":{"$ref":"#/components/schemas/VoiceMigrationVoice"},"to":{"$ref":"#/components/schemas/VoiceMigrationVoice"},"scope":{"type":"object","properties":{"projectId":{"type":["string","null"]},"character":{"type":["string","null"]}}},"changes":{"type":"array","items":{"$ref":"#/components/schemas/VoiceMigrationChange"}},"skipped":{"type":"array","items":{"$ref":"#/components/schemas/VoiceMigrationSkippedRead"}},"undoneAt":{"type":["string","null"],"format":"date-time"},"note":{"type":"string"}}},"VoiceMigrationListResponse":{"type":"object","required":["migrations"],"additionalProperties":false,"properties":{"migrations":{"type":"array","items":{"$ref":"#/components/schemas/VoiceMigrationRecord"}}}},"VoiceMigrationUndoResponse":{"type":"object","required":["migration","note"],"additionalProperties":false,"properties":{"migration":{"$ref":"#/components/schemas/VoiceMigrationRecord"},"note":{"type":"string"}}},"StudioVoiceDeleteResponse":{"type":"object","required":["deleted","voiceId","name","uses"],"additionalProperties":false,"properties":{"deleted":{"type":"boolean","enum":[true]},"voiceId":{"type":"string"},"name":{"type":"string"},"uses":{"$ref":"#/components/schemas/VoiceUses"}}},"VoiceCloneLinkResponse":{"type":"object","required":["recordingUrl","expiresAt"],"additionalProperties":false,"description":"A short-lived browser URL where the user records a sample for voice cloning. Hand the URL to the human; the clone lands in their studio.","properties":{"recordingUrl":{"type":"string","format":"uri"},"expiresAt":{"type":"string","format":"date-time"}}},"PerformanceDirectiveResponse":{"type":"object","required":["artifactId","status","character","matchedLines","linesUpdated"],"additionalProperties":true,"description":"Result of a performance directive: how many of the character's lines matched and were updated, plus the resolved values applied.","properties":{"artifactId":{"type":"string"},"status":{"type":"string","enum":["ready"]},"character":{"type":"string","description":"Resolved target (\"ALL\" when targeting every voiced line)."},"matchedLines":{"type":"integer","minimum":0},"linesUpdated":{"type":"integer","minimum":0},"emotions":{"oneOf":[{"type":"array","items":{"type":"string"}},{"type":"null"}]},"speed":{"oneOf":[{"type":"number"},{"type":"null"}]},"speedFactor":{"oneOf":[{"type":"number"},{"type":"null"}]},"volume":{"oneOf":[{"type":"number"},{"type":"null"}]},"volumeFactor":{"oneOf":[{"type":"number"},{"type":"null"}]},"pauseAfterMs":{"oneOf":[{"type":"integer"},{"type":"null"}]},"speedUnsupportedLines":{"type":"integer","minimum":0,"description":"Matched lines whose voice takes no speed setting (OpenAI, Hume, Eleven v3, Cartesia before sonic-3, Deepgram Aura-1, a Cartesia professional clone): they keep their pace."},"speedNote":{"type":["string","null"],"description":"Why a line kept its pace, and what to do. Null when every matched line took the speed."}}},"SfxManageRequest":{"type":"object","additionalProperties":false,"description":"Add, update, or remove a timed sound-effect cue on the read backing a `table_read` artifact. `op` defaults to `add`; `update` and `remove` target an existing cue by `id`. Mutating cues does not create a new artifact revision or change share URLs.","properties":{"op":{"type":"string","enum":["add","update","remove"],"default":"add"},"entryIndex":{"type":"integer","minimum":0,"description":"Dialogue entry index for add or update operations."},"label":{"type":"string","minLength":1},"prompt":{"type":"string","minLength":1},"volume":{"type":"number","minimum":0,"maximum":1},"triggerOffset":{"type":"string","enum":["before","with","after"]},"offsetMs":{"type":"integer","description":"Millisecond timing nudge relative to the cue trigger."},"generatedDurationS":{"type":"number","minimum":0.5,"maximum":10,"description":"Generated cue duration in seconds."},"enabled":{"type":"boolean","description":"false mutes/excludes the cue without deleting it."},"regenerate":{"type":"boolean","description":"For update operations, regenerate cue audio from the revised prompt."},"id":{"type":"string","description":"Cue id for update or remove operations."}}},"AvatarRecastRequest":{"type":"object","required":["character"],"additionalProperties":false,"description":"Regenerate one character's avatar portrait. The render follows project Series Bible canon plus any episode override. Supplying `style` persists an explicit episode portrait override for this and subsequent episode renders; it does not change project canon.","properties":{"character":{"type":"string","minLength":1,"description":"Character cue whose avatar to regenerate, e.g. \"NARRATOR\" or \"MAYA\"."},"refinement":{"type":"string","description":"Optional freeform art-direction nudge for this render, e.g. \"older, weathered, dim backlight\"."},"style":{"type":"string","maxLength":8000,"description":"Optional explicit episode portrait override (at most 8000 characters: it is saved as the episode `avatar` override and takes that override's cap). It persists for subsequent renders in this episode without changing the project Series Bible. To apply it to the whole cast, use POST /artifacts/{id}/cast with `avatarStyle`."},"referenceImageUrl":{"type":"string","format":"uri","description":"Optional already-uploaded image URL (http or https) to seed the portrait."},"confirmed":{"type":"boolean","description":"PRICED (three images: the headshot, and the body shot and sheet redrawn to match it; 3 credits). Omit to get the price only (`quotedOnly: true`, nothing drawn or charged); send `true` to draw and charge."}}},"CastArtQuote":{"type":"object","required":["quotedOnly","credits","kind","characters","note"],"additionalProperties":true,"description":"The answer to a priced cast-art call WITHOUT `confirmed`: what it costs, what it would leave, and the fact that nothing was drawn or charged. The same price on every surface (the app's Cast panel, this API, MCP, the CLI and Slug).","properties":{"artifactId":{"type":"string"},"quotedOnly":{"type":"boolean","enum":[true]},"credits":{"type":"integer","minimum":0},"balanceAfter":{"type":"integer","description":"Quote only. Studio Credits the writer would have left once this is spent: the balance available now (as Billing shows it) minus the price. Negative means they are that many short."},"sufficient":{"type":"boolean","description":"Quote only. True when the available balance covers the price. False means the writer must top up first (the note names the Billing link) — do not confirm; the reserve would refuse."},"costLabel":{"type":["string","null"]},"kind":{"type":"string","enum":["portrait","figure"]},"characters":{"type":"array","items":{"type":"string"}},"note":{"type":"string","description":"The same sentence to show the writer, in their terms."}}},"AvatarOp":{"type":"object","required":["character","status","chatToolJobId"],"additionalProperties":false,"description":"A queued per-character avatar render. Poll GET /artifacts/{artifactId}/cast and watch the character's `avatarStatus` until it reaches `ready`.","properties":{"character":{"type":"string"},"status":{"type":"string","enum":["queued","rendering","ready","failed"],"description":"Lifecycle of the avatar render op."},"chatToolJobId":{"type":"string","description":"Id of the queued `regenerate_avatar` job backing this render."}}},"AvatarRecastResponse":{"type":"object","required":["artifactId","quotedOnly","character","status","chatToolJobId","credits"],"additionalProperties":false,"description":"Acknowledgement that a single-character avatar regeneration has been dispatched and charged (`credits`). Poll GET /artifacts/{artifactId}/cast until the character's `avatarStatus` reaches `ready`. A draw that fails gives its credits back.","properties":{"artifactId":{"type":"string"},"quotedOnly":{"type":"boolean","enum":[false]},"credits":{"type":"integer","minimum":0,"description":"Studio Credits held for the draw (settled when the picture lands)."},"character":{"type":"string"},"status":{"type":"string","enum":["queued","rendering","ready","failed"],"description":"Lifecycle of the avatar render op."},"chatToolJobId":{"type":"string","description":"Id of the queued `regenerate_avatar` job backing this render."}}},"FigureRecastRequest":{"type":"object","required":["character"],"additionalProperties":false,"description":"Regenerate one character's FULL-BODY FIGURE — the cutout a render uses for that character, as opposed to the avatar headshot the identity gate judges faces against. The two must depict the same person: when they drift apart, frames build from the figure and are judged against the avatar, so that character fails identity on every render. The figure is seeded from the character's own avatar unless `referenceImageUrl` overrides it. There is deliberately no `style` field — figure art direction follows the avatar rather than diverging from it.","properties":{"character":{"type":"string","minLength":1,"description":"Character cue whose figure to regenerate, e.g. \"ALYOSHA\"."},"refinement":{"type":"string","description":"Optional freeform art-direction nudge for the BODY render only. Never rewrites the headshot prompt."},"referenceImageUrl":{"type":"string","format":"uri","description":"Optional already-uploaded image URL (http or https) to seed the figure from. Omit to seed from the character's avatar, which is what keeps the pair consistent."},"confirmed":{"type":"boolean","description":"PRICED (one image; 1 credit). Omit to get the price only (`quotedOnly: true`, nothing drawn or charged); send `true` to draw and charge."}}},"FigureRecastResponse":{"type":"object","required":["artifactId","quotedOnly","character","status","chatToolJobId","credits"],"additionalProperties":false,"description":"Acknowledgement that a single-character figure regeneration has been dispatched and charged (`credits`). GET /artifacts/{artifactId}/cast exposes only avatar status and does not reflect figure render state; use `chatToolJobId` to track the queued `generate_figure` job. A draw that fails gives its credits back.","properties":{"artifactId":{"type":"string"},"quotedOnly":{"type":"boolean","enum":[false]},"credits":{"type":"integer","minimum":0,"description":"Studio Credits held for the draw (settled when the picture lands)."},"character":{"type":"string"},"status":{"type":"string","enum":["queued","rendering","ready","failed"],"description":"Lifecycle of the figure render op."},"chatToolJobId":{"type":"string","description":"Id of the queued `generate_figure` job backing this render."}}},"CastEntryUpdate":{"type":"object","required":["character"],"additionalProperties":false,"description":"One per-character cast update: reassign the voice, queue an avatar re-render, or both.","properties":{"character":{"type":"string","minLength":1,"description":"Character cue to update."},"voiceId":{"type":"string","description":"New voice id to assign."},"voiceName":{"type":"string","description":"Human-readable voice name for display."},"gender":{"type":"string","description":"Optional voice gender hint. Preserved from the prior assignment when omitted."},"provider":{"type":"string","enum":["cartesia","elevenlabs","openai","deepgram","hume"],"description":"Optional TTS provider. Inferred when omitted."},"avatarRefinement":{"type":"string","description":"Optional per-character art-direction nudge for this character's avatar render."},"regenerateAvatar":{"type":"boolean","description":"Set true to queue an avatar re-render for this character."},"avatarUrl":{"type":"string","format":"uri","description":"Directly set this character's portrait to existing hosted art (a series' canonical character sheet); no generative render. Must be an https URL."}}},"CastBatchRequest":{"type":"object","additionalProperties":false,"description":"Batch cast update. Provide at least one `entries` item OR a top-level `avatarStyle` — a body with neither is rejected as `validation_failed`. A top-level `avatarStyle` is an episode-only override that re-renders ALL portraits without changing project Bible canon.","properties":{"entries":{"type":"array","items":{"$ref":"#/components/schemas/CastEntryUpdate"},"description":"Per-character updates. Optional when a top-level `avatarStyle` is supplied."},"avatarStyle":{"type":"string","maxLength":8000,"description":"Optional episode portrait override applied across the whole cast (at most 8000 characters: it is saved as the episode `avatar` override and takes that override's cap). Re-renders every character without changing project Bible canon."},"confirmed":{"type":"boolean","description":"PRICED when the batch draws portraits (a restyle, `regenerateAvatar` or `avatarRefinement`; 3 credits a character). Omit to get the price of the whole batch only (`quotedOnly: true`; nothing, voice changes included, is applied); send `true` to apply it and charge. A batch that draws no portrait is free and needs no confirmation."}},"anyOf":[{"required":["entries"]},{"required":["avatarStyle"]}]},"CastBatchResponse":{"type":"object","required":["artifactId","quotedOnly","voiceMap","credits","avatars"],"additionalProperties":false,"description":"Result of a batch cast update. Voice reassignments are already applied in `voiceMap`; each item in `avatars` is a queued avatar render to poll, charged in `credits` (0 when the batch drew no portrait).","properties":{"artifactId":{"type":"string"},"quotedOnly":{"type":"boolean","enum":[false]},"credits":{"type":"integer","minimum":0,"description":"Studio Credits held for the portraits the batch draws."},"voiceMap":{"type":"object","additionalProperties":true,"description":"Full updated cast: character → { voiceId, voiceName, gender, provider }."},"avatars":{"type":"array","items":{"$ref":"#/components/schemas/AvatarOp"},"description":"One op per queued avatar render."}}},"EnrichedCastEntry":{"type":"object","required":["character","voiceId","voiceName","gender","provider","avatarUrl","avatarStyle","avatarStatus","avatarVideoUrl","avatarVideoStatus"],"additionalProperties":false,"description":"One character's assigned voice merged with its avatar state.","properties":{"character":{"type":"string"},"voiceId":{"oneOf":[{"type":"string"},{"type":"null"}]},"voiceName":{"oneOf":[{"type":"string"},{"type":"null"}]},"gender":{"oneOf":[{"type":"string"},{"type":"null"}]},"provider":{"oneOf":[{"type":"string"},{"type":"null"}]},"avatarUrl":{"oneOf":[{"type":"string","format":"uri"},{"type":"null"}]},"avatarStyle":{"oneOf":[{"type":"string"},{"type":"null"}],"description":"The art-direction this character's avatar was rendered against."},"avatarStatus":{"type":"string","enum":["none","queued","rendering","ready","failed"],"description":"`none` (no avatar), `queued` / `rendering` (a render is in flight), `ready`, or `failed`. Poll until `ready` after a render."},"avatarVideoUrl":{"oneOf":[{"type":"string","format":"uri"},{"type":"null"}],"description":"The animated talking portrait (a short loop), when one was made (POST /artifacts/{artifactId}/avatar-video)."},"avatarVideoStatus":{"type":"string","enum":["none","queued","rendering","ready","failed"],"description":"The animated portrait's render: `none` (never asked for), `queued` / `rendering`, `ready`, or `failed`."}}},"CastEnvelope":{"type":"object","required":["artifactId","cast","avatarStyle","identity","restorable"],"additionalProperties":false,"description":"Enriched cast for a table read artifact: each character's voice merged with its avatar state, plus the resolved episode portrait override when present.","properties":{"artifactId":{"type":"string"},"cast":{"type":"array","items":{"$ref":"#/components/schemas/EnrichedCastEntry"}},"avatarStyle":{"oneOf":[{"type":"string"},{"type":"null"}],"description":"The artifact's explicit episode portrait override (`table_read_avatar_style`). Project-level visual canon lives in the Series Bible. `null` when no override exists."},"identity":{"$ref":"#/components/schemas/CastIdentityReport"},"restorable":{"type":"array","items":{"$ref":"#/components/schemas/RestorableVoice"},"description":"Characters recast on this read whose previous voice POST /artifacts/{artifactId}/voice/restore can put back, newest recast per character."}}},"PublishingMedium":{"type":"string","enum":["audio","video","short_form","text","mixed"]},"PublishingProvider":{"type":"string","enum":["rss","apple_podcasts","spotify","youtube","tiktok","instagram","linkedin","x","other"]},"PublishingSeries":{"type":"object","required":["id","projectId","title","slug","medium","format","status","feedUrl","createdAt","updatedAt"],"additionalProperties":true,"properties":{"id":{"type":"string"},"projectId":{"type":"string"},"seriesBibleDocumentId":{"oneOf":[{"type":"string"},{"type":"null"}]},"title":{"type":"string"},"slug":{"type":"string"},"description":{"oneOf":[{"type":"string"},{"type":"null"}]},"medium":{"$ref":"#/components/schemas/PublishingMedium"},"format":{"type":"string","example":"podcast"},"status":{"type":"string","enum":["draft","active","paused","archived"]},"cadence":{"oneOf":[{"$ref":"#/components/schemas/ShowCadence"},{"type":"null"}]},"timezone":{"type":"string"},"language":{"type":"string"},"rating":{"type":"string","enum":["clean","explicit","unknown"]},"explicitContent":{"type":"boolean"},"podcastType":{"type":"string","enum":["episodic","serial"]},"podcastCategory":{"type":"string","example":"Fiction"},"podcastSubcategory":{"oneOf":[{"type":"string"},{"type":"null"}],"example":"Drama"},"coverArtUrl":{"oneOf":[{"type":"string","format":"uri"},{"type":"null"}]},"websiteUrl":{"oneOf":[{"type":"string","format":"uri"},{"type":"null"}]},"promptDirection":{"oneOf":[{"type":"string"},{"type":"null"}]},"syntheticDisclosure":{"type":"string","minLength":1,"maxLength":300,"description":"The show's synthetic-media line (legal gap G8a): it ends the feed's description and every episode's notes, is the feed's `podcast:txt` (purpose synthetic-media) and shows on the show's pages. Reword it; it can never be empty (default: \"Voices and music in this show are synthetic.\")."},"metadata":{"nullable":true},"feedUrl":{"oneOf":[{"type":"string","format":"uri"},{"type":"null"}]},"productionMode":{"type":"string","enum":["manual","seasonal","recurring"]},"visibility":{"type":"string","enum":["public","unlisted","private"],"description":"What the show's slug exposes: public (landing page, archive, and the podcast feed), unlisted (shared episode links and the podcast feed, reached by link: never indexed, and the feed asks podcast directories not to list it), or private (nothing; the feed answers 404). A draft show's feed is served too."},"showConfig":{"oneOf":[{"$ref":"#/components/schemas/ShowConfig"},{"type":"null"}]},"cadenceEnabled":{"type":"boolean","description":"A recurring show does nothing until this is true. Recurring series are audio-only."},"episodeCreditCap":{"oneOf":[{"type":"integer"},{"type":"null"}],"description":"The most one episode may spend, in Studio Credits. A show runs only with both caps."},"monthlyCreditCap":{"oneOf":[{"type":"integer"},{"type":"null"}],"description":"The most a calendar month's episodes may spend (the series' timezone)."},"publishPolicy":{"type":"string","enum":["auto","hold"],"description":"`auto`: each episode publishes under the standing approval. `hold`: each is made and left `ready` for the creator to review."},"consecutiveFailures":{"type":"integer","description":"Episodes in a row that were not made; three pause the show."},"pausedReason":{"oneOf":[{"type":"string"},{"type":"null"}],"description":"Why the runner paused the show, until the creator resumes it (`status: active`)."},"standingApproval":{"oneOf":[{"$ref":"#/components/schemas/PublishingStandingApproval"},{"type":"null"}],"description":"The show's standing approval, or null when none is granted."},"destinations":{"type":"array","items":{"$ref":"#/components/schemas/PublishingDestination"}},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"PublishingStandingApproval":{"type":"object","required":["keyId","keyName","keyStart","grantedAt","grantedBy"],"additionalProperties":false,"description":"ONE API key may approve this show's table-read plans (`POST /story-plans/{planId}/approve`) and publish its audio releases (`POST /publishing-releases/{releaseId}/publish`) without `userConfirmed`, for plans and releases created after `grantedAt`. Every other caller still needs `userConfirmed`. Not exercised while the series is paused or archived; never covers season runs.","properties":{"keyId":{"oneOf":[{"type":"string"},{"type":"null"}],"description":"The bound API key; null when only the built-in cadence runner holds the grant."},"keyName":{"oneOf":[{"type":"string"},{"type":"null"}]},"keyStart":{"oneOf":[{"type":"string"},{"type":"null"}],"description":"The key's visible prefix, never the secret."},"grantedAt":{"type":"string","format":"date-time"},"grantedBy":{"oneOf":[{"type":"string"},{"type":"null"}]}}},"ShowCadence":{"type":"object","additionalProperties":false,"description":"When the show makes an episode: `{ \"schedules\": [{ \"freq\": \"daily\"|\"weekly\"|\"monthly\", \"hour\": 0-23, \"weekday\": 0-6 (0 = Sunday; weekly only), \"dayOfMonth\": 1-28 (monthly only), \"timezone\": IANA zone (defaults to the series timezone), \"episodeType\": a showConfig.episodeTypes key (\"episode\" by default), \"fanout\": \"single\" (one episode from all the material, the default) | \"per-item\" (one episode per source item, at most 5 a firing) }] }`. Example: {\"schedules\":[{\"freq\":\"weekly\",\"weekday\":1,\"hour\":7,\"timezone\":\"America/New_York\",\"episodeType\":\"episode\"}]}.","properties":{"schedules":{"type":"array","maxItems":8,"items":{"type":"object","additionalProperties":false,"required":["freq","hour","episodeType"],"properties":{"freq":{"type":"string","enum":["daily","weekly","monthly"]},"hour":{"type":"integer","minimum":0,"maximum":23},"weekday":{"type":"integer","minimum":0,"maximum":6,"description":"Weekly only; 0 = Sunday."},"dayOfMonth":{"type":"integer","minimum":1,"maximum":28,"description":"Monthly only."},"timezone":{"type":"string","description":"IANA zone; defaults to the series timezone."},"episodeType":{"type":"string","minLength":1,"maxLength":40},"fanout":{"type":"string","enum":["single","per-item"],"default":"single"}}}},"onDemand":{"type":"boolean","default":true}}},"ShowConfig":{"type":"object","additionalProperties":false,"description":"The show's production switches (its cast, tone, format and language live in the Series Bible and are refused here): `sources[]` ({ adapter, params, label }: rss { feedUrl (required), maxItems, lookbackDays, order }; web-search { query (required), topic, site, maxItems, lookbackDays }; hackernews { pick, count, minComments, maxComments, maxCharsEach, itemId }; github { repo (required), lookbackDays }; url-watch { url (required), label }; upload { text (required), label }), `pageTarget` (the episode length in script pages, about a minute each), `profanity` (none|mild|open), `musicPrompt`, `extraNotes`, `episodeTypes[]`, `voicePinning`, `themeBank`, `memory`. Example: {\"sources\":[{\"adapter\":\"rss\",\"label\":\"arXiv robotics\",\"params\":{\"feedUrl\":\"https://rss.arxiv.org/rss/cs.RO\"}}],\"pageTarget\":9,\"profanity\":\"none\"}.","properties":{"sources":{"type":"array","maxItems":10,"items":{"oneOf":[{"type":"object","additionalProperties":false,"required":["adapter","params"],"description":"An RSS or Atom feed: a blog, a newsletter (Substack: https://<name>.substack.com/feed), an arXiv listing (https://rss.arxiv.org/rss/cs.RO), Product Hunt. Each feed entry is one item.","properties":{"adapter":{"type":"string","enum":["rss"]},"params":{"type":"object","additionalProperties":false,"properties":{"feedUrl":{"type":"string","format":"uri","description":"The feed URL (http or https)."},"maxItems":{"type":"integer","minimum":1,"maximum":30,"description":"The most entries read per firing. Default 12."},"lookbackDays":{"type":"integer","minimum":1,"maximum":90,"description":"How many days back to read. Defaults to the schedule: 1 for daily, 7 for weekly, 31 for monthly."},"order":{"type":"string","pattern":"^(newest|oldest)$","description":"\"newest\" (the default) reads what the feed published in the window. \"oldest\" works through the feed's archive instead: the oldest entries no episode has covered yet, whatever their date, so a feed that carries its whole history (a newsletter, a podcast, a blog) becomes a backlog the show reads in order."}},"required":["feedUrl"]},"label":{"type":"string","maxLength":120}}},{"type":"object","additionalProperties":false,"required":["adapter","params"],"description":"A topic searched on the web at each firing: the newest articles about it, each one item, never one an earlier episode covered. Searches run on Sleeper's own search service (no key needed), up to 40 a day per account.","properties":{"adapter":{"type":"string","enum":["web-search"]},"params":{"type":"object","additionalProperties":false,"properties":{"query":{"type":"string","minLength":3,"maxLength":200,"description":"What to search for, as you would type it (\"solid-state battery breakthroughs\", \"Mars sample return\")."},"topic":{"type":"string","pattern":"^(news|general)$","description":"\"news\" (the default) searches news coverage; \"general\" searches the whole web."},"site":{"type":"string","maxLength":120,"pattern":"^[a-z0-9.-]+\\.[a-z]{2,}$","description":"Only results from this site, as a host name (\"nasa.gov\")."},"maxItems":{"type":"integer","minimum":1,"maximum":10,"description":"The most articles taken per firing. Default 5."},"lookbackDays":{"type":"integer","minimum":1,"maximum":90,"description":"How many days back to read. Defaults to the schedule: 1 for daily, 7 for weekly, 31 for monthly."}},"required":["query"]},"label":{"type":"string","maxLength":120}}},{"type":"object","additionalProperties":false,"required":["adapter"],"description":"Hacker News front-page discussions: each story with enough comments is one item, carrying its comment thread.","properties":{"adapter":{"type":"string","enum":["hackernews"]},"params":{"type":"object","additionalProperties":false,"properties":{"pick":{"type":"string","pattern":"^top-stories$","description":"Which list to read. Only \"top-stories\"."},"count":{"type":"integer","minimum":1,"maximum":10,"description":"The most stories taken per firing. Default 5."},"minComments":{"type":"integer","minimum":0,"maximum":1000,"description":"A story needs at least this many comments. Default 10."},"maxComments":{"type":"integer","minimum":1,"maximum":2000,"description":"The most comments carried per story. Default 240."},"maxCharsEach":{"type":"integer","minimum":100,"maximum":8000,"description":"The longest a single comment is kept. Default 1600."},"itemId":{"type":"string","pattern":"^\\d+$","description":"One specific story id instead of the front page."}}},"label":{"type":"string","maxLength":120}}},{"type":"object","additionalProperties":false,"required":["adapter","params"],"description":"A public GitHub repository: its releases, merged pull requests and commit flow over the window, as one item.","properties":{"adapter":{"type":"string","enum":["github"]},"params":{"type":"object","additionalProperties":false,"properties":{"repo":{"type":"string","pattern":"^[\\w.-]+/[\\w.-]+$","description":"The repository as owner/name."},"lookbackDays":{"type":"integer","minimum":1,"maximum":90,"description":"How many days back to read. Defaults to the schedule: 1 for daily, 7 for weekly, 31 for monthly."}},"required":["repo"]},"label":{"type":"string","maxLength":120}}},{"type":"object","additionalProperties":false,"required":["adapter","params"],"description":"One web page, read whole at each firing (a changelog, a status page, a running list).","properties":{"adapter":{"type":"string","enum":["url-watch"]},"params":{"type":"object","additionalProperties":false,"properties":{"url":{"type":"string","format":"uri","description":"The page URL (http or https)."},"label":{"type":"string","maxLength":120,"description":"What to call the page in the episode material."}},"required":["url"]},"label":{"type":"string","maxLength":120}}},{"type":"object","additionalProperties":false,"required":["adapter","params"],"description":"Text the writer supplies once (notes, an archive excerpt). The same text every firing, so pair it with another source or an on-demand episode type.","properties":{"adapter":{"type":"string","enum":["upload"]},"params":{"type":"object","additionalProperties":false,"properties":{"text":{"type":"string","minLength":1,"maxLength":12000,"description":"The text itself, up to 12,000 characters."},"label":{"type":"string","maxLength":120,"description":"What to call the text in the episode material."}},"required":["text"]},"label":{"type":"string","maxLength":120}}}],"discriminator":{"propertyName":"adapter"}}},"pageTarget":{"type":"integer","minimum":4,"maximum":20,"description":"The episode length in script pages: one page is about a minute of audio (a cast of hosts runs about 1.3 minutes a page, so ~9 pages is a ~12-minute episode). 4–20. Unset, the length follows the amount of material (5–10 pages)."},"profanity":{"type":"string","enum":["none","mild","open"],"default":"none"},"musicPrompt":{"type":"string","maxLength":2000},"extraNotes":{"type":"string","maxLength":10000},"voicePinning":{"type":"boolean","default":true},"themeBank":{"type":"boolean","default":true},"episodeTypes":{"type":"array","minItems":1,"maxItems":8,"items":{"type":"object","additionalProperties":false,"required":["key"],"properties":{"key":{"type":"string","minLength":1,"maxLength":40},"label":{"type":"string","maxLength":120},"notes":{"type":"string","maxLength":2000},"scheduled":{"type":"boolean","default":true},"promptRequired":{"type":"boolean","default":false}}}},"memory":{"type":"object","additionalProperties":true,"description":"What the show rotates so episodes stay fresh."}}},"ShowSource":{"oneOf":[{"type":"object","additionalProperties":false,"required":["adapter","params"],"description":"An RSS or Atom feed: a blog, a newsletter (Substack: https://<name>.substack.com/feed), an arXiv listing (https://rss.arxiv.org/rss/cs.RO), Product Hunt. Each feed entry is one item.","properties":{"adapter":{"type":"string","enum":["rss"]},"params":{"type":"object","additionalProperties":false,"properties":{"feedUrl":{"type":"string","format":"uri","description":"The feed URL (http or https)."},"maxItems":{"type":"integer","minimum":1,"maximum":30,"description":"The most entries read per firing. Default 12."},"lookbackDays":{"type":"integer","minimum":1,"maximum":90,"description":"How many days back to read. Defaults to the schedule: 1 for daily, 7 for weekly, 31 for monthly."},"order":{"type":"string","pattern":"^(newest|oldest)$","description":"\"newest\" (the default) reads what the feed published in the window. \"oldest\" works through the feed's archive instead: the oldest entries no episode has covered yet, whatever their date, so a feed that carries its whole history (a newsletter, a podcast, a blog) becomes a backlog the show reads in order."}},"required":["feedUrl"]},"label":{"type":"string","maxLength":120}}},{"type":"object","additionalProperties":false,"required":["adapter","params"],"description":"A topic searched on the web at each firing: the newest articles about it, each one item, never one an earlier episode covered. Searches run on Sleeper's own search service (no key needed), up to 40 a day per account.","properties":{"adapter":{"type":"string","enum":["web-search"]},"params":{"type":"object","additionalProperties":false,"properties":{"query":{"type":"string","minLength":3,"maxLength":200,"description":"What to search for, as you would type it (\"solid-state battery breakthroughs\", \"Mars sample return\")."},"topic":{"type":"string","pattern":"^(news|general)$","description":"\"news\" (the default) searches news coverage; \"general\" searches the whole web."},"site":{"type":"string","maxLength":120,"pattern":"^[a-z0-9.-]+\\.[a-z]{2,}$","description":"Only results from this site, as a host name (\"nasa.gov\")."},"maxItems":{"type":"integer","minimum":1,"maximum":10,"description":"The most articles taken per firing. Default 5."},"lookbackDays":{"type":"integer","minimum":1,"maximum":90,"description":"How many days back to read. Defaults to the schedule: 1 for daily, 7 for weekly, 31 for monthly."}},"required":["query"]},"label":{"type":"string","maxLength":120}}},{"type":"object","additionalProperties":false,"required":["adapter"],"description":"Hacker News front-page discussions: each story with enough comments is one item, carrying its comment thread.","properties":{"adapter":{"type":"string","enum":["hackernews"]},"params":{"type":"object","additionalProperties":false,"properties":{"pick":{"type":"string","pattern":"^top-stories$","description":"Which list to read. Only \"top-stories\"."},"count":{"type":"integer","minimum":1,"maximum":10,"description":"The most stories taken per firing. Default 5."},"minComments":{"type":"integer","minimum":0,"maximum":1000,"description":"A story needs at least this many comments. Default 10."},"maxComments":{"type":"integer","minimum":1,"maximum":2000,"description":"The most comments carried per story. Default 240."},"maxCharsEach":{"type":"integer","minimum":100,"maximum":8000,"description":"The longest a single comment is kept. Default 1600."},"itemId":{"type":"string","pattern":"^\\d+$","description":"One specific story id instead of the front page."}}},"label":{"type":"string","maxLength":120}}},{"type":"object","additionalProperties":false,"required":["adapter","params"],"description":"A public GitHub repository: its releases, merged pull requests and commit flow over the window, as one item.","properties":{"adapter":{"type":"string","enum":["github"]},"params":{"type":"object","additionalProperties":false,"properties":{"repo":{"type":"string","pattern":"^[\\w.-]+/[\\w.-]+$","description":"The repository as owner/name."},"lookbackDays":{"type":"integer","minimum":1,"maximum":90,"description":"How many days back to read. Defaults to the schedule: 1 for daily, 7 for weekly, 31 for monthly."}},"required":["repo"]},"label":{"type":"string","maxLength":120}}},{"type":"object","additionalProperties":false,"required":["adapter","params"],"description":"One web page, read whole at each firing (a changelog, a status page, a running list).","properties":{"adapter":{"type":"string","enum":["url-watch"]},"params":{"type":"object","additionalProperties":false,"properties":{"url":{"type":"string","format":"uri","description":"The page URL (http or https)."},"label":{"type":"string","maxLength":120,"description":"What to call the page in the episode material."}},"required":["url"]},"label":{"type":"string","maxLength":120}}},{"type":"object","additionalProperties":false,"required":["adapter","params"],"description":"Text the writer supplies once (notes, an archive excerpt). The same text every firing, so pair it with another source or an on-demand episode type.","properties":{"adapter":{"type":"string","enum":["upload"]},"params":{"type":"object","additionalProperties":false,"properties":{"text":{"type":"string","minLength":1,"maxLength":12000,"description":"The text itself, up to 12,000 characters."},"label":{"type":"string","maxLength":120,"description":"What to call the text in the episode material."}},"required":["text"]},"label":{"type":"string","maxLength":120}}}],"discriminator":{"propertyName":"adapter"}},"ShowEpisodeCredit":{"type":"object","required":["itemKey","title"],"additionalProperties":false,"description":"One item an episode covers, as its show notes credit it.","properties":{"itemKey":{"type":"string"},"title":{"type":"string"},"author":{"type":"string","description":"The author or public handle, when the source names one."},"url":{"type":"string"},"publishedAt":{"type":"string","format":"date-time"}}},"ShowEpisode":{"type":"object","required":["id","seriesId","episodeKey","preview","status","skippedReason","failureCode","failureMessage","releaseId","artifactId","creditsSpent","attempts","items","createdAt","finishedAt"],"additionalProperties":false,"properties":{"id":{"type":"string"},"seriesId":{"type":"string"},"episodeKey":{"type":"string"},"preview":{"type":"boolean","description":"Made before the show went live, held for the creator, never published."},"status":{"type":"string","enum":["queued","producing","published","ready","skipped","failed"]},"skippedReason":{"oneOf":[{"type":"string","enum":["no_material","not_runnable","not_ready","not_authorized","cap_reached","held","preview"]},{"type":"null"}]},"failureCode":{"oneOf":[{"type":"string"},{"type":"null"}]},"failureMessage":{"oneOf":[{"type":"string"},{"type":"null"}]},"releaseId":{"oneOf":[{"type":"string"},{"type":"null"}]},"artifactId":{"oneOf":[{"type":"string"},{"type":"null"}]},"creditsSpent":{"oneOf":[{"type":"integer"},{"type":"null"}],"description":"What it cost, from the ledger, once it ended."},"attempts":{"type":"integer"},"items":{"type":"array","items":{"$ref":"#/components/schemas/ShowEpisodeCredit"}},"createdAt":{"type":"string","format":"date-time"},"finishedAt":{"oneOf":[{"type":"string","format":"date-time"},{"type":"null"}]}}},"ShowEpisodeList":{"type":"object","required":["episodes","nextCursor"],"additionalProperties":false,"properties":{"episodes":{"type":"array","items":{"$ref":"#/components/schemas/ShowEpisode"}},"nextCursor":{"oneOf":[{"type":"string"},{"type":"null"}]}}},"ShowEpisodeProduce":{"type":"object","required":["status","preview","credits","caps","monthToDate","episodeId","note"],"additionalProperties":false,"properties":{"status":{"type":"string","enum":["quoted","queued"]},"preview":{"type":"boolean"},"credits":{"type":"integer","description":"Studio Credits one episode is quoted at."},"caps":{"type":"object","required":["episodeCreditCap","monthlyCreditCap"],"additionalProperties":false,"properties":{"episodeCreditCap":{"oneOf":[{"type":"integer"},{"type":"null"}]},"monthlyCreditCap":{"oneOf":[{"type":"integer"},{"type":"null"}]}}},"monthToDate":{"type":"object","required":["spent","from"],"additionalProperties":false,"properties":{"spent":{"type":"integer"},"from":{"type":"string","format":"date-time"}}},"episodeId":{"oneOf":[{"type":"string"},{"type":"null"}]},"note":{"type":"string"}}},"ShowSourcePreview":{"type":"object","required":["seriesId","lookbackDays","items","failures","note"],"additionalProperties":false,"properties":{"seriesId":{"type":"string"},"lookbackDays":{"type":"integer"},"items":{"type":"array","items":{"type":"object","required":["itemKey","title","label","chars","covered"],"additionalProperties":false,"properties":{"itemKey":{"type":"string"},"title":{"type":"string"},"author":{"type":"string"},"url":{"type":"string"},"publishedAt":{"type":"string","format":"date-time"},"label":{"type":"string"},"chars":{"type":"integer","description":"How much text the item gives an episode."},"covered":{"type":"boolean","description":"An earlier episode of the show already covered it."}}}},"failures":{"type":"array","items":{"type":"string"}},"note":{"type":"string"}}},"ShowSourceTemplate":{"type":"object","required":["id","title","pitch","source","suggested","richness","rights","topics"],"properties":{"id":{"type":"string"},"title":{"type":"string"},"pitch":{"type":"string","description":"What a show from it is like."},"source":{"oneOf":[{"type":"object","additionalProperties":false,"required":["adapter","params"],"description":"An RSS or Atom feed: a blog, a newsletter (Substack: https://<name>.substack.com/feed), an arXiv listing (https://rss.arxiv.org/rss/cs.RO), Product Hunt. Each feed entry is one item.","properties":{"adapter":{"type":"string","enum":["rss"]},"params":{"type":"object","additionalProperties":false,"properties":{"feedUrl":{"type":"string","format":"uri","description":"The feed URL (http or https)."},"maxItems":{"type":"integer","minimum":1,"maximum":30,"description":"The most entries read per firing. Default 12."},"lookbackDays":{"type":"integer","minimum":1,"maximum":90,"description":"How many days back to read. Defaults to the schedule: 1 for daily, 7 for weekly, 31 for monthly."},"order":{"type":"string","pattern":"^(newest|oldest)$","description":"\"newest\" (the default) reads what the feed published in the window. \"oldest\" works through the feed's archive instead: the oldest entries no episode has covered yet, whatever their date, so a feed that carries its whole history (a newsletter, a podcast, a blog) becomes a backlog the show reads in order."}},"required":["feedUrl"]},"label":{"type":"string","maxLength":120}}},{"type":"object","additionalProperties":false,"required":["adapter","params"],"description":"A topic searched on the web at each firing: the newest articles about it, each one item, never one an earlier episode covered. Searches run on Sleeper's own search service (no key needed), up to 40 a day per account.","properties":{"adapter":{"type":"string","enum":["web-search"]},"params":{"type":"object","additionalProperties":false,"properties":{"query":{"type":"string","minLength":3,"maxLength":200,"description":"What to search for, as you would type it (\"solid-state battery breakthroughs\", \"Mars sample return\")."},"topic":{"type":"string","pattern":"^(news|general)$","description":"\"news\" (the default) searches news coverage; \"general\" searches the whole web."},"site":{"type":"string","maxLength":120,"pattern":"^[a-z0-9.-]+\\.[a-z]{2,}$","description":"Only results from this site, as a host name (\"nasa.gov\")."},"maxItems":{"type":"integer","minimum":1,"maximum":10,"description":"The most articles taken per firing. Default 5."},"lookbackDays":{"type":"integer","minimum":1,"maximum":90,"description":"How many days back to read. Defaults to the schedule: 1 for daily, 7 for weekly, 31 for monthly."}},"required":["query"]},"label":{"type":"string","maxLength":120}}},{"type":"object","additionalProperties":false,"required":["adapter"],"description":"Hacker News front-page discussions: each story with enough comments is one item, carrying its comment thread.","properties":{"adapter":{"type":"string","enum":["hackernews"]},"params":{"type":"object","additionalProperties":false,"properties":{"pick":{"type":"string","pattern":"^top-stories$","description":"Which list to read. Only \"top-stories\"."},"count":{"type":"integer","minimum":1,"maximum":10,"description":"The most stories taken per firing. Default 5."},"minComments":{"type":"integer","minimum":0,"maximum":1000,"description":"A story needs at least this many comments. Default 10."},"maxComments":{"type":"integer","minimum":1,"maximum":2000,"description":"The most comments carried per story. Default 240."},"maxCharsEach":{"type":"integer","minimum":100,"maximum":8000,"description":"The longest a single comment is kept. Default 1600."},"itemId":{"type":"string","pattern":"^\\d+$","description":"One specific story id instead of the front page."}}},"label":{"type":"string","maxLength":120}}},{"type":"object","additionalProperties":false,"required":["adapter","params"],"description":"A public GitHub repository: its releases, merged pull requests and commit flow over the window, as one item.","properties":{"adapter":{"type":"string","enum":["github"]},"params":{"type":"object","additionalProperties":false,"properties":{"repo":{"type":"string","pattern":"^[\\w.-]+/[\\w.-]+$","description":"The repository as owner/name."},"lookbackDays":{"type":"integer","minimum":1,"maximum":90,"description":"How many days back to read. Defaults to the schedule: 1 for daily, 7 for weekly, 31 for monthly."}},"required":["repo"]},"label":{"type":"string","maxLength":120}}},{"type":"object","additionalProperties":false,"required":["adapter","params"],"description":"One web page, read whole at each firing (a changelog, a status page, a running list).","properties":{"adapter":{"type":"string","enum":["url-watch"]},"params":{"type":"object","additionalProperties":false,"properties":{"url":{"type":"string","format":"uri","description":"The page URL (http or https)."},"label":{"type":"string","maxLength":120,"description":"What to call the page in the episode material."}},"required":["url"]},"label":{"type":"string","maxLength":120}}},{"type":"object","additionalProperties":false,"required":["adapter","params"],"description":"Text the writer supplies once (notes, an archive excerpt). The same text every firing, so pair it with another source or an on-demand episode type.","properties":{"adapter":{"type":"string","enum":["upload"]},"params":{"type":"object","additionalProperties":false,"properties":{"text":{"type":"string","minLength":1,"maxLength":12000,"description":"The text itself, up to 12,000 characters."},"label":{"type":"string","maxLength":120,"description":"What to call the text in the episode material."}},"required":["text"]},"label":{"type":"string","maxLength":120}}}],"discriminator":{"propertyName":"adapter"},"description":"Ready to save as one of `showConfig.sources`."},"customize":{"type":"string","description":"How to make it your own, when there is something to change (a category, a query)."},"suggested":{"type":"object","required":["freq","fanout"],"properties":{"freq":{"type":"string","enum":["daily","weekly","monthly"]},"fanout":{"type":"string","enum":["single","per-item"]}}},"richness":{"type":"string","enum":["articles","summaries","headlines"],"description":"How much text each item gives an episode; a headlines feed suits a weekly digest."},"rights":{"type":"string","description":"Who owns the material. Every episode credits its sources in the show notes."},"topics":{"type":"array","items":{"type":"string"}}}},"ShowSourceCatalog":{"type":"object","required":["templates"],"properties":{"templates":{"type":"array","items":{"$ref":"#/components/schemas/ShowSourceTemplate"}}}},"ShowQuote":{"type":"object","required":["seriesId","episode","suggested","caps","monthToDate"],"additionalProperties":false,"properties":{"seriesId":{"type":"string"},"episode":{"type":"object","required":["credits","lines"],"additionalProperties":false,"properties":{"credits":{"type":"integer","description":"Studio Credits for one episode with nothing rerolled."},"lines":{"type":"array","items":{"type":"object","required":["step","label","credits"],"additionalProperties":false,"properties":{"step":{"type":"string","enum":["table_read"],"description":"The paid steps of an episode. Only the read costs; the MP3 is free."},"label":{"type":"string"},"credits":{"type":"integer"}}}}}},"suggested":{"type":"object","required":["episodeCreditCap","monthlyCreditCap","episodesPerMonth"],"additionalProperties":false,"properties":{"episodeCreditCap":{"type":"integer","description":"One episode's quote + 25%."},"monthlyCreditCap":{"type":"integer","description":"The cadence's episodes in its busiest month × the episode cap."},"episodesPerMonth":{"type":"integer"}}},"caps":{"type":"object","required":["episodeCreditCap","monthlyCreditCap","problem"],"additionalProperties":false,"properties":{"episodeCreditCap":{"oneOf":[{"type":"integer"},{"type":"null"}]},"monthlyCreditCap":{"oneOf":[{"type":"integer"},{"type":"null"}]},"problem":{"oneOf":[{"type":"string"},{"type":"null"}],"description":"Why these caps cannot run the show yet, or null."}}},"monthToDate":{"type":"object","required":["spent","from"],"additionalProperties":false,"properties":{"spent":{"type":"integer"},"from":{"type":"string","format":"date-time","description":"The start of the month, in the series' timezone."}}}}},"PublishingSeriesCreateRequest":{"type":"object","required":[],"additionalProperties":false,"properties":{"title":{"type":"string","description":"Series title. When omitted, defaults to the project name."},"slug":{"type":"string"},"description":{"type":"string"},"medium":{"$ref":"#/components/schemas/PublishingMedium"},"format":{"type":"string","example":"podcast"},"status":{"type":"string","enum":["draft","active","paused","archived"]},"cadence":{"$ref":"#/components/schemas/ShowCadence"},"timezone":{"type":"string"},"language":{"type":"string"},"rating":{"type":"string","enum":["clean","explicit","unknown"]},"explicitContent":{"type":"boolean"},"podcastType":{"type":"string","enum":["episodic","serial"]},"podcastCategory":{"type":"string","example":"Fiction"},"podcastSubcategory":{"type":"string","example":"Drama"},"coverArtUrl":{"type":"string","format":"uri"},"websiteUrl":{"type":"string","format":"uri"},"copyright":{"type":"string"},"syntheticDisclosure":{"type":"string","minLength":1,"maxLength":300,"description":"The show's synthetic-media line (legal gap G8a): it ends the feed's description and every episode's notes, is the feed's `podcast:txt` (purpose synthetic-media) and shows on the show's pages. Reword it; it can never be empty (default: \"Voices and music in this show are synthetic.\")."},"ownerName":{"type":"string"},"ownerEmail":{"type":"string"},"promptDirection":{"type":"string"},"metadata":{"type":"object","additionalProperties":true},"productionMode":{"type":"string","enum":["manual","seasonal","recurring"]},"visibility":{"type":"string","enum":["public","unlisted","private"],"description":"What the show's slug exposes: public (landing page, archive, and the podcast feed), unlisted (shared episode links and the podcast feed, reached by link: never indexed, and the feed asks podcast directories not to list it), or private (nothing; the feed answers 404). A draft show's feed is served too."},"showConfig":{"oneOf":[{"$ref":"#/components/schemas/ShowConfig"},{"type":"null"}]},"cadenceEnabled":{"type":"boolean","description":"Arms the recurring cadence. Refused unless productionMode is recurring, medium is audio (recurring series are audio-only), showConfig has at least one source, the cadence has at least one schedule for an episode type the show defines, both spending caps are set, the project can make an episode (its Bible gate and cast), and a PREVIEW of it has been made for the creator to hear (`POST /publishing-series/{seriesId}/episodes`) — so a show is created unarmed and armed on update, after its preview. Arming GRANTS the show's standing approval, so it requires `userConfirmed: true`."},"episodeCreditCap":{"oneOf":[{"type":"integer","minimum":0,"maximum":100000},{"type":"null"}],"description":"The most one episode may spend, in Studio Credits; at least one episode's quote (GET /publishing-series/{seriesId}/quote suggests quote + 25%). Setting it needs `userConfirmed: true`."},"monthlyCreditCap":{"oneOf":[{"type":"integer","minimum":0,"maximum":100000},{"type":"null"}],"description":"The most a calendar month's episodes may spend; at least the episode cap (the quote suggests the cadence's episodes a month × the episode cap). Setting it needs `userConfirmed: true`."},"publishPolicy":{"type":"string","enum":["auto","hold"],"description":"`auto` (default): each episode publishes under the standing approval. `hold`: each is made and left `ready` for the creator to review and publish."},"standingApprovalKeyId":{"oneOf":[{"type":"string"},{"type":"null"}],"description":"Grant the show's standing approval to ONE API key (its id, or `\"self\"` for the calling key; an OAuth connection cannot hold one), or null to revoke it — which also turns the cadence off. Granting requires `userConfirmed: true`; audio series only."},"userConfirmed":{"type":"boolean","enum":[true],"description":"Required when the request grants a standing approval (`standingApprovalKeyId` set, or `cadenceEnabled` turned on), after the user explicitly agrees the show may approve and publish episodes without asking each time."}}},"PublishingSeriesUpdateRequest":{"type":"object","additionalProperties":false,"properties":{"title":{"type":"string"},"slug":{"type":"string"},"description":{"oneOf":[{"type":"string"},{"type":"null"}]},"medium":{"$ref":"#/components/schemas/PublishingMedium"},"format":{"type":"string","example":"podcast"},"status":{"type":"string","enum":["draft","active","paused","archived"]},"cadence":{"oneOf":[{"$ref":"#/components/schemas/ShowCadence"},{"type":"null"}]},"timezone":{"type":"string"},"language":{"type":"string"},"rating":{"type":"string","enum":["clean","explicit","unknown"]},"explicitContent":{"type":"boolean"},"podcastType":{"type":"string","enum":["episodic","serial"]},"podcastCategory":{"type":"string","example":"Fiction"},"podcastSubcategory":{"oneOf":[{"type":"string"},{"type":"null"}],"example":"Drama"},"coverArtUrl":{"oneOf":[{"type":"string","format":"uri"},{"type":"null"}]},"websiteUrl":{"oneOf":[{"type":"string","format":"uri"},{"type":"null"}]},"copyright":{"oneOf":[{"type":"string"},{"type":"null"}]},"syntheticDisclosure":{"type":"string","minLength":1,"maxLength":300,"description":"The show's synthetic-media line (legal gap G8a): it ends the feed's description and every episode's notes, is the feed's `podcast:txt` (purpose synthetic-media) and shows on the show's pages. Reword it; it can never be empty (default: \"Voices and music in this show are synthetic.\"). Null or empty is refused."},"ownerName":{"oneOf":[{"type":"string"},{"type":"null"}]},"ownerEmail":{"oneOf":[{"type":"string"},{"type":"null"}]},"promptDirection":{"oneOf":[{"type":"string"},{"type":"null"}]},"metadata":{"oneOf":[{"type":"object","additionalProperties":true},{"type":"null"}]},"productionMode":{"type":"string","enum":["manual","seasonal","recurring"]},"visibility":{"type":"string","enum":["public","unlisted","private"],"description":"What the show's slug exposes: public (landing page, archive, and the podcast feed), unlisted (shared episode links and the podcast feed, reached by link: never indexed, and the feed asks podcast directories not to list it), or private (nothing; the feed answers 404). A draft show's feed is served too."},"showConfig":{"oneOf":[{"$ref":"#/components/schemas/ShowConfig"},{"type":"null"}]},"cadenceEnabled":{"type":"boolean","description":"Arms the recurring cadence. Refused unless productionMode is recurring, medium is audio (recurring series are audio-only), showConfig has at least one source, the cadence has at least one schedule for an episode type the show defines, both spending caps are set, the project can make an episode (its Bible gate and cast), and a PREVIEW of it has been made for the creator to hear (`POST /publishing-series/{seriesId}/episodes`) — so a show is created unarmed and armed on update, after its preview. Arming GRANTS the show's standing approval, so it requires `userConfirmed: true`."},"episodeCreditCap":{"oneOf":[{"type":"integer","minimum":0,"maximum":100000},{"type":"null"}],"description":"The most one episode may spend, in Studio Credits; at least one episode's quote (GET /publishing-series/{seriesId}/quote suggests quote + 25%). Setting it needs `userConfirmed: true`."},"monthlyCreditCap":{"oneOf":[{"type":"integer","minimum":0,"maximum":100000},{"type":"null"}],"description":"The most a calendar month's episodes may spend; at least the episode cap (the quote suggests the cadence's episodes a month × the episode cap). Setting it needs `userConfirmed: true`."},"publishPolicy":{"type":"string","enum":["auto","hold"],"description":"`auto` (default): each episode publishes under the standing approval. `hold`: each is made and left `ready` for the creator to review and publish."},"standingApprovalKeyId":{"oneOf":[{"type":"string"},{"type":"null"}],"description":"Grant the show's standing approval to ONE API key (its id, or `\"self\"` for the calling key; an OAuth connection cannot hold one), or null to revoke it — which also turns the cadence off. Granting requires `userConfirmed: true`; audio series only."},"userConfirmed":{"type":"boolean","enum":[true],"description":"Required when the request grants a standing approval (`standingApprovalKeyId` set, or `cadenceEnabled` turned on), after the user explicitly agrees the show may approve and publish episodes without asking each time."}}},"PublishingSeriesEnvelope":{"type":"object","required":["series"],"additionalProperties":false,"properties":{"series":{"$ref":"#/components/schemas/PublishingSeries"}}},"PublishingSeriesList":{"type":"object","required":["series","nextCursor"],"additionalProperties":false,"properties":{"series":{"type":"array","items":{"$ref":"#/components/schemas/PublishingSeries"}},"nextCursor":{"oneOf":[{"type":"string"},{"type":"null"}]}}},"PublishingCoverArtGenerateRequest":{"type":"object","additionalProperties":false,"description":"Optional context overrides for cover-art generation. When omitted the server reads the stored series values. Lengths mirror series field limits to prevent prompt abuse.","properties":{"title":{"type":"string","maxLength":200,"description":"Override the series title for this generation."},"description":{"oneOf":[{"type":"string","maxLength":5000},{"type":"null"}],"description":"Override the series description. Null treats it as blank."},"podcastCategory":{"oneOf":[{"type":"string","maxLength":80},{"type":"null"}],"example":"Fiction","description":"Override the podcast category. Null treats it as blank."},"podcastSubcategory":{"oneOf":[{"type":"string","maxLength":80},{"type":"null"}],"example":"Drama","description":"Override the podcast subcategory. Null treats it as blank."}}},"PublishingCoverArtGenerateResponse":{"type":"object","required":["coverArtUrl"],"additionalProperties":false,"properties":{"coverArtUrl":{"type":"string","format":"uri","description":"Durable R2 URL of the generated cover image. Persist it with PATCH /publishing-series/{seriesId}."},"realPeopleOmitted":{"type":"array","items":{"type":"string"},"description":"Present when the cover would have shown real, identifiable people whose likeness needs approval: their names. The cover was drawn without them, or (when it still would have shown them) `coverArtUrl` is the show's current cover. Never an error: a show is never held for its cover."},"note":{"oneOf":[{"type":"string"},{"type":"null"}],"description":"What happened, in a sentence, when `realPeopleOmitted` is present."}}},"PublishingDestination":{"type":"object","required":["id","seriesId","projectId","provider","medium","key","status","createdAt","updatedAt"],"additionalProperties":true,"properties":{"id":{"type":"string"},"seriesId":{"type":"string"},"projectId":{"type":"string"},"provider":{"$ref":"#/components/schemas/PublishingProvider"},"medium":{"$ref":"#/components/schemas/PublishingMedium"},"key":{"type":"string"},"label":{"oneOf":[{"type":"string"},{"type":"null"}]},"status":{"type":"string","enum":["draft","connected","needs_auth","needs_setup","disconnected","failed","archived"]},"providerAccountId":{"oneOf":[{"type":"string"},{"type":"null"}]},"providerConfigId":{"oneOf":[{"type":"string"},{"type":"null"}]},"scopes":{"type":"array","items":{"type":"string"}},"settings":{"nullable":true},"externalUrl":{"oneOf":[{"type":"string","format":"uri"},{"type":"null"}]},"lastVerifiedAt":{"oneOf":[{"type":"string","format":"date-time"},{"type":"null"}]},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"PublishingDestinationUpsertRequest":{"type":"object","additionalProperties":false,"properties":{"provider":{"$ref":"#/components/schemas/PublishingProvider"},"medium":{"$ref":"#/components/schemas/PublishingMedium"},"key":{"type":"string"},"label":{"oneOf":[{"type":"string"},{"type":"null"}]},"status":{"type":"string","enum":["draft","connected","needs_auth","needs_setup","disconnected","failed","archived"]},"providerAccountId":{"oneOf":[{"type":"string"},{"type":"null"}]},"providerConfigId":{"oneOf":[{"type":"string"},{"type":"null"}]},"scopes":{"type":"array","items":{"type":"string"}},"settings":{"oneOf":[{"type":"object","additionalProperties":true},{"type":"null"}]},"externalUrl":{"oneOf":[{"type":"string","format":"uri"},{"type":"null"}]}}},"PublishingDestinationEnvelope":{"type":"object","required":["destination"],"additionalProperties":false,"properties":{"destination":{"$ref":"#/components/schemas/PublishingDestination"}}},"PublishingDestinationList":{"type":"object","required":["destinations"],"additionalProperties":false,"properties":{"destinations":{"type":"array","items":{"$ref":"#/components/schemas/PublishingDestination"}}}},"PublishingProviderConfig":{"type":"object","required":["id","provider","medium","status","scopes","createdAt","updatedAt"],"additionalProperties":true,"properties":{"id":{"type":"string"},"provider":{"$ref":"#/components/schemas/PublishingProvider"},"medium":{"$ref":"#/components/schemas/PublishingMedium"},"label":{"oneOf":[{"type":"string"},{"type":"null"}]},"status":{"type":"string","enum":["draft","connected","needs_auth","needs_setup","disconnected","failed","archived"]},"scopes":{"type":"array","items":{"type":"string"}},"authMetadata":{"nullable":true},"providerMetadata":{"nullable":true},"lastVerifiedAt":{"oneOf":[{"type":"string","format":"date-time"},{"type":"null"}]},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"PublishingProviderConfigUpsertRequest":{"type":"object","required":["provider","medium"],"additionalProperties":false,"properties":{"provider":{"$ref":"#/components/schemas/PublishingProvider"},"medium":{"$ref":"#/components/schemas/PublishingMedium"},"label":{"oneOf":[{"type":"string"},{"type":"null"}]},"status":{"type":"string"},"scopes":{"type":"array","items":{"type":"string"}},"encryptedApiKeyId":{"description":"Reference to an encrypted credential row. RSS v1 does not need this.","oneOf":[{"type":"string"},{"type":"null"}]},"authMetadata":{"oneOf":[{"type":"object","additionalProperties":true},{"type":"null"}]},"providerMetadata":{"oneOf":[{"type":"object","additionalProperties":true},{"type":"null"}]}}},"PublishingProviderConfigEnvelope":{"type":"object","required":["providerConfig"],"additionalProperties":false,"properties":{"providerConfig":{"$ref":"#/components/schemas/PublishingProviderConfig"}}},"PublishingProviderConfigList":{"type":"object","required":["providerConfigs"],"additionalProperties":false,"properties":{"providerConfigs":{"type":"array","items":{"$ref":"#/components/schemas/PublishingProviderConfig"}}}},"PublishingReleaseAsset":{"type":"object","required":["id","releaseId","kind","status","createdAt","updatedAt"],"additionalProperties":true,"properties":{"id":{"type":"string"},"releaseId":{"type":"string"},"sourceArtifactId":{"oneOf":[{"type":"string"},{"type":"null"}]},"kind":{"type":"string","enum":["mp3","mp4","wav","m4a","mov","captions","transcript","cover_art","image","manifest"]},"status":{"type":"string","enum":["ready","stale","failed"]},"url":{"oneOf":[{"type":"string","format":"uri"},{"type":"null"}]},"mimeType":{"oneOf":[{"type":"string"},{"type":"null"}]},"durationMs":{"oneOf":[{"type":"integer","minimum":0},{"type":"null"}]},"byteSize":{"oneOf":[{"type":"integer","minimum":0},{"type":"null"}]},"checksum":{"oneOf":[{"type":"string"},{"type":"null"}]},"sourceFingerprint":{"oneOf":[{"type":"string"},{"type":"null"}]},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"PublishingReleaseTarget":{"type":"object","required":["id","releaseId","destinationId","provider","medium","status","visibility","attempts","createdAt","updatedAt"],"additionalProperties":true,"properties":{"id":{"type":"string"},"releaseId":{"type":"string"},"destinationId":{"type":"string"},"provider":{"$ref":"#/components/schemas/PublishingProvider"},"medium":{"$ref":"#/components/schemas/PublishingMedium"},"status":{"type":"string","enum":["pending","queued","publishing","published","failed","skipped","needs_setup","stale"]},"visibility":{"type":"string","enum":["public","unlisted","private","draft"]},"externalId":{"oneOf":[{"type":"string"},{"type":"null"}]},"externalUrl":{"oneOf":[{"type":"string","format":"uri"},{"type":"null"}]},"publishedAt":{"oneOf":[{"type":"string","format":"date-time"},{"type":"null"}]},"attempts":{"type":"integer","minimum":0},"sourceFingerprint":{"oneOf":[{"type":"string"},{"type":"null"}]},"destination":{"$ref":"#/components/schemas/PublishingDestination"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"PublishingRelease":{"type":"object","required":["id","seriesId","projectId","type","medium","status","title","createdAt","updatedAt"],"additionalProperties":true,"properties":{"id":{"type":"string"},"seriesId":{"type":"string"},"projectId":{"type":"string"},"seriesBibleDocumentId":{"oneOf":[{"type":"string"},{"type":"null"}]},"storyPlanId":{"oneOf":[{"type":"string"},{"type":"null"}]},"storyJobId":{"oneOf":[{"type":"string"},{"type":"null"}]},"sourceArtifactId":{"oneOf":[{"type":"string"},{"type":"null"}]},"type":{"type":"string","enum":["episode","trailer","bonus","clip","announcement"]},"medium":{"$ref":"#/components/schemas/PublishingMedium"},"status":{"type":"string","enum":["ready","scheduled","publishing","published","failed","canceled","stale"]},"title":{"type":"string"},"subtitle":{"oneOf":[{"type":"string"},{"type":"null"}]},"description":{"oneOf":[{"type":"string"},{"type":"null"}]},"seasonNumber":{"oneOf":[{"type":"integer","minimum":0},{"type":"null"}]},"episodeNumber":{"oneOf":[{"type":"integer","minimum":0},{"type":"null"}]},"scheduledAt":{"oneOf":[{"type":"string","format":"date-time"},{"type":"null"}]},"publishedAt":{"oneOf":[{"type":"string","format":"date-time"},{"type":"null"}]},"publishedVia":{"oneOf":[{"type":"string","enum":["user_confirmed","standing_approval","owner","season_run"]},{"type":"null"}],"description":"Who let the release into the feed: a person's `userConfirmed`, the show's standing approval (published unasked), the owner in the app, or a season run's confirmed step. Written by a publish, or by `POST /publishing-releases/{releaseId}/schedule`; null until one of those."},"publishedByKeyId":{"oneOf":[{"type":"string"},{"type":"null"}],"description":"The Story API key that published or scheduled it — under a standing approval, the bound key; null in the app and for the built-in runner."},"sourceFingerprint":{"oneOf":[{"type":"string"},{"type":"null"}]},"staleReason":{"oneOf":[{"type":"string"},{"type":"null"}]},"assets":{"type":"array","items":{"$ref":"#/components/schemas/PublishingReleaseAsset"}},"targets":{"type":"array","items":{"$ref":"#/components/schemas/PublishingReleaseTarget"}},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"PublishingReleaseCreateRequest":{"type":"object","required":["title","sourceArtifactId"],"additionalProperties":false,"properties":{"title":{"type":"string"},"subtitle":{"type":"string"},"description":{"type":"string"},"type":{"type":"string","enum":["episode","trailer","bonus","clip","announcement"]},"medium":{"$ref":"#/components/schemas/PublishingMedium"},"sourceArtifactId":{"type":"string"},"seasonNumber":{"type":"integer","description":"Feed season number. Left out (or null), it is the season of the episode Script the source artifact performs — for a project with `seasonCadence: \"monthly\"`, the month its source was made. An explicit value wins."},"episodeNumber":{"type":"integer"},"scheduledAt":{"type":"string","format":"date-time"},"promptDirection":{"type":"string"},"manifest":{"type":"object","additionalProperties":true},"rights":{"type":"object","additionalProperties":true},"generationContext":{"type":"object","additionalProperties":true}}},"PublishingReleaseUpdateRequest":{"type":"object","additionalProperties":false,"properties":{"title":{"type":"string"},"subtitle":{"oneOf":[{"type":"string"},{"type":"null"}]},"description":{"oneOf":[{"type":"string"},{"type":"null"}]},"type":{"type":"string","enum":["episode","trailer","bonus","clip","announcement"]},"medium":{"$ref":"#/components/schemas/PublishingMedium"},"seasonNumber":{"oneOf":[{"type":"integer"},{"type":"null"}]},"episodeNumber":{"oneOf":[{"type":"integer"},{"type":"null"}]},"scheduledAt":{"oneOf":[{"type":"string","format":"date-time"},{"type":"null"}]},"promptDirection":{"oneOf":[{"type":"string"},{"type":"null"}]},"manifest":{"oneOf":[{"type":"object","additionalProperties":true},{"type":"null"}]},"rights":{"oneOf":[{"type":"object","additionalProperties":true},{"type":"null"}]},"generationContext":{"oneOf":[{"type":"object","additionalProperties":true},{"type":"null"}]}}},"PublishingReleaseRefreshMediaRequest":{"type":"object","required":["sourceArtifactId"],"additionalProperties":false,"description":"Explicitly rebind an already-published release to the latest finalized media from its existing source artifact.","properties":{"sourceArtifactId":{"type":"string","description":"Must exactly match the sourceArtifactId already bound to the published release."}}},"PublishingReleaseEnvelope":{"type":"object","required":["release"],"additionalProperties":false,"properties":{"release":{"$ref":"#/components/schemas/PublishingRelease"}}},"PublishingReleaseList":{"type":"object","required":["releases","nextCursor"],"additionalProperties":false,"properties":{"releases":{"type":"array","items":{"$ref":"#/components/schemas/PublishingRelease"}},"nextCursor":{"oneOf":[{"type":"string"},{"type":"null"}]}}},"PublishingTargetList":{"type":"object","required":["targets"],"additionalProperties":false,"properties":{"targets":{"type":"array","items":{"$ref":"#/components/schemas/PublishingReleaseTarget"}}}},"PublishingAnalyticsSnapshot":{"type":"object","required":["id","seriesId","provider","medium","capturedAt","metrics","createdAt"],"additionalProperties":true,"properties":{"id":{"type":"string"},"seriesId":{"type":"string"},"releaseId":{"oneOf":[{"type":"string"},{"type":"null"}]},"destinationId":{"oneOf":[{"type":"string"},{"type":"null"}]},"targetId":{"oneOf":[{"type":"string"},{"type":"null"}]},"provider":{"$ref":"#/components/schemas/PublishingProvider"},"medium":{"$ref":"#/components/schemas/PublishingMedium"},"capturedAt":{"type":"string","format":"date-time"},"metrics":{"type":"object","additionalProperties":true},"rawPayload":{"nullable":true},"createdAt":{"type":"string","format":"date-time"}}},"PublishingAnalyticsList":{"type":"object","required":["snapshots"],"additionalProperties":false,"properties":{"snapshots":{"type":"array","items":{"$ref":"#/components/schemas/PublishingAnalyticsSnapshot"}}}},"ArtifactRenderAck":{"type":"object","required":["artifactId","render"],"additionalProperties":false,"description":"Acknowledgement that the opt-in MP4 render has been requested for an artifact. `render` carries the share-render status returned by the table-read share renderer.","properties":{"artifactId":{"type":"string"},"render":{"type":"object","additionalProperties":true}}},"BrandCoverageRequest":{"type":"object","required":["contentText"],"additionalProperties":false,"description":"A piece of brand/marketing narrative to analyze.","properties":{"contentText":{"type":"string","maxLength":60000,"description":"The brand/marketing narrative (copy, script, pitch, case study). Up to 60k chars."},"contentType":{"type":"string","description":"What it is, e.g. \"landing page\", \"30s spot\", \"founder pitch\"."},"audience":{"type":"string","description":"The target audience, e.g. \"engineering leaders at mid-market SaaS\"."},"industry":{"type":"string","description":"The industry, e.g. \"B2B SaaS\", \"healthcare\"."},"channel":{"type":"string","description":"The delivery channel/format, e.g. \"website\", \"email\", \"out-of-home\"."},"brandVoice":{"type":"string","description":"Brand voice / values to judge consistency against."}}},"BrandCoverageAck":{"type":"object","required":["reportId","status"],"additionalProperties":true,"description":"Acknowledgement that a brand coverage analysis was queued.","properties":{"reportId":{"type":"string"},"status":{"type":"string","description":"e.g. \"generating\"; poll GET /brand-coverage/{reportId} until terminal."}}},"BrandCoverageReport":{"type":"object","required":["report"],"additionalProperties":true,"description":"A brand-rubric report from the shared coverage lifecycle. Its payload remains specific to brand storytelling.","properties":{"report":{"type":"object","additionalProperties":true,"description":"The brand coverage report payload."}}},"BrandNarrativeRequest":{"type":"object","required":["brief"],"additionalProperties":false,"description":"A brief (and optional grounding) for a piece of brand storytelling.","properties":{"brief":{"type":"string","description":"What to make and the goal — the product/launch/cause, the point, the desired outcome."},"contentType":{"type":"string","description":"The form, e.g. \"landing page\", \"30s spot\", \"founder pitch\"."},"audience":{"type":"string"},"industry":{"type":"string"},"channel":{"type":"string","description":"The delivery channel/format."},"brandVoice":{"type":"string","description":"Brand voice / values to write in."},"lengthTarget":{"type":"string","description":"Rough length, e.g. \"60 words\", \"90 seconds\"."},"sourceFacts":{"type":"string","description":"Real facts/proof to ground the piece; the model must not invent claims beyond these."},"storytellerSubset":{"type":"string","description":"Storyteller focus override for this one request."}}},"BrandNarrativeResponse":{"type":"object","required":["narrative"],"additionalProperties":true,"description":"The generated brand narrative.","properties":{"narrative":{"type":"string","description":"The finished piece of brand storytelling."},"modelId":{"type":"string","description":"The model that produced it."}}},"TheaterMotionBeatRequest":{"type":"object","required":["sceneIndex","entryIndex","compositionKind"],"additionalProperties":false,"description":"A single motion-graphics beat for a table-read theater scene.","properties":{"sceneIndex":{"type":"integer","minimum":0,"description":"Zero-based scene index."},"entryIndex":{"type":"integer","minimum":0,"description":"Zero-based script entry (spoken line) this beat occupies."},"compositionKind":{"type":"string","enum":["motion-only","motion-over-clip","performance-motion-overlay"],"description":"How the motion graphic composites."},"captionText":{"type":"string","description":"On-screen copy the motion layer renders."},"direction":{"type":"string","description":"Natural-language motion direction."},"durationSeconds":{"type":"number","minimum":0.5,"maximum":30,"description":"Motion timeline length in seconds."},"backgroundClipPrompt":{"type":"string","description":"Look for the background clip (for a motion-over-clip beat; generated separately)."}}},"TheaterMotionResult":{"type":"object","required":["artifactId"],"additionalProperties":true,"description":"The committed motion beat plus a shareable link to the enhanced theater video.","properties":{"artifactId":{"type":"string"},"shareUrl":{"type":"string","description":"Watchable link to the enhanced theater video."}}},"TheaterMotionClipRequest":{"type":"object","required":["sceneIndex","entryIndex"],"additionalProperties":false,"description":"Background clip generation for a motion-over-clip theater beat.","properties":{"sceneIndex":{"type":"integer","minimum":0,"description":"Zero-based scene index."},"entryIndex":{"type":"integer","minimum":0,"description":"Zero-based script entry of the motion-over-clip beat."},"prompt":{"type":"string","description":"Override look for the background clip (falls back to the beat's stored prompt)."},"durationSeconds":{"type":"integer","minimum":4,"maximum":30,"description":"Clip length in seconds (4–30). ⚠️ Renders past 10s were MEASURED to drift off-prompt, invent their own internal cuts and lose their location entirely; everything at 4–10s held. Keep a beat at 10s or under unless a long take is a deliberate choice."}}},"ProVoiceCloneStartRequest":{"type":"object","required":["name"],"additionalProperties":false,"description":"Start a studio-grade (Pro) voice clone.","properties":{"name":{"type":"string","description":"User-facing name for the cloned voice."},"language":{"type":"string","description":"ISO 639-1 language of the speech (default \"en\")."},"upgradeFromVoiceId":{"type":"string","description":"Optional CustomVoice id of an existing instant clone this Pro clone upgrades."}}},"ProVoiceCloneStartResult":{"type":"object","required":["jobId","recordingUrl"],"additionalProperties":true,"description":"A Pro voice clone job plus its browser recording link.","properties":{"jobId":{"type":"string"},"recordingUrl":{"type":"string","description":"Long-lived browser recording URL for collecting the speaker audio."},"expiresAt":{"type":"string","description":"When the recording link expires."}}},"PitchDeckChapters":{"type":"object","required":["jobId","status","deckMode","shotDensity","visualGrammar","reviewUrl","deliveryResolution","master","motion","chapters","interludes","titleSequence","closingSequence","finish","camera","warnings","videoModelPreflight","reassembly","videoCoverage","assembly","autoFix","verdict","earlyLook","musicPlan"],"additionalProperties":true,"description":"A pitch deck's chapters as they render (reads only: it never compiles, renders, voices or makes a plate). Statuses: draft → rendering → review → finishing → complete | failed.","properties":{"jobId":{"type":"string"},"deckMode":{"type":"string","enum":["pitch","showcase"],"description":"What the deck is for: a `pitch` (tells the story), or a `showcase` cast reel, judged as one. Set when it was planned; never changes."},"shotDensity":{"type":"string","enum":["held","standard","dense"],"description":"How many shots each render of a chapter carries (`held`, `standard` or `dense`), set when the deck was planned. Cuts inside a render are free: it never changes the price."},"videoModel":{"type":"string","enum":["wan3-prime","vidu-q4"],"description":"The video model the deck renders on (`/video-model`): `wan3-prime`, the default, or `vidu-q4`. A chapter rendered on the other model reads `stale`."},"intent":{"allOf":[{"$ref":"#/components/schemas/VideoStoryIntent"}],"nullable":true,"description":"The deck's intent — what it is for. Null for a deck planned before intents existed (its plan coverage fails on `video_intent_missing` until one is set). Read it beside plan coverage's first-time-viewer takeaway at GET /pitch-decks/{jobId}/intent."},"visualGrammar":{"type":"string","nullable":true,"description":"The deck's visual grammar. The visual grammar is the deck's one camera language across every chapter: how its world is lit and framed, how the camera moves or holds, and how chapters hand off. It is plan words: plan coverage judges every chapter against it, and nothing rendered reads it, so changing it never makes a take out of date. Replace it with POST /pitch-decks/{jobId}/visual-grammar. Null when the plan cannot be read."},"status":{"type":"string","enum":["pending","draft","rendering","review","finishing","complete","failed"],"description":"Where the deck is. A retired storyboard-era status is terminal and read-only."},"reviewUrl":{"type":"string","nullable":true,"description":"Logged-in browser link to the deck in the Videos tab, where the cut plays — motion cannot play in a terminal."},"deliveryResolution":{"type":"string","enum":["720p","1080p"],"description":"What the finish delivers: the writer's choice, else 720p (the plan never raises it)."},"master":{"type":"object","additionalProperties":false,"required":["url","stage","resolution","interim"],"description":"The stitched master: a 480p `preview` in review, `finished` once the finish completes. Only a finished master exports.","properties":{"url":{"type":"string","nullable":true},"stage":{"type":"string","nullable":true,"enum":["preview","finished",null]},"resolution":{"type":"string","nullable":true},"interim":{"type":"boolean","description":"True while the master is an EARLY LOOK: a take landed while it was stitched, so the cut with every new take is put together after it and replaces it by itself. Nothing reviews an early look; only the latest master is watched."}}},"motion":{"type":"object","additionalProperties":false,"required":["rendered","requested","stillOnly","complete"],"description":"How many footage chapters have a cut: `requested` footage chapters, `rendered` with a cut, `stillOnly` without one yet, `complete` when every one has (null before anything rendered).","properties":{"rendered":{"type":"integer","minimum":0},"requested":{"type":"integer","minimum":0},"stillOnly":{"type":"integer","minimum":0},"complete":{"type":"boolean","nullable":true}}},"chapters":{"type":"array","items":{"$ref":"#/components/schemas/PitchDeckChapter"}},"interludes":{"type":"array","items":{"$ref":"#/components/schemas/PitchDeckInterlude"},"description":"The deck's black interludes, in cut order, with what the master made of each. A black interlude is a beat of pure black between two chapters (0.5 to 2 seconds), keyed by the chapter it follows (`afterChapterIndex`), that may carry one sound (a breath, a bell) or one short line heard over the dark, the narrator's or a character's own, always as a voice-over. It costs no render: the master draws the black and voices its line and its sound at no charge. Replace them with POST /pitch-decks/{jobId}/interludes."},"titleSequence":{"allOf":[{"$ref":"#/components/schemas/PitchDeckTitleSequenceView"}],"nullable":true,"description":"The deck's title sequence with what each flash cut goes to, or null when it has none. A deck's title sequence opens its master, before the first chapter: optionally an epigraph (`epigraph`: its words and where they are from) and a source credit (`sourceCredit`), each quiet on black and held long enough to read; then flash cuts of six to ten frames to the people it names (`cast`, in order; `inShadow` darkens one), each the person's LIVING PORTRAIT — looking into the lens, still, breathing, rendered for the flash (priced first, its own step) — and until it is made a still, silent close from the deck's own takes or another rendered deck of the project (free), never one in which anyone speaks or moves; a still canon portrait image only when the writer asks for one (`flashSource: 'portrait'`); then the title (`title`, one word, in capitals) in a bold sans on black. The flash cuts and the title run `seconds` (3 to 5). With `nameSupers: true` each flash cut carries the person's NAME in the TITLE'S LOOK — the face and colour the title is drawn in (blood red, the heavy condensed sans, when the title is directed so), uppercase, large enough to read at 480p — on the bell beat with the face, set in the black BESIDE the picture (or over the top of it), never over the face: LABEL is the first word of the person's canon name in capitals unless their cast entry sets `label`, and `role` (up to 40 characters, optional) is a smaller line under it; a person `inShadow` gets no name unless their `label` is set (the shadow withholds who they are); how the names look and where they sit is directed with `titleElement: nameSupers`. With `beatGrid: true` the intros cut on the bell's beat: from the sequence's first frame a beat every `beatSeconds` (0.5 to 4; 2 when left out), each flash cut starting on a beat and holding exactly one beat (the one in shadow too), the title on the next beat for `titleBeats` beats (1 to 8; 2 when left out) — their seconds are then derived (beats × beatSeconds), and a score composed to the deck strikes its bell on every intro and on the title. Saving it is free: nothing is rendered for it (a motion close is its own priced step). `enabled: false` keeps it without playing it. How its words look and move is directed separately (`titleElement` on the direct-text-motion call; read back as `textMotion`), and a save keeps those directions. Set it with POST /pitch-decks/{jobId}/title-sequence."},"closingSequence":{"allOf":[{"$ref":"#/components/schemas/ClosingSequenceView"}],"nullable":true,"description":"The deck's closing sequence with each card's length, start and look, or null when it has none. A deck's closing sequence ends its master, after the last chapter, and mirrors its title sequence: optionally a closing quote (`quote`: its words, quoted exactly, and where they are from, cited exactly — one that answers the opening's epigraph, often from the same source), then \"Coming Soon\" (`comingSoon`, up to 40 characters; \"Coming Soon\" when left out), then the production credit (`productionCredit`; \"Sleeper Hit Studio\" when left out), each quiet on black, faded in and out and held long enough to read. It ADDS to the deck's length: no chapter is shortened for it. When the title sequence cuts on its bell beat grid, each card starts on a beat of that same grid and the bell lane accents it. Across it the music (the score or the beds) and the bell lane fade out smoothly to silence on the master's last frame: what still sounds at the last chapter's cut plays on under the cards. A card with no direction of its own wears the opening's look (the quote the epigraph's, the credit the source credit's, \"Coming Soon\" the title's or the epigraph's background), so the deck is sandwiched by one style. It is free: nothing is rendered for it, and a rendered deck's master is stitched again with it. `enabled: false` keeps it without playing it. How its words look and move is directed separately (`closingElement` on the direct-text-motion call; read back as `textMotion`), and a save keeps those directions. Set it with POST /pitch-decks/{jobId}/closing-sequence."},"finish":{"allOf":[{"$ref":"#/components/schemas/Finish"}],"description":"The film finish laid on the picture at master assembly. The finish is a film look laid on the picture when the master is assembled: a look (camera, period-35mm, night-interior, none), an intensity (light, standard, strong; standard by default) and, optionally, single effects set to a level (adjust: { effect: off | subtle | medium | strong }; effects: steady-light, match-shots, halation, mist, shoulder, colour, vignette, weave, grain, soften). New decks and trailers start on camera. Titles and captions are laid after it, so type stays crisp, and the sound and timing are never touched. Setting it is free; on a rendered deck or trailer the master is assembled again with it (free), and setting the same finish again changes nothing. Set it with POST /pitch-decks/{jobId}/look."},"camera":{"allOf":[{"$ref":"#/components/schemas/CameraReadSummary"}],"description":"The camera motion: the handheld strength and each chapter the camera touches, with its move and what the last master laid. Read it in full and set it at /pitch-decks/{jobId}/camera."},"warnings":{"type":"array","items":{"type":"string"},"description":"One per flagged take the writer kept (a line not heard or in another character's voice, an opening off its plan), which the finish quote and the export repeat; and one per chapter the master leaves out (`inMaster` false: \"Chapter 6 is left out of the master: …\")."},"videoModelPreflight":{"allOf":[{"$ref":"#/components/schemas/PitchDeckVideoModelPreflight"}],"nullable":true,"description":"Present in draft and review: what each chapter's render needs and what is missing. Null otherwise."},"assembly":{"allOf":[{"$ref":"#/components/schemas/MasterAssembly"}],"nullable":true,"description":"The deck's master being put together right now, step by step; null when nothing is being assembled."},"reassembly":{"allOf":[{"$ref":"#/components/schemas/RenderReassembly"}],"nullable":true,"description":"POST /pitch-decks/{jobId}/reassemble, as it stands now (free): for a FAILED deck, the stage it failed in and whether it would be put back together; in `review` or `complete`, whether its master would be REBUILT with the current sound rules (`available`) or why not (`refusal`: a master already being assembled, a take in flight). Null in any other status."},"verdict":{"$ref":"#/components/schemas/VideoVerdict"},"autoFix":{"allOf":[{"$ref":"#/components/schemas/AutoFix"}],"nullable":true,"description":"The deck's automatic fixes: the loop that watches the 480p preview and retakes what its review flags, inside the allowance the render approval reserved. Null for a deck approved before it."},"earlyLook":{"allOf":[{"$ref":"#/components/schemas/VideoEarlyLook"}],"nullable":true,"description":"What can be watched before the preview is stitched: every chapter whose shots all have a take, and a rough cut once two have. Null once the preview exists (and in draft)."},"musicPlan":{"allOf":[{"$ref":"#/components/schemas/VideoMusicPlan"}],"nullable":true,"description":"The music the deck will carry, known before the stitch. Null for a failed deck."},"videoCoverage":{"allOf":[{"$ref":"#/components/schemas/VideoCoverage"}],"nullable":true,"description":"The deck's video coverage: the stitched master (the 480p preview, then the finished master) watched with its sound and scored against the bar, its findings located by timecode on chapters, calls and shots. While `blocksFinish` is true the finish is refused unless the writer finishes anyway (`acknowledgeVideoCoverage`). Null for a deck with no master yet. The same object GET /pitch-decks/{jobId}/video-coverage answers."}}},"PitchDeckInterlude":{"type":"object","required":["afterChapterIndex","afterChapterNumber","seconds","durationMs","audioUrl","soundUrl","error"],"additionalProperties":false,"description":"A black interlude is a beat of pure black between two chapters (0.5 to 2 seconds), keyed by the chapter it follows (`afterChapterIndex`), that may carry one sound (a breath, a bell) or one short line heard over the dark, the narrator's or a character's own, always as a voice-over. It costs no render: the master draws the black and voices its line and its sound at no charge.","properties":{"afterChapterIndex":{"type":"integer","minimum":0,"description":"The `chapterIndex` of the chapter the black follows."},"afterChapterNumber":{"type":"integer","minimum":1,"nullable":true,"description":"The place of that chapter in the deck, from 1: \"after chapter 3\"."},"seconds":{"type":"number","minimum":0.5,"maximum":2,"description":"How long the black holds, as planned."},"line":{"type":"string","description":"The words heard over the black, when it carries a line."},"speaker":{"type":"string","description":"Who says `line`: a canon cast name, or NARRATOR. Always heard as a voice-over."},"sound":{"type":"string","description":"The one sound heard in the dark, when it carries one."},"durationMs":{"type":"integer","minimum":0,"description":"How long it holds black in the master: its seconds, or as long as its voiced line needs (at most 5 s)."},"audioUrl":{"type":"string","nullable":true,"description":"The voiced line, once an assembly has made it for the line as it stands."},"soundUrl":{"type":"string","nullable":true,"description":"The sound, once an assembly has made it for the sound as it stands."},"error":{"type":"string","nullable":true,"description":"Why its line or its sound could not be made, when one could not."}}},"PitchDeckInterludeInput":{"type":"object","required":["afterChapterIndex","seconds"],"additionalProperties":false,"properties":{"afterChapterIndex":{"type":"integer","minimum":0,"description":"The chapter the black follows — never the last chapter, at most one interlude after any chapter. The chapter's `chapterIndex`, exactly as the chapters read returns it: a key, not a position — a plan may key its chapters from 0 or from 1. A person's \"chapter 2\" is the chapter whose `chapterNumber` is 2; pass that chapter's `chapterIndex`."},"seconds":{"type":"number","minimum":0.5,"maximum":2,"description":"How long the screen holds black (a line over it holds it as long as the line needs)."},"line":{"type":"string","maxLength":160,"description":"One short sentence heard over the black (at most 12 words)."},"speaker":{"type":"string","nullable":true,"maxLength":160,"description":"Who says `line`: an exact canon cast name with a voice, or NARRATOR (the default)."},"sound":{"type":"string","maxLength":120,"description":"One sound heard in the dark, never music."}}},"PitchDeckInterludesUpdateRequest":{"type":"object","required":["interludes"],"additionalProperties":false,"properties":{"interludes":{"type":"array","maxItems":6,"items":{"$ref":"#/components/schemas/PitchDeckInterludeInput"},"description":"Every interlude the deck should have; [] removes them all."}}},"PitchDeckInterludesUpdateAck":{"type":"object","required":["success","jobId","interludes","reassembling","planCoverage"],"additionalProperties":false,"properties":{"success":{"type":"boolean","enum":[true]},"jobId":{"type":"string"},"interludes":{"type":"array","items":{"$ref":"#/components/schemas/PitchDeckInterlude"}},"reassembling":{"type":"boolean","description":"True when the master is re-assembled for the change (a deck in review or complete; free)."},"planCoverage":{"type":"string","nullable":true,"enum":["queued","current","failed",null],"description":"On a draft: the changed plan's coverage (`queued` for the latest edit). Null otherwise."}}},"TrailerInterlude":{"type":"object","required":["afterBeatIndex","afterBeatNumber","seconds","durationMs","audioUrl","soundUrl","error"],"additionalProperties":false,"description":"A black interlude is a beat of pure black between two beats of the trailer (0.5 to 2 seconds), keyed by the beat it follows (`afterBeatIndex`, the beat's place from 0; at most one after any beat and never after the last), that may carry one sound (a breath, a bell) or one short line heard over the dark (at most 12 words), the narrator's or a character's own, always as a voice-over. It costs no render: the master draws the black and voices its line and its sound at no charge. Inserting, moving or removing a beat moves each black with the beat it follows. A whole list replaces the trailer's (an empty list removes them all); on a rendered trailer the master is stitched again with it, free.","properties":{"afterBeatIndex":{"type":"integer","minimum":0,"description":"The `beatIndex` of the beat the black follows (its place in the trailer, from 0)."},"afterBeatNumber":{"type":"integer","minimum":1,"description":"The place of that beat in the trailer, from 1: \"after beat 3\"."},"seconds":{"type":"number","minimum":0.5,"maximum":2,"description":"How long the black holds, as planned."},"line":{"type":"string","description":"The words heard over the black, when it carries a line."},"speaker":{"type":"string","description":"Who says `line`: a canon cast name, or NARRATOR (the trailer's narrator). Always heard as a voice-over."},"sound":{"type":"string","description":"The one sound heard in the dark, when it carries one."},"durationMs":{"type":"integer","minimum":0,"description":"How long it holds black in the master: its seconds, or as long as its voiced line needs (at most 5 s)."},"audioUrl":{"type":"string","nullable":true,"description":"The voiced line, once an assembly has made it for the line as it stands."},"soundUrl":{"type":"string","nullable":true,"description":"The sound, once an assembly has made it for the sound as it stands."},"error":{"type":"string","nullable":true,"description":"Why its line or its sound could not be made, when one could not (a narrator line on a trailer with no narrator, a cast member with no voice, a sound with a voice in it)."}}},"TrailerInterludeInput":{"type":"object","required":["afterBeatIndex","seconds"],"additionalProperties":false,"properties":{"afterBeatIndex":{"type":"integer","minimum":0,"description":"The beat the black follows (its `beatIndex`, from 0) — never the last beat, at most one interlude after any beat."},"seconds":{"type":"number","minimum":0.5,"maximum":2,"description":"How long the screen holds black (a line over it holds it as long as the line needs)."},"line":{"type":"string","maxLength":160,"description":"One short sentence heard over the black (at most 12 words)."},"speaker":{"type":"string","nullable":true,"maxLength":160,"description":"Who says `line`: an exact canon cast name with a voice, or NARRATOR (the default: the trailer's narrator)."},"sound":{"type":"string","maxLength":120,"description":"One sound heard in the dark, never music."}}},"TrailerInterludesUpdateRequest":{"type":"object","required":["interludes"],"additionalProperties":false,"properties":{"interludes":{"type":"array","maxItems":6,"items":{"$ref":"#/components/schemas/TrailerInterludeInput"},"description":"Every interlude the trailer should have; [] removes them all."}}},"TrailerInterludesUpdateAck":{"type":"object","required":["success","jobId","interludes","reassembling","planCoverage"],"additionalProperties":false,"properties":{"success":{"type":"boolean","enum":[true]},"jobId":{"type":"string"},"interludes":{"type":"array","items":{"$ref":"#/components/schemas/TrailerInterlude"}},"reassembling":{"type":"boolean","description":"True when the master is re-assembled for the change (a trailer in review or complete; free)."},"planCoverage":{"type":"string","nullable":true,"enum":["queued","current","failed",null],"description":"On a draft: the changed plan's coverage (`queued` for the latest edit). Null otherwise."}}},"PitchDeckVisualGrammarUpdateRequest":{"type":"object","required":["visualGrammar"],"additionalProperties":false,"properties":{"visualGrammar":{"type":"string","minLength":1,"maxLength":2000,"description":"The deck's whole visual grammar, as it should read (spacing is collapsed). The visual grammar is the deck's one camera language across every chapter: how its world is lit and framed, how the camera moves or holds, and how chapters hand off. It is plan words: plan coverage judges every chapter against it, and nothing rendered reads it, so changing it never makes a take out of date."}}},"PitchDeckVisualGrammarUpdateAck":{"type":"object","required":["success","jobId","status","edited","visualGrammar","changed","planCoverage"],"additionalProperties":false,"properties":{"success":{"type":"boolean","enum":[true]},"jobId":{"type":"string"},"status":{"type":"string","enum":["draft","review","complete"]},"edited":{"type":"string","enum":["draft_plan","rendered_deck"],"description":"Which version changed: the draft plan, or a rendered deck's plan. Tell the writer which one."},"visualGrammar":{"type":"string","description":"The visual grammar the stored plan now holds (read back)."},"changed":{"type":"boolean","description":"False when the deck already said exactly this: nothing was written and nothing queued."},"planCoverage":{"type":"string","nullable":true,"enum":["queued","current","failed",null],"description":"On a draft: the changed plan's coverage (`queued` for the latest edit). Null otherwise."}}},"PitchDeckTitleSequence":{"type":"object","required":["enabled","title","cast","seconds"],"additionalProperties":false,"description":"A deck's title sequence opens its master, before the first chapter: optionally an epigraph (`epigraph`: its words and where they are from) and a source credit (`sourceCredit`), each quiet on black and held long enough to read; then flash cuts of six to ten frames to the people it names (`cast`, in order; `inShadow` darkens one), each the person's LIVING PORTRAIT — looking into the lens, still, breathing, rendered for the flash (priced first, its own step) — and until it is made a still, silent close from the deck's own takes or another rendered deck of the project (free), never one in which anyone speaks or moves; a still canon portrait image only when the writer asks for one (`flashSource: 'portrait'`); then the title (`title`, one word, in capitals) in a bold sans on black. The flash cuts and the title run `seconds` (3 to 5). With `nameSupers: true` each flash cut carries the person's NAME in the TITLE'S LOOK — the face and colour the title is drawn in (blood red, the heavy condensed sans, when the title is directed so), uppercase, large enough to read at 480p — on the bell beat with the face, set in the black BESIDE the picture (or over the top of it), never over the face: LABEL is the first word of the person's canon name in capitals unless their cast entry sets `label`, and `role` (up to 40 characters, optional) is a smaller line under it; a person `inShadow` gets no name unless their `label` is set (the shadow withholds who they are); how the names look and where they sit is directed with `titleElement: nameSupers`. With `beatGrid: true` the intros cut on the bell's beat: from the sequence's first frame a beat every `beatSeconds` (0.5 to 4; 2 when left out), each flash cut starting on a beat and holding exactly one beat (the one in shadow too), the title on the next beat for `titleBeats` beats (1 to 8; 2 when left out) — their seconds are then derived (beats × beatSeconds), and a score composed to the deck strikes its bell on every intro and on the title. Saving it is free: nothing is rendered for it (a motion close is its own priced step). `enabled: false` keeps it without playing it. How its words look and move is directed separately (`titleElement` on the direct-text-motion call; read back as `textMotion`), and a save keeps those directions.","properties":{"enabled":{"type":"boolean","description":"Whether the deck opens on it. False keeps it without playing it."},"title":{"type":"string","minLength":1,"maxLength":24,"description":"One word, shown in capitals (\"KARAMAZOV\")."},"cast":{"type":"array","minItems":1,"maxItems":7,"description":"Who the flash cuts go to, in order: the father first, then each son, the one in shadow last. Each must be in the cast canon when the project has one.","items":{"type":"object","required":["name"],"additionalProperties":false,"properties":{"name":{"type":"string","minLength":1,"maxLength":160,"description":"An exact canon cast name, or an alias the canon lists."},"inShadow":{"type":"boolean","description":"Show them in shadow: a dark flash, the face half-lost. A person in shadow gets no name super unless their `label` is set."},"role":{"type":"string","minLength":1,"maxLength":40,"description":"A smaller line under their name (\"the father\"), shown when `nameSupers` is on. Leave it out for the name alone (the default)."},"label":{"type":"string","minLength":1,"maxLength":32,"description":"The name their super shows, set in capitals, instead of the first word of their canon name (\"GRUSHENKA\"). Setting it is also what gives a person in shadow a super."},"flashSource":{"type":"string","enum":["deck","project","motion","portrait"],"description":"Where their flash cut is taken from first: `motion` (their living portrait: into the lens, still, breathing, rendered for the flash, priced first), `deck` (a still, silent close from this deck's shots), `project` (a still, silent close from another rendered deck of the project, free) or `portrait` (their STILL portrait image — only when the writer asks for one). Left out: their living portrait, else a still, silent close from this deck, then the project; a close in which anyone speaks or moves is never used."}}}},"seconds":{"type":"number","minimum":3,"maximum":5,"description":"How long the flash cuts and the title run."},"epigraph":{"type":"object","required":["text"],"additionalProperties":false,"description":"An epigraph shown first, alone on black, held long enough to read (about 3 words a second, at least 4 s).","properties":{"text":{"type":"string","minLength":1,"maxLength":400,"description":"Its words, exactly as the source gives them."},"attribution":{"type":"string","minLength":1,"maxLength":120,"description":"Where the words are from (\"John 12:24\")."}}},"sourceCredit":{"type":"string","minLength":1,"maxLength":160,"description":"A credit shown after the epigraph and before the title (\"From the novel by Fyodor Dostoevsky\")."},"nameSupers":{"type":"boolean","description":"Name each face: every flash cut carries the person's name (`LABEL`, e.g. \"FYODOR\", with a smaller `role` line under it when one is given) in the title's look — its face and colour, uppercase — beside the picture (or over the top of it), never over the face; directed with `titleElement: nameSupers`. A person in shadow gets none unless their `label` is set. Off when left out. Free."},"beatGrid":{"type":"boolean","description":"Cut the intros on the bell's beat: from the sequence's first frame a beat every `beatSeconds`, each flash cut starting on a beat and holding exactly one beat (the one in shadow too), the title on the next beat for `titleBeats` beats. Their seconds are then derived (beats × beatSeconds; `seconds` is not used), and a score composed to the deck strikes its bell on every intro and on the title. Off when left out. Free."},"beatSeconds":{"type":"number","minimum":0.5,"maximum":4,"description":"The bell's beat in seconds (0.5 to 4; 2 when left out), kept to the millisecond."},"titleBeats":{"type":"integer","minimum":1,"maximum":8,"description":"On the grid, how many whole beats the title holds (1 to 8; 2 when left out — 4 s on a 2 s bell, long enough for a score to give the title its own section)."}}},"PitchDeckTitleSequenceView":{"allOf":[{"$ref":"#/components/schemas/PitchDeckTitleSequence"},{"type":"object","required":["playing","durationMs","flashes","nameSupers","supers","beatTimeline","motionCloses","motionClosesNeeded"],"properties":{"playing":{"type":"boolean","description":"True when it plays in the master (enabled, with a title, someone to cut to, and a chapter after it)."},"textMotion":{"type":"object","readOnly":true,"additionalProperties":false,"description":"How its words look and move, as directed (absent for words in the sequence's own type). Set by POST /pitch-decks/{jobId}/storyboard/direct-text-motion with a `titleElement`, never by a save of the sequence (which keeps them). The title sequence's words are directed like a chapter's text, in the writer's own words, through the same direct-text-motion call with a `titleElement` instead of a `chapterIndex`: `title` (\"KARAMAZOV huge, blood red, a stark sans, the ends cut off by the frame\"), `epigraph` (\"a subtle background behind the verse: grain, a candle's flicker, a few falling seeds\"), `sourceCredit`, or `nameSupers` (every name alike: its face, its colour, and where it sits — beside the face on the left or the right, over the top of the picture, or under it; never over a face). The director compiles the sentence into a motion-text spec — the face (the show's, or a bundled grotesque or condensed grotesque), the colour, the size (up to edge to edge, wider than the frame so the frame cuts the first and last letters), how it moves, and a seeded background behind it (film grain, a breathing vignette, a flickering warm light, banks of fog rolling across the frame, slowly falling motes), as quiet or as strong and moving as asked (\"the same fog as the verse, but stronger\"), held under the words' own brightness (behind the blood-red title it is a haze of the same red) — and saves it with the sentence on the title sequence (`textMotion`). The words stay the sequence's: a direction says how they look, never what they say. `clear: true` puts an element back in the sequence's own type. Free: an LLM call, no credits; a rendered deck's master is stitched again with it, and nothing else renders (no chapter, no shot). A save of the title sequence keeps every direction (an element whose words are removed loses its own).","properties":{"title":{"$ref":"#/components/schemas/PitchDeckTitleTextMotion"},"epigraph":{"$ref":"#/components/schemas/PitchDeckTitleTextMotion"},"sourceCredit":{"$ref":"#/components/schemas/PitchDeckTitleTextMotion"},"nameSupers":{"$ref":"#/components/schemas/PitchDeckTitleTextMotion"}}},"nameSupers":{"type":"boolean","description":"Whether the flash cuts carry name supers (false when the sequence leaves it out)."},"beatTimeline":{"type":"object","nullable":true,"additionalProperties":false,"required":["beatSeconds","titleBeats","fps","beats","seconds","intros","title"],"description":"The bell beat grid (null when `beatGrid` is off): the beat, the derived length of the flash cuts and the title (`beats` × `beatSeconds` = `seconds`), and where each intro and the title land — the beat each starts on, its frames, and its start in the sequence and in the master (frames and seconds; the master's are null while the sequence does not play).","properties":{"beatSeconds":{"type":"number"},"titleBeats":{"type":"integer","description":"How many beats the title holds (2 when the sequence leaves `titleBeats` out)."},"fps":{"type":"integer"},"beats":{"type":"integer","description":"The intros (one beat each) and the title's beats."},"seconds":{"type":"number","description":"The derived length of the flash cuts and the title."},"intros":{"type":"array","items":{"type":"object","additionalProperties":false,"required":["name","beat","frames","sequenceFrame","sequenceSeconds","masterFrame","masterSeconds"],"properties":{"name":{"type":"string"},"beat":{"type":"integer","description":"The beat it starts on (beat 0 is the sequence's first frame)."},"frames":{"type":"integer","description":"How long it holds: exactly one beat."},"sequenceFrame":{"type":"integer"},"sequenceSeconds":{"type":"number"},"masterFrame":{"type":"integer","nullable":true},"masterSeconds":{"type":"number","nullable":true}}}},"title":{"type":"object","additionalProperties":false,"required":["beat","beats","frames","sequenceFrame","sequenceSeconds","masterFrame","masterSeconds"],"properties":{"beat":{"type":"integer"},"beats":{"type":"integer","description":"Whole beats the title holds."},"frames":{"type":"integer"},"sequenceFrame":{"type":"integer"},"sequenceSeconds":{"type":"number"},"masterFrame":{"type":"integer","nullable":true},"masterSeconds":{"type":"number","nullable":true}}}}},"supers":{"type":"array","description":"Each person's name super as the plan resolves it now, from the cast canon, in the cast's order.","items":{"type":"object","required":["name","text"],"additionalProperties":false,"properties":{"name":{"type":"string"},"text":{"type":"string","nullable":true,"description":"How the super reads (`LABEL · role`, or `LABEL`), or null when they get none (supers off, or in shadow without a label)."}}}},"durationMs":{"type":"integer","minimum":0,"description":"How long it runs in the master, its cards included."},"flashes":{"type":"array","description":"What each flash cut goes to, once an assembly has resolved them (empty before the first). Every intro is a LIVING PORTRAIT (owner, 2026-10-07: \"They should just be looking at the camera and breathing but not in motion … we don't want them to be speaking unless we're using words\"). Each person's flash is, in order: their living portrait (`source: motion`: one short WAN shot rendered for the flash from their cast portrait, the person looking straight into the lens, still, breathing, an occasional blink, lips closed, lit and graded like the deck, 480p, a few seconds, PRICED FIRST through the motion-close step and never spent silently; one another deck of the project made is reused free); until one is made, a STILL, SILENT close of them from the deck's own cut (`source: cut`, free) or another rendered deck of the project (`source: reuse`, free; `fromDeck` names it), with `needsMotionClose: true` while their living portrait is not made. A close stands in only when nobody speaks on camera in its shot, the mouth check and the take watcher saw no mouth move there, the person is alone, and its picture measured still (no walk, gesture, turn or camera move) — in motion means speaking with words, and a flash plays the score, never a line, so a close in which anyone speaks or moves never opens an intro. A person `inShadow` stays in shadow, as still. A cast entry's `flashSource` (`motion`, `deck`, `project` or `portrait`) puts that source first; `portrait` is their still canon portrait image, used only when asked for. With neither, `source` is `missing` with `needsMotionClose: true`: their beat stays black, and `motionClosesNeeded` prices the living portrait that would fill it.","items":{"type":"object","required":["name","inShadow","source","moving","url","startSeconds","chapterNumber","framing","note","nameSuper","fromDeck","needsMotionClose"],"additionalProperties":false,"properties":{"name":{"type":"string"},"inShadow":{"type":"boolean"},"source":{"type":"string","enum":["cut","reuse","motion","portrait","missing"],"description":"`motion`: their living portrait; `cut`: a still, silent close from the deck's own cut; `reuse`: a still, silent close from another rendered deck of the project (free); `portrait`: their still portrait image (only when asked for); `missing`: nothing to cut to (the beat stays black; the note says what to do)."},"moving":{"type":"boolean","description":"True when the flash is footage (a living portrait or a close), not a still image."},"url":{"type":"string","nullable":true},"startSeconds":{"type":"number","nullable":true,"description":"Where in its video the flash starts (a moving flash)."},"chapterNumber":{"type":"integer","nullable":true,"description":"The chapter of this deck the close comes from, by its place in the deck from 1 (a `cut` flash)."},"framing":{"type":"string","nullable":true},"note":{"type":"string","nullable":true},"nameSuper":{"type":"string","nullable":true,"description":"The name this flash cut carried when the master was last assembled (with its role after a ` · ` when one is given), or null."},"fromDeck":{"type":"object","nullable":true,"additionalProperties":false,"required":["jobId","chapterNumber"],"description":"The other deck of the project a reused close (or a reused motion close) is from.","properties":{"jobId":{"type":"string"},"chapterNumber":{"type":"integer","nullable":true,"description":"Its chapter's place in that deck (null for a motion close)."}}},"needsMotionClose":{"type":"boolean","description":"True when their living portrait is not made: it is what would fill their beat, or replace the still close standing in."}}}},"motionCloses":{"type":"array","description":"The living portraits rendered (or on their way) for the intros. A motion close is a person's LIVING PORTRAIT for their title intro: one WAN shot of their face and shoulders from their cast portrait, the person looking straight into the lens and holding still — breath, an occasional blink, lips closed — lit and graded like the deck (its Series Bible's look), at 480p, silent, 3 seconds on the 2 s bell (the flash's beat plus room), in shadow when the sequence keeps them in shadow. PRICED FIRST — TWO CALLS on the API and the CLI, two tools on MCP and in chat (quote_render_pitch_deck_title_motion_closes, then render_pitch_deck_title_motion_closes): without `confirmed` it prices every person whose intro is not yet their living portrait (`motionClosesNeeded` in the read: about 6 people × 3 s at 480p for a father and his sons), or the people named in `names` (a new take of someone who has one is priced again), and spends nothing; with `confirmed: true` it reserves exactly those lines and renders them. Allowed while the deck is in review or complete. When each lands it is checked like a take (a file that decodes, silent, its length) and the master is re-assembled with it, free; a render that fails or is refused is released (nothing charged). Read each one back as `titleSequence.motionCloses` (`livingPortrait` false: a moving close made before 2026-10-07, which no longer opens an intro).","items":{"type":"object","additionalProperties":false,"required":["name","take","status","url","seconds","credits","inShadow","failure","requestedAt","livingPortrait"],"properties":{"name":{"type":"string"},"take":{"type":"integer","minimum":1},"status":{"type":"string","enum":["queued","rendering","accepted","failed"],"description":"`accepted`: it plays; `failed`: released, nothing charged."},"url":{"type":"string","nullable":true},"seconds":{"type":"number"},"credits":{"type":"number"},"inShadow":{"type":"boolean"},"failure":{"type":"string","nullable":true},"requestedAt":{"type":"string","format":"date-time"},"livingPortrait":{"type":"boolean","description":"True for a living portrait; false for a moving close made before 2026-10-07, which no longer opens an intro."}}}},"motionClosesNeeded":{"type":"array","description":"Whose intro is not yet their living portrait (nothing fills their beat, or a still close stands in), with the price of each. Empty when every intro opens on one.","items":{"type":"object","additionalProperties":false,"required":["name","seconds","credits","renderable"],"properties":{"name":{"type":"string"},"seconds":{"type":"number"},"credits":{"type":"number"},"renderable":{"type":"boolean","description":"False when they have no cast portrait to render from (give them one first)."}}}}}}]},"PitchDeckTitleMotionClosesRequest":{"type":"object","additionalProperties":false,"properties":{"names":{"type":"array","maxItems":7,"items":{"type":"string","minLength":1,"maxLength":160},"description":"The people to make one for, named as the title sequence names them. Left out: everyone in `titleSequence.motionClosesNeeded`."},"confirmed":{"type":"boolean","description":"Left out: price only, nothing reserved. True: reserve exactly the priced lines and render them."}}},"PitchDeckTitleMotionClosesAnswer":{"type":"object","required":["quotedOnly","jobId","credits"],"description":"The price (`quotedOnly: true`, with `lines`, `balanceAfter`, `sufficient`, `note`, `costLabel`) or the queued renders (`quotedOnly: false`, `queued: true`, `names`, `message`).","properties":{"quotedOnly":{"type":"boolean"},"queued":{"type":"boolean"},"jobId":{"type":"string"},"credits":{"type":"number"},"costLabel":{"type":"string","nullable":true},"balanceAfter":{"type":"number"},"sufficient":{"type":"boolean"},"note":{"type":"string"},"names":{"type":"array","items":{"type":"string"}},"message":{"type":"string"},"lines":{"type":"array","items":{"type":"object","additionalProperties":false,"required":["name","take","seconds","credits","idempotencyKey"],"properties":{"name":{"type":"string"},"take":{"type":"integer"},"seconds":{"type":"number"},"credits":{"type":"number"},"idempotencyKey":{"type":"string"}}}}}},"PitchDeckTitleSequenceUpdateRequest":{"type":"object","required":["titleSequence"],"additionalProperties":false,"properties":{"titleSequence":{"allOf":[{"$ref":"#/components/schemas/PitchDeckTitleSequence"}],"nullable":true,"description":"The whole title sequence, or null to remove it."}}},"PitchDeckTitleSequenceUpdateAck":{"type":"object","required":["success","jobId","titleSequence","reassembling","planCoverage"],"additionalProperties":false,"properties":{"success":{"type":"boolean","enum":[true]},"jobId":{"type":"string"},"titleSequence":{"allOf":[{"$ref":"#/components/schemas/PitchDeckTitleSequenceView"}],"nullable":true},"reassembling":{"type":"boolean","description":"True when the master is re-assembled for the change (a deck in review or complete; free)."},"planCoverage":{"type":"string","nullable":true,"enum":["queued","current","failed",null],"description":"On a draft: the changed plan's coverage (`queued` for the latest edit). Null otherwise."}}},"ClosingSequence":{"type":"object","required":["enabled"],"additionalProperties":false,"description":"A deck's closing sequence ends its master, after the last chapter, and mirrors its title sequence: optionally a closing quote (`quote`: its words, quoted exactly, and where they are from, cited exactly — one that answers the opening's epigraph, often from the same source), then \"Coming Soon\" (`comingSoon`, up to 40 characters; \"Coming Soon\" when left out), then the production credit (`productionCredit`; \"Sleeper Hit Studio\" when left out), each quiet on black, faded in and out and held long enough to read. It ADDS to the deck's length: no chapter is shortened for it. When the title sequence cuts on its bell beat grid, each card starts on a beat of that same grid and the bell lane accents it. Across it the music (the score or the beds) and the bell lane fade out smoothly to silence on the master's last frame: what still sounds at the last chapter's cut plays on under the cards. A card with no direction of its own wears the opening's look (the quote the epigraph's, the credit the source credit's, \"Coming Soon\" the title's or the epigraph's background), so the deck is sandwiched by one style. It is free: nothing is rendered for it, and a rendered deck's master is stitched again with it. `enabled: false` keeps it without playing it. How its words look and move is directed separately (`closingElement` on the direct-text-motion call; read back as `textMotion`), and a save keeps those directions.","properties":{"enabled":{"type":"boolean","description":"Whether the deck or trailer ends on it. False keeps it without playing it."},"quote":{"type":"object","required":["text"],"additionalProperties":false,"description":"A closing quote shown first, alone on black, held long enough to read (about 3 words a second, at least 4 s).","properties":{"text":{"type":"string","minLength":1,"maxLength":400,"description":"Its words, exactly as the source gives them."},"attribution":{"type":"string","minLength":1,"maxLength":120,"description":"Where the words are from, cited exactly."}}},"comingSoon":{"type":"string","minLength":1,"maxLength":40,"description":"The second card's words (\"Coming Soon\" when left out)."},"productionCredit":{"type":"string","minLength":1,"maxLength":120,"description":"The production credit, last (\"Sleeper Hit Studio\" when left out)."}}},"ClosingSequenceView":{"allOf":[{"$ref":"#/components/schemas/ClosingSequence"},{"type":"object","required":["comingSoon","productionCredit","playing","durationMs","startSeconds","onBeatGrid","cards","fadeOut"],"properties":{"comingSoon":{"type":"string","description":"\"Coming Soon\" as the master sets it (the default when left out)."},"productionCredit":{"type":"string","description":"The production credit as the master sets it (the studio's name when left out)."},"playing":{"type":"boolean","description":"True when it plays in the master (enabled, and a chapter or beat before it)."},"durationMs":{"type":"integer","minimum":0,"description":"How long it runs in the master: what it adds to the deck's or trailer's length."},"startSeconds":{"type":"number","nullable":true,"description":"Where it starts in the master (null while it does not play)."},"onBeatGrid":{"type":"boolean","description":"Whether its cards keep the title sequence's bell beat grid (each starting on a beat)."},"textMotion":{"type":"object","readOnly":true,"additionalProperties":false,"description":"How its cards look and move, as directed (absent for a card that wears the opening's look or its own type). Set by POST /pitch-decks/{jobId}/storyboard/direct-text-motion with a `closingElement`, never by a save (which keeps them). The closing sequence's words are directed like the title sequence's, in the writer's own words, through the same direct-text-motion call with a `closingElement` instead of a `chapterIndex`: `quote`, `comingSoon` or `productionCredit` (\"the same candlelight and grain as the epigraph\", \"Coming Soon wide and slow, a faint vignette breathing behind it\"). The same director compiles the sentence into a motion-text spec (face, colour, size, motion and a quiet background) and saves it with the sentence on the closing sequence (`textMotion`). The words stay the closing's: a direction says how they look, never what they say. `clear: true` puts a card back in the opening's look (or its own type). Free: an LLM call, no credits; a rendered deck's master is stitched again with it, and nothing else renders.","properties":{"quote":{"$ref":"#/components/schemas/PitchDeckTitleTextMotion"},"comingSoon":{"$ref":"#/components/schemas/PitchDeckTitleTextMotion"},"productionCredit":{"$ref":"#/components/schemas/PitchDeckTitleTextMotion"}}},"cards":{"type":"array","description":"Its cards in order: the quote (when it has one), \"Coming Soon\", the production credit.","items":{"type":"object","additionalProperties":false,"required":["card","text","attribution","seconds","startSeconds","beat","look","lookFrom","backgroundOnly"],"properties":{"card":{"type":"string","enum":["quote","comingSoon","productionCredit"]},"text":{"type":"string"},"attribution":{"type":"string","nullable":true},"seconds":{"type":"number","description":"How long it is on screen, its fades included."},"startSeconds":{"type":"number","nullable":true,"description":"Where it starts in the master (null while the closing does not play)."},"beat":{"type":"integer","nullable":true,"description":"The bell beat it starts on, on the grid (null off the grid)."},"look":{"type":"string","nullable":true,"enum":["own","opening",null],"description":"`own`: its own direction; `opening`: the opening's look, borrowed; null: its own type on black."},"lookFrom":{"type":"string","nullable":true,"enum":["title","epigraph","sourceCredit","nameSupers",null],"description":"The opening element its look is borrowed from."},"backgroundOnly":{"type":"boolean","description":"True when only a background is borrowed: the card keeps its own type over it."}}}},"fadeOut":{"type":"object","nullable":true,"additionalProperties":false,"required":["fromSeconds","toSeconds"],"description":"Where the master fades the music (the score or the beds) and the bell lane out: from the closing's first frame to silence on the master's last (null while it does not play).","properties":{"fromSeconds":{"type":"number"},"toSeconds":{"type":"number"}}}}}]},"TrailerClosingSequenceUpdateRequest":{"type":"object","required":["closingSequence"],"additionalProperties":false,"description":"A trailer's closing sequence ends its master, after the last beat: optionally a closing quote (`quote`: its words, quoted exactly, and where they are from, cited exactly), then \"Coming Soon\" (`comingSoon`, up to 40 characters; \"Coming Soon\" when left out), then the production credit (`productionCredit`; \"Sleeper Hit Studio\" when left out), each quiet on black, faded in and out and held long enough to read. It ADDS to the trailer's length: no beat is shortened for it. Across it the music (the score or the beds) fades out smoothly to silence on the master's last frame: what still sounds at the last beat's cut plays on under the cards. It is free: nothing is rendered for it, and a rendered trailer's master is stitched again with it. `enabled: false` keeps it without playing it. How its words look and move is directed separately (`closingElement` on the trailer's direct-text-motion call; read back as `textMotion`), and a save keeps those directions.","properties":{"closingSequence":{"allOf":[{"$ref":"#/components/schemas/ClosingSequence"}],"nullable":true,"description":"The whole closing sequence, or null to remove it."}}},"TrailerClosingSequenceUpdateAck":{"type":"object","required":["success","jobId","closingSequence","reassembling"],"additionalProperties":false,"properties":{"success":{"type":"boolean","enum":[true]},"jobId":{"type":"string"},"closingSequence":{"allOf":[{"$ref":"#/components/schemas/ClosingSequenceView"}],"nullable":true},"reassembling":{"type":"boolean","description":"True when the master is re-assembled for the change (a trailer in review or complete; free)."}}},"PitchDeckClosingSequenceUpdateRequest":{"type":"object","required":["closingSequence"],"additionalProperties":false,"properties":{"closingSequence":{"allOf":[{"$ref":"#/components/schemas/ClosingSequence"}],"nullable":true,"description":"The whole closing sequence, or null to remove it."}}},"PitchDeckClosingSequenceUpdateAck":{"type":"object","required":["success","jobId","closingSequence","reassembling"],"additionalProperties":false,"properties":{"success":{"type":"boolean","enum":[true]},"jobId":{"type":"string"},"closingSequence":{"allOf":[{"$ref":"#/components/schemas/ClosingSequenceView"}],"nullable":true},"reassembling":{"type":"boolean","description":"True when the master is re-assembled for the change (a deck in review or complete; free)."}}},"Finish":{"type":"object","required":["look","intensity"],"additionalProperties":false,"description":"The finish is a film look laid on the picture when the master is assembled: a look (camera, period-35mm, night-interior, none), an intensity (light, standard, strong; standard by default) and, optionally, single effects set to a level (adjust: { effect: off | subtle | medium | strong }; effects: steady-light, match-shots, halation, mist, shoulder, colour, vignette, weave, grain, soften). New decks and trailers start on camera. Titles and captions are laid after it, so type stays crisp, and the sound and timing are never touched. Setting it is free; on a rendered deck or trailer the master is assembled again with it (free), and setting the same finish again changes nothing.","properties":{"look":{"type":"string","enum":["camera","period-35mm","night-interior","none"],"description":"The look: `camera` (Shot on a camera: fine moving grain, dense blacks under gently rolled-off highlights, a touch of mist on the lights, softer digital edges, steadied exposure and matched shots. No colour shift.); `period-35mm` (Period film: a muted 19th-century palette (warm candle and amber interiors, cold blue-grey exteriors, quieter greens and blues), a warm glow around flames, fine 35mm grain, a soft vignette and a very slight gate weave, with steadied exposure and matched shots.); `night-interior` (Candle and lamplight: cool shadows under warm highlights, a glow and a little mist around every flame, a deeper vignette and fine grain. The flame's own flicker is kept.); `none` (As rendered: no finish, the picture plays as it was rendered.)."},"intensity":{"type":"string","enum":["light","standard","strong"],"description":"How strong: `light` (Barely there: every effect one step down (the grain and the invisible corrections stay on).); `standard` (Noticeable: the look as designed.); `strong` (Strong: every effect one step up.)."},"adjust":{"type":"object","additionalProperties":false,"description":"Single effects set to an explicit level, over what the look and intensity give them.","properties":{"steady-light":{"type":"string","enum":["off","subtle","medium","strong"],"description":"Steadies the exposure pumping from frame to frame, within each cut."},"match-shots":{"type":"string","enum":["off","subtle","medium","strong"],"description":"Matches the colour and exposure of the shots inside a cut that share a room and its light, so they play as one camera; a shot set somewhere else is left alone."},"halation":{"type":"string","enum":["off","subtle","medium","strong"],"description":"A warm red-amber glow bleeding from the brightest highlights, as on film."},"mist":{"type":"string","enum":["off","subtle","medium","strong"],"description":"A faint mist around the lights, like a diffusion filter."},"shoulder":{"type":"string","enum":["off","subtle","medium","strong"],"description":"A gentle highlight shoulder over dense blacks."},"colour":{"type":"string","enum":["off","subtle","medium","strong"],"description":"Mutes over-saturated colour a little."},"vignette":{"type":"string","enum":["off","subtle","medium","strong"],"description":"Darkens the corners a little."},"weave":{"type":"string","enum":["off","subtle","medium","strong"],"description":"A very slight gate weave: the frame wanders a pixel or two."},"grain":{"type":"string","enum":["off","subtle","medium","strong"],"description":"Fine moving film grain, strongest in the mid-tones and none in pure black or white."},"soften":{"type":"string","enum":["off","subtle","medium","strong"],"description":"Takes the digital edge off, scaled to the resolution."}}}}},"FinishView":{"type":"object","required":["finish","effects","looks","intensities"],"additionalProperties":false,"description":"The finish is a film look laid on the picture when the master is assembled: a look (camera, period-35mm, night-interior, none), an intensity (light, standard, strong; standard by default) and, optionally, single effects set to a level (adjust: { effect: off | subtle | medium | strong }; effects: steady-light, match-shots, halation, mist, shoulder, colour, vignette, weave, grain, soften). New decks and trailers start on camera. Titles and captions are laid after it, so type stays crisp, and the sound and timing are never touched. Setting it is free; on a rendered deck or trailer the master is assembled again with it (free), and setting the same finish again changes nothing.","properties":{"finish":{"$ref":"#/components/schemas/Finish"},"effects":{"type":"array","description":"Every effect at its level in this finish (`off` when the finish does not lay it), with what it does.","items":{"type":"object","required":["id","level","description"],"additionalProperties":false,"properties":{"id":{"type":"string","enum":["steady-light","match-shots","halation","mist","shoulder","colour","vignette","weave","grain","soften"]},"level":{"type":"string","enum":["off","subtle","medium","strong"]},"description":{"type":"string"}}}},"looks":{"type":"array","description":"Every look there is, with what it does.","items":{"type":"object","required":["id","description"],"additionalProperties":false,"properties":{"id":{"type":"string","enum":["camera","period-35mm","night-interior","none"]},"description":{"type":"string"}}}},"intensities":{"type":"array","description":"Every intensity there is, with what it does.","items":{"type":"object","required":["id","description"],"additionalProperties":false,"properties":{"id":{"type":"string","enum":["light","standard","strong"]},"description":{"type":"string"}}}}}},"FinishSetRequest":{"type":"object","required":["finish"],"additionalProperties":false,"properties":{"finish":{"type":"object","required":["look"],"additionalProperties":false,"description":"The whole finish: what it leaves out is the default (intensity `standard`, no adjustments).","properties":{"look":{"type":"string","enum":["camera","period-35mm","night-interior","none"],"description":"The look (`none`: as rendered, no finish)."},"intensity":{"type":"string","enum":["light","standard","strong"],"description":"How strong; `standard` when left out."},"adjust":{"type":"object","additionalProperties":false,"description":"Optional single effects at a level, e.g. `{ \"grain\": \"subtle\", \"weave\": \"off\" }`.","properties":{"steady-light":{"type":"string","enum":["off","subtle","medium","strong"],"description":"Steadies the exposure pumping from frame to frame, within each cut."},"match-shots":{"type":"string","enum":["off","subtle","medium","strong"],"description":"Matches the colour and exposure of the shots inside a cut that share a room and its light, so they play as one camera; a shot set somewhere else is left alone."},"halation":{"type":"string","enum":["off","subtle","medium","strong"],"description":"A warm red-amber glow bleeding from the brightest highlights, as on film."},"mist":{"type":"string","enum":["off","subtle","medium","strong"],"description":"A faint mist around the lights, like a diffusion filter."},"shoulder":{"type":"string","enum":["off","subtle","medium","strong"],"description":"A gentle highlight shoulder over dense blacks."},"colour":{"type":"string","enum":["off","subtle","medium","strong"],"description":"Mutes over-saturated colour a little."},"vignette":{"type":"string","enum":["off","subtle","medium","strong"],"description":"Darkens the corners a little."},"weave":{"type":"string","enum":["off","subtle","medium","strong"],"description":"A very slight gate weave: the frame wanders a pixel or two."},"grain":{"type":"string","enum":["off","subtle","medium","strong"],"description":"Fine moving film grain, strongest in the mid-tones and none in pure black or white."},"soften":{"type":"string","enum":["off","subtle","medium","strong"],"description":"Takes the digital edge off, scaled to the resolution."}}}}}}},"FinishSetAck":{"allOf":[{"$ref":"#/components/schemas/FinishView"},{"type":"object","required":["reassembling"],"properties":{"reassembling":{"type":"boolean","description":"True when the master is assembled again with the new finish (a rendered deck or trailer; free)."}}}]},"MotionBlurView":{"type":"object","required":["strength","on","chosenSeconds","capSeconds","units","strengths","renderBox","description"],"additionalProperties":false,"description":"Motion blur makes chosen shots move like film: what moves in them blurs the way a camera's shutter blurs it, while still frames, faces held in frame and straight edges stay sharp. It is chosen per shot (or per chapter or beat: its moving shots, never a shot where someone speaks on camera unless that shot is named), at one of two strengths (standard, subtle; standard by default). It is free, up to 300 seconds of blurred picture per deck or trailer, and it is laid when the master is assembled, before the film finish. The sound and timing are never touched.","properties":{"strength":{"type":"string","enum":["standard","subtle"],"description":"The strength (`standard` when nothing is chosen)."},"on":{"type":"boolean","description":"True when any shot is chosen."},"chosenSeconds":{"type":"number","description":"Seconds of picture the choice blurs."},"capSeconds":{"type":"number","description":"The most a deck or trailer may choose, free."},"units":{"type":"array","description":"Every chapter (deck) or beat (trailer), in order.","items":{"type":"object","required":["unit","unitNumber","hasCut","state","sentence","shots","staleShots"],"additionalProperties":false,"properties":{"unit":{"type":"integer","description":"The chapter's `chapterIndex` or the beat's `beatIndex`: the key a set names it by."},"unitNumber":{"type":"integer","description":"Its place, 1-based: the number a writer sees."},"hasCut":{"type":"boolean","description":"It has a rendered cut (shots can be chosen only then)."},"state":{"type":"string","enum":["on","waiting","skipped","off"],"description":"`on`: the last master laid the blur; `waiting`: chosen, not in a master yet; `skipped`: the last master played it unblurred (`reason`); `off`: nothing chosen."},"reason":{"type":"string","nullable":true,"enum":["unconfigured","offline","failed","slow"],"description":"Why a skipped blur was not laid."},"sentence":{"type":"string","nullable":true,"description":"What the review says for it."},"shots":{"type":"array","items":{"type":"object","required":["number","startSeconds","endSeconds","framing","speaksOnCamera","action","chosen"],"properties":{"number":{"type":"integer","description":"The shot's number, 1-based, as the cut plays them: what a set names."},"startSeconds":{"type":"number"},"endSeconds":{"type":"number"},"framing":{"type":"string"},"speaksOnCamera":{"type":"boolean","description":"Someone says a line on camera in it (never chosen by `moving`)."},"action":{"type":"string","description":"What happens in it."},"chosen":{"type":"boolean"}}}},"staleShots":{"type":"integer","description":"Chosen shots a recompile removed: ignored until chosen again."}}}},"strengths":{"type":"array","items":{"type":"object","required":["id","description"],"additionalProperties":false,"properties":{"id":{"type":"string","enum":["standard","subtle"]},"description":{"type":"string"}}}},"renderBox":{"type":"string","nullable":true,"enum":["online","offline","unconfigured"],"description":"Whether a render box answers now (null when not checked)."},"description":{"type":"string"}}},"MotionBlurSetRequest":{"type":"object","additionalProperties":false,"description":"The units to set (units not listed keep their choice), a strength, or `off: true` to clear every choice.","properties":{"units":{"type":"array","items":{"type":"object","required":["unit"],"additionalProperties":false,"properties":{"unit":{"type":"integer","minimum":0,"description":"The chapter's `chapterIndex` / the beat's `beatIndex`."},"shots":{"description":"Shot numbers from the read; `moving` (the default: every shot where nobody speaks on camera); `[]` clears the unit.","oneOf":[{"type":"array","items":{"type":"integer","minimum":1}},{"type":"string","enum":["moving"]}]}}}},"strength":{"type":"string","enum":["standard","subtle"]},"off":{"type":"boolean"}}},"MotionBlurSetAck":{"allOf":[{"$ref":"#/components/schemas/MotionBlurView"},{"type":"object","required":["reassembling"],"properties":{"reassembling":{"type":"boolean","description":"True when the master is assembled again with the new choice (a rendered deck or trailer; free)."}}}]},"QuietTrackView":{"type":"object","required":["quietTrack","on","quietCalls","tooLong","takesWithTrack","options","description"],"additionalProperties":false,"description":"A deck's: The quiet track sends every render of the deck that has no spoken line a voice track with nobody speaking in it (silence, or a soft room tone), the way a spoken render is sent the cast's lines, to test whether quiet faces keep their lips closed when the render hears a track. It is off by default and free, and it changes only the takes rendered after it is set: each take records the track it was sent, and a take already made keeps what it had. A render with a spoken line, and a chapter that keeps its own sound (bells, singers, a crowd), never gets one. A chapter that played its render's own room tone plays the track (silence, or the hush) instead. A trailer's: The quiet track sends every render of the trailer that has no spoken line a voice track with nobody speaking in it (silence, or a soft room tone), the way a spoken render is sent the cast's lines, to test whether quiet faces keep their lips closed when the render hears a track. It is off by default and free, and it changes only the takes rendered after it is set: each take records the track it was sent, and a take already made keeps what it had. A render with a spoken line never gets one. A beat that played its render's own room tone plays the track (silence, or the hush) instead.","properties":{"quietTrack":{"type":"string","nullable":true,"enum":["silence","room_tone"],"description":"What the master's renders with no spoken line are sent as their voice track; null: none (the default)."},"on":{"type":"boolean"},"quietCalls":{"type":"integer","minimum":0,"description":"Renders with no spoken line it reaches (counted as if on; a deck chapter that keeps its own sound is left out)."},"tooLong":{"type":"integer","minimum":0,"description":"Quiet renders over 15 s: WAN hears no more reference audio than that, so they are never sent one."},"takesWithTrack":{"type":"object","additionalProperties":false,"required":["silence","room_tone"],"description":"Takes already rendered with each track.","properties":{"silence":{"type":"integer","minimum":0},"room_tone":{"type":"integer","minimum":0}}},"options":{"type":"array","items":{"type":"object","required":["id","description"],"additionalProperties":false,"properties":{"id":{"type":"string","enum":["silence","room_tone"]},"description":{"type":"string"}}}},"description":{"type":"string"}}},"PitchDeckVideoModelView":{"type":"object","required":["videoModel","label","isDefault","chaptersOnAnotherModel","renderedChapters","options","description"],"additionalProperties":false,"description":"The video model renders every chapter of the deck: WAN 3 (the default) or Vidu Q4. Changing it keeps the plan and every edit, and marks every rendered chapter out of date: the next render of the deck is quoted again and re-renders them on the new model. Takes already made are kept as they are. A render is priced in Studio Credits per second exactly as before; the quote states the model's own cost per second.","properties":{"videoModel":{"type":"string","enum":["wan3-prime","vidu-q4"],"description":"The model every chapter renders on."},"label":{"type":"string","description":"Its name as a person reads it."},"isDefault":{"type":"boolean","description":"True on wan3-prime, the default."},"chaptersOnAnotherModel":{"type":"integer","minimum":0,"description":"Rendered chapters made on the other model: out of date until they render again (priced first)."},"renderedChapters":{"type":"integer","minimum":0,"description":"Chapters with rendered takes."},"options":{"type":"array","items":{"type":"object","required":["id","label","description"],"additionalProperties":false,"properties":{"id":{"type":"string","enum":["wan3-prime","vidu-q4"]},"label":{"type":"string"},"description":{"type":"string"}}}},"description":{"type":"string"}}},"PitchDeckVideoModelSetRequest":{"type":"object","required":["videoModel"],"additionalProperties":false,"properties":{"videoModel":{"type":"string","enum":["wan3-prime","vidu-q4"],"description":"The model every chapter renders on from now on."}}},"PitchDeckVideoModelSetAck":{"allOf":[{"$ref":"#/components/schemas/PitchDeckVideoModelView"},{"type":"object","required":["changed"],"properties":{"changed":{"type":"boolean","description":"False when the deck already renders on this model (nothing was written)."}}}]},"VideoVersionZip":{"type":"object","required":["status","bytes","builtAt","error","url","expiresAt"],"description":"A version's zip: its manifest.json and every clip of its folder, stored as they are. Made on request, then kept.","properties":{"status":{"type":"string","enum":["none","queued","building","ready","failed"],"description":"`none` until asked for; `queued` / `building` while it is made; `ready`; `failed` (ask again)."},"bytes":{"type":"integer","nullable":true},"builtAt":{"type":"string","format":"date-time","nullable":true},"error":{"type":"string","nullable":true,"description":"Why it could not be made (`failed` only)."},"url":{"type":"string","nullable":true,"description":"A signed download link while `ready` (it expires: read again for a fresh one)."},"expiresAt":{"type":"string","format":"date-time","nullable":true}}},"PitchDeckVersionSummary":{"type":"object","required":["number","stage","earlyLook","publishedAt","masterUrl","masterDurationMs","masterResolution","chapterCount","takeCount","folder","archive","zip","label"],"description":"Every time a deck's master is published (a preview, an early look or the finished deck) it is kept as a numbered version: the master, and for every chapter in order the take each shot played (with its trim, and its finished copy on a finished deck), the chapter's cut before the film finish, its narration, speech, music, sound effects and text layer, and the finish it was laid with. Each version's cuts and takes are copied, untouched, into a folder of their own, one per chapter (pitch-decks/<deck>/versions/<n>-<YYYYMMDD-HHMM>/chapters/<NN>-<title>/), with a manifest.json, so a later re-render never loses the clips an earlier version played. Reading versions is free, and a zip of a version's clips is made on request, free.","properties":{"number":{"type":"integer","minimum":1,"description":"The version's number: 1, 2, 3… in the order the deck's masters were published."},"stage":{"type":"string","enum":["preview","finished","current"],"description":"The master's stage: a 480p `preview`, the `finished` deck, or `current` (one recorded after the fact)."},"earlyLook":{"type":"boolean","description":"True when the master was an early look (published while a newer cut was being assembled)."},"publishedAt":{"type":"string","format":"date-time"},"masterUrl":{"type":"string","nullable":true},"masterDurationMs":{"type":"integer","nullable":true},"masterResolution":{"type":"string","nullable":true},"chapterCount":{"type":"integer","minimum":0,"description":"Chapters the master played."},"takeCount":{"type":"integer","minimum":0,"description":"Takes their cuts played (a scene group's counted once)."},"folder":{"type":"string","description":"The version's folder in storage: `pitch-decks/<jobId>/versions/<n>-<YYYYMMDD-HHMM>/`."},"archive":{"type":"object","required":["status","files","missing","bytes","archivedAt","error"],"description":"Where the copy of its clips into the folder stands.","properties":{"status":{"type":"string","enum":["pending","archiving","archived","partial","failed"],"description":"`partial`: a clip was already gone when it was copied (the manifest lists it)."},"files":{"type":"integer","minimum":0},"missing":{"type":"integer","minimum":0},"bytes":{"type":"integer","nullable":true},"archivedAt":{"type":"string","format":"date-time","nullable":true},"error":{"type":"string","nullable":true}}},"zip":{"$ref":"#/components/schemas/VideoVersionZip"},"label":{"type":"string","description":"\"Version 3 · Finished 1080p · 2026-10-09 14:02 UTC\"."}}},"TrailerVersionSummary":{"type":"object","required":["number","stage","earlyLook","publishedAt","masterUrl","masterDurationMs","masterResolution","beatCount","takeCount","folder","archive","zip","label"],"description":"Every time a trailer's master is published (a preview, an early look or the finished trailer) it is kept as a numbered version, so re-assembling a trailer never loses the master it replaces: the master, and for every beat in order the take each shot played (with its trim, and its finished copy on a finished trailer), the beat's cut before the film finish, its narration, speech, music, sound effects and text layer, and the finish it was laid with. Each version's cuts and takes are copied, untouched, into a folder of their own, one per beat (trailers/<trailer>/versions/<n>-<YYYYMMDD-HHMM>/beats/<NN>-<label>/), with a manifest.json, so a later re-render never loses the clips an earlier version played. Reading versions is free, and a zip of a version's clips is made on request, free.","properties":{"number":{"type":"integer","minimum":1,"description":"The version's number: 1, 2, 3… in the order the trailer's masters were published."},"stage":{"type":"string","enum":["preview","finished","current"],"description":"The master's stage: a 480p `preview`, the `finished` trailer, or `current` (one recorded after the fact)."},"earlyLook":{"type":"boolean","description":"True when the master was an early look (published while a newer cut was being assembled)."},"publishedAt":{"type":"string","format":"date-time"},"masterUrl":{"type":"string","nullable":true},"masterDurationMs":{"type":"integer","nullable":true},"masterResolution":{"type":"string","nullable":true},"beatCount":{"type":"integer","minimum":0,"description":"Beats the master played."},"takeCount":{"type":"integer","minimum":0,"description":"Takes their cuts played."},"folder":{"type":"string","description":"The version's folder in storage: `trailers/<jobId>/versions/<n>-<YYYYMMDD-HHMM>/`."},"archive":{"type":"object","required":["status","files","missing","bytes","archivedAt","error"],"description":"Where the copy of its clips into the folder stands.","properties":{"status":{"type":"string","enum":["pending","archiving","archived","partial","failed"],"description":"`partial`: a clip was already gone when it was copied (the manifest lists it)."},"files":{"type":"integer","minimum":0},"missing":{"type":"integer","minimum":0},"bytes":{"type":"integer","nullable":true},"archivedAt":{"type":"string","format":"date-time","nullable":true},"error":{"type":"string","nullable":true}}},"zip":{"$ref":"#/components/schemas/VideoVersionZip"},"label":{"type":"string","description":"\"Version 3 · Finished 1080p · 2026-10-09 14:02 UTC\"."}}},"PitchDeckVersions":{"type":"object","required":["jobId","versions","description"],"properties":{"jobId":{"type":"string"},"versions":{"type":"array","description":"Newest first.","items":{"$ref":"#/components/schemas/PitchDeckVersionSummary"}},"description":{"type":"string"}}},"TrailerVersions":{"type":"object","required":["jobId","versions","description"],"properties":{"jobId":{"type":"string"},"versions":{"type":"array","description":"Newest first.","items":{"$ref":"#/components/schemas/TrailerVersionSummary"}},"description":{"type":"string"}}},"PitchDeckVersionFile":{"type":"object","required":["path","role","chapterNumber","chapterIndex","callId","take","sourceUrl","archivedUrl"],"properties":{"path":{"type":"string","description":"Relative to the version's folder (`chapters/03-the-cell/take-c_…-2.mp4`)."},"role":{"type":"string","enum":["manifest","cut","take","delivery"]},"chapterNumber":{"type":"integer","nullable":true},"chapterIndex":{"type":"integer","nullable":true},"callId":{"type":"string","nullable":true},"take":{"type":"integer","nullable":true},"sourceUrl":{"type":"string","nullable":true,"description":"Where the file was when the version was recorded."},"archivedUrl":{"type":"string","nullable":true,"description":"The copy in the version's folder; null until it is made, or when its source was already gone."}}},"TrailerVersionFile":{"type":"object","required":["path","role","beatNumber","beatIndex","callId","take","sourceUrl","archivedUrl"],"properties":{"path":{"type":"string","description":"Relative to the version's folder (`beats/02-the-door/take-c_…-1.mp4`)."},"role":{"type":"string","enum":["manifest","cut","take","delivery"]},"beatNumber":{"type":"integer","nullable":true},"beatIndex":{"type":"integer","nullable":true},"callId":{"type":"string","nullable":true},"take":{"type":"integer","nullable":true},"sourceUrl":{"type":"string","nullable":true,"description":"Where the file was when the version was recorded."},"archivedUrl":{"type":"string","nullable":true,"description":"The copy in the version's folder; null until it is made, or when its source was already gone."}}},"PitchDeckVersionRead":{"type":"object","required":["jobId","version"],"properties":{"jobId":{"type":"string"},"version":{"allOf":[{"$ref":"#/components/schemas/PitchDeckVersionSummary"},{"type":"object","required":["finish","chapters","audio","interludes","titleFlashes","files","external"],"properties":{"finish":{"type":"object","additionalProperties":true,"description":"The finish (`finish`, its `key`), camera and motion blur the master was laid with, as stored then, and what they laid."},"chapters":{"type":"array","description":"Every chapter in journey order: `cut` (before the film finish; `lookedUrl` the finished one the master played), `takes` (each call's take as played: `takeUrl`, `trim`, `deliveryUrl`, `path`, `archivedUrl`), `narration`, `speechClips`, `music`, `sfx`, `textLayer`, `takesHeldBy` (a scene group's member).","items":{"type":"object","additionalProperties":true,"required":["number","chapterIndex","title","folder","cut","takes"]}},"audio":{"type":"object","additionalProperties":true,"description":"The deck's audio timeline as played: music clips, sound effects, the bell, the score."},"interludes":{"type":"array","items":{"type":"object","additionalProperties":true}},"titleFlashes":{"type":"array","items":{"type":"object","additionalProperties":true}},"files":{"type":"array","items":{"$ref":"#/components/schemas/PitchDeckVersionFile"}},"external":{"type":"array","description":"Clips stored on another host: listed by URL, never copied.","items":{"type":"object","additionalProperties":true}}}}]}}},"TrailerVersionRead":{"type":"object","required":["jobId","version"],"properties":{"jobId":{"type":"string"},"version":{"allOf":[{"$ref":"#/components/schemas/TrailerVersionSummary"},{"type":"object","required":["finish","beats","audio","files","external"],"properties":{"finish":{"type":"object","additionalProperties":true,"description":"The finish (`finish`, its `key`), camera and motion blur the master was laid with, as stored then, and what they laid."},"beats":{"type":"array","description":"Every beat in plan order: `cut` (before the film finish; `lookedUrl` the finished one the master played), `takes` (each call's take as played: `takeUrl`, `trim`, `deliveryUrl`, `path`, `archivedUrl`), `narration`, `speechClips`, `music`, `sfx`, `textLayer`.","items":{"type":"object","additionalProperties":true,"required":["number","beatIndex","title","folder","cut","takes"]}},"audio":{"type":"object","additionalProperties":true,"description":"The trailer's audio timeline as played: music clips, sound effects, the score."},"files":{"type":"array","items":{"$ref":"#/components/schemas/TrailerVersionFile"}},"external":{"type":"array","description":"Clips stored on another host: listed by URL, never copied.","items":{"type":"object","additionalProperties":true}}}}]}}},"VideoVersionZipAck":{"type":"object","required":["jobId","number","zip","queued","note"],"properties":{"jobId":{"type":"string"},"number":{"type":"integer","minimum":1},"zip":{"$ref":"#/components/schemas/VideoVersionZip"},"queued":{"type":"boolean","description":"True when this call asked for the zip to be made."},"note":{"type":"string"}}},"QuietTrackSetRequest":{"type":"object","required":["quietTrack"],"additionalProperties":false,"properties":{"quietTrack":{"type":"string","nullable":true,"enum":["silence","room_tone"],"description":"What to send, or null to send none again."}}},"QuietTrackSetAck":{"allOf":[{"$ref":"#/components/schemas/QuietTrackView"},{"type":"object","required":["changed"],"properties":{"changed":{"type":"boolean","description":"False when the deck already sent this (nothing was written)."}}}]},"CameraMove":{"type":"object","nullable":true,"required":["move","shot","target","from","zoom"],"additionalProperties":false,"description":"A chapter's or beat's move, as chosen.","properties":{"move":{"type":"string","enum":["snap","reframe"],"description":"`snap`: a snap zoom onto a reaction; `reframe`: a slow reframe from one face to another."},"shot":{"type":"integer","description":"The shot's number in the cut, 1-based."},"target":{"type":"string","description":"The face the move lands on."},"from":{"type":"string","nullable":true,"description":"A reframe's first face."},"zoom":{"type":"number","nullable":true,"description":"A snap zoom's zoom."}}},"CameraView":{"type":"object","required":["handheld","on","units","handhelds","moves","snapZoom","description"],"additionalProperties":false,"description":"Camera motion gives the held shots an operator, the way a documentary camera moves: a handheld sway on the shots that were rendered still (off, subtle, medium; never on a shot whose camera already moves or was filmed handheld, and never so far that a speaking mouth leaves the frame), and at most one move per chapter or beat, on a held shot where nobody speaks on camera: a snap zoom onto a reaction (up to 1.5×, right after a line) or a slow reframe from one face to another already in the frame. It is free, and laid when the master is assembled, as the first step of the film finish and before any type. The sound and timing are never touched.","properties":{"handheld":{"type":"string","enum":["off","subtle","medium"],"description":"The handheld sway on the held shots (`off` when the camera is off)."},"on":{"type":"boolean","description":"True when the camera does anything (a handheld strength or a move)."},"units":{"type":"array","description":"Every chapter (deck) or beat (trailer), in order.","items":{"type":"object","required":["unit","unitNumber","hasCut","state","move","moveState","moveReason","staleMove","sentence","shots"],"additionalProperties":false,"properties":{"unit":{"type":"integer","description":"The chapter's `chapterIndex` or the beat's `beatIndex`: the key a set names it by."},"unitNumber":{"type":"integer","description":"Its place, 1-based: the number a writer sees."},"hasCut":{"type":"boolean","description":"It has a rendered cut (a move can be chosen only then)."},"state":{"type":"string","enum":["on","waiting","partly","off"],"description":"`on`: the last master laid it as chosen; `waiting`: chosen, not in a master yet; `partly`: the sway was laid but the move was left out (`moveReason`); `off`: the camera does nothing to it."},"move":{"$ref":"#/components/schemas/CameraMove"},"moveState":{"type":"string","nullable":true,"enum":["applied","dropped"],"description":"What the last master made of the move, when laid for this choice."},"moveReason":{"type":"string","nullable":true,"enum":["moving","speaks","too_short","not_found","no_clean_framing","letterboxed"],"description":"Why a dropped move was left out."},"staleMove":{"type":"boolean","description":"The move named a shot a recompile removed: ignored until chosen again."},"sentence":{"type":"string","nullable":true,"description":"What the review says for it."},"shots":{"type":"array","items":{"type":"object","required":["number","startSeconds","endSeconds","framing","camera","speaksOnCamera","cast","action"],"additionalProperties":false,"properties":{"number":{"type":"integer","description":"The shot's number, 1-based, as the cut plays them: what a set names."},"startSeconds":{"type":"number"},"endSeconds":{"type":"number"},"framing":{"type":"string"},"camera":{"type":"string","enum":["held","moving","handheld"],"description":"`held`: rendered still (it takes the sway and a move); `moving`: the camera already travels; `handheld`: filmed handheld (never swayed again)."},"speaksOnCamera":{"type":"boolean","description":"Someone says a line on camera in it (no move goes on it; its sway keeps the speaker's mouth in frame)."},"cast":{"type":"array","items":{"type":"string"},"description":"Who the call renders: who a move may aim at."},"action":{"type":"string","description":"What happens in it."}}}}}}},"handhelds":{"type":"array","items":{"type":"object","required":["id","label","description"],"additionalProperties":false,"properties":{"id":{"type":"string","enum":["off","subtle","medium"]},"label":{"type":"string"},"description":{"type":"string"}}}},"moves":{"type":"array","items":{"type":"object","required":["id","label","description"],"additionalProperties":false,"properties":{"id":{"type":"string","enum":["snap","reframe"]},"label":{"type":"string"},"description":{"type":"string"}}}},"snapZoom":{"type":"object","required":["min","default","max"],"additionalProperties":false,"properties":{"min":{"type":"number"},"default":{"type":"number"},"max":{"type":"number"}}},"description":{"type":"string"}}},"CameraSetRequest":{"type":"object","additionalProperties":false,"description":"A handheld strength, the units' moves (units not listed keep theirs; one move per unit), or `off: true` to turn the camera off.","properties":{"handheld":{"type":"string","enum":["off","subtle","medium"]},"moves":{"type":"array","items":{"type":"object","required":["unit","move"],"additionalProperties":false,"properties":{"unit":{"type":"integer","minimum":0,"description":"The chapter's `chapterIndex` / the beat's `beatIndex`."},"move":{"type":"string","enum":["snap","reframe","none"],"description":"`snap`, `reframe`, or `none` to clear the unit's move."},"shot":{"type":"integer","minimum":1,"description":"The shot number from the read (required unless `none`)."},"target":{"type":"string","description":"The face the move lands on (required unless `none`)."},"from":{"type":"string","description":"A reframe's first face."},"zoom":{"type":"number","minimum":1.15,"maximum":1.5,"description":"A snap zoom's zoom (default 1.4)."}}}},"off":{"type":"boolean"}}},"CameraSetAck":{"allOf":[{"$ref":"#/components/schemas/CameraView"},{"type":"object","required":["reassembling"],"properties":{"reassembling":{"type":"boolean","description":"True when the master is assembled again with the new choice (a rendered deck or trailer; free)."}}}]},"CameraReadSummary":{"type":"object","required":["handheld","on","units"],"additionalProperties":false,"description":"The camera motion in a deck's or trailer's read: the handheld strength, and each unit the camera touches with its move and state (the full read is `/camera`).","properties":{"handheld":{"type":"string","enum":["off","subtle","medium"]},"on":{"type":"boolean"},"units":{"type":"array","items":{"type":"object","required":["unit","unitNumber","state","move","moveState","moveReason","sentence"],"additionalProperties":false,"properties":{"unit":{"type":"integer"},"unitNumber":{"type":"integer"},"state":{"type":"string","enum":["on","waiting","partly","off"]},"move":{"$ref":"#/components/schemas/CameraMove"},"moveState":{"type":"string","nullable":true,"enum":["applied","dropped"]},"moveReason":{"type":"string","nullable":true,"enum":["moving","speaks","too_short","not_found","no_clean_framing","letterboxed"]},"sentence":{"type":"string","nullable":true}}}}}},"PitchDeckNarrator":{"type":"object","required":["voiceId","provider","source","overridden","overrideVoiceId","overrideProvider"],"additionalProperties":false,"description":"A deck's narrator speaks every line the narrator carries (chapter narration and lines over black). It resolves in one order: the voice cast for THIS deck (`set_pitch_deck_narrator`), else the cast canon's NARRATOR, else the table read's NARRATOR, else the default narrator. Casting or clearing it is free; on a rendered deck the narrated lines are voiced again in the new voice and the master is re-assembled, at no charge.","properties":{"voiceId":{"type":"string","description":"The voice every narrated line is spoken in."},"provider":{"type":"string","description":"Its TTS provider."},"source":{"type":"string","enum":["override","cast_canon","table_read_voice_map","default"],"description":"Where it comes from: `override` (cast for this deck), `cast_canon` (the canon's NARRATOR), `table_read_voice_map` (the table read's NARRATOR) or `default` (the default narrator)."},"overridden":{"type":"boolean","description":"True when this deck casts its own narrator."},"overrideVoiceId":{"type":"string","nullable":true,"description":"The deck's own casting, as stored."},"overrideProvider":{"type":"string","nullable":true}}},"ArtifactRenameRequest":{"type":"object","additionalProperties":false,"required":["title"],"description":"A new name. A pitch deck or trailer takes null (or an empty string) to go back to its automatic name; a plan and a result always keep one.","properties":{"title":{"type":["string","null"],"maxLength":200,"description":"The new name, at most 200 characters (trimmed)."}}},"RenamedVideo":{"type":"object","additionalProperties":false,"required":["jobId","title","name"],"description":"A pitch deck or trailer after its rename.","properties":{"jobId":{"type":"string"},"title":{"type":["string","null"],"description":"The writer's own name for it, or null when it is named automatically."},"name":{"type":"string","description":"The name every list shows now: `title`, or the automatic one."}}},"RenamedArtifact":{"type":"object","additionalProperties":false,"required":["artifactId","type","title","scriptId","jobId"],"description":"Renames a result. A table read is called by the script it performs, so its new name is that script's title (the episode's, where it is one), as its share page and the Writers Lab show it; the pages are never touched. A finished pitch deck or trailer is renamed as its deck or trailer is. Free and reversible (rename it again).","properties":{"artifactId":{"type":"string"},"type":{"type":"string","enum":["table_read","pitch_deck","trailer"]},"title":{"type":"string","description":"The name it has now."},"scriptId":{"type":["string","null"],"description":"A table read: the script it performs, whose title it is."},"jobId":{"type":["string","null"],"description":"A pitch deck or trailer: its job id, which the deck and trailer endpoints take."}}},"PitchDeckDuplicateRequest":{"type":"object","additionalProperties":false,"description":"Everything is optional: an empty body duplicates the deck under its own title.","properties":{"title":{"type":"string","minLength":1,"maxLength":200,"description":"The copy's title (its plan's title). Omit to keep the source deck's title."}}},"TrailerDuplicateRequest":{"type":"object","additionalProperties":false,"description":"Everything is optional: an empty body duplicates the trailer under its own name.","properties":{"title":{"type":"string","minLength":1,"maxLength":200,"description":"The copy's name. Omit to keep the source trailer's name (or its automatic one)."}}},"TrailerDuplicate":{"type":"object","additionalProperties":false,"required":["sourceJobId","jobId","status","title","beats","interludes","aspectRatio","audioPolicy","narrator","deliveryResolution","planCoverage","nextStep","reviewUrl","credits","note"],"description":"Duplicating a trailer makes a NEW `draft` trailer that carries the source trailer's CURRENT plan — every beat with its edits, its hook, its intent, its montage marks, its black interludes and its closing sequence — and the choices made on the trailer itself (its shape, its audio policy, the narrator cast for it, its delivery class, its film finish, its quiet track and its name), and leaves the source trailer untouched. Nothing rendered rides along: no takes, cuts, masters, versions, share links or coverage reports. It is free and renders and reserves nothing; the copy's plan is queued for coverage, and its next step is approve_trailer_plan on the NEW trailer's jobId (CLI: trailer approve-plan <newJobId>).","properties":{"sourceJobId":{"type":"string","description":"The trailer the plan was copied from. It is untouched."},"jobId":{"type":"string","description":"The NEW draft trailer. Every trailer tool takes this id from here on."},"status":{"type":"string","enum":["draft"]},"title":{"type":"string","description":"The copy's name."},"beats":{"type":"integer","minimum":0,"description":"How many beats the copied plan has."},"interludes":{"type":"integer","minimum":0,"description":"How many black interludes the copied plan holds."},"aspectRatio":{"type":"string","description":"The shape it renders at, carried from the source."},"audioPolicy":{"type":"string","description":"`narrate` or `suppress`, carried from the source."},"narrator":{"type":"object","additionalProperties":false,"required":["voiceId","provider"],"description":"The narrator cast for the source trailer, carried to the copy. A null `voiceId`: it had none, so the copy narrates in the default order.","properties":{"voiceId":{"type":"string","nullable":true},"provider":{"type":"string","nullable":true}}},"deliveryResolution":{"type":"string","nullable":true,"description":"The delivery class chosen on the source trailer, carried; null when it had none."},"planCoverage":{"type":"string","enum":["queued","current","failed"],"description":"The copy's plan coverage: `queued` for the new draft, `current` already scored, or `failed` when it could not be queued (request it with POST /trailers/{jobId}/storyboard/coverage; approving an unscored plan is refused)."},"nextStep":{"type":"string","enum":["approve_trailer_plan"]},"reviewUrl":{"type":"string","nullable":true,"description":"A logged-in deep link that opens the copy."},"credits":{"type":"number","enum":[0],"description":"Always 0: a duplicate renders and reserves nothing."},"note":{"type":"string","description":"What was copied and what happens next, in one paragraph to read out."}}},"PitchDeckDuplicate":{"type":"object","additionalProperties":false,"required":["sourceJobId","jobId","status","deckMode","title","chapters","interludes","narrator","deliveryResolution","planCoverage","nextStep","reviewUrl","credits","note"],"description":"Duplicating a deck makes a NEW `draft` deck that carries the source deck's CURRENT plan — every chapter with its edits, the deck's intent, its black interludes and montage marks — and the choices made on the deck itself (the narrator cast for it, its delivery class, its strategic target), and leaves the source deck untouched. Nothing rendered rides along: no takes, cuts, masters, share links or coverage reports. It is free and renders and reserves nothing; the copy's plan is queued for coverage, and its next step is approve_pitch_deck_plan on the NEW deck's jobId (CLI: pitch-deck approve-plan <newJobId>).","properties":{"sourceJobId":{"type":"string","description":"The deck the plan was copied from. It is untouched."},"jobId":{"type":"string","description":"The NEW draft deck. Every deck tool takes this id from here on."},"status":{"type":"string","enum":["draft"]},"deckMode":{"type":"string","enum":["pitch","showcase"]},"title":{"type":"string","description":"The copy's title."},"chapters":{"type":"integer","minimum":0,"description":"How many chapters the copied plan has."},"interludes":{"type":"integer","minimum":0,"description":"How many black interludes it carries."},"narrator":{"type":"object","additionalProperties":false,"required":["voiceId","provider"],"description":"The narrator cast for the source deck, carried to the copy. A null `voiceId`: it had none, so the copy narrates in the default order (the cast canon's NARRATOR, else the table read's, else the default).","properties":{"voiceId":{"type":"string","nullable":true},"provider":{"type":"string","nullable":true}}},"deliveryResolution":{"type":"string","nullable":true,"description":"The delivery class chosen on the source deck, carried; null when it had none."},"planCoverage":{"type":"string","enum":["queued","current","failed"],"description":"The copy's plan coverage: `queued` for the new draft, `current` already scored, or `failed` when it could not be queued (request it with POST /pitch-decks/{jobId}/storyboard/coverage; approving an unscored plan is refused)."},"nextStep":{"type":"string","enum":["approve_pitch_deck_plan"]},"reviewUrl":{"type":"string","nullable":true,"description":"A logged-in deep link to the deck list the copy appears in."},"credits":{"type":"number","enum":[0],"description":"Always 0: a duplicate renders and reserves nothing."},"note":{"type":"string","description":"What was copied and what happens next, in one paragraph to read out."}}},"PitchDeckPublish":{"type":"object","additionalProperties":false,"required":["shareToken","writerSlug","shareUrl","renderedVideoStatus","pdfStatus"],"description":"A published deck: its full share link (relay it verbatim), its share token and slug, and the master's state after publishing.","properties":{"shareToken":{"type":"string","description":"The deck's share token — the `t` in its public share link. `shareUrl` is the same link already assembled; use it, not this."},"writerSlug":{"type":"string","description":"The writer's public slug, where the shared deck lives."},"shareUrl":{"type":"string","description":"The deck's full public share link, built server-side. Give it to the user verbatim — never rebuild it from the token and slug."},"renderedVideoStatus":{"type":"string","description":"The master's state after publishing: a missing, idle, stale or failed master is re-stitched."},"pdfStatus":{"type":"string","nullable":true,"description":"The deck's PDF state, unchanged by publishing."}}},"PitchDeckUnpublish":{"type":"object","additionalProperties":false,"required":["success"],"description":"A deck taken down: its share link is dark and it can be deleted again.","properties":{"success":{"type":"boolean","enum":[true]}}},"PitchDeckPdf":{"type":"object","additionalProperties":false,"required":["pdfStatus"],"description":"The deck's PDF state after the request: `queued` for a new render, else the queued/rendering export already in flight, or its ready/failed state. A queued or rendering export with no job behind it stopped, and is queued again.","properties":{"pdfStatus":{"type":"string","description":"The PDF state: `queued`, `rendering`, `ready`, `failed` or `idle`."}}},"PitchDeckChapter":{"type":"object","required":["chapterIndex","chapterNumber","title","storyBeat","visualPlan","compositionKind","propContinuity","wardrobeContinuity","screenDirection","continuityNotes","speaker","speakerOnCamera","narrated","calls","stale"],"additionalProperties":true,"description":"One chapter of the plan, and how it rendered.","properties":{"chapterIndex":{"type":"integer","minimum":0,"description":"The chapter's key: pass it, exactly as read, to every chapter edit, render and take selection. A plan may key its chapters from 0 or from 1."},"chapterNumber":{"type":"integer","minimum":1,"description":"The chapter's place in the deck, counted from 1: the number a person says (\"chapter 2\") and every sentence uses. Never an input; chapter edits take `chapterIndex`."},"title":{"type":"string"},"chapterType":{"type":"string"},"storyBeat":{"type":"string","description":"What the chapter delivers dramatically."},"audiencePurpose":{"type":"string"},"visualPlan":{"type":"string","description":"What the chapter shows on screen: the source its shots are compiled from."},"motionPlan":{"type":"string"},"motionPrompt":{"type":"string","description":"The chapter's camera-motion prompt (`refine-chapter --motion-prompt`). When set, the render and plan coverage's storyboard use it IN PLACE OF `motionPlan`; a refine that changes `motionPlan` without one clears it; '' when unset (then `motionPlan` is used)."},"transitionPrompt":{"type":"string","description":"The handoff out of this chapter into the next (`refine-chapter --transition-prompt`); '' when unset."},"captionText":{"type":"string"},"narrationText":{"type":"string","description":"The chapter's spoken line."},"narrationVoice":{"type":"string","nullable":true,"description":"Who speaks the line: a cast member (in their own voice inside the cut: on camera when they are on visibleCharacters, else as a voice-over) or the narrator."},"textMode":{"type":"string","nullable":true,"enum":["voiceover","caption",null]},"compositionKind":{"type":"string","enum":["image","text","both"],"description":"image = footage only, text = on-screen text only, both = text over footage."},"chapterFocus":{"type":"string","enum":["world","cast"],"nullable":true,"description":"What the chapter is of: `world` = the place itself, establishing its scale and life, with no cast on camera; `cast` = cast members on camera. Null when the plan does not mark it."},"propContinuity":{"type":"string","description":"The props this chapter's shots carry — what is held, set down or kept in the frame. The render is told it as \"Props: …\", so a prop you are steering away from must leave this field too. After the render, changing it puts the chapter's takes out of date. An empty string clears it. Empty when the plan states none."},"wardrobeContinuity":{"type":"string","description":"What the people in this chapter wear, kept the same across its shots. The render is told it as \"Wardrobe: …\". After the render, changing it puts the chapter's takes out of date. An empty string clears it. Empty when the plan states none."},"screenDirection":{"type":"string","description":"Which way people and things face and move on screen (left to right, toward the camera), kept consistent across the cut. The render is told it as \"Screen direction: …\". After the render, changing it puts the chapter's takes out of date. An empty string clears it. Empty when the plan states none."},"continuityNotes":{"type":"string","description":"Notes on what this chapter must keep consistent with the chapters around it, for the writer and the planner. The render is never told them, so changing them never puts a take out of date. An empty string clears them. Empty when there are none."},"arcStage":{"type":"string","nullable":true,"enum":["setup","escalation","turn","closing_question",null],"description":"The stage of the intent's arc this unit serves (setup, escalation, turn, closing_question), or null when the plan names none — a blocking plan defect on a plan with an intent."},"advancesQuestion":{"type":"string","nullable":true,"description":"How this unit advances the intent's central question, or null when the plan says nothing — a blocking plan defect on a plan with an intent."},"whyItFollows":{"type":"string","nullable":true,"description":"Why this unit follows the one before it — the link a viewer sees or hears (a cause, an answer, a contrast, a match cut, a look, an object, a sound) and what it moves forward; never on screen. Null when the plan says nothing: after the first unit, the blocking plan defect `transition_unexplained`."},"montage":{"type":"boolean","description":"True when the chapter is a rapid intercut run. A montage chapter is a rapid intercut run: several brief shots of about one to two seconds each across the people and places it names, cut tight (a run across more than three named people renders as a few short calls of the chapter). Marking or unmarking it changes what renders, so a rendered chapter goes stale."},"keepsSceneSound":{"type":"boolean","description":"True when the chapter's own sound is part of its beat. A chapter that keeps its scene sound plays the sound its own render made (harness bells, singers, a crowd: voices and all) under the score; every other chapter plays only its cast's lines over quiet room tone. On a chapter with no spoken line it also decides whether the render is made with sound, so keeping the sound of takes made silent needs them rendered again; any other change re-cuts the chapter from the takes it has, free."},"lineDelivery":{"type":"string","nullable":true,"description":"How the cast member says the chapter's spoken line, or null when the writer set none. How the cast member says the chapter's spoken line, in a few words (\"quietly, broken, through tears, almost a whisper\"; at most 300 characters, an empty string clears it). The line is voiced with it: the words reach the voice as written, and the feeling, pace and level they name (sad, angry, quiet, slow) set its tone. It directs the voice only: the face and body are directed in the motion plan, and the footage's prompt never carries it. Only a line a cast member speaks is voiced with it; a narrated chapter's narration is unchanged. On a rendered chapter a change voices the line again, so its takes go out of date (rendering it again is priced first)."},"cutIntoNext":{"type":"string","enum":["dip","cut"],"description":"How the chapter hands into the next one in the master: `dip` unless the writer chose `cut`. How the chapter hands into the next one in the master: `dip` (the default) falls to black and rises again over about a second; `cut` is a hard cut, the next chapter's first frame right after this one's last. A black interlude after the chapter still plays between them, and the last chapter's choice does nothing. It changes only how the master is stitched: nothing is rendered again and no take goes out of date; on a rendered deck the master is stitched again with it, free."},"shotType":{"type":["string","null"],"description":"The chapter's shot size as the plan states it, or null. The chapter's shot size in a few words (\"close-up on Smerdyakov\", \"wide establishing of the square\"). It must name a scale on the shot ladder — establishing, wide, medium or close — or it is refused; the scale word decides, read in that order: \"medium close\" is a MEDIUM, and a two-shot or over-the-shoulder with no scale word is a medium. The render is told it (\"Framing: …\"), so on a rendered chapter it puts the takes out of date; plan coverage reads it for the shot ladder. An empty string clears it."},"shotLadderRung":{"type":["string","null"],"enum":["establishing","wide","medium","close",null],"description":"The rung of the shot ladder `shotType` places the chapter on, read the way plan coverage reads it (\"medium close\" is a medium), or null when it names none."},"shotTypeDirected":{"type":"boolean","description":"True when the writer set `shotType` (refine-chapter): the render is told it (\"Framing: …\"). A shot type the planner wrote guides plan coverage only."},"speaker":{"type":"string","nullable":true,"description":"The cast member whose voice speaks the chapter's line inside its cut, or null."},"speakerOnCamera":{"type":"boolean","description":"True when `speaker` is in the picture (visibleCharacters) and says the line on camera, lip-synced; false when the line is their voice-over (the chapter does not show them) or there is no cast speaker."},"narrated":{"type":"boolean","description":"True when the master lays the chapter's line as narration over its cut."},"visibleCharacters":{"type":"array","items":{"type":"string"},"description":"The named characters in the picture (an identity reference each). Unnamed people are not listed."},"location":{"type":"string","nullable":true,"description":"Where the chapter is set: a canonical location key (the room's key in the location plates read, e.g. INT_TAVERN), never a scene heading."},"intendedDurationSeconds":{"type":"integer","nullable":true},"clipUrl":{"type":"string","nullable":true,"description":"The chapter's cut, assembled from its calls' selected takes."},"clipResolution":{"type":"string","nullable":true},"posterUrl":{"type":"string","nullable":true,"description":"A poster frame taken from the chapter's takes (never a generated still)."},"textClipUrl":{"type":"string","nullable":true},"textStale":{"type":"boolean"},"audioUrl":{"type":"string","nullable":true},"durationMs":{"type":"number","nullable":true},"stale":{"type":"boolean","description":"The plan changed since this chapter rendered: render it again to bring it in line. Its takes stay playable."},"motionError":{"type":"string","nullable":true},"inMaster":{"type":"boolean","description":"Whether the chapter plays in the deck's master. False for a footage chapter with no cut (its shot was refused or has not rendered; `motionError` says why): the master leaves it out and plays the chapters around it end to end. Render the chapter again to put it back."},"renderedWith":{"type":"object","nullable":true,"properties":{"chapterIndex":{"type":"integer"},"chapterNumber":{"type":"integer"}},"description":"The chapter whose renders film this one: consecutive chapters in one place (at most two with a line in a cast voice) render as one long sequence, and its shots and takes are listed (and retaken) on its first chapter. Null when the chapter's shots are its own."},"motionRemedy":{"type":"object","nullable":true,"required":["action","chapterIndex","callId","take","message"],"properties":{"action":{"type":"string","enum":["select_take"]},"chapterIndex":{"type":"integer","description":"The chapter holding the scene group's takes: the select-take path's `{chapter}`."},"callId":{"type":"string"},"take":{"type":"integer"},"message":{"type":"string"}},"description":"The free way back for a chapter whose scene group is held: one of the group's selected takes is a trim that cuts away a chapter's only shot, so the group keeps its last cut (`motionError` says which take of which shot). Choose take `take` of call `callId` on the chapter keyed `chapterIndex` with select-take (free, nothing renders) and the group is cut again on the next assembly. Null when nothing is held, or when no take of that shot keeps every chapter (then `motionError` says to render the shot again)."},"lineDrop":{"allOf":[{"$ref":"#/components/schemas/PitchDeckLineDrop"}],"nullable":true,"description":"What cutting the chapter's line out of its footage would do now (POST …/chapters/{chapterIndex}/drop-line): the takes it trims and the span each keeps, or why it cannot. Null for a chapter with no spoken line."},"droppedLine":{"allOf":[{"$ref":"#/components/schemas/PitchDeckDroppedLine"}],"nullable":true,"description":"The line already cut out of this chapter's footage: its words, speaker and delivery, where it moved, and each trim's state. Null when none was."},"calls":{"type":"array","items":{"$ref":"#/components/schemas/PitchDeckCall"}}}},"PitchDeckCall":{"type":"object","required":["callId","durationSeconds","audioMode","plateKey","cast","takes","selectedTake","rendering","retake"],"additionalProperties":true,"description":"One render call of a chapter (at most 10s): what it renders, its takes and its selection.","properties":{"callId":{"type":"string"},"durationSeconds":{"type":"number"},"audioMode":{"type":"string","enum":["dialogue","ambient","silent"]},"plateKey":{"type":"string","nullable":true,"description":"The location plate the render opens on (the rung matched to shot 1's framing), or null for a text-only render: no named character on camera and no speech track, so no reference image is sent."},"cast":{"type":"array","items":{"type":"string"},"description":"Who is on camera (a body figure each)."},"blocking":{"type":"string"},"shots":{"type":"array","items":{"type":"object","additionalProperties":true,"properties":{"framing":{"type":"string"},"onCamera":{"type":"array","items":{"type":"string"}},"setup":{"type":"string"},"action":{"type":"string"},"startSeconds":{"type":"number"},"endSeconds":{"type":"number"},"lines":{"type":"array","items":{"type":"object","additionalProperties":true,"properties":{"lineRef":{"type":"integer"},"speaker":{"type":"string"},"text":{"type":"string"},"voice":{"type":"string"}}}}}}},"speechTrack":{"type":"object","nullable":true,"additionalProperties":false,"description":"The cast member's recorded line the selected take was sent with (a chapter spoken in a cast voice).","properties":{"url":{"type":"string"},"seconds":{"type":"number"}}},"takes":{"type":"array","items":{"$ref":"#/components/schemas/PitchDeckTake"}},"selectedTake":{"type":"integer","nullable":true,"description":"The take the cut plays and the finish delivers. Null when the call has no accepted take yet: render it again."},"notices":{"type":"array","items":{"type":"object","additionalProperties":true,"properties":{"take":{"type":"integer"},"code":{"type":"string"},"message":{"type":"string","description":"Plain words for the writer. For a content refusal it is the action that can work: an IP refusal (`code: reference_render_provider_ip_refused`) names what to edit and the likely trigger."},"moderation":{"type":"boolean","description":"True for a content refusal: released, never retried."}}}},"rendering":{"type":"boolean","description":"A take of this call is in flight now."},"retake":{"type":"object","additionalProperties":false,"description":"A priced new take of this call (the render-clip op): its take number and credits.","properties":{"take":{"type":"integer"},"credits":{"type":"number"}}}}},"PitchDeckTake":{"type":"object","required":["take","status","atOurCost","selected","voiceMissLineRefs","ownReadingLineRefs","openingMiss","mouthMiss","duplicatePerson","duplicateNotChecked","mouthNotChecked","mouthUncertain","mouthFindings","checksPending","abandoned","identitySwaps"],"additionalProperties":true,"description":"One take of one call, with its checks.","properties":{"take":{"type":"integer","minimum":1},"status":{"type":"string","enum":["accepted","rejected","failed","pending"]},"videoUrl":{"type":"string","nullable":true},"atOurCost":{"type":"boolean","description":"True for the call's ONE automatic retake (a line not heard or said in another character's voice, a mouth that did not do what its line asks, or an opening off its framing): we paid for it, never the writer."},"selected":{"type":"boolean"},"selectedBy":{"type":"string","nullable":true,"enum":["default","writer",null]},"voiceMissLineRefs":{"type":"array","items":{"type":"integer"},"description":"Lines this take failed by voice: not heard where they belong, or said in a voice too like another speaking character's. A flag: the take is retaken once automatically, ranks below a take without one, and may still be selected by the writer."},"ownReadingLineRefs":{"type":"array","items":{"type":"integer"},"description":"Lines this take said in its own reading, heard and distinct from the other voices, and kept: never a flag. Every take plays its own sound under its own picture, so the lips follow what is heard; such a line does not sound like the cast card's voice."},"openingMiss":{"type":"boolean","description":"True when shot 1 opened off its planned framing."},"mouthMiss":{"type":"boolean","description":"True when the mouth check measured a mouth that did not do what its line asks, past the band around the limit, or the quiet-mouth check measured a face moving its lips as if speaking where no line is heard (`quiet_mouth_motion`): the speaker's mouth not following an on-camera line (or following it too late or early, or still moving after it), or lips moving under someone else's voice or a voice-over. `mouthFindings` names each criterion and its number. Where the speaker's mouth could not be measured (a face too small, not found or hidden — or a miss read on a face too small to trust), the take watcher's own look at the lips decides the line (unmeasurable → decided by the watcher, finding `watcher_lip_sync`): a mouth it saw not speak the line is a miss. A reading inside the band, or an unread speaker the watcher saw speak, is `mouthUncertain`, never a miss."},"mouthUncertain":{"type":"boolean","description":"True when the take's lip sync is UNCERTAIN: measured, but a criterion sat inside the band around its limit — r within 0.10 of 0.35, one frame too many open after the line, a listener's openings just past the limit — so the same file could read either side of it; or unmeasurable → decided by the watcher, which saw the speaker's mouth speak the line (`watcher_lip_sync`, uncertain). Never a pass and never a retake (automatic or priced): the take ranks below a measured pass and above a measured miss, and its `warning` names the criterion and its number. Also true when a face where no line is heard is TOO CLOSE TO CALL (`quietMouths`: read near its bar, or a pod reading of lips moving that the take watcher did not see too): its words are in `mouthFindings` (outcome `uncertain`), it is the writer's to retake (the call's `retake`), and it is never retaken automatically nor ranks the take down."},"mouthFindings":{"type":"array","description":"Every criterion the mouth check missed (`outcome: fail`) or was uncertain on (`outcome: uncertain`), misses first. Speaker: `correlation` (r, the least that passes), `lag` (ms the mouth trails the voice, negative when it leads; the most that passes), `trailing_motion` (the share of the frames after the line with the mouth open), `articulation` (the lips barely part under the voice: the mean openness of the voiced frames, 0 closed to 3 wide; the least that passes), `articulation_stretch` (voiced frames running on lips that never open; the most that passes). Still face: `still_open_run` (frames), `still_open_share`, `still_openings`. `watcher_lip_sync`: unmeasurable → decided by the watcher — a speaker whose mouth could not be measured (or whose miss was read on a face too small to trust) is decided by the take watcher's own look at the lips: `fail` when it saw the mouth not speak the line (a miss, retaken like any), `uncertain` when it saw it speak; `value` is the watcher's severity on the speaker (0 none, 1 minor, 2 major, 3 critical), `limit` 1. Empty on a pass and when the lips were not checked.","items":{"type":"object","additionalProperties":false,"required":["lineRef","name","role","criterion","outcome","value","limit","words"],"properties":{"lineRef":{"type":"integer"},"name":{"type":"string"},"role":{"type":"string","enum":["speaker","still"]},"criterion":{"type":"string","enum":["correlation","lag","trailing_motion","articulation","articulation_stretch","still_open_run","still_open_share","still_openings","watcher_lip_sync","quiet_mouth_motion"],"description":"`quiet_mouth_motion`: a face whose lips were MEASURED moving as if speaking where NO line is heard (the quiet-mouth check; `lineRef` 0, `startSeconds`–`endSeconds` say where in the take; `value` its lip movement or mouth motion, `limit` the bar it crossed)."},"outcome":{"type":"string","enum":["fail","uncertain"]},"value":{"type":"number"},"limit":{"type":"number"},"words":{"type":"string","description":"The sentence every surface shows, with the numbers."},"startSeconds":{"type":"number","description":"Where in the take it was seen, when narrower than its line (`articulation_stretch`, `quiet_mouth_motion`)."},"endSeconds":{"type":"number"}}}},"playthrough":{"type":"object","nullable":true,"additionalProperties":true,"description":"The per-line check that the take played the cast's recorded line: `{ status: checked | unavailable | not_applicable, pass, lines[], reason }`."},"opening":{"type":"object","nullable":true,"additionalProperties":true,"description":"The opening-framing check: `{ status: judged | unavailable, pass, plannedFraming, observedFraming, detail, reason }`."},"quietMouths":{"type":"object","nullable":true,"additionalProperties":true,"description":"The QUIET-MOUTH check — every named face on camera where NO line is heard (a silent or ambient take throughout, a dialogue take between and around its lines), MEASURED, two ways (`measure`): `lips` — the render box found the face and outlined its lips (Apple Vision), and `energy` is how far the lips' openness swung across the stretch once its slow drift is out — a smile, a head turn, a push-in or the light read still (still under 0.06, speaking at 0.08 or more; `box` is where the face was); `motion` — no box answered, so the face was located and its lower face's motion that no head or camera move explains was measured against its upper face's (still under 0.13, speaking at 0.18 or more), and a `motion` reading of `speaking` counts only where the take watcher saw lips moving too (else `uncertain`, with why: a push-in, a flare or a smile reads the same). `{ status: checked | unmeasurable | unavailable | not_applicable, pass, uncertain, spans[{ shot, startSeconds, endSeconds, onCamera, faces[{ name, status: measured | unmeasurable, verdict: still | uncertain | speaking | null, energy, measure: lips | motion, box, exempt, faceHeightPx, startSeconds, endSeconds, pairs, pairsSetAside, words }], keepsSceneSound }], reason, version, clipSha256, cached }`. A face the box read in a shot with more than one person is named by where it sits (\"the face on the left\") unless it moved, when the face locator names it. Where the chapter keeps its own sound (`keepsSceneSound`: a crowd, singers), only the cast's named faces are held: a moving face none of the cast is recorded with `exempt: kept_scene_sound`, never a miss. A face `speaking` is a mouth miss (`mouthMiss`, a `quiet_mouth_motion` finding), but never retaken on its own: the same staging brings it back, so the take's decision offers a new take held `restage_needed` (restage the shot: one face, framed closer), unless the lips run on past the take's last line, which is trimmed free (`trim_overrun`). `uncertain` is shown (`mouthUncertain`, and a minor review finding marked `uncertain`) and is the writer's to retake, never retaken automatically and never a veto. Null when it never ran on this take."},"quietTrack":{"type":"string","nullable":true,"enum":["silence","room_tone"],"description":"The deck's quiet track this take was sent as its voice track (a render with no spoken line, while the deck sends one: get_pitch_deck_quiet_track); null: none. Compare the quiet-mouth checks of takes with and without it."},"videoModel":{"type":"string","enum":["wan3-prime","vidu-q4"],"description":"The video model this take was made on. On `vidu-q4` the take speaks its lines in its own reading of each speaker's voice sample: its `playthrough` is `measureOnly` (measured and shown, never a flag or a retake)."},"mouths":{"type":"object","nullable":true,"additionalProperties":true,"description":"The mouth check: `{ status: checked | unmeasurable | unavailable | not_applicable, pass, uncertain, lines[{ lineRef, speaker, voice, shot, status: measured | unmeasurable, pass, uncertain, reason, faces[{ name, role: speaker | still, status, pass, uncertain, findings[{ criterion, outcome, value, limit, words }], correlation, lagMs, openShareAfter, speechOnCameraSeconds, box, states }], locatorFrames, locatorRetried, decidedBy: mouth_reader | watcher | null, watcher: { basis: unread | small_face, verdict: rigid | speaking, finding, words } | null }], reason, version, clipSha256, cached }`. A line with `decidedBy: watcher` is unmeasurable → decided by the watcher: its speaker's mouth could not be measured (or its miss was read on a face shorter than the reader is trusted on alone), so the take watcher's judgment decided it — `rigid` is a miss, `speaking` is uncertain. The verdict is stored by the take file's content hash (`clipSha256`) and the check `version` (the code, its numbers, both judging rows and their models): the same file always returns the same verdict (`cached: true` on a re-check). The face locator reads frames across each line's speech (`locatorFrames`, in seconds) with each person's identity image, and once more, denser, when it finds the speaker in none (`locatorRetried`); in `states`, one character per tenth of a second, `.` is off camera. Each line's `measure` says how its mouths were read: `lips` (the render box's lip outlines, measured; no `locatorFrames`) or `reader` (the face locator and the mouth reader). Null when it never ran on this take."},"abandoned":{"type":"boolean","description":"True for A TAKE THAT NEVER FINISHED (RELEASED): the job holding its claim died (a restart, a redeploy) before anything was sent, so the claim was released, any credits reserved for it were released once, and the shot's next take was decided again. It is recorded `failed` (`failure.code` `take_never_finished`, `failure.message` says why), never selected, and never counts against the shot's automatic retake. Say \"a take that never finished (released)\"."},"checksPending":{"type":"boolean","description":"True while the take's CHECKS HAVE NOT COMPLETED — its QA, its mouth or duplicate-person check, or its watch (a redeploy can interrupt them; the worker runs them again): nothing proves the take is clean yet. It ranks below every take whose checks completed, so it is never selected by default over one, and its `warning` says so. Say \"checks pending\", never \"clean\"."},"identitySwaps":{"type":"array","description":"Every line the WRONG PERSON speaks: the mouth check found the line's `speaker` nowhere on camera across it, and `face` — another member of the call's cast, matched by their identity image — following it. A measured miss of its own kind (`mouthMiss` is true too): it earns the call's automatic retake, told whose face must say the line, and the take never plays over one the right person speaks. Empty when none.","items":{"type":"object","additionalProperties":false,"required":["lineRef","speaker","face"],"properties":{"lineRef":{"type":"integer"},"speaker":{"type":"string"},"face":{"type":"string"}}}},"mouthNotChecked":{"type":"boolean","description":"True when the take's lip sync was NOT CHECKED: the mouth check could not read an on-camera speaker's mouth (the face not found even after the locator's denser retry, too small or hidden) and no watch of the take could decide it instead, read no line, or could not run. A speaker the take watcher decided is a `mouthMiss` or `mouthUncertain`, not this. Never a pass: the take ranks below every take whose mouths were measured passing, and its `warning` says why. It earns no retake, automatic or priced: only a measured `mouthMiss` does."},"duplicatePerson":{"type":"boolean","description":"True when the duplicate-person check saw one person in the picture twice at once. It earns the call's one automatic retake, and the take ranks below every other take for the default selection."},"duplicateNotChecked":{"type":"boolean","description":"True when the take was NOT CHECKED for a person shown twice: the duplicate-person check could not judge it, even on its one retry (or never ran on it). Never a pass: the take ranks below every take whose check passed, and its `warning` says so. It earns no automatic retake."},"duplicates":{"type":"object","nullable":true,"additionalProperties":true,"description":"The duplicate-person check: `{ status: judged | unavailable, pass, duplicates[{ name, shot, seconds, detail }], reason }`. `unavailable` (after one retry) proves nothing and is never a pass: see `duplicateNotChecked`. Null when it never ran on this take."},"warning":{"type":"string","nullable":true,"description":"The sentence the finish quote and the export carry if this take is kept."},"failure":{"type":"object","nullable":true,"additionalProperties":true,"description":"Why the take produced nothing. `moderation: true` is a content refusal: released (nothing charged) and never retried. `code: reference_render_provider_ip_refused` is the provider refusing the shot as possibly someone else's intellectual property: the same shot is refused again, so the call's notice says what to edit (and the likely trigger: currency, a logo or brand, a named work) before it is rendered again.","properties":{"code":{"type":"string"},"message":{"type":"string"},"moderation":{"type":"boolean"}}},"billedSeconds":{"type":"number","nullable":true},"recordedAt":{"type":"string","format":"date-time"},"watch":{"allOf":[{"$ref":"#/components/schemas/TakeWatch"}],"nullable":true,"description":"What the take watcher saw on this take, with its sound; null until it watched."},"repair":{"allOf":[{"$ref":"#/components/schemas/TakeRepairDecision"}],"nullable":true,"description":"The remedy the repair engine decided for this take and the rule that fired; null until it decided."},"lineage":{"allOf":[{"$ref":"#/components/schemas/TakeLineage"}],"nullable":true,"description":"Set on a take a remedy made: the take it repairs (`aleph_repair`), trims (`trim`) or follows as a new take (`wan_retake`)."},"repairCost":{"allOf":[{"$ref":"#/components/schemas/TakeRepairCost"}],"nullable":true,"description":"Set on a repaired take: what the repair cost."},"repairQuote":{"allOf":[{"$ref":"#/components/schemas/TakeRepairQuoteLine"}],"nullable":true,"description":"What repairing this take in place would cost now, and whether it can be asked for (null for a take with no file)."},"trimBounds":{"allOf":[{"$ref":"#/components/schemas/TakeTrimBounds"}],"nullable":true,"description":"What a free trim of this take may keep (its length, where its spoken lines sit, the shortest it may become, the engine's own suggestion); null for a take with no file."},"trim":{"allOf":[{"$ref":"#/components/schemas/TakeTrimPlan"}],"nullable":true,"description":"Set on a trimmed take: the span of the take it came from that it keeps (its cut plays this length)."},"voiceOver":{"allOf":[{"$ref":"#/components/schemas/TakeVoiceOver"}],"nullable":true,"description":"Set on a take that plays on-camera lines of its call as a voice-over (the speaker seen from behind, the line heard over the picture)."},"voiceOverRestage":{"allOf":[{"$ref":"#/components/schemas/TakeVoiceOverRestage"}],"nullable":true,"description":"Whether staging this take's line as a voice-over in a new take (priced like a new take) can be asked for now, the lines it would stage and its price; null for a call that says nothing on camera."}}},"PitchDeckVideoModelPreflight":{"type":"object","required":["ready","checkedAt","perCall","blockingMissingCount","optionalMissingCount","locationsMadeAtRender","chapters","missingActions"],"additionalProperties":true,"description":"Backend-owned asset truth for the deck's render: every chapter's cast (a body figure each on screen, a cast voice for a speaker) and location. A location with no plate yet reads `will_generate`: the render makes it first, as a plate line in the approval quote — the same list the quote prices. A location in a chapter that shows nobody by name and speaks no line in a cast voice reads `not_sent`: its render is built on no plate, so none is made or priced. `cast_face` and `character_sheet` items are informational and never block. Read only.","properties":{"ready":{"type":"boolean"},"checkedAt":{"type":"string","format":"date-time"},"perCall":{"type":"object","additionalProperties":false,"description":"What ONE render call carries at most: one location plate (none for a call with no named character and no speech), `maxFigures` identity references, one speech track.","properties":{"plates":{"type":"integer"},"maxFigures":{"type":"integer"},"maxSpeechTracks":{"type":"integer"}}},"targetDurationSeconds":{"type":"number"},"blockingMissingCount":{"type":"integer","minimum":0},"optionalMissingCount":{"type":"integer","minimum":0},"error":{"type":"string","nullable":true},"seriesBible":{"type":"object","additionalProperties":true,"properties":{"ready":{"type":"boolean"},"missingFields":{"type":"array","items":{"type":"string"}},"href":{"type":"string","nullable":true}}},"locationsMadeAtRender":{"type":"array","items":{"type":"string"},"description":"The rooms whose location plate the render makes first: exactly the rooms of the quote's plate lines (from the chapter's stored compile once it has one)."},"chapters":{"type":"array","items":{"type":"object","additionalProperties":true,"properties":{"chapterIndex":{"type":"integer","minimum":0},"title":{"type":"string"},"compositionKind":{"type":"string","enum":["image","text","both"]},"visibleCharacters":{"type":"array","items":{"type":"string"}},"location":{"type":"string","nullable":true},"speaker":{"type":"string","nullable":true,"description":"The cast member whose voice speaks the chapter's line, or null (narrated, or no line)."},"ready":{"type":"boolean"},"assets":{"type":"array","items":{"type":"object","additionalProperties":true,"properties":{"key":{"type":"string"},"kind":{"type":"string","enum":["canonical_location","cast_body","cast_voice","cast_face","character_sheet","text_composition","cast_signature_items"],"description":"`cast_signature_items`: what only this person wears or carries, required of everyone who shares a render with someone else (words in the prompt, never a file sent)."},"label":{"type":"string"},"status":{"type":"string","enum":["attached","will_generate","not_sent","missing"],"description":"`will_generate`: a plate the render makes first, a plate line in the quote. `not_sent`: the chapter's renders are built on no plate (nobody named on camera, no line in a cast voice), so none is made or priced."},"required":{"type":"boolean"},"sentToRender":{"type":"boolean","description":"True when the render sends this file (a plate, a body figure, a speech track in this voice)."},"destination":{"type":"string","enum":["series_bible","casting","table_read","chapters"]},"destinationLabel":{"type":"string"},"url":{"type":"string","nullable":true},"note":{"type":"string","nullable":true},"href":{"type":"string","nullable":true,"description":"The app link that fixes this one item when it is fixed on one person's Cast card (their signature items or pictures)."}}}},"castBlocks":{"type":"array","description":"What the render would refuse this chapter for, found from its on-camera cast before anything is priced or compiled: the compile's own issue (`missing_signature_items`: a person who shares a render with someone else has no signature items), with its `message` and `action` (`add_signature_items` with `character`). The plan approval and its quote refuse with these at once (`pitch_deck_render_blocked`).","items":{"type":"object","additionalProperties":true,"properties":{"code":{"type":"string"},"severity":{"type":"string"},"message":{"type":"string"},"action":{"type":"object","additionalProperties":true}}}}}}},"missingActions":{"type":"array","items":{"type":"object","additionalProperties":true,"properties":{"destination":{"type":"string","enum":["series_bible","casting","table_read","chapters"]},"label":{"type":"string"},"href":{"type":"string","nullable":true},"blocking":{"type":"boolean"},"items":{"type":"array","items":{"type":"string"}},"links":{"type":"array","description":"The items fixed on one person's Cast card, each with the app link straight to it.","items":{"type":"object","additionalProperties":false,"properties":{"item":{"type":"string"},"href":{"type":"string"}}}}}}},"castHref":{"type":"string","nullable":true,"description":"The episode's Cast tab in the app, where figures and signature items are set."}}},"PitchDeckChapterRefineRequest":{"type":"object","required":["chapterIndex"],"additionalProperties":false,"description":"Edit to one pitch-deck chapter's plan (provide at least one editable field) while the deck is a draft plan, in review or complete. Free. On a draft nothing goes stale: the next plan approval recompiles just this chapter, and the changed plan is queued for plan coverage again. In review or complete an edit to what the chapter films marks its takes stale, and rendering it again is the priced render-clip op. There is no `imagePrompt` and no `castReferenceMode` — a chapter has no still, and its cast reaches the render as body figures; either is refused by name.","properties":{"chapterIndex":{"type":"integer","minimum":0,"description":"The chapter to edit. The chapter's `chapterIndex`, exactly as the chapters read returns it: a key, not a position — a plan may key its chapters from 0 or from 1. A person's \"chapter 2\" is the chapter whose `chapterNumber` is 2; pass that chapter's `chapterIndex`."},"storyBeat":{"type":"string","minLength":1,"maxLength":4000,"description":"What the chapter delivers dramatically."},"visualPlan":{"type":"string","minLength":1,"maxLength":4000,"description":"What the chapter shows on screen: the scene, who is in it, and what happens — the source its shots are compiled from."},"location":{"type":"string","nullable":true,"description":"Where the chapter is set, a canonical location key (pass the key, or a room name that resolves to the room's stored key). A room with no plate yet is legal: the render makes it, priced as a plate line. Null clears it."},"motionPlan":{"type":"string","minLength":1,"maxLength":4000,"description":"What the people in the chapter physically DO, in the order they do it — the business the shots stage (crossing to someone, seizing something, putting something down and not letting go) and, where the chapter wants it, what the camera does (a push-in, a crane over the town, an aerial, a handheld follow). An edit marks the chapter's takes stale."},"transitionPrompt":{"type":"string","description":"How this chapter hands off into the next one."},"motionPrompt":{"type":"string","description":"The camera direction for this chapter's on-screen text layer. Not what the people do — that is motionPlan. When set it is read in place of motionPlan, so changing motionPlan without sending a motionPrompt clears the old one: a repurposed chapter never keeps the old scene's camera."},"compositionKind":{"type":"string","enum":["image","text","both"],"description":"What the chapter is made of: footage only, on-screen text only, or text over footage."},"chapterFocus":{"type":"string","enum":["world","cast"],"description":"What the chapter is of: `world` = the place itself, establishing its scale and life, with no cast on camera; `cast` = cast members on camera, in the world, doing something."},"arcStage":{"type":"string","enum":["setup","escalation","turn","closing_question"],"description":"The stage of the deck's intent this chapter serves: setup, escalation, turn or closing_question. The chapters follow the arc in order, every stage is served, and the last chapter is the closing_question — plan coverage refuses a plan that breaks it. Plan words: never makes a take stale."},"advancesQuestion":{"type":"string","minLength":1,"maxLength":400,"description":"One sentence: how this chapter advances the intent's central question — what it raises, sharpens or makes costlier."},"whyItFollows":{"type":"string","maxLength":300,"description":"One line, never on screen: why this chapter comes after the one before it — the link a viewer sees or hears and what it moves forward. Required on every chapter after the first; \"\" clears it. Plan words: never makes a take stale."},"montage":{"type":"boolean","description":"true makes the chapter a rapid intercut run; false makes it hold or travel. A montage chapter is a rapid intercut run: several brief shots of about one to two seconds each across the people and places it names, cut tight (a run across more than three named people renders as a few short calls of the chapter). Marking or unmarking it changes what renders, so a rendered chapter goes stale."},"keepsSceneSound":{"type":"boolean","description":"true keeps the chapter's own sound in the master; false leaves it out. A chapter that keeps its scene sound plays the sound its own render made (harness bells, singers, a crowd: voices and all) under the score; every other chapter plays only its cast's lines over quiet room tone. On a chapter with no spoken line it also decides whether the render is made with sound, so keeping the sound of takes made silent needs them rendered again; any other change re-cuts the chapter from the takes it has, free. The answer's `sceneSound` says what follows: `reassembling` (in review, the chapter is re-cut and the master rebuilt, free) or `at_finish` (a complete deck makes it at its next finish)."},"lineDelivery":{"type":"string","maxLength":300,"description":"How the cast member says the chapter's spoken line; '' clears it. How the cast member says the chapter's spoken line, in a few words (\"quietly, broken, through tears, almost a whisper\"; at most 300 characters, an empty string clears it). The line is voiced with it: the words reach the voice as written, and the feeling, pace and level they name (sad, angry, quiet, slow) set its tone. It directs the voice only: the face and body are directed in the motion plan, and the footage's prompt never carries it. Only a line a cast member speaks is voiced with it; a narrated chapter's narration is unchanged. On a rendered chapter a change voices the line again, so its takes go out of date (rendering it again is priced first)."},"cutIntoNext":{"type":"string","enum":["dip","cut"],"description":"`cut` makes the hand-off into the next chapter a hard cut in the master; `dip` puts back the dip through black. How the chapter hands into the next one in the master: `dip` (the default) falls to black and rises again over about a second; `cut` is a hard cut, the next chapter's first frame right after this one's last. A black interlude after the chapter still plays between them, and the last chapter's choice does nothing. It changes only how the master is stitched: nothing is rendered again and no take goes out of date; on a rendered deck the master is stitched again with it, free. The answer's `restitching` says the master is being stitched again."},"captionText":{"type":"string","description":"On-screen caption copy."},"textTreatmentPrompt":{"type":"string","description":"Motion-graphic / type treatment for the on-screen text."},"textMode":{"type":"string","enum":["voiceover","caption"],"description":"Whether the on-screen text is voiced or silent."},"narrationText":{"type":"string","description":"The chapter's spoken line: narrated over the cut, or spoken by the cast member `narrationVoice` names."},"narrationVoice":{"type":"string","nullable":true,"description":"Who speaks the line: a cast member (in their own cast voice) or the narrator. Null clears it."},"intendedDurationSeconds":{"type":"integer","minimum":4,"maximum":30,"description":"The chapter's length in seconds (4–30). A chapter renders as calls of at most 10s each."},"visibleCharacters":{"type":"array","maxItems":5,"items":{"type":"string","minLength":1,"maxLength":120},"description":"The NAMED characters in the picture (max 5), by exact project-canon name — each needs an identity reference. Unnamed people (a crowd, passers-by) are not listed and need none. Pass [] for a chapter with no cast member on screen, e.g. a vista."},"featuredCharacter":{"type":"string","nullable":true,"maxLength":120,"description":"The chapter's lead character (null clears it)."},"propContinuity":{"type":"string","maxLength":2000,"description":"The props this chapter's shots carry — what is held, set down or kept in the frame. The render is told it as \"Props: …\", so a prop you are steering away from must leave this field too. After the render, changing it puts the chapter's takes out of date. An empty string clears it."},"wardrobeContinuity":{"type":"string","maxLength":2000,"description":"What the people in this chapter wear, kept the same across its shots. The render is told it as \"Wardrobe: …\". After the render, changing it puts the chapter's takes out of date. An empty string clears it."},"screenDirection":{"type":"string","maxLength":2000,"description":"Which way people and things face and move on screen (left to right, toward the camera), kept consistent across the cut. The render is told it as \"Screen direction: …\". After the render, changing it puts the chapter's takes out of date. An empty string clears it."},"continuityNotes":{"type":"string","maxLength":2000,"description":"Notes on what this chapter must keep consistent with the chapters around it, for the writer and the planner. The render is never told them, so changing them never puts a take out of date. An empty string clears them."},"shotType":{"type":"string","maxLength":120,"description":"The chapter's shot size in a few words (\"close-up on Smerdyakov\", \"wide establishing of the square\"). It must name a scale on the shot ladder — establishing, wide, medium or close — or it is refused; the scale word decides, read in that order: \"medium close\" is a MEDIUM, and a two-shot or over-the-shoulder with no scale word is a medium. The render is told it (\"Framing: …\"), so on a rendered chapter it puts the takes out of date; plan coverage reads it for the shot ladder. An empty string clears it."},"title":{"type":"string","minLength":1,"maxLength":200,"description":"The chapter's title, as the deck and its plan show it: a few words specific to what the chapter now is. Plan words only — the render is never told it, so changing it never puts a take out of date. Never empty."}}},"PitchDeckChapterRefineAck":{"type":"object","required":["success","needsRerender","chapterIndex","chapterNumber","planCoverage","edited","jobId","status"],"additionalProperties":true,"description":"The saved chapter edit plus a shareable link to preview the chapter. Nothing was rendered and nothing was charged.","properties":{"success":{"type":"boolean"},"needsRerender":{"type":"boolean","description":"True when the chapter's takes (or its text layer) are now out of date. They stay playable; render the chapter again (priced) to bring it in line. Always false on a draft plan: nothing has rendered."},"chapterIndex":{"type":"integer","minimum":0,"description":"The key of the chapter that changed."},"chapterNumber":{"type":"integer","minimum":1,"description":"The chapter's place in the deck, counted from 1: the number a person says (\"chapter 2\") and every sentence uses. Never an input; chapter edits take `chapterIndex`."},"planCoverage":{"type":"string","nullable":true,"enum":["queued","current","failed",null],"description":"On a draft plan the edit changed: its plan coverage — `queued` (for the latest edit: it starts once the plan has been left alone for three minutes, a later edit replaces it, and the plan approval starts it at once and waits for its score), `current` (this revision is already scored or being scored) or `failed` (it could not be queued; the approval says what is missing). A burst of edits is one coverage run, on the revision it ends on. Null outside draft, or when the edit changed nothing."},"edited":{"type":"string","enum":["draft_plan","rendered_deck"],"description":"Which version of the deck the edit changed: `draft_plan` (a `draft` deck — nothing has rendered) or `rendered_deck` (a deck in review or complete). Tell the writer which one changed."},"jobId":{"type":"string","description":"The deck that changed."},"status":{"type":"string","enum":["draft","review","complete"],"description":"The status of the deck that changed; the refine does not move it."},"shotLadderRung":{"type":"string","enum":["establishing","wide","medium","close"],"description":"When the edit set `shotType`: the rung of the shot ladder it placed the chapter on, read the way plan coverage reads it (\"medium close\" is a medium)."},"sceneSound":{"type":"string","enum":["reassembling","at_finish"],"description":"When the edit changed `keepsSceneSound` and the chapter's takes still hold: `reassembling` — in review, the chapter is cut again from the same takes and the master rebuilt (free); `at_finish` — a complete deck keeps its finished cuts (and a rebuild keeps a cut with a text composite laid over it), so the next finish makes it. Absent otherwise (a draft, or `needsRerender`)."},"restitching":{"type":"boolean","description":"True when the edit changed how the chapter hands into the next one (`cutIntoNext`) on a rendered deck: the master is stitched again from the cuts it has (free; nothing renders, no take goes out of date). Absent otherwise."},"previewUrl":{"type":"string","description":"Shareable link to watch this chapter's current cut."}}},"PitchDeckChapterPreview":{"type":"object","additionalProperties":true,"description":"The saved chapter plus a shareable link to preview it.","properties":{"success":{"type":"boolean"},"needsRerender":{"type":"boolean","description":"True when the chapter's takes (or its text layer) are now out of date. They stay playable; render the chapter again (priced) to bring it in line."},"previewUrl":{"type":"string","description":"Shareable link to watch this chapter's current cut."}}},"PitchDeckExportAck":{"type":"object","required":["renderedVideoStatus"],"additionalProperties":true,"description":"The finished-master re-stitch, queued (or already queued / rendering).","properties":{"renderedVideoStatus":{"type":"string","enum":["queued","rendering"]}}},"PitchDeckChapterIndexRequest":{"type":"object","required":["chapterIndex"],"additionalProperties":false,"description":"Target one chapter by index, and say whether this call is the price or the purchase.","properties":{"chapterIndex":{"type":"integer","minimum":0,"description":"The chapter whose text layer to render. The chapter's `chapterIndex`, exactly as the chapters read returns it: a key, not a position — a plan may key its chapters from 0 or from 1. A person's \"chapter 2\" is the chapter whose `chapterNumber` is 2; pass that chapter's `chapterIndex`."},"confirmed":{"type":"boolean","description":"Omit (or false) to get the price WITHOUT spending anything — nothing is reserved and nothing is queued. Send true only after the writer has seen that price and said go. It is optional rather than a required `true` because a field that must be true to validate cannot separate a confirmed call from an unconfirmed one."}}},"PitchDeckDirectTextMotionRequest":{"type":"object","additionalProperties":false,"description":"Say in plain words how one chapter's on-screen text should move (`chapterIndex` + `direction`) — or how one of the title sequence's words should look and move (`titleElement` + `direction`), or put one of those back in the sequence's own type (`titleElement` + `clear: true`) — or one of the closing sequence's cards (`closingElement` + `direction`, or + `clear: true` to put it back in the opening's look). Exactly one of `chapterIndex`, `titleElement` and `closingElement`. Free — no `confirmed` field, because there is no spend to confirm. The title sequence's words are directed like a chapter's text, in the writer's own words, through the same direct-text-motion call with a `titleElement` instead of a `chapterIndex`: `title` (\"KARAMAZOV huge, blood red, a stark sans, the ends cut off by the frame\"), `epigraph` (\"a subtle background behind the verse: grain, a candle's flicker, a few falling seeds\"), `sourceCredit`, or `nameSupers` (every name alike: its face, its colour, and where it sits — beside the face on the left or the right, over the top of the picture, or under it; never over a face). The director compiles the sentence into a motion-text spec — the face (the show's, or a bundled grotesque or condensed grotesque), the colour, the size (up to edge to edge, wider than the frame so the frame cuts the first and last letters), how it moves, and a seeded background behind it (film grain, a breathing vignette, a flickering warm light, banks of fog rolling across the frame, slowly falling motes), as quiet or as strong and moving as asked (\"the same fog as the verse, but stronger\"), held under the words' own brightness (behind the blood-red title it is a haze of the same red) — and saves it with the sentence on the title sequence (`textMotion`). The words stay the sequence's: a direction says how they look, never what they say. `clear: true` puts an element back in the sequence's own type. Free: an LLM call, no credits; a rendered deck's master is stitched again with it, and nothing else renders (no chapter, no shot). A save of the title sequence keeps every direction (an element whose words are removed loses its own).","properties":{"chapterIndex":{"type":"integer","minimum":0,"description":"The chapter whose on-screen text should move. The chapter's `chapterIndex`, exactly as the chapters read returns it: a key, not a position — a plan may key its chapters from 0 or from 1. A person's \"chapter 2\" is the chapter whose `chapterNumber` is 2; pass that chapter's `chapterIndex`."},"titleElement":{"type":"string","enum":["title","epigraph","sourceCredit","nameSupers"],"description":"Instead of a chapter: one of the title sequence's words — title, epigraph (with where it is from), sourceCredit, or nameSupers (every super alike)."},"closingElement":{"type":"string","enum":["quote","comingSoon","productionCredit"],"description":"Instead of a chapter: one of the closing sequence's cards — quote (with where it is from), comingSoon, or productionCredit."},"direction":{"type":"string","minLength":1,"maxLength":2000,"description":"How the text should look and move, in the writer's own words — e.g. \"type it out letter by letter, then draw an underline beneath it\", or \"KARAMAZOV huge, blood red, a stark sans, the ends cut off by the frame\". Required unless `clear` is true."},"clear":{"type":"boolean","enum":[true],"description":"With `titleElement`: put it back in the title sequence's own type. With `closingElement`: put the card back in the opening's look."}}},"PitchDeckDirectTextMotionAck":{"type":"object","required":["success","direction"],"additionalProperties":true,"description":"The direction now on the chapter (or on one of the title sequence's words). Nothing was charged. A chapter's text layer is re-rendered to see it move; a title sequence word is drawn by the master, which a rendered deck stitches again (free; `reassembling`).","properties":{"success":{"type":"boolean"},"chapterIndex":{"type":"integer","minimum":0,"description":"The chapter directed (a chapter direction)."},"titleElement":{"type":"string","enum":["title","epigraph","sourceCredit","nameSupers"],"description":"The title sequence word directed (a title direction)."},"direction":{"type":"string","nullable":true,"description":"The writer's own words, saved beside the compiled spec so the next direction is not authored blind; null when a title sequence word was put back in its own type. A title direction is read back from the stored deck before it is reported (one another plan edit replaced is refused with 409 `pitch_deck_state_invalid`)."},"look":{"type":"string","nullable":true,"description":"What the master draws for a directed title sequence word, read from the stored spec rather than the writer's sentence: the face, size and colour of its words, or that the direction is a background only under its own type. Null when it was put back in its own type."},"previewUrl":{"type":"string","description":"Shareable link to watch this chapter's current render — so seeing what the direction did costs nothing (a chapter direction)."},"titleSequence":{"allOf":[{"$ref":"#/components/schemas/PitchDeckTitleSequenceView"}],"nullable":true,"description":"The title sequence as every read shows it, with the direction in `textMotion` (a title direction)."},"closingElement":{"type":"string","enum":["quote","comingSoon","productionCredit"],"description":"The closing card directed (a closing direction)."},"closingSequence":{"allOf":[{"$ref":"#/components/schemas/ClosingSequenceView"}],"nullable":true,"description":"The closing sequence as every read shows it, with the direction in `textMotion` (a closing direction)."},"reassembling":{"type":"boolean","description":"True when the deck's master is stitched again with it (a rendered deck; free — no chapter renders)."}}},"PitchDeckTitleTextMotion":{"type":"object","required":["direction","spec"],"additionalProperties":false,"description":"One of the title sequence's words, directed: the writer's sentence and the motion-text spec compiled from it, which the master executes.","properties":{"direction":{"type":"string","description":"The writer's own words for how it looks and moves."},"spec":{"type":"object","additionalProperties":true,"description":"The motion-text spec: version, layers (each a text, atmosphere, particle… layer with its face, fit, colours and keyframed steps)."}}},"PitchDeckTextRenderAnswer":{"type":"object","required":["quotedOnly"],"additionalProperties":true,"description":"Either the PRICE of a chapter's text-layer render or the queued render itself — branch on `quotedOnly`. Sent without `confirmed`, the call quotes and nothing is reserved; sent with `confirmed: true`, the credit is held and the render is queued.","properties":{"quotedOnly":{"type":"boolean","description":"True when this was a price and NOTHING was spent; false when the render was reserved and queued."},"chapterIndex":{"type":"integer","minimum":0},"previewUrl":{"type":"string","description":"Shareable link to watch this chapter's current cut."},"reviewUrl":{"type":"string","nullable":true,"description":"Logged-in browser link to the deck. Present on a confirmed call."},"credits":{"type":"number","description":"Credits charged for this render, or — when `quotedOnly` is true — what a confirmed call would charge."},"costLabel":{"type":"string","nullable":true,"description":"The rate-card line the price came from (quote only)."},"balanceAfter":{"type":"integer","description":"Quote only. Studio Credits the writer would have left once this is spent: the balance available now (as Billing shows it) minus the price. Negative means they are that many short."},"sufficient":{"type":"boolean","description":"Quote only. True when the available balance covers the price. False means the writer must top up first (the note names the Billing link) — do not confirm; the reserve would refuse."},"note":{"type":"string","description":"The price in the writer's own words, what it would leave (or how short they are, with the top-up link), and how to proceed (quote only)."}}},"PlanCoverageAcknowledgement":{"type":"object","required":["reason"],"additionalProperties":false,"description":"Only on the writer's explicit choice, with their reason: render over a plan the coverage judge scored under the bar. It overrides the judge's verdict only (an overall score or a judged veto under the bar on a complete report of the plan as it stands), never a code-checked blocking defect, and never a missing, running, failed, stale or old-rubric report. The confirmed approval records who, when, why and what it overrode on the report. Send it on the price and the confirm alike. Refused (400, with `details.overridable: false` and `details.overrideWhy`) for anything but a judged fail of a complete, current report.","properties":{"reason":{"type":"string","minLength":3,"maxLength":500,"description":"Why the writer renders over the plan gate, in their own words. Recorded on the report."}}},"PlanCoverageOverride":{"type":"object","required":["reason","reportId","planRevision","overallScore","gateThreshold","gateReasons"],"additionalProperties":false,"description":"A render over a failed plan gate on the writer's word: the report it overrode, the plan revision it graded, its score, the bar, and the reasons it gave. A recorded override (on the confirmed approval and on the coverage read) also carries `at` and `by`.","properties":{"reason":{"type":"string"},"reportId":{"type":"string"},"planRevision":{"type":"integer","minimum":0},"overallScore":{"type":"number","nullable":true},"gateThreshold":{"type":"number"},"gateReasons":{"type":"array","items":{"type":"string"}},"at":{"type":"string","format":"date-time","description":"Recorded overrides only: when the confirmed approval recorded it."},"by":{"type":"object","additionalProperties":false,"required":["userId","surface","keyId"],"description":"Recorded overrides only: who approved it — `api` (the Story API: MCP and the CLI) with the API key or `oauth:…` connection, or `app` (the Writers Lab).","properties":{"userId":{"type":"string"},"surface":{"type":"string","enum":["api","app"]},"keyId":{"type":"string","nullable":true}}}}},"PitchDeckPlanApproveRequest":{"type":"object","additionalProperties":false,"description":"Say whether this call is the price or the purchase.","properties":{"confirmed":{"type":"boolean","description":"Omit (or false) to get the 480p render quote WITHOUT spending anything — the chapters are compiled and voiced (free to the writer), nothing is reserved. Send true only after the writer has seen that price and said go: it reserves exactly the quoted calls and starts the render."},"acknowledgePlanCoverage":{"$ref":"#/components/schemas/PlanCoverageAcknowledgement"}}},"PitchDeckRenderClipRequest":{"type":"object","required":["chapterIndex"],"additionalProperties":false,"description":"Render one chapter again (the writer's priced retake), and say whether this call is the price or the purchase.","properties":{"chapterIndex":{"type":"integer","minimum":0,"description":"The chapter to render again. The chapter's `chapterIndex`, exactly as the chapters read returns it: a key, not a position — a plan may key its chapters from 0 or from 1. A person's \"chapter 2\" is the chapter whose `chapterNumber` is 2; pass that chapter's `chapterIndex`."},"callId":{"type":"string","description":"One of the chapter's calls (from the chapters read); omit to render a new take of every call of the chapter. A chapter whose plan changed is recompiled first, and a call id is then refused — render the whole chapter."},"confirmed":{"type":"boolean","description":"Omit (or false) to get the price WITHOUT spending anything. Send true only after the writer has seen that price and said go."}}},"PitchDeckRenderAnswer":{"type":"object","required":["quotedOnly","jobId","credits"],"additionalProperties":true,"description":"Either the PRICE of a render (plan approval, or a chapter rendered again) or the queued render itself — branch on `quotedOnly`. Without `confirmed` nothing is reserved; with `confirmed: true` exactly the quoted lines are held and the calls are queued.","properties":{"quotedOnly":{"type":"boolean","description":"True when this was a price and NOTHING was spent; false when the render was reserved and queued."},"queued":{"type":"boolean","description":"Confirmed call only."},"jobId":{"type":"string"},"credits":{"type":"number","description":"What a confirmed call reserves (quote), or what it reserved (confirmed)."},"costLabel":{"type":"string","nullable":true,"description":"What the whole quote buys, in one label — the shots, the chapters they span, their seconds at 480p and the location plates made first, e.g. \"Pitch deck render: 7 shots across 4 chapters (33s at 480p) + 1 location plate\" (quote only)."},"lines":{"type":"array","description":"Quote only. Every line a confirmed call would reserve: one per call at its voiced seconds, one per location plate the renders make first, and — a plan approval — the automatic-fix allowance (kind `allowance`).","items":{"type":"object","additionalProperties":true,"properties":{"kind":{"type":"string","enum":["call","plate","allowance"],"description":"`allowance`: a plan approval's automatic-fix allowance (see `autoFixAllowance`)."},"idempotencyKey":{"type":"string"},"label":{"type":"string"},"credits":{"type":"number"},"callId":{"type":"string","nullable":true},"plateKey":{"type":"string","nullable":true},"seconds":{"type":"number","nullable":true}}}},"calls":{"description":"Quote: the priced calls `[{ chapterIndex, callId, take, seconds, audioMode, credits }]`. Confirmed: how many calls were queued.","oneOf":[{"type":"array","items":{"type":"object","additionalProperties":true,"properties":{"chapterIndex":{"type":"integer"},"callId":{"type":"string"},"take":{"type":"integer"},"seconds":{"type":"number"},"audioMode":{"type":"string","enum":["dialogue","ambient","silent"]},"credits":{"type":"number"}}}},{"type":"integer","minimum":0}]},"warnings":{"type":"array","items":{"type":"string"},"description":"Quote only. Compile warnings worth showing (advisory; nothing blocks on them)."},"balanceAfter":{"type":"integer","description":"Quote only. Studio Credits the writer would have left once this is spent: the balance available now (as Billing shows it) minus the price. Negative means they are that many short."},"sufficient":{"type":"boolean","description":"Quote only. True when the available balance covers the price. False means the writer must top up first (the note names the Billing link) — do not confirm; the reserve would refuse."},"note":{"type":"string","description":"The price in the writer's own words, what it would leave (or how short they are, with the top-up link), and how to proceed (quote only)."},"message":{"type":"string","description":"Confirmed call only: what was queued."},"previewUrl":{"type":"string","description":"A chapter render only: a shareable link to watch the chapter's current cut."},"autoFixAllowance":{"type":"object","nullable":true,"additionalProperties":false,"required":["credits","note"],"description":"A plan approval's quote only: the AUTOMATIC-FIX ALLOWANCE, part of the one approval and included in `credits`. A ceiling the engine draws on to retake the shots the review of the 480p preview flags (and render flagged on-screen text again), reserved with the render; only what the fixes draw is charged and the rest comes back when the loop stops. 0 when the balance covers only the render (the preview is still reviewed). The 720p finish is a separate approval.","properties":{"credits":{"type":"integer","minimum":0},"note":{"type":"string","description":"The allowance in the writer's words."}}},"planCoverageOverride":{"allOf":[{"$ref":"#/components/schemas/PlanCoverageOverride"}],"nullable":true,"description":"A plan approval over a failed plan gate (`acknowledgePlanCoverage`): what the price would override, or — confirmed — the override as recorded on the report (null when the record could not be written; the render still runs)."}}},"PitchDeckSelectTakeRequest":{"type":"object","required":["callId","take"],"additionalProperties":false,"description":"Choose one take of one of this chapter's calls. Free and reversible.","properties":{"callId":{"type":"string","description":"One of this chapter's call ids (from the chapters read). A call of another chapter is refused."},"take":{"type":"integer","minimum":1,"description":"A take number from the call's `takes`. A take that produced no file cannot be selected."}}},"PitchDeckSelectTakeAck":{"type":"object","required":["jobId","chapterIndex","callId","take","warning"],"additionalProperties":true,"description":"The take now selected. Nothing was rendered and nothing was charged; the chapter's preview cut is re-assembled from it.","properties":{"jobId":{"type":"string"},"chapterIndex":{"type":"integer","minimum":0},"callId":{"type":"string"},"take":{"type":"integer","minimum":1},"warning":{"type":"string","nullable":true,"description":"Set when the chosen take is flagged: the finish quote and the export carry this sentence."}}},"PitchDeckInsertChapterRequest":{"type":"object","additionalProperties":false,"description":"Where the new blank chapter goes: `atIndex` or `afterChapterIndex` (not both); neither appends it.","properties":{"atIndex":{"type":"integer","minimum":0,"description":"The POSITION the new chapter takes (0 = first) — a position, not a chapter key."},"afterChapterIndex":{"type":"integer","minimum":0,"description":"Insert just after this chapter. The chapter's `chapterIndex`, exactly as the chapters read returns it: a key, not a position — a plan may key its chapters from 0 or from 1. A person's \"chapter 2\" is the chapter whose `chapterNumber` is 2; pass that chapter's `chapterIndex`."}}},"PitchDeckInsertChapterAck":{"type":"object","required":["success","insertedChapterIndex","chapterCount"],"additionalProperties":true,"description":"Where the new blank chapter landed. Nothing was rendered and nothing was charged.","properties":{"success":{"type":"boolean"},"insertedChapterIndex":{"type":"integer","minimum":0,"description":"The new chapter's key. The chapters are renumbered from 0 on insert, so it is chapter `insertedChapterIndex + 1` of the deck."},"chapterCount":{"type":"integer","minimum":1},"planCoverage":{"type":"string","nullable":true,"enum":["queued","current","failed",null],"description":"On a draft plan: the changed plan's plan coverage (`queued` for the latest edit, `current`, or `failed` — the approval says what is missing). Null in review or complete."},"edited":{"type":"string","enum":["draft_plan","rendered_deck"],"description":"Which version of the deck the edit changed: `draft_plan` (a `draft` deck — nothing has rendered) or `rendered_deck` (a deck in review or complete). Tell the writer which one changed."},"jobId":{"type":"string","description":"The deck that changed."},"status":{"type":"string","enum":["draft","review","complete"],"description":"The deck's status after the edit (a complete deck goes back to review when a chapter is added)."}}},"PitchDeckRemoveChapterRequest":{"type":"object","required":["chapterIndex"],"additionalProperties":false,"description":"The chapter to remove.","properties":{"chapterIndex":{"type":"integer","minimum":0,"description":"The chapter to remove. The chapter's `chapterIndex`, exactly as the chapters read returns it: a key, not a position — a plan may key its chapters from 0 or from 1. A person's \"chapter 2\" is the chapter whose `chapterNumber` is 2; pass that chapter's `chapterIndex`."}}},"PitchDeckRemoveChapterAck":{"type":"object","required":["success","chapterCount"],"additionalProperties":true,"description":"The deck's length after the edit. Nothing was charged and nothing was refunded.","properties":{"success":{"type":"boolean"},"chapterCount":{"type":"integer","minimum":1},"planCoverage":{"type":"string","nullable":true,"enum":["queued","current","failed",null],"description":"On a draft plan: the changed plan's plan coverage (`queued` for the latest edit, `current`, or `failed` — the approval says what is missing). Null in review or complete."},"edited":{"type":"string","enum":["draft_plan","rendered_deck"],"description":"Which version of the deck the edit changed: `draft_plan` (a `draft` deck — nothing has rendered) or `rendered_deck` (a deck in review or complete). Tell the writer which one changed."},"jobId":{"type":"string","description":"The deck that changed."},"status":{"type":"string","enum":["draft","review","complete"],"description":"The deck's status after the edit (a complete deck goes back to review when a chapter is added)."}}},"PitchDeckReorderChaptersRequest":{"type":"object","additionalProperties":false,"description":"The new order: `order` (every chapter once) or one `move` — exactly one of them.","properties":{"order":{"type":"array","minItems":1,"items":{"type":"integer","minimum":0},"description":"Every chapter's chapterIndex once, in the order they will play. The chapter's `chapterIndex`, exactly as the chapters read returns it: a key, not a position — a plan may key its chapters from 0 or from 1. A person's \"chapter 2\" is the chapter whose `chapterNumber` is 2; pass that chapter's `chapterIndex`."},"move":{"type":"object","required":["chapterIndex","afterChapterIndex"],"additionalProperties":false,"description":"One chapter (with the scene group it belongs to) put just after another chapter, or first.","properties":{"chapterIndex":{"type":"integer","minimum":0,"description":"The chapter to move. The chapter's `chapterIndex`, exactly as the chapters read returns it: a key, not a position — a plan may key its chapters from 0 or from 1. A person's \"chapter 2\" is the chapter whose `chapterNumber` is 2; pass that chapter's `chapterIndex`."},"afterChapterIndex":{"type":"integer","minimum":0,"nullable":true,"description":"The chapter it lands just after, or null to move it first. The chapter's `chapterIndex`, exactly as the chapters read returns it: a key, not a position — a plan may key its chapters from 0 or from 1. A person's \"chapter 2\" is the chapter whose `chapterNumber` is 2; pass that chapter's `chapterIndex`."}}}}},"PitchDeckReorderChaptersAck":{"type":"object","required":["success","moved","chapterCount","chapters","restitching","droppedInterludes","score","bellLane","message"],"additionalProperties":true,"description":"The chapters in their new order. Nothing was rendered and nothing was charged.","properties":{"success":{"type":"boolean"},"moved":{"type":"boolean","description":"False when the deck was already in that order: nothing was saved and nothing is stitched."},"chapterCount":{"type":"integer","minimum":1},"chapters":{"type":"array","description":"Every chapter in its new order, renumbered from 0.","items":{"type":"object","required":["chapterIndex","chapterNumber","title","previousChapterIndex"],"additionalProperties":false,"properties":{"chapterIndex":{"type":"integer","minimum":0,"description":"Its key now."},"chapterNumber":{"type":"integer","minimum":1,"description":"Its place in the deck, from 1."},"title":{"type":"string"},"previousChapterIndex":{"type":"integer","minimum":0,"description":"Its key before the move."}}}},"restitching":{"type":"boolean","description":"A rendered deck that moved: its video is stitched again in the new order (free; nothing renders)."},"droppedInterludes":{"type":"array","description":"Blacks taken out because their chapter now ends the deck (a black only sits between two chapters).","items":{"type":"object","required":["afterChapterNumber","line"],"additionalProperties":false,"properties":{"afterChapterNumber":{"type":"integer","minimum":1,"description":"The number its chapter had before the move."},"line":{"type":"string","nullable":true,"description":"The line heard over it, if it had one."}}}},"score":{"type":"string","nullable":true,"enum":["no_longer_fits",null],"description":"`no_longer_fits` when a score composed to the cut is laid: it still plays under the whole deck but no longer lands on the chapters until it is scored again (priced)."},"bellLane":{"type":"boolean","description":"The deck has a bell lane: it still strikes the title sequence's beat, laid again with the new order (free)."},"message":{"type":"string","description":"What happened, in one sentence to show the writer."},"planCoverage":{"type":"string","nullable":true,"enum":["queued","current","failed",null],"description":"On a draft plan: the changed plan's plan coverage (`queued` for the latest edit). Null in review or complete."},"edited":{"type":"string","enum":["draft_plan","rendered_deck"],"description":"Which version of the deck the edit changed: `draft_plan` (a `draft` deck — nothing has rendered) or `rendered_deck` (a deck in review or complete). Tell the writer which one changed."},"jobId":{"type":"string","description":"The deck that changed."},"status":{"type":"string","enum":["draft","review","complete"],"description":"The deck's status, unchanged by a reorder."}}},"PitchDeckFinishRequest":{"type":"object","additionalProperties":false,"description":"Finish the deck at the delivery class, and say whether this call is the price or the purchase.","properties":{"deliveryResolution":{"type":"string","enum":["720p","1080p"],"description":"Omit to finish at the deck's delivery class (720p unless chosen)."},"confirmed":{"type":"boolean","description":"Omit (or false) to get the finish price WITHOUT spending anything. Send true only after the writer has seen that price and said go."},"acknowledgeVideoCoverage":{"type":"boolean","description":"Finish although the master the writer is looking at FAILED its video coverage (otherwise refused with 409 `pitch_deck_video_coverage_failed`, `details.videoCoverage` carrying the score, the bar and the findings). Send true ONLY after the writer has seen those findings and chosen, in their own words, to finish anyway — never on your own."}}},"PitchDeckFinishAnswer":{"type":"object","required":["quotedOnly","jobId","deliveryResolution","credits","warnings"],"additionalProperties":true,"description":"Either the PRICE of the finish or the running finish — branch on `quotedOnly`.","properties":{"quotedOnly":{"type":"boolean"},"queued":{"type":"boolean","description":"Confirmed call only."},"jobId":{"type":"string"},"deliveryResolution":{"type":"string","enum":["720p","1080p"]},"credits":{"type":"number"},"costLabel":{"type":"string","nullable":true,"description":"Quote only."},"lines":{"type":"array","description":"Quote only. One line per selected take upscaled (a take already finished at the class is not charged again) and one per chapter's on-screen text layer.","items":{"type":"object","additionalProperties":true,"properties":{"idempotencyKey":{"type":"string"},"label":{"type":"string"},"credits":{"type":"number"},"kind":{"type":"string","enum":["finish","text"]},"callId":{"type":"string","nullable":true},"take":{"type":"integer","nullable":true},"chapterIndex":{"type":"integer","nullable":true},"seconds":{"type":"number","nullable":true}}}},"takes":{"type":"integer","description":"Confirmed call only: how many selected takes the finish upscales."},"warnings":{"type":"array","items":{"type":"string"},"description":"One per selected take the writer kept although it was flagged (a line not heard or in another character's voice, an opening off its plan)."},"balanceAfter":{"type":"integer","description":"Quote only. Studio Credits the writer would have left once this is spent: the balance available now (as Billing shows it) minus the price. Negative means they are that many short."},"sufficient":{"type":"boolean","description":"Quote only. True when the available balance covers the price. False means the writer must top up first (the note names the Billing link) — do not confirm; the reserve would refuse."},"note":{"type":"string","description":"Quote only: the price in the writer's words and how to proceed."},"message":{"type":"string","description":"Confirmed call only."}}},"PitchDeckDeliveryResolution":{"type":"object","required":["jobId","deliveryResolution","isDefault","renderResolution","options","editable"],"additionalProperties":false,"description":"The class the finish delivers the deck at. Raising it upscales the selected takes at finish — it quotes a finish, never a render.","properties":{"jobId":{"type":"string"},"deliveryResolution":{"type":"string","enum":["720p","1080p"],"description":"The writer's choice, else 720p (the plan never raises it)."},"isDefault":{"type":"boolean","description":"True when nobody chose on the deck, so the 720p default applies."},"renderResolution":{"type":"string","enum":["480p"],"description":"Every take renders at this class first, whatever the delivery class."},"options":{"type":"array","items":{"type":"string","enum":["720p","1080p"]}},"editable":{"type":"boolean","description":"False while a render or finish pass is running."}}},"AutoFix":{"type":"object","required":["state","stopReason","allowanceCredits","drawnCredits","heldCredits","round","maxRounds","fixesInFlight","rounds","moreFixes","stoppedBecause","sentence"],"additionalProperties":false,"description":"`autoFix` is the AUTOMATIC FIXES the render approval paid for up front: the approval quote carries an automatic-fix allowance beside the 480p render (`autoFixAllowance`), reserved with it. After the preview is stitched the engine watches it (video coverage) and retakes the shots the review flags — each new take told what the review saw, a shot at most twice, re-staged as a voice-over where the take rules allow — and renders flagged on-screen text again, all inside that allowance; then it stitches and watches the preview again. It stops at a clean 480p preview (`state: clean`), when the allowance is spent, when the review flags nothing a fix can clear, or after `maxRounds` rounds (`state: stopped`, `stopReason`, `stoppedBecause`). `state` is `waiting` (first pass rendering), `reviewing` or `fixing` while it works — do not retake or finish then, just poll. `allowanceCredits` is the ceiling, `drawnCredits` what fixes took from it (each charged only when its take lands), `heldCredits` what is still reserved (released when it stops); `rounds[]` lists each round's fixes with `why`. When it stopped short, `moreFixes` (if not null) offers one more grant of `credits` (the more-fixes op, priced first); otherwise the writer finishes anyway. The 720p finish is always the writer's own approval.","properties":{"state":{"type":"string","enum":["waiting","reviewing","fixing","clean","stopped"],"description":"`waiting` (the first pass renders), `reviewing` (the preview is being watched), `fixing` (fixes are rendering), `clean`, or `stopped`."},"stopReason":{"type":"string","nullable":true,"enum":["clean","allowance_spent","cannot_clear","rounds_spent","review_unavailable","writer_stopped","finished","no_allowance","deleted",null],"description":"Why it stopped (null while it works)."},"allowanceCredits":{"type":"integer","minimum":0,"description":"The ceiling approved for fixes: every grant together."},"drawnCredits":{"type":"integer","minimum":0,"description":"What fixes drew from it (each charged only when its take lands; back to the writer when it fails)."},"heldCredits":{"type":"integer","minimum":0,"description":"What the allowance still holds for fixes; 0 once it stops (released)."},"round":{"type":"integer","minimum":0,"description":"Rounds of fixes run."},"maxRounds":{"type":"integer","minimum":0,"description":"The most rounds it runs."},"fixesInFlight":{"type":"integer","minimum":0,"description":"Fixes still rendering."},"rounds":{"type":"array","description":"Each round: the review it answered and the fixes it applied.","items":{"type":"object","required":["round","reportId","overallScore","startedAt","fixes"],"additionalProperties":false,"properties":{"round":{"type":"integer","minimum":1},"reportId":{"type":"string","nullable":true,"description":"The video coverage report it answered."},"overallScore":{"type":"number","nullable":true},"startedAt":{"type":"string","format":"date-time"},"fixes":{"type":"array","items":{"type":"object","required":["kind","unitIndex","unitNumber","callId","take","credits","ended","why"],"additionalProperties":false,"properties":{"kind":{"type":"string","enum":["retake","text"],"description":"`retake`: a new take of a shot; `text`: the unit's on-screen text rendered again."},"unitIndex":{"type":"integer","description":"The chapter's or beat's key."},"unitNumber":{"type":"integer","minimum":1,"description":"Its place in the cut."},"callId":{"type":"string","nullable":true},"take":{"type":"integer","nullable":true},"credits":{"type":"integer","minimum":0,"description":"What it drew from the allowance (0 when it could not start)."},"ended":{"type":"boolean","description":"Its job has ended (landed, failed or refused)."},"why":{"type":"array","items":{"type":"string"},"description":"What the review saw that it answers; a retake is told to avoid it."}}}}}}},"moreFixes":{"type":"object","nullable":true,"additionalProperties":false,"required":["credits"],"description":"When it stopped short and one more grant could help: what that grant would hold (POST …/more-fixes). Null otherwise.","properties":{"credits":{"type":"integer","minimum":0}}},"stoppedBecause":{"type":"string","nullable":true,"description":"Why it stopped short of clean, in plain words."},"sentence":{"type":"string","description":"Where it stands, in one sentence."}}},"VideoVerdict":{"type":"object","required":["state","working","progress","verdict","choices"],"additionalProperties":false,"description":"`verdict` is the review in the writer's words — lead with it: `progress` (one line: rendering, fixing N shots the review flagged, ready to watch), `verdict` (one paragraph naming the problems that are left, by chapter or beat), `working` (true while the engine works: nothing to do but wait), and `choices` (at most two, only once it has stopped: `finish`, `finish_anyway` — the finish with `acknowledgeVideoCoverage` — or `more_fixes` with its `credits`). The scores, findings, takes and checks stay in `videoCoverage`, the chapters' calls and `autoFix` for anyone who wants to step in.","properties":{"state":{"type":"string","enum":["rendering","assembling","reviewing","fixing","ready","your_call","finishing","finished","failed"]},"working":{"type":"boolean","description":"True while the engine works and nothing needs the writer."},"progress":{"type":"string","description":"One plain progress line."},"verdict":{"type":"string","nullable":true,"description":"One paragraph in the writer's words, naming the problems that are left."},"choices":{"type":"array","maxItems":2,"description":"Empty while the engine works.","items":{"type":"object","required":["action","label","credits"],"additionalProperties":false,"properties":{"action":{"type":"string","enum":["finish","finish_anyway","more_fixes"],"description":"`finish` and `finish_anyway` are the finish op (`finish_anyway` with `acknowledgeVideoCoverage: true`); `more_fixes` is the more-fixes op."},"label":{"type":"string"},"credits":{"type":"integer","nullable":true,"description":"Known up front only for `more_fixes`; a finish is priced when chosen."}}}}}},"VideoEarlyLook":{"type":"object","required":["units","of","roughCut","sentence"],"additionalProperties":false,"description":"`earlyLook` (while the first pass renders; null once the preview exists) lists every chapter or beat whose shots all have a take, with each take's clip in shot order (`units[].clips[].url`), and `roughCut` once two have landed: they can be watched as they land, nothing to do. `musicPlan` is the music the cut will carry, known before the stitch: the beds on its timeline (`source: timeline`), or for a deck with none the table read's score as the assembly will lay it (`table_read_score`), each bed with the chapters or beats it plays under and a `url` to listen to — say it early, so a music change is made before the finish. A score to picture asked for (`score`, from a draft on) leads with its direction and, once its sketch to the planned cut is composed, `score.sketchUrl` to listen to; while one is asked for, the table read's beds are not laid or projected. When a score asked for with a direction could not be composed (`score.status: failed`), the beds are not laid in its place either: the sentence says so plainly (\"The score couldn't be composed (reason in Details). The bell plays alone. Score to picture to try again.\") — say it to the writer.","properties":{"units":{"type":"array","items":{"type":"object","required":["unitIndex","unitNumber","title","clips"],"additionalProperties":false,"properties":{"unitIndex":{"type":"integer"},"unitNumber":{"type":"integer","minimum":1},"title":{"type":"string"},"clips":{"type":"array","items":{"type":"object","required":["callId","url","seconds"],"additionalProperties":false,"properties":{"callId":{"type":"string"},"url":{"type":"string"},"seconds":{"type":"number"}}}}}}},"of":{"type":"integer","minimum":0,"description":"How many units have footage to render."},"roughCut":{"type":"boolean","description":"Two or more units have landed: a rough cut can play."},"sentence":{"type":"string"}}},"VideoMusicPlan":{"type":"object","required":["source","beds","score","sentence"],"additionalProperties":false,"description":"`earlyLook` (while the first pass renders; null once the preview exists) lists every chapter or beat whose shots all have a take, with each take's clip in shot order (`units[].clips[].url`), and `roughCut` once two have landed: they can be watched as they land, nothing to do. `musicPlan` is the music the cut will carry, known before the stitch: the beds on its timeline (`source: timeline`), or for a deck with none the table read's score as the assembly will lay it (`table_read_score`), each bed with the chapters or beats it plays under and a `url` to listen to — say it early, so a music change is made before the finish. A score to picture asked for (`score`, from a draft on) leads with its direction and, once its sketch to the planned cut is composed, `score.sketchUrl` to listen to; while one is asked for, the table read's beds are not laid or projected. When a score asked for with a direction could not be composed (`score.status: failed`), the beds are not laid in its place either: the sentence says so plainly (\"The score couldn't be composed (reason in Details). The bell plays alone. Score to picture to try again.\") — say it to the writer.","properties":{"source":{"type":"string","enum":["timeline","table_read_score","none"]},"beds":{"type":"array","items":{"type":"object","required":["label","url","units"],"additionalProperties":false,"properties":{"label":{"type":"string"},"url":{"type":"string","nullable":true,"description":"The bed to listen to (null while it is still being made)."},"units":{"type":"array","items":{"type":"integer"},"description":"The chapters or beats it plays under, by their number in the cut."}}}},"score":{"type":"object","nullable":true,"additionalProperties":false,"required":["status","direction","url","sketchUrl"],"description":"A score to picture asked for (from a draft on): its music direction, shown before anything renders, where it stands, and its sketch to the planned cut. While one is asked for, a deck projects none of the table read's beds: they are not laid under it.","properties":{"status":{"type":"string","description":"`waiting_for_master`, `queued`, `composing`, `laid` or `failed`."},"direction":{"type":"string","nullable":true},"url":{"type":"string","nullable":true,"description":"The laid score, once laid."},"sketchUrl":{"type":"string","nullable":true,"description":"The sketch composed to the planned cut while the score waits for its master: listen before anything renders. Never laid; null until composed and once the score is on its way to the real cut."}}},"sentence":{"type":"string"}}},"MoreFixesRequest":{"type":"object","additionalProperties":false,"description":"Let the automatic fixes try more. Metered, two calls: omit `confirmed` for the price; send `confirmed: true` only after the writer has seen it and said go.","properties":{"confirmed":{"type":"boolean","description":"Omit (or false) to get the price WITHOUT spending anything. Send true only after the writer has seen that price and said go."}}},"MoreFixesAnswer":{"type":"object","required":["quotedOnly","jobId","credits"],"additionalProperties":true,"description":"Either the PRICE of one more automatic-fix grant (`quotedOnly: true`; nothing reserved) or the grant itself (`quotedOnly: false`, `queued: true`: reserved, and the loop answers the same review again).","properties":{"quotedOnly":{"type":"boolean"},"queued":{"type":"boolean","description":"Confirmed call only."},"jobId":{"type":"string"},"credits":{"type":"integer","minimum":0,"description":"What the grant holds: only what the fixes draw is charged."},"costLabel":{"type":"string","nullable":true,"description":"Quote only."},"balanceAfter":{"type":"integer","description":"Quote only. Studio Credits the writer would have left once this is spent: the balance available now (as Billing shows it) minus the price. Negative means they are that many short."},"sufficient":{"type":"boolean","description":"Quote only. True when the available balance covers the price. False means the writer must top up first (the note names the Billing link) — do not confirm; the reserve would refuse."},"note":{"type":"string","description":"Quote only: the price in the writer's own words."},"message":{"type":"string","description":"Confirmed call only."},"autoFix":{"allOf":[{"$ref":"#/components/schemas/AutoFix"}],"nullable":true,"description":"Confirmed call only: the loop as it now stands."}}},"MasterAssembly":{"type":"object","required":["stage","step","unit","done","total","percent","finishLook","title","runtimeMs","startedAt","stepStartedAt","updatedAt","steps","sentence"],"additionalProperties":false,"description":"`assembly` is the master being put together right now, null when nothing is being assembled: `stage` (the 480p `preview` or the `finished` cut), the `step` it is on (`cutting`: joining each chapter's or beat's chosen takes, with `unit` the one being joined and `done` of `total` joined so far; `text`: laying the on-screen text (the finished cut only); `sound`: laying the voices, music and sound under the cut; `finish_look`: laying the finish look on the picture (only when one is set); `stitching`: stitching everything into one video, with `percent` from the renderer's own frame count; `delivering`: levelling the sound and saving the file), `steps` in order, `startedAt`, and `sentence`, which says it in words. Nothing renders and nothing is charged while a cut is assembled, and it cannot be stopped: it is free and ends on its own.","properties":{"stage":{"type":"string","enum":["preview","finished"],"description":"Which master: the 480p preview the writer reviews, or the finished cut."},"step":{"type":"string","enum":["cutting","text","sound","finish_look","stitching","delivering"],"description":"The step the assembly is on, in the order it runs them."},"unit":{"type":"integer","nullable":true,"description":"`cutting`: the chapter or beat whose takes are being joined, by its number in the cut (from 1)."},"done":{"type":"integer","nullable":true,"description":"`cutting`: how many of this assembly's chapters or beats are joined so far."},"total":{"type":"integer","nullable":true,"description":"`cutting`: how many chapters or beats this assembly joins (only the ones whose chosen takes changed)."},"percent":{"type":"integer","nullable":true,"description":"`stitching`: how far the stitch is, 0 to 100, from the renderer's own frame count."},"finishLook":{"type":"boolean","description":"Whether a finish look is laid on the picture (its step runs only then)."},"title":{"type":"string","nullable":true,"description":"What is being stitched, as the writer knows it."},"runtimeMs":{"type":"integer","nullable":true,"description":"How long the cut being stitched runs, as planned."},"startedAt":{"type":"string","format":"date-time","description":"When the assembly began."},"stepStartedAt":{"type":"string","format":"date-time","description":"When the current step began."},"updatedAt":{"type":"string","format":"date-time","description":"When the worker last reported. A record no worker has touched for twenty minutes is not returned."},"steps":{"type":"array","description":"The steps this assembly runs, in order, each done, current or upcoming.","items":{"type":"object","required":["step","label","state"],"additionalProperties":false,"properties":{"step":{"type":"string","enum":["cutting","text","sound","finish_look","stitching","delivering"]},"label":{"type":"string"},"state":{"type":"string","enum":["done","current","upcoming"]}}}},"sentence":{"type":"string","description":"The assembly in one sentence, as every surface says it to the writer."}}},"RenderReassembly":{"type":"object","required":["failedStage","available","stage","refusal"],"additionalProperties":false,"description":"A deck's or trailer's free re-assembly. A cut whose `assemble` (the 480p preview) or `finish_assemble` (the finished master) failed for good lost nothing it paid for, and is put back together free from the takes it already has. A cut in `review` or `complete` has its master REBUILT, free and fresh, with the current sound rules: the preview assembly in review (`assemble`), the finished master stitched again at its class when complete (`restitch`, the cut stays complete).","properties":{"failedStage":{"type":"string","nullable":true,"description":"The stage a FAILED cut failed in: rendering, assemble, finishing or finish_assemble. Null when it failed before the stage was recorded, and for a cut in review or complete."},"available":{"type":"boolean","description":"True when a re-assembly (or a rebuild) would run now (free: nothing rendered, upscaled or reserved)."},"stage":{"type":"string","nullable":true,"enum":["assemble","finish_assemble","restitch",null],"description":"The stage it runs: `assemble` / `finish_assemble` for a failed cut, `assemble` (review) or `restitch` (complete) for a rebuild; null when it is not available."},"refusal":{"type":"string","nullable":true,"description":"Why it would be refused, in plain words (a rebuild: a master already being assembled, a take in flight, a finished take missing); null when it is available."},"leftOut":{"type":"array","items":{"$ref":"#/components/schemas/RenderReassemblyLeftOut"},"description":"What a re-assembly that runs LEAVES OUT of the master. A deck's preview follows the master's rule: a chapter with no accepted take has no footage, so it is left out (the master plays the chapters around it) and named here — tell the writer which chapter is missing. Empty when nothing is, when it would not run, and always for a trailer and for the finished master (those refuse instead)."}}},"RenderReassemblyLeftOut":{"type":"object","required":["name","number","reason","message"],"additionalProperties":false,"description":"A unit a re-assembly leaves out of the master because it has no footage to cut.","properties":{"name":{"type":"string","description":"How a person names it: \"chapter 4\"."},"number":{"type":"integer","nullable":true,"description":"Its place in the deck from 1 (the chapterNumber a person says)."},"reason":{"type":"string","description":"Why it has no footage: the shot with no accepted take."},"message":{"type":"string","description":"The sentence to show: \"Chapter 4 is left out of the master: … Render it again to put it back.\""}}},"RenderReassembleAck":{"type":"object","required":["jobId","queued","stage","status","credits","message"],"additionalProperties":false,"description":"The re-assembly (or the rebuild) started. Nothing was rendered, upscaled or reserved.","properties":{"jobId":{"type":"string"},"queued":{"type":"boolean","enum":[true]},"stage":{"type":"string","enum":["assemble","finish_assemble","restitch"],"description":"The stage it runs: the 480p preview (`assemble`), the failed finished master (`finish_assemble`), or a complete cut's finished master stitched again at its class (`restitch`)."},"status":{"type":"string","enum":["rendering","finishing","review","complete"],"description":"Where the cut is while it runs: a failed cut goes `rendering` then `review`, or `finishing` then `complete`; a rebuilt master leaves the cut where it is (`review` or `complete`)."},"credits":{"type":"number","enum":[0],"description":"Always 0."},"message":{"type":"string","description":"What runs, and each chapter the re-assembled preview leaves out of the master."},"leftOut":{"type":"array","items":{"$ref":"#/components/schemas/RenderReassemblyLeftOut"},"description":"A deck's preview only: the chapters with no accepted take, LEFT OUT of the master it re-assembles (render each again to put it back). Absent or empty otherwise."}}},"VideoCoverageRemedy":{"type":"object","additionalProperties":false,"required":["kind"],"description":"`retake_call`: render a new take of `callId` (priced); `rerender_text`: render the unit's on-screen text again (priced); `rescore`: the score laid under the master is measured silent there — score the cut to picture again (a retake never fixes dead air in the score); `revise_plan`: a cut that does not follow because the plan gives it no link (or one too weak to carry it) — give the unit its link in the plan (the chapter or beat refine: `whyItFollows`, and the look, object or sound staged in it); `none`: nothing one action fixes.","properties":{"kind":{"type":"string","enum":["retake_call","rerender_text","rescore","revise_plan","none"]},"callId":{"type":"string"}}},"VideoCoverageFinding":{"type":"object","additionalProperties":false,"required":["dimension","severity","startSeconds","endSeconds","timecode","observation","fix","shouldShow","unit","callId","shotIndex","remedy"],"description":"One problem seen in the video, located on the master's timeline in code: the chapter (deck) or beat (trailer), the render call playing at that moment and the shot inside it, and the remedy that fixes it.","properties":{"dimension":{"type":"string","enum":["lipSync","promptAdherence","visualFidelity","motionNaturalness","identityContinuity","textPlacement","audioSync","transitions"]},"severity":{"type":"string","enum":["minor","major","critical"],"description":"`critical`: a viewer cannot watch past it; `major`: clearly wrong; `minor`: noticed on a second look."},"startSeconds":{"type":"number","minimum":0,"description":"Seconds from the start of the master (a player seeks here)."},"endSeconds":{"type":"number","minimum":0},"timecode":{"type":"string","description":"`m:ss–m:ss`, for a person."},"observation":{"type":"string","description":"Exactly what is on screen or heard that falls short."},"fix":{"type":"string","description":"The concrete production change that would fix it."},"shouldShow":{"type":"string","description":"What the picture should show there instead, in positive words that never name the problem; empty when no change to the picture fixes it. A new take of the shot (`retake_call`) is told this, never the observation."},"unit":{"type":"object","nullable":true,"additionalProperties":false,"required":["kind","index","number","title"],"properties":{"kind":{"type":"string","enum":["chapter","beat","interlude","title"],"description":"`interlude` is a deck's black interlude between two chapters: `index` is the chapter it follows. `title` is a deck's title sequence."},"index":{"type":"integer","minimum":0,"description":"The chapter's or beat's key (what every edit and render takes)."},"number":{"type":"integer","minimum":1,"description":"Its place in the cut, from 1: the number a person says."},"title":{"type":"string"}}},"callId":{"type":"string","nullable":true,"description":"The render call playing at that moment (retake it with the render-clip op and this `callId`)."},"shotIndex":{"type":"integer","nullable":true,"minimum":0,"description":"The shot inside that call, from 0."},"remedy":{"$ref":"#/components/schemas/VideoCoverageRemedy"},"source":{"type":"string","enum":["judge","measured"],"description":"Who saw it: `judge` (the model that watched the master) or `measured` (code measured it: a spoken line's mouth read against its voice on the take it plays from, or the score's dead air). A measured lip-sync miss holds the lip-sync score down (major 6.0, critical 4.0) whatever the judge scored: the judge cannot clear it. Absent on a finding stored before this was recorded (the judge's)."},"uncertain":{"type":"boolean","enum":[true],"description":"A measurement TOO CLOSE TO CALL: a quiet face read near its bar, or a pod reading of lips moving where no line is heard that the take watcher did not see too (a push-in, a flare or a smile reads the same). Severity minor: shown with its retake (`remedy`) for the writer to choose, never retaken by the automatic fixes and never holding a score down. Absent on every other finding."}}},"VideoCoverageLipSyncLine":{"type":"object","additionalProperties":false,"required":["unit","callId","take","lineRef","speaker","text","voice","startSeconds","endSeconds","timecode","verdict","severity","criteria","reason","faceHeightPx","correlation","voicedOpenness","rejudged"],"description":"One spoken line of the master and what the mouth check MEASURED on the take it plays from: the speaker's mouth (its judged openness, ten frames a second) against the voice that take was sent, and every face that must stay shut under it.","properties":{"unit":{"type":"object","nullable":true,"additionalProperties":false,"required":["kind","index","number","title"],"properties":{"kind":{"type":"string","enum":["chapter","beat"]},"index":{"type":"integer","minimum":0},"number":{"type":"integer","minimum":1},"title":{"type":"string"}}},"callId":{"type":"string","description":"The render call the line plays from (a miss is retaken with the render-clip op and this `callId`)."},"take":{"type":"integer","nullable":true,"description":"The selected take the master plays."},"lineRef":{"type":"integer"},"speaker":{"type":"string"},"text":{"type":"string","description":"The line's words."},"voice":{"type":"string","description":"`on_camera`, `voice_over` or `off_screen`."},"startSeconds":{"type":"number","minimum":0,"description":"Where it is spoken, in seconds of the master."},"endSeconds":{"type":"number","minimum":0},"timecode":{"type":"string"},"verdict":{"type":"string","enum":["pass","miss","uncertain","not_measured"],"description":"`miss`: a measured miss — also a `measured` lip-sync finding the judge cannot clear; `uncertain`: a reading inside the band around its limit (neither); `pass`; `not_measured`: no face could be read or the check never ran (a take watcher's look is not a measurement) — the judge's own look decides it."},"severity":{"type":"string","enum":["minor","major","critical"],"nullable":true,"description":"A miss's severity: `critical` when the mouth does not move at all or the wrong face speaks the line, else `major`. Null otherwise."},"criteria":{"type":"array","description":"Each criterion missed or uncertain, misses first: the face, `criterion` (`correlation`, `lag`, `trailing_motion`, `articulation` — the lips barely part under the voice — `articulation_stretch`, `still_open_run`, `still_open_share`, `still_openings`, `watcher_lip_sync`), its `value`, its `limit` and its `words`.","items":{"type":"object","additionalProperties":false,"required":["name","role","criterion","outcome","value","limit","words"],"properties":{"name":{"type":"string"},"role":{"type":"string","enum":["speaker","still"]},"criterion":{"type":"string"},"outcome":{"type":"string","enum":["fail","uncertain"]},"value":{"type":"number"},"limit":{"type":"number"},"words":{"type":"string"}}}},"reason":{"type":"string","nullable":true,"description":"Why it missed, is uncertain, or could not be measured, with the numbers. Null on a pass."},"faceHeightPx":{"type":"number","nullable":true,"description":"The speaker's face height in the take's pixels."},"correlation":{"type":"number","nullable":true,"description":"The speaker's mouth against the voice (r at the best lag)."},"voicedOpenness":{"type":"number","nullable":true,"description":"The speaker's mean openness under the voice, 0 closed to 3 wide (a speaking mouth averages at least 1.35)."},"rejudged":{"type":"boolean","description":"True when the take's stored reading was judged again under the current numbers against the voice it was sent."}}},"VideoCoverageLipSync":{"type":"object","additionalProperties":false,"required":["metricVersion","lines"],"description":"MEASURED LIP SYNC: every spoken line of the master, measured on the take it plays from. The judge is shown these; each `miss` is also a `measured` lip-sync finding.","properties":{"metricVersion":{"type":"string","description":"The mouth check's code version the lines were judged under."},"lines":{"type":"array","items":{"$ref":"#/components/schemas/VideoCoverageLipSyncLine"}}}},"VideoCoverageReport":{"type":"object","additionalProperties":false,"required":["overallScore","verdict","oneLineDiagnosis","dimensions","findings","priorityFixes"],"description":"What the model reported after watching the master with its sound: its quality and its adherence to the shot list, never its story.","properties":{"overallScore":{"type":"number","minimum":0,"maximum":10,"description":"The model's own holistic score. The bar is decided on the composed score (VideoCoverage.overallScore)."},"verdict":{"type":"string","enum":["pass","needs_work","fail"]},"oneLineDiagnosis":{"type":"string"},"dimensions":{"type":"object","additionalProperties":false,"required":["lipSync","promptAdherence","visualFidelity","motionNaturalness","identityContinuity","textPlacement","audioSync","transitions"],"description":"Each scored 0–10: lipSync (the right mouth moves to each line, in time; voice-overs over closed mouths), promptAdherence (each shot does what its call asked: framing, who is on screen, staging, room, camera), visualFidelity (artifacts, morphing, frozen or waxy faces, warped hands, burned-in text), motionNaturalness, identityContinuity (people, wardrobe, rooms), textPlacement (never over a face or other text, legible, a short dwell), audioSync, transitions (each cut follows the part before it by a link a viewer sees or hears).","properties":{"lipSync":{"type":"object","additionalProperties":false,"required":["score","finding"],"properties":{"score":{"type":"number","minimum":0,"maximum":10,"description":"The score the bar reads: the median across the watches that judged it, or the score carried from an earlier master with the same picture (`judging.dimensions`). A watch's dimension held down by its worst finding's severity (critical caps it at 4.0, major at 6.0) keeps the model's own number in `modelScore`."},"finding":{"type":"string"},"modelScore":{"type":"number","minimum":0,"maximum":10,"description":"Present only when the score was held down by a finding's severity: the model's own number."}}},"promptAdherence":{"type":"object","additionalProperties":false,"required":["score","finding"],"properties":{"score":{"type":"number","minimum":0,"maximum":10,"description":"The score the bar reads: the median across the watches that judged it, or the score carried from an earlier master with the same picture (`judging.dimensions`). A watch's dimension held down by its worst finding's severity (critical caps it at 4.0, major at 6.0) keeps the model's own number in `modelScore`."},"finding":{"type":"string"},"modelScore":{"type":"number","minimum":0,"maximum":10,"description":"Present only when the score was held down by a finding's severity: the model's own number."}}},"visualFidelity":{"type":"object","additionalProperties":false,"required":["score","finding"],"properties":{"score":{"type":"number","minimum":0,"maximum":10,"description":"The score the bar reads: the median across the watches that judged it, or the score carried from an earlier master with the same picture (`judging.dimensions`). A watch's dimension held down by its worst finding's severity (critical caps it at 4.0, major at 6.0) keeps the model's own number in `modelScore`."},"finding":{"type":"string"},"modelScore":{"type":"number","minimum":0,"maximum":10,"description":"Present only when the score was held down by a finding's severity: the model's own number."}}},"motionNaturalness":{"type":"object","additionalProperties":false,"required":["score","finding"],"properties":{"score":{"type":"number","minimum":0,"maximum":10,"description":"The score the bar reads: the median across the watches that judged it, or the score carried from an earlier master with the same picture (`judging.dimensions`). A watch's dimension held down by its worst finding's severity (critical caps it at 4.0, major at 6.0) keeps the model's own number in `modelScore`."},"finding":{"type":"string"},"modelScore":{"type":"number","minimum":0,"maximum":10,"description":"Present only when the score was held down by a finding's severity: the model's own number."}}},"identityContinuity":{"type":"object","additionalProperties":false,"required":["score","finding"],"properties":{"score":{"type":"number","minimum":0,"maximum":10,"description":"The score the bar reads: the median across the watches that judged it, or the score carried from an earlier master with the same picture (`judging.dimensions`). A watch's dimension held down by its worst finding's severity (critical caps it at 4.0, major at 6.0) keeps the model's own number in `modelScore`."},"finding":{"type":"string"},"modelScore":{"type":"number","minimum":0,"maximum":10,"description":"Present only when the score was held down by a finding's severity: the model's own number."}}},"textPlacement":{"type":"object","additionalProperties":false,"required":["score","finding"],"properties":{"score":{"type":"number","minimum":0,"maximum":10,"description":"The score the bar reads: the median across the watches that judged it, or the score carried from an earlier master with the same picture (`judging.dimensions`). A watch's dimension held down by its worst finding's severity (critical caps it at 4.0, major at 6.0) keeps the model's own number in `modelScore`."},"finding":{"type":"string"},"modelScore":{"type":"number","minimum":0,"maximum":10,"description":"Present only when the score was held down by a finding's severity: the model's own number."}}},"audioSync":{"type":"object","additionalProperties":false,"required":["score","finding"],"properties":{"score":{"type":"number","minimum":0,"maximum":10,"description":"The score the bar reads: the median across the watches that judged it, or the score carried from an earlier master with the same picture (`judging.dimensions`). A watch's dimension held down by its worst finding's severity (critical caps it at 4.0, major at 6.0) keeps the model's own number in `modelScore`."},"finding":{"type":"string"},"modelScore":{"type":"number","minimum":0,"maximum":10,"description":"Present only when the score was held down by a finding's severity: the model's own number."}}},"transitions":{"type":"object","additionalProperties":false,"required":["score","finding"],"properties":{"score":{"type":"number","minimum":0,"maximum":10,"description":"The score the bar reads: the median across the watches that judged it, or the score carried from an earlier master with the same picture (`judging.dimensions`). A watch's dimension held down by its worst finding's severity (critical caps it at 4.0, major at 6.0) keeps the model's own number in `modelScore`."},"finding":{"type":"string"},"modelScore":{"type":"number","minimum":0,"maximum":10,"description":"Present only when the score was held down by a finding's severity: the model's own number."}}}}},"findings":{"type":"array","items":{"$ref":"#/components/schemas/VideoCoverageFinding"}},"priorityFixes":{"type":"array","items":{"type":"string"},"description":"At most eight, most important first."},"judging":{"allOf":[{"$ref":"#/components/schemas/VideoCoverageJudging"}],"nullable":true,"description":"How the verdict was reached: every watch of the master, each score's distribution and median, and the scores carried from an earlier master with a frame-identical picture. Absent on a report written before watches were recorded (one watch decided it)."},"lipSync":{"allOf":[{"$ref":"#/components/schemas/VideoCoverageLipSync"}],"nullable":true,"description":"MEASURED LIP SYNC: every spoken line of the master and what the mouth check measured on its take — which lines missed and why. Each miss is also a `measured` lip-sync finding that holds the lip-sync score down whatever the judge scored. Absent on a report written before lines were measured."},"transitions":{"type":"array","nullable":true,"items":{"$ref":"#/components/schemas/VideoCoverageTransition"},"description":"EVERY CUT of the master, in order, and whether it follows the part before it. Each cut that is `weak` or `lost` is also a `transitions` finding with the same remedy. Absent on a report written before cuts were judged."}}},"VideoCoverageTransition":{"type":"object","additionalProperties":false,"required":["from","to","atSeconds","timecode","verdict","observation","planned","plannedLink","fix","remedy"],"description":"One cut of the master, judged on the actual video: does the part it goes INTO follow the part before it? Located in code (the cut falls where `to` starts; a black interlude between them is part of the cut).","properties":{"from":{"type":"object","nullable":true,"additionalProperties":false,"required":["kind","index","number","title"],"properties":{"kind":{"type":"string","enum":["chapter","beat","title"]},"index":{"type":"integer","minimum":0},"number":{"type":"integer","minimum":0},"title":{"type":"string"}}},"to":{"type":"object","additionalProperties":false,"required":["kind","index","number","title"],"properties":{"kind":{"type":"string","enum":["chapter","beat","title"]},"index":{"type":"integer","minimum":0,"description":"The key every edit takes."},"number":{"type":"integer","minimum":1},"title":{"type":"string"}}},"atSeconds":{"type":"number","minimum":0,"description":"Where the cut falls, in seconds of the master."},"timecode":{"type":"string","description":"`m:ss`, for a person."},"verdict":{"type":"string","enum":["follows","weak","lost"],"description":"`follows`; `weak` (the link is faint, or half carries the viewer); `lost` (nothing connects the two: a non-sequitur)."},"observation":{"type":"string","description":"What carries a viewer across the cut — the look, the object, the sound, the cause, the contrast — or what is missing."},"planned":{"type":"string","nullable":true,"description":"The link the plan states for this cut (`whyItFollows` of `to`), or null when it states none."},"plannedLink":{"type":"string","enum":["seen","not_seen","none_planned"],"description":"Whether the watcher saw or heard the planned link: `seen`, `not_seen`, or `none_planned` (always, when the plan states none)."},"fix":{"type":"string","description":"The concrete change that makes the cut follow; empty when it follows."},"remedy":{"$ref":"#/components/schemas/VideoCoverageRemedy"}}},"VideoCoverageCarriedFrom":{"type":"object","additionalProperties":false,"required":["reportId","reportVersion","masterUrl"],"description":"The earlier report a score was carried from: a master of the same cut whose picture is frame-identical (its sampled decoded frames hash the same) and was asked the same things.","properties":{"reportId":{"type":"string"},"reportVersion":{"type":"integer","nullable":true},"masterUrl":{"type":"string","description":"The master that report watched."}}},"VideoCoverageScoreDistribution":{"type":"object","additionalProperties":false,"required":["scores","median","watches","carriedFrom"],"description":"How one score was decided: the scores of the watches it used and their median (the number the bar reads), or the report it was carried from.","properties":{"scores":{"type":"array","items":{"type":"number"},"description":"In watch order (carried: the carried report's own)."},"median":{"type":"number","description":"The median of `scores` (an even count: the mean of the middle two) — equal to the report's score."},"watches":{"type":"array","items":{"type":"integer","minimum":1},"description":"Which of this report's watches it used; empty when carried."},"carriedFrom":{"allOf":[{"$ref":"#/components/schemas/VideoCoverageCarriedFrom"}],"nullable":true}}},"VideoCoverageWatch":{"type":"object","additionalProperties":false,"required":["watch","reason","model","watchedAt","overallScore","composedScore","gatePassed","dimensions","costUsd","priced"],"description":"One watch of the master, as recorded.","properties":{"watch":{"type":"integer","minimum":1,"description":"1, 2, 3 — in the order taken."},"reason":{"type":"string","enum":["first","near_gate","disagreement"],"description":"`first`; `near_gate`: the first watch sat within `nearGateMargin` of the bar (its composed overall or a veto dimension); `disagreement`: two watches sat on opposite sides of the bar on some gating score."},"model":{"type":"string"},"watchedAt":{"type":"string","format":"date-time"},"overallScore":{"type":"number","description":"The model's own holistic number on this watch."},"composedScore":{"type":"number","nullable":true,"description":"The bar's composed score for this watch alone, carried scores in place."},"gatePassed":{"type":"boolean","description":"Whether this watch alone cleared the bar."},"dimensions":{"type":"object","additionalProperties":false,"required":["lipSync","promptAdherence","visualFidelity","motionNaturalness","identityContinuity","textPlacement","audioSync","transitions"],"description":"Each dimension as this watch scored it, after its own findings' severity caps.","properties":{"lipSync":{"type":"number","minimum":0,"maximum":10},"promptAdherence":{"type":"number","minimum":0,"maximum":10},"visualFidelity":{"type":"number","minimum":0,"maximum":10},"motionNaturalness":{"type":"number","minimum":0,"maximum":10},"identityContinuity":{"type":"number","minimum":0,"maximum":10},"textPlacement":{"type":"number","minimum":0,"maximum":10},"audioSync":{"type":"number","minimum":0,"maximum":10},"transitions":{"type":"number","minimum":0,"maximum":10}}},"costUsd":{"type":"number","description":"What this watch counts against the extra-watch spend cap (USD)."},"priced":{"type":"boolean","description":"False when the model has no price on file and `costUsd` is the flat per-watch estimate."}}},"VideoCoverageJudging":{"type":"object","additionalProperties":false,"required":["watches","dimensions","overall","carriedFrom","stoppedBecause","nearGateMargin","maxWatches","extraSpendUsd","extraSpendCapUsd","pictureFingerprint","soundFingerprint"],"description":"HOW THE VERDICT WAS REACHED. One watch is too noisy to gate on near the bar (two watches of one frame-identical picture have scored prompt adherence 9.5 and 6.0), so a first watch within `nearGateMargin` of the bar is followed by a second, two watches on opposite sides of the bar by a third, and the bar reads the MEDIAN of each score. A master whose picture is frame-identical to an earlier judged master carries that report's picture scores (prompt adherence, visual fidelity, motion naturalness, identity continuity, text placement) and judges only lip sync, audio sync and transitions again — nothing at all when the sound is identical too. The rubric, the weights and the bar are unchanged.","properties":{"watches":{"type":"array","items":{"$ref":"#/components/schemas/VideoCoverageWatch"},"description":"This report's watches, in order (empty when every score was carried)."},"dimensions":{"type":"object","additionalProperties":false,"required":["lipSync","promptAdherence","visualFidelity","motionNaturalness","identityContinuity","textPlacement","audioSync","transitions"],"properties":{"lipSync":{"$ref":"#/components/schemas/VideoCoverageScoreDistribution"},"promptAdherence":{"$ref":"#/components/schemas/VideoCoverageScoreDistribution"},"visualFidelity":{"$ref":"#/components/schemas/VideoCoverageScoreDistribution"},"motionNaturalness":{"$ref":"#/components/schemas/VideoCoverageScoreDistribution"},"identityContinuity":{"$ref":"#/components/schemas/VideoCoverageScoreDistribution"},"textPlacement":{"$ref":"#/components/schemas/VideoCoverageScoreDistribution"},"audioSync":{"$ref":"#/components/schemas/VideoCoverageScoreDistribution"},"transitions":{"$ref":"#/components/schemas/VideoCoverageScoreDistribution"}}},"overall":{"allOf":[{"$ref":"#/components/schemas/VideoCoverageScoreDistribution"}],"description":"The model's own holistic number across the watches."},"carriedFrom":{"allOf":[{"$ref":"#/components/schemas/VideoCoverageCarriedFrom"}],"nullable":true,"description":"The earlier report whose scores were carried, or null."},"stoppedBecause":{"type":"string","enum":["decisive","agreement","max_watches","spend_cap","watch_failed","carried"],"description":"`decisive`: one watch, clear of the bar by the margin; `agreement`: two watches on the same side of the bar on every gating score; `max_watches`: three; `spend_cap`: another was wanted but this master's extra-watch spend cap was reached; `watch_failed`: another was wanted but could not run; `carried`: no watch, picture and sound identical to an earlier judged master."},"nearGateMargin":{"type":"number","description":"Within this many points of the bar a single watch is not trusted."},"maxWatches":{"type":"integer"},"extraSpendUsd":{"type":"number","description":"What this report spent on watches beyond its first (USD)."},"extraSpendCapUsd":{"type":"number","description":"The most every report of one master may spend on watches beyond their first (USD)."},"pictureFingerprint":{"type":"string","nullable":true,"description":"Hash of the master's sampled decoded frames and what each part was asked to show (null: not measured)."},"soundFingerprint":{"type":"string","nullable":true,"description":"Hash of its decoded sound on that picture and what it was meant to sound like (null: not measured, or no sound)."}}},"StoryboardCoverageScoreDistribution":{"type":"object","additionalProperties":false,"required":["scores","median"],"description":"One plan-coverage score across the judgments of a revision.","properties":{"scores":{"type":"array","items":{"type":"number","minimum":0,"maximum":10},"description":"The readable scores, in judgment order."},"median":{"type":"number","nullable":true,"minimum":0,"maximum":10,"description":"Their median (an even count: the mean of the middle two) — the number the gate reads. Null when none could be read."}}},"StoryboardCoverageOpening":{"type":"object","additionalProperties":false,"required":["present","whatItSays","servesIntent","nextMove"],"description":"The report's `opening` is the judge's note on the deck's title sequence, written before it scores anything: `whatItSays` (what the epigraph, the source credit, the flash cuts and the title tell a stranger before the first chapter), `servesIntent` (whether it poses the intent's question and names the people that question is about, and whether the cut pays it off) and `nextMove`; `present` is false for a cut that opens on its first unit (every trailer), and `opening` is null on a report judged before the section existed. It is not scored on its own — it counts in intent clarity.","properties":{"present":{"type":"boolean","description":"Whether the cut opens on a title sequence before its first unit."},"whatItSays":{"type":"string","description":"What the opening tells a stranger before the first unit: the epigraph and its source, the credit, whom the flash cuts go to in order (and who is in shadow), the title. Empty when `present` is false."},"servesIntent":{"type":"string","description":"Whether it serves the intent: the question it poses, whether the people it names are the people that question is about, whether the cut pays it off. Empty when `present` is false."},"nextMove":{"type":"string","description":"The one change that would make the opening serve the intent better, or \"None required.\""}}},"StoryboardCoverageJudgment":{"type":"object","additionalProperties":false,"required":["judgment","reason","model","judgedAt","overallScore","composedScore","gatePassed","dimensions","costUsd","priced","prompts"],"description":"One judgment of the plan revision.","properties":{"judgment":{"type":"integer","minimum":1,"description":"1, 2, 3 — in the order taken."},"reason":{"type":"string","enum":["first","near_bar","disagreement"],"description":"`first`; `near_bar`: the first sat within the band of a gating line; `disagreement`: two sat on opposite sides of a line, so a third breaks the tie."},"model":{"type":"string"},"judgedAt":{"type":"string","format":"date-time"},"overallScore":{"type":"number","nullable":true,"description":"The judge's own overall on this judgment."},"composedScore":{"type":"number","nullable":true,"description":"The score the gate reads for this judgment alone: the lower of its overall and its weighted dimensions."},"gatePassed":{"type":"boolean","description":"Whether this judgment alone cleared the gate."},"dimensions":{"type":"object","additionalProperties":{"type":"number","nullable":true},"description":"Each rubric dimension as this judgment scored it."},"costUsd":{"type":"number","description":"What this judgment cost us (USD) — never the writer."},"priced":{"type":"boolean","description":"False when the model has no price on file and `costUsd` is the flat per-judgment estimate."},"prompts":{"type":"object","nullable":true,"additionalProperties":false,"required":["judge","viewerTakeaway"],"description":"Which prompt text this judgment ran on: the first 16 hex characters of the sha256 of the judge's exact system text (the rubric, then the showcase rubric for a showcase deck) and of the blind first-time viewer's rubric. Null on a judgment recorded before these were kept.","properties":{"judge":{"type":"string"},"viewerTakeaway":{"type":"string"}}}}},"StoryboardCoverageJudging":{"type":"object","additionalProperties":false,"required":["pending","planRevision","judgments","overall","dimensions","medianScore","decidedOnMedian","stoppedBecause","nearGateBand","maxJudgments","extraSpendUsd","extraSpendCapUsd"],"description":"HOW A PLAN COVERAGE VERDICT WAS REACHED. One judgment is too noisy to gate a paid render on near the bar (near-identical plans have scored 6.2 and 7.4), so a revision whose first judgment lands within `nearGateBand` of a gating line — the composed overall against the bar, or a veto dimension against its veto line — is judged again: the SAME revision, up to `maxJudgments`, a third only when two disagree. The gate reads the MEDIAN of each score, vetoes included. A plan edit starts a fresh set. Every judgment is free to the writer. The band, the cap and the spend cap are the `storyboard_coverage_judging` site setting.","properties":{"pending":{"type":"boolean","description":"True while another judgment of this revision is queued or running: the gate has not decided yet (the approval waits)."},"planRevision":{"type":"integer","nullable":true,"description":"The plan revision (fingerprint) every judgment scored."},"judgments":{"type":"array","items":{"$ref":"#/components/schemas/StoryboardCoverageJudgment"}},"overall":{"allOf":[{"$ref":"#/components/schemas/StoryboardCoverageScoreDistribution"}],"description":"The judge's own overall across the judgments."},"dimensions":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/StoryboardCoverageScoreDistribution"},"description":"Each rubric dimension across the judgments; the report's dimension scores are these medians."},"medianScore":{"type":"number","nullable":true,"description":"The score the gate read: the composed score of the median report (null while pending)."},"decidedOnMedian":{"type":"boolean","description":"True when the gate read the median of more than one judgment."},"stoppedBecause":{"type":"string","nullable":true,"enum":["decisive","agreement","max_judgments","spend_cap","judging_incomplete","judgment_failed",null],"description":"`decisive`: one judgment clear of every line by the band (or a blocking defect, or an unreadable score); `agreement`: two on the same side of every line; `max_judgments`; `spend_cap`; `judging_incomplete`: another was wanted near the bar and could not run even queued again, so nothing was decided (the report is `failed`, neither a pass nor a veto: score again); `judgment_failed`: written before `judging_incomplete`: another could not run, so the recorded ones decided. Null while pending."},"nearGateBand":{"type":"number","description":"Within this many points of a gating line, one judgment is not trusted on its own."},"maxJudgments":{"type":"integer","description":"The most judgments one revision takes."},"extraSpendUsd":{"type":"number","description":"What the judgments beyond the first cost us (USD)."},"extraSpendCapUsd":{"type":"number","description":"The most the judgments beyond the first may cost for one revision (USD)."}}},"VideoCoverage":{"type":"object","additionalProperties":false,"required":["status","planKind","jobId","stage","masterUrl","currentMasterUrl","current","available","gatePassed","gateThreshold","vetoDimensions","gateReasons","blocksFinish","summary","trigger","reportId","reportVersion","rubricVersion","overallScore","model","progressDetail","failureMessage","report","createdAt","updatedAt"],"description":"VIDEO COVERAGE of a deck's or trailer's stitched master: a model watched the video with its sound and scored its quality and its adherence to the shot list against a bar (`gateThreshold`, with `vetoDimensions` that fail it on their own). It runs by itself, free to the writer, when the 480p preview is stitched and again when the finished master is. While `blocksFinish` is true the finish is refused unless the writer finishes anyway; a report still running, one that could not run, an unavailable provider, or a report of an earlier master never holds the finish.","properties":{"status":{"type":"string","enum":["missing","generating","complete","failed"]},"planKind":{"type":"string","enum":["pitch_deck","trailer"]},"jobId":{"type":"string"},"stage":{"type":"string","nullable":true,"enum":["preview","finished",null],"description":"Which master the report watched."},"masterUrl":{"type":"string","nullable":true,"description":"The master the report watched."},"currentMasterUrl":{"type":"string","nullable":true,"description":"The master the cut has now."},"current":{"type":"boolean","description":"True when the report watched the master the cut has now."},"available":{"type":"boolean","description":"False when no provider can watch a video right now (that never holds the finish)."},"gatePassed":{"type":"boolean"},"gateThreshold":{"type":"number","minimum":0,"maximum":10,"description":"The bar the composed overall and every veto dimension must reach."},"vetoDimensions":{"type":"array","items":{"type":"string"},"description":"Dimensions that fail the video on their own below the bar."},"gateReasons":{"type":"array","items":{"type":"string"},"description":"Every reason the bar was not cleared; empty on a pass."},"blocksFinish":{"type":"boolean","description":"True only when this report holds the paid finish: complete, of the current master, below the bar."},"summary":{"type":"string","description":"One sentence for the writer: where the coverage stands and what it means for the finish."},"trigger":{"type":"string","nullable":true,"enum":["auto","manual",null],"description":"`auto`: run after a stitch; `manual`: asked for."},"reportId":{"type":"string","nullable":true},"reportVersion":{"type":"integer","nullable":true},"rubricVersion":{"type":"integer","nullable":true},"overallScore":{"type":"number","nullable":true,"description":"The composed score the bar was decided on: the lower of the model's overall and the weighted dimensions — over the MEDIAN of each score when more than one watch judged it (`report.judging`)."},"model":{"type":"string","nullable":true},"progressDetail":{"type":"string","nullable":true},"failureMessage":{"type":"string","nullable":true},"report":{"allOf":[{"$ref":"#/components/schemas/VideoCoverageReport"}],"nullable":true},"createdAt":{"type":"string","nullable":true,"format":"date-time"},"updatedAt":{"type":"string","nullable":true,"format":"date-time"}}},"VideoCoverageEnvelope":{"type":"object","required":["coverage"],"additionalProperties":false,"properties":{"coverage":{"$ref":"#/components/schemas/VideoCoverage"}}},"VideoCoverageGenerateRequest":{"type":"object","additionalProperties":false,"properties":{"focusPrompt":{"type":"string","maxLength":5000,"description":"Optional: what to look at hardest this time."}}},"VideoCoverageRefusalDetails":{"type":"object","additionalProperties":false,"required":["reportId","stage","overallScore","gateThreshold","gateReasons","findings","priorityFixes","judging","override"],"description":"`error.details.videoCoverage` on a finish refused because the master failed its video coverage (`pitch_deck_video_coverage_failed` / `trailer_video_coverage_failed`). Nothing was reserved.","properties":{"reportId":{"type":"string","nullable":true},"stage":{"type":"string","nullable":true,"enum":["preview","finished",null]},"overallScore":{"type":"number","nullable":true},"gateThreshold":{"type":"number"},"gateReasons":{"type":"array","items":{"type":"string"}},"findings":{"type":"array","items":{"$ref":"#/components/schemas/VideoCoverageFinding"},"description":"The most severe first, each with its fix."},"priorityFixes":{"type":"array","items":{"type":"string"}},"judging":{"allOf":[{"$ref":"#/components/schemas/VideoCoverageJudging"}],"nullable":true,"description":"How the verdict was reached: the watches, each score's median, and what was carried (null: not recorded)."},"override":{"type":"string","enum":["acknowledgeVideoCoverage"],"description":"The finish body field that finishes anyway — only once the writer has chosen to."}}},"PitchDeckRefusalDetails":{"type":"object","required":["refusal"],"additionalProperties":true,"description":"`error.details` on a refused deck operation (409 pitch_deck_state_invalid, 400 for a chapter/call/take the deck does not have, 402 for credits). Nothing was reserved.","properties":{"refusal":{"type":"object","required":["code","action","fix"],"additionalProperties":false,"properties":{"code":{"type":"string","description":"The engine's refusal code, e.g. `pitch_deck_render_blocked`, `pitch_deck_not_draft`, `pitch_deck_not_reviewable`, `pitch_deck_finish_missing_takes`, `pitch_deck_chapter_text_only`, `pitch_deck_chapter_changed`, `pitch_deck_delivery_locked`."},"action":{"allOf":[{"$ref":"#/components/schemas/ReferenceRenderAction"}],"nullable":true},"fix":{"type":"object","nullable":true,"additionalProperties":false,"description":"Where the action is taken, on every surface.","properties":{"kind":{"type":"string","enum":["recast_figure","add_signature_items","recast_voice"]},"subject":{"type":"string","description":"The character or plate the action is about."},"sentence":{"type":"string","description":"One sentence for the writer."},"mcpTool":{"type":"string"},"cliCommand":{"type":"string"}}}}},"videoCoverage":{"$ref":"#/components/schemas/VideoCoverageRefusalDetails"},"blocked":{"type":"array","description":"On `pitch_deck_render_blocked`: every chapter the compile could not render, with its issues and the actions that fix them. An issue with no action (a sentence too long for one render, a name nobody is on camera for) is fixed by refining that chapter — on a draft plan too.","items":{"type":"object","additionalProperties":true,"properties":{"chapterIndex":{"type":"integer","minimum":0,"description":"The blocked chapter's key: refine it by this."},"chapterNumber":{"type":"integer","minimum":1,"description":"The chapter's place in the deck, counted from 1: the number a person says (\"chapter 2\") and every sentence uses. Never an input; chapter edits take `chapterIndex`."},"issues":{"type":"array","items":{"type":"object","additionalProperties":true}},"actions":{"type":"array","items":{"$ref":"#/components/schemas/ReferenceRenderAction"}}}}}}},"TrailerBeats":{"type":"object","required":["jobId","status","shotDensity","reviewUrl","deliveryResolution","master","beats","finish","camera","warnings","reassembly","videoCoverage","assembly","autoFix","verdict","earlyLook","musicPlan","interludes","closingSequence"],"additionalProperties":true,"description":"A trailer's beats as they render (reads only: it never compiles, renders or makes a plate). Statuses: draft → rendering → review → finishing → complete | failed.","properties":{"jobId":{"type":"string"},"status":{"type":"string","enum":["pending","draft","rendering","review","finishing","complete","failed"],"description":"Where the trailer is. A retired storyboard-era status is terminal and read-only."},"shotDensity":{"type":"string","enum":["held","standard","dense"],"description":"How many shots each render of a beat carries (`held`, `standard` or `dense`), set when the trailer was planned. Cuts inside a render are free: it never changes the price."},"intent":{"allOf":[{"$ref":"#/components/schemas/VideoStoryIntent"}],"nullable":true,"description":"The trailer's intent — what it is for. Null for a trailer planned before intents existed (its plan coverage fails on `video_intent_missing` until one is set). Read it beside plan coverage's first-time-viewer takeaway at GET /trailers/{jobId}/intent."},"reviewUrl":{"type":"string","nullable":true,"description":"Logged-in browser link to the trailer in the Videos tab, where the cut plays — motion cannot play in a terminal."},"deliveryResolution":{"type":"string","enum":["720p","1080p"],"description":"What the finish delivers: the writer's choice, else 720p."},"master":{"type":"object","additionalProperties":false,"required":["url","stage","resolution","interim"],"description":"The stitched master: a 480p `preview` in review, `finished` once the finish completes. Only a finished master exports.","properties":{"url":{"type":"string","nullable":true},"stage":{"type":"string","nullable":true,"enum":["preview","finished",null]},"resolution":{"type":"string","nullable":true},"interim":{"type":"boolean","description":"True while the master is an EARLY LOOK: a take landed while it was stitched, so the cut with every new take is put together after it and replaces it by itself. Nothing reviews an early look; only the latest master is watched."}}},"beats":{"type":"array","items":{"$ref":"#/components/schemas/TrailerBeat"}},"finish":{"allOf":[{"$ref":"#/components/schemas/Finish"}],"description":"The film finish laid on the picture at master assembly. The finish is a film look laid on the picture when the master is assembled: a look (camera, period-35mm, night-interior, none), an intensity (light, standard, strong; standard by default) and, optionally, single effects set to a level (adjust: { effect: off | subtle | medium | strong }; effects: steady-light, match-shots, halation, mist, shoulder, colour, vignette, weave, grain, soften). New decks and trailers start on camera. Titles and captions are laid after it, so type stays crisp, and the sound and timing are never touched. Setting it is free; on a rendered deck or trailer the master is assembled again with it (free), and setting the same finish again changes nothing. Set it with POST /trailers/{jobId}/look."},"camera":{"allOf":[{"$ref":"#/components/schemas/CameraReadSummary"}],"description":"The camera motion: the handheld strength and each beat the camera touches, with its move and what the last master laid. Read it in full and set it at /trailers/{jobId}/camera."},"warnings":{"type":"array","items":{"type":"string"},"description":"Warnings the finish quote and the export carry: one per flagged take the writer kept (a line not heard or in another character's voice, an opening off its plan)."},"assembly":{"allOf":[{"$ref":"#/components/schemas/MasterAssembly"}],"nullable":true,"description":"The trailer's master being put together right now, step by step; null when nothing is being assembled."},"reassembly":{"allOf":[{"$ref":"#/components/schemas/RenderReassembly"}],"nullable":true,"description":"POST /trailers/{jobId}/reassemble, as it stands now (free): for a FAILED trailer, the stage it failed in and whether it would be put back together; in `review` or `complete`, whether its master would be REBUILT with the current sound rules (`available`) or why not (`refusal`: a master already being assembled, a take in flight). Null in any other status."},"verdict":{"$ref":"#/components/schemas/VideoVerdict"},"autoFix":{"allOf":[{"$ref":"#/components/schemas/AutoFix"}],"nullable":true,"description":"The trailer's automatic fixes: the loop that watches the 480p preview and retakes what its review flags, inside the allowance the render approval reserved. Null for a trailer approved before it."},"earlyLook":{"allOf":[{"$ref":"#/components/schemas/VideoEarlyLook"}],"nullable":true,"description":"What can be watched before the preview is stitched: every beat whose shots all have a take, and a rough cut once two have. Null once the preview exists (and in draft)."},"musicPlan":{"allOf":[{"$ref":"#/components/schemas/VideoMusicPlan"}],"nullable":true,"description":"The music the trailer will carry, known before the stitch. Null for a failed trailer."},"interludes":{"type":"array","items":{"$ref":"#/components/schemas/TrailerInterlude"},"description":"The trailer's black interludes, in cut order, with what the master made of each. Replace them with POST /trailers/{jobId}/interludes."},"closingSequence":{"allOf":[{"$ref":"#/components/schemas/ClosingSequenceView"}],"nullable":true,"description":"The trailer's closing sequence, with each card's length and where it starts in the master; null when it has none. Set it with POST /trailers/{jobId}/closing-sequence."},"videoCoverage":{"allOf":[{"$ref":"#/components/schemas/VideoCoverage"}],"nullable":true,"description":"The trailer's video coverage: the stitched master (the 480p preview, then the finished master) watched with its sound and scored against the bar, its findings located by timecode on beats, calls and shots. While `blocksFinish` is true the finish is refused unless the writer finishes anyway (`acknowledgeVideoCoverage`). Null for a trailer with no master yet. The same object GET /trailers/{jobId}/video-coverage answers."}}},"TrailerBeatRenderStates":{"type":"array","description":"Every beat's clip and text render state, derived from the queue, the manifest and the credit ledger, plus each attempt's credit trail (reads only: it never compiles, renders, voices or makes a plate). One entry per beat, in the order the cut plays them.","items":{"$ref":"#/components/schemas/TrailerBeatRenderStatus"}},"TrailerBeatRenderStatus":{"type":"object","additionalProperties":false,"required":["beatIndex","clip","text"],"description":"One beat's durable render state: its clip and its text layer.","properties":{"beatIndex":{"type":"integer","minimum":0},"clip":{"$ref":"#/components/schemas/TrailerBeatRenderState"},"text":{"$ref":"#/components/schemas/TrailerBeatRenderState"}}},"TrailerBeatRenderState":{"type":"object","additionalProperties":false,"required":["kind","state","queueJobId","assetUrl","error","attempt","credits"],"description":"One beat's clip or text layer, as the queue, the manifest and the credit ledger actually see it. Every `state` is DERIVED, never asserted: `ready` is the only one that means done.","properties":{"kind":{"type":"string","enum":["clip","text"]},"state":{"type":"string","enum":["ready","queued","running","completed","failed","missing","unknown","never_submitted"],"description":"`ready` (the asset is in the manifest — the only state that means done); `queued` / `running` (the queue still holds the recorded job id); `completed` (the queue finished but the asset is NOT in the manifest: the worker ran and wrote nothing durable); `failed` (the queue marked it failed/cancelled/expired; `error` carries the worker's payload); `missing` (a job id was recorded but the queue has no such job); `unknown` (the queue could not be reached — say \"unknown\", never guess); `never_submitted` (no job id and no asset)."},"queueJobId":{"type":"string","nullable":true,"description":"The pg-boss job id backing this state, when one was recorded at submit."},"assetUrl":{"type":"string","nullable":true,"description":"The asset in the manifest, when the render actually landed."},"error":{"type":"string","nullable":true,"description":"The queue row's failure payload, when state is `failed`."},"attempt":{"allOf":[{"$ref":"#/components/schemas/TrailerBeatRenderAttempt"}],"nullable":true,"description":"The recorded attempt, independent of the asset; null when no job id was recorded."},"credits":{"type":"array","description":"THE CREDIT TRAIL for this beat's recorded attempts, newest first: each row is one attempt's current ledger position — `RESERVE` (held), `CONSUME_PROVISION` (settled), `RELEASE` (returned).","items":{"type":"object","additionalProperties":false,"required":["reason","delta","label","createdAt"],"properties":{"reason":{"type":"string"},"delta":{"type":"number"},"label":{"type":"string","nullable":true},"createdAt":{"type":"string"}}}}}},"TrailerBeatRenderAttempt":{"type":"object","additionalProperties":false,"required":["queueJobId","state","error","creditIdempotencyKey","providerTaskId","providerModelId","timeline"],"description":"THE RECORDED RENDER ATTEMPT for one beat's clip or text layer, independent of any asset. A re-render over an existing asset would otherwise be invisible: the asset reads `ready` while the new attempt is still in the queue.","properties":{"queueJobId":{"type":"string"},"state":{"type":"string","enum":["queued","running","completed","failed","missing","unknown"]},"error":{"type":"string","nullable":true},"creditIdempotencyKey":{"type":"string","nullable":true,"description":"The submission's identity: the credit line idempotency key reserved at submit."},"providerTaskId":{"type":"string","nullable":true,"description":"The provider's own task id, when the worker recorded one (the video path)."},"providerModelId":{"type":"string","nullable":true,"description":"The provider model that accepted the task."},"timeline":{"$ref":"#/components/schemas/TrailerBeatRenderTimeline"}}},"TrailerBeatRenderTimeline":{"type":"object","additionalProperties":false,"required":["submittedAt","enqueuedAt","startedAt","completedAt","persistedAt"],"description":"THE TIMESTAMPED LEDGER for one render attempt: submitted → enqueued → provider-started → completed → persisted. Each stamp is ISO or null (null = that stage was never reached, or the record predates the field).","properties":{"submittedAt":{"type":"string","nullable":true},"enqueuedAt":{"type":"string","nullable":true},"startedAt":{"type":"string","nullable":true},"completedAt":{"type":"string","nullable":true},"persistedAt":{"type":"string","nullable":true}}},"TrailerBeat":{"type":"object","required":["beatIndex","storyBeat","compositionKind","characterAction","stagingNotes","calls","stale"],"additionalProperties":true,"description":"One beat of the plan, and how it rendered.","properties":{"beatIndex":{"type":"integer","minimum":0},"label":{"type":"string","nullable":true},"storyBeat":{"type":"string","description":"What the beat delivers dramatically: the source its shots are compiled from."},"captionText":{"type":"string"},"compositionKind":{"type":"string","enum":["image","text","both"],"description":"image = footage only, text = on-screen text only, both = text over footage."},"shotType":{"type":"string","nullable":true,"description":"The beat's framing as the plan wrote it."},"characterAction":{"type":"string","description":"What the people in this beat physically do, in order. A beat held as one shot is filmed from it; a beat with a cut list is filmed from its shots' own characterAction instead. When it changes what the beat films, its takes go out of date. Never on-screen text — words on the screen belong in captionText. An empty string clears it. Empty when the plan states none."},"stagingNotes":{"type":"string","description":"Where the people and things in this beat are and how the picture is arranged. A beat held as one shot is filmed from it; a beat with a cut list is filmed from its shots instead. When it changes what the beat films, its takes go out of date. Never on-screen text. An empty string clears it. Empty when the plan states none."},"motionPrompt":{"type":"string","description":"The beat's camera-motion prompt (`trailer refine --motion-prompt`), which the render reads; '' when unset."},"transitionPrompt":{"type":"string","description":"The handoff out of this beat into the next (`trailer refine --transition-prompt`); '' when unset."},"transitionIn":{"type":"string","description":"The incoming side of the seam into this beat (`trailer refine --transition-in`); '' when unset."},"shots":{"type":"array","nullable":true,"items":{"$ref":"#/components/schemas/TrailerShot"},"description":"The beat's cut list, or null when the beat is one held shot."},"visibleCharacters":{"type":"array","items":{"type":"string"},"description":"The named characters in the picture (an identity reference each). Unnamed people are not listed."},"location":{"type":"string","nullable":true,"description":"Where the beat is set: a canonical location key (the room's key in the location plates read, e.g. INT_TAVERN), never a scene heading. A beat names where it is set in its `location`: a canonical location key, the place its location image is made of. A beat may travel: a shot set somewhere else names that place in its own `location` (a key, or a place in plain words), and the render follows it there."},"arcStage":{"type":"string","nullable":true,"enum":["setup","escalation","turn","closing_question",null],"description":"The stage of the intent's arc this unit serves (setup, escalation, turn, closing_question), or null when the plan names none — a blocking plan defect on a plan with an intent."},"advancesQuestion":{"type":"string","nullable":true,"description":"How this unit advances the intent's central question, or null when the plan says nothing — a blocking plan defect on a plan with an intent."},"whyItFollows":{"type":"string","nullable":true,"description":"Why this unit follows the one before it — the link a viewer sees or hears (a cause, an answer, a contrast, a match cut, a look, an object, a sound) and what it moves forward; never on screen. Null when the plan says nothing: after the first unit, the blocking plan defect `transition_unexplained`."},"montage":{"type":"boolean","description":"True when the beat is a rapid intercut run: brief shots cut tight across the people and places it names (its shots may run as short as 1 s)."},"clipUrl":{"type":"string","nullable":true,"description":"The beat's cut, assembled from its calls' selected takes."},"clipResolution":{"type":"string","nullable":true},"posterUrl":{"type":"string","nullable":true},"textClipUrl":{"type":"string","nullable":true},"audioUrl":{"type":"string","nullable":true},"stale":{"type":"boolean","description":"The plan changed since this beat rendered: render it again to bring it in line. Its takes stay playable."},"motionError":{"type":"string","nullable":true},"calls":{"type":"array","items":{"$ref":"#/components/schemas/TrailerCall"}},"lineDrops":{"type":"array","items":{"$ref":"#/components/schemas/TrailerLineDrop"},"description":"Each of the beat's spoken lines, in order (its shots' lines, then its own), and what cutting it out of the footage would do now (POST …/beats/{beatIndex}/drop-line with its `line`). Empty outside review and complete."},"droppedLines":{"type":"array","items":{"$ref":"#/components/schemas/TrailerDroppedLine"},"description":"The lines already cut out of this beat's footage, oldest first, with each trim's state."}}},"TrailerCall":{"type":"object","required":["callId","durationSeconds","audioMode","plateKey","cast","takes","selectedTake","rendering","retake"],"additionalProperties":true,"description":"One render call of a beat (at most 10s): what it renders, its takes and its selection.","properties":{"callId":{"type":"string"},"durationSeconds":{"type":"number"},"audioMode":{"type":"string","enum":["dialogue","ambient","silent"]},"plateKey":{"type":"string","nullable":true,"description":"The location plate the render opens on (the rung matched to shot 1's framing), or null for a text-only render: no named character on camera and no speech track, so no reference image is sent."},"cast":{"type":"array","items":{"type":"string"},"description":"Who is on camera (a body figure each)."},"blocking":{"type":"string"},"shots":{"type":"array","items":{"type":"object","additionalProperties":true,"properties":{"framing":{"type":"string"},"onCamera":{"type":"array","items":{"type":"string"}},"setup":{"type":"string"},"action":{"type":"string"},"startSeconds":{"type":"number"},"endSeconds":{"type":"number"},"lines":{"type":"array","items":{"type":"object","additionalProperties":true,"properties":{"lineRef":{"type":"integer"},"speaker":{"type":"string"},"text":{"type":"string"},"voice":{"type":"string"}}}}}}},"speechTrack":{"type":"object","nullable":true,"additionalProperties":false,"description":"The cast's recorded lines the selected take was sent with (dialogue calls).","properties":{"url":{"type":"string"},"seconds":{"type":"number"}}},"takes":{"type":"array","items":{"$ref":"#/components/schemas/TrailerTake"}},"selectedTake":{"type":"integer","nullable":true,"description":"The take the cut plays and the finish delivers. Null when the call has no accepted take yet: render it again."},"notices":{"type":"array","items":{"type":"object","additionalProperties":true,"properties":{"take":{"type":"integer"},"code":{"type":"string"},"message":{"type":"string","description":"Plain words for the writer. For a content refusal it is the action that can work: an IP refusal (`code: reference_render_provider_ip_refused`) names what to edit and the likely trigger."},"moderation":{"type":"boolean","description":"True for a content refusal: released, never retried."}}}},"rendering":{"type":"boolean","description":"A take of this call is in flight now."},"retake":{"type":"object","additionalProperties":false,"description":"A priced new take of this call (the render-clip op): its take number and credits.","properties":{"take":{"type":"integer"},"credits":{"type":"number"}}}}},"TrailerTake":{"type":"object","required":["take","status","atOurCost","selected","voiceMissLineRefs","ownReadingLineRefs","openingMiss","mouthMiss","duplicatePerson","duplicateNotChecked","mouthNotChecked","mouthUncertain","mouthFindings","checksPending","abandoned","identitySwaps"],"additionalProperties":true,"description":"One take of one call, with its checks.","properties":{"take":{"type":"integer","minimum":1},"status":{"type":"string","enum":["accepted","rejected","failed","pending"]},"videoUrl":{"type":"string","nullable":true},"atOurCost":{"type":"boolean","description":"True for the call's ONE automatic retake (a line not heard or said in another character's voice, a mouth that did not do what its line asks, or an opening off its framing): we paid for it, never the writer."},"selected":{"type":"boolean"},"selectedBy":{"type":"string","nullable":true,"enum":["default","writer",null]},"voiceMissLineRefs":{"type":"array","items":{"type":"integer"},"description":"Lines this take failed by voice: not heard where they belong, or said in a voice too like another speaking character's. A flag: the take is retaken once automatically, ranks below a take without one, and may still be selected by the writer."},"ownReadingLineRefs":{"type":"array","items":{"type":"integer"},"description":"Lines this take said in its own reading, heard and distinct from the other voices, and kept: never a flag. Every take plays its own sound under its own picture, so the lips follow what is heard; such a line does not sound like the cast card's voice."},"openingMiss":{"type":"boolean","description":"True when shot 1 opened off its planned framing."},"mouthMiss":{"type":"boolean","description":"True when the mouth check measured a mouth that did not do what its line asks, past the band around the limit, or the quiet-mouth check measured a face moving its lips as if speaking where no line is heard (`quiet_mouth_motion`): the speaker's mouth not following an on-camera line (or following it too late or early, or still moving after it), or lips moving under someone else's voice or a voice-over. `mouthFindings` names each criterion and its number. Where the speaker's mouth could not be measured (a face too small, not found or hidden — or a miss read on a face too small to trust), the take watcher's own look at the lips decides the line (unmeasurable → decided by the watcher, finding `watcher_lip_sync`): a mouth it saw not speak the line is a miss. A reading inside the band, or an unread speaker the watcher saw speak, is `mouthUncertain`, never a miss."},"mouthUncertain":{"type":"boolean","description":"True when the take's lip sync is UNCERTAIN: measured, but a criterion sat inside the band around its limit — r within 0.10 of 0.35, one frame too many open after the line, a listener's openings just past the limit — so the same file could read either side of it; or unmeasurable → decided by the watcher, which saw the speaker's mouth speak the line (`watcher_lip_sync`, uncertain). Never a pass and never a retake (automatic or priced): the take ranks below a measured pass and above a measured miss, and its `warning` names the criterion and its number. Also true when a face where no line is heard is TOO CLOSE TO CALL (`quietMouths`: read near its bar, or a pod reading of lips moving that the take watcher did not see too): its words are in `mouthFindings` (outcome `uncertain`), it is the writer's to retake (the call's `retake`), and it is never retaken automatically nor ranks the take down."},"mouthFindings":{"type":"array","description":"Every criterion the mouth check missed (`outcome: fail`) or was uncertain on (`outcome: uncertain`), misses first. Speaker: `correlation` (r, the least that passes), `lag` (ms the mouth trails the voice, negative when it leads; the most that passes), `trailing_motion` (the share of the frames after the line with the mouth open), `articulation` (the lips barely part under the voice: the mean openness of the voiced frames, 0 closed to 3 wide; the least that passes), `articulation_stretch` (voiced frames running on lips that never open; the most that passes). Still face: `still_open_run` (frames), `still_open_share`, `still_openings`. `watcher_lip_sync`: unmeasurable → decided by the watcher — a speaker whose mouth could not be measured (or whose miss was read on a face too small to trust) is decided by the take watcher's own look at the lips: `fail` when it saw the mouth not speak the line (a miss, retaken like any), `uncertain` when it saw it speak; `value` is the watcher's severity on the speaker (0 none, 1 minor, 2 major, 3 critical), `limit` 1. Empty on a pass and when the lips were not checked.","items":{"type":"object","additionalProperties":false,"required":["lineRef","name","role","criterion","outcome","value","limit","words"],"properties":{"lineRef":{"type":"integer"},"name":{"type":"string"},"role":{"type":"string","enum":["speaker","still"]},"criterion":{"type":"string","enum":["correlation","lag","trailing_motion","articulation","articulation_stretch","still_open_run","still_open_share","still_openings","watcher_lip_sync","quiet_mouth_motion"],"description":"`quiet_mouth_motion`: a face whose lips were MEASURED moving as if speaking where NO line is heard (the quiet-mouth check; `lineRef` 0, `startSeconds`–`endSeconds` say where in the take; `value` its lip movement or mouth motion, `limit` the bar it crossed)."},"outcome":{"type":"string","enum":["fail","uncertain"]},"value":{"type":"number"},"limit":{"type":"number"},"words":{"type":"string","description":"The sentence every surface shows, with the numbers."},"startSeconds":{"type":"number","description":"Where in the take it was seen, when narrower than its line (`articulation_stretch`, `quiet_mouth_motion`)."},"endSeconds":{"type":"number"}}}},"playthrough":{"type":"object","nullable":true,"additionalProperties":true,"description":"The per-line check that the take played the cast's recorded lines: `{ status: checked | unavailable | not_applicable, pass, lines[], reason }`."},"opening":{"type":"object","nullable":true,"additionalProperties":true,"description":"The opening-framing check: `{ status: judged | unavailable, pass, plannedFraming, observedFraming, detail, reason }`."},"quietMouths":{"type":"object","nullable":true,"additionalProperties":true,"description":"The QUIET-MOUTH check — every named face on camera where NO line is heard (a silent or ambient take throughout, a dialogue take between and around its lines), MEASURED, two ways (`measure`): `lips` — the render box found the face and outlined its lips (Apple Vision), and `energy` is how far the lips' openness swung across the stretch once its slow drift is out — a smile, a head turn, a push-in or the light read still (still under 0.06, speaking at 0.08 or more; `box` is where the face was); `motion` — no box answered, so the face was located and its lower face's motion that no head or camera move explains was measured against its upper face's (still under 0.13, speaking at 0.18 or more), and a `motion` reading of `speaking` counts only where the take watcher saw lips moving too (else `uncertain`, with why: a push-in, a flare or a smile reads the same). `{ status: checked | unmeasurable | unavailable | not_applicable, pass, uncertain, spans[{ shot, startSeconds, endSeconds, onCamera, faces[{ name, status: measured | unmeasurable, verdict: still | uncertain | speaking | null, energy, measure: lips | motion, box, exempt, faceHeightPx, startSeconds, endSeconds, pairs, pairsSetAside, words }], keepsSceneSound }], reason, version, clipSha256, cached }`. A face the box read in a shot with more than one person is named by where it sits (\"the face on the left\") unless it moved, when the face locator names it. Where the chapter keeps its own sound (`keepsSceneSound`: a crowd, singers), only the cast's named faces are held: a moving face none of the cast is recorded with `exempt: kept_scene_sound`, never a miss. A face `speaking` is a mouth miss (`mouthMiss`, a `quiet_mouth_motion` finding), but never retaken on its own: the same staging brings it back, so the take's decision offers a new take held `restage_needed` (restage the shot: one face, framed closer), unless the lips run on past the take's last line, which is trimmed free (`trim_overrun`). `uncertain` is shown (`mouthUncertain`, and a minor review finding marked `uncertain`) and is the writer's to retake, never retaken automatically and never a veto. Null when it never ran on this take."},"quietTrack":{"type":"string","nullable":true,"enum":["silence","room_tone"],"description":"The trailer's quiet track this take was sent as its voice track (a render with no spoken line, while the trailer sends one: get_trailer_quiet_track); null: none. Compare the quiet-mouth checks of takes with and without it."},"mouths":{"type":"object","nullable":true,"additionalProperties":true,"description":"The mouth check: `{ status: checked | unmeasurable | unavailable | not_applicable, pass, uncertain, lines[{ lineRef, speaker, voice, shot, status: measured | unmeasurable, pass, uncertain, reason, faces[{ name, role: speaker | still, status, pass, uncertain, findings[{ criterion, outcome, value, limit, words }], correlation, lagMs, openShareAfter, speechOnCameraSeconds, box, states }], locatorFrames, locatorRetried, decidedBy: mouth_reader | watcher | null, watcher: { basis: unread | small_face, verdict: rigid | speaking, finding, words } | null }], reason, version, clipSha256, cached }`. A line with `decidedBy: watcher` is unmeasurable → decided by the watcher: its speaker's mouth could not be measured (or its miss was read on a face shorter than the reader is trusted on alone), so the take watcher's judgment decided it — `rigid` is a miss, `speaking` is uncertain. The verdict is stored by the take file's content hash (`clipSha256`) and the check `version` (the code, its numbers, both judging rows and their models): the same file always returns the same verdict (`cached: true` on a re-check). The face locator reads frames across each line's speech (`locatorFrames`, in seconds) with each person's identity image, and once more, denser, when it finds the speaker in none (`locatorRetried`); in `states`, one character per tenth of a second, `.` is off camera. Each line's `measure` says how its mouths were read: `lips` (the render box's lip outlines, measured; no `locatorFrames`) or `reader` (the face locator and the mouth reader). Null when it never ran on this take."},"abandoned":{"type":"boolean","description":"True for A TAKE THAT NEVER FINISHED (RELEASED): the job holding its claim died (a restart, a redeploy) before anything was sent, so the claim was released, any credits reserved for it were released once, and the shot's next take was decided again. It is recorded `failed` (`failure.code` `take_never_finished`, `failure.message` says why), never selected, and never counts against the shot's automatic retake. Say \"a take that never finished (released)\"."},"checksPending":{"type":"boolean","description":"True while the take's CHECKS HAVE NOT COMPLETED — its QA, its mouth or duplicate-person check, or its watch (a redeploy can interrupt them; the worker runs them again): nothing proves the take is clean yet. It ranks below every take whose checks completed, so it is never selected by default over one, and its `warning` says so. Say \"checks pending\", never \"clean\"."},"identitySwaps":{"type":"array","description":"Every line the WRONG PERSON speaks: the mouth check found the line's `speaker` nowhere on camera across it, and `face` — another member of the call's cast, matched by their identity image — following it. A measured miss of its own kind (`mouthMiss` is true too): it earns the call's automatic retake, told whose face must say the line, and the take never plays over one the right person speaks. Empty when none.","items":{"type":"object","additionalProperties":false,"required":["lineRef","speaker","face"],"properties":{"lineRef":{"type":"integer"},"speaker":{"type":"string"},"face":{"type":"string"}}}},"mouthNotChecked":{"type":"boolean","description":"True when the take's lip sync was NOT CHECKED: the mouth check could not read an on-camera speaker's mouth (the face not found even after the locator's denser retry, too small or hidden) and no watch of the take could decide it instead, read no line, or could not run. A speaker the take watcher decided is a `mouthMiss` or `mouthUncertain`, not this. Never a pass: the take ranks below every take whose mouths were measured passing, and its `warning` says why. It earns no retake, automatic or priced: only a measured `mouthMiss` does."},"duplicatePerson":{"type":"boolean","description":"True when the duplicate-person check saw one person in the picture twice at once. It earns the call's one automatic retake, and the take ranks below every other take for the default selection."},"duplicateNotChecked":{"type":"boolean","description":"True when the take was NOT CHECKED for a person shown twice: the duplicate-person check could not judge it, even on its one retry (or never ran on it). Never a pass: the take ranks below every take whose check passed, and its `warning` says so. It earns no automatic retake."},"duplicates":{"type":"object","nullable":true,"additionalProperties":true,"description":"The duplicate-person check: `{ status: judged | unavailable, pass, duplicates[{ name, shot, seconds, detail }], reason }`. `unavailable` (after one retry) proves nothing and is never a pass: see `duplicateNotChecked`. Null when it never ran on this take."},"warning":{"type":"string","nullable":true,"description":"The sentence the finish quote and the export carry if this take is kept."},"failure":{"type":"object","nullable":true,"additionalProperties":true,"description":"Why the take produced nothing. `moderation: true` is a content refusal: released (nothing charged) and never retried. `code: reference_render_provider_ip_refused` is the provider refusing the shot as possibly someone else's intellectual property: the same shot is refused again, so the call's notice says what to edit (and the likely trigger: currency, a logo or brand, a named work) before it is rendered again.","properties":{"code":{"type":"string"},"message":{"type":"string"},"moderation":{"type":"boolean"}}},"billedSeconds":{"type":"number","nullable":true},"recordedAt":{"type":"string","format":"date-time"},"watch":{"allOf":[{"$ref":"#/components/schemas/TakeWatch"}],"nullable":true,"description":"What the take watcher saw on this take, with its sound; null until it watched."},"repair":{"allOf":[{"$ref":"#/components/schemas/TakeRepairDecision"}],"nullable":true,"description":"The remedy the repair engine decided for this take and the rule that fired; null until it decided."},"lineage":{"allOf":[{"$ref":"#/components/schemas/TakeLineage"}],"nullable":true,"description":"Set on a take a remedy made: the take it repairs (`aleph_repair`), trims (`trim`) or follows as a new take (`wan_retake`)."},"repairCost":{"allOf":[{"$ref":"#/components/schemas/TakeRepairCost"}],"nullable":true,"description":"Set on a repaired take: what the repair cost."},"repairQuote":{"allOf":[{"$ref":"#/components/schemas/TakeRepairQuoteLine"}],"nullable":true,"description":"What repairing this take in place would cost now, and whether it can be asked for (null for a take with no file)."},"trimBounds":{"allOf":[{"$ref":"#/components/schemas/TakeTrimBounds"}],"nullable":true,"description":"What a free trim of this take may keep (its length, where its spoken lines sit, the shortest it may become, the engine's own suggestion); null for a take with no file."},"trim":{"allOf":[{"$ref":"#/components/schemas/TakeTrimPlan"}],"nullable":true,"description":"Set on a trimmed take: the span of the take it came from that it keeps (its cut plays this length)."},"voiceOver":{"allOf":[{"$ref":"#/components/schemas/TakeVoiceOver"}],"nullable":true,"description":"Set on a take that plays on-camera lines of its call as a voice-over (the speaker seen from behind, the line heard over the picture)."},"voiceOverRestage":{"allOf":[{"$ref":"#/components/schemas/TakeVoiceOverRestage"}],"nullable":true,"description":"Whether staging this take's line as a voice-over in a new take (priced like a new take) can be asked for now, the lines it would stage and its price; null for a call that says nothing on camera."}}},"TrailerBeatRefineRequest":{"type":"object","required":["beatIndex"],"additionalProperties":false,"description":"Edit to one trailer beat's plan (provide at least one editable field). Free; an edit to what the beat films marks its takes stale, and rendering it again is the priced render-clip op.","properties":{"beatIndex":{"type":"integer","minimum":0,"description":"Zero-based index of the beat to edit."},"storyBeat":{"type":"string","minLength":1,"maxLength":1200,"description":"What the beat delivers dramatically: the source its shots are compiled from."},"transitionPrompt":{"type":"string","description":"The OUTGOING side of this beat's seam — how this beat hands off to the next one."},"transitionIn":{"type":"string","maxLength":400,"description":"The INCOMING side of this beat's seam — how this beat receives the handoff from the PREVIOUS beat. A seam is two independent fields on two beats (beat N's transitionPrompt, beat N+1's transitionIn) and nothing reconciles them, so both must be written to agree. Rejected, not truncated, above 400 characters."},"shotType":{"type":"string","maxLength":120,"description":"The beat's framing (a single-shot beat's only framing): shot 1's framing picks the plate rung the render opens on — wide, medium or close. Prefer a canonical token: establishing, wide, medium-wide, medium, two-shot, over-the-shoulder, insert, closeup, extreme-closeup. Rejected, not truncated, above 120 characters."},"motionPrompt":{"type":"string","description":"How the beat moves: what the figures do and what moves in the room."},"characterAction":{"type":"string","maxLength":1200,"description":"What the people in this beat physically do, in order. A beat held as one shot is filmed from it; a beat with a cut list is filmed from its shots' own characterAction instead. When it changes what the beat films, its takes go out of date. Never on-screen text — words on the screen belong in captionText. An empty string clears it."},"stagingNotes":{"type":"string","maxLength":1200,"description":"Where the people and things in this beat are and how the picture is arranged. A beat held as one shot is filmed from it; a beat with a cut list is filmed from its shots instead. When it changes what the beat films, its takes go out of date. Never on-screen text. An empty string clears it."},"captionText":{"type":"string","description":"The beat's on-screen text."},"compositionKind":{"type":"string","enum":["image","text","both"],"description":"What the beat is made of: footage only, on-screen text only, or text over footage."},"intendedDurationSeconds":{"type":"integer","minimum":4,"maximum":30,"description":"The beat's length in seconds (4–30). A beat renders as calls of at most 10s each."},"visibleCharacters":{"type":"array","maxItems":8,"items":{"type":"string","minLength":1,"maxLength":160},"description":"The NAMED characters in the picture (max 8), by exact project-canon name — each renders from their own identity reference. Unnamed people (a crowd, passers-by) are not listed and need none. Pass [] for a beat with no cast member on screen, e.g. a street or a vista."},"featuredCharacter":{"type":"string","nullable":true,"maxLength":160,"description":"The beat's lead character (null clears it)."},"location":{"type":"string","nullable":true,"maxLength":240,"description":"Where the beat is set, stored as a canonical location key (pass the key, or a room name, resolved to the room's stored key). A room with no plate yet is legal: the render makes it, priced as a plate line. Null clears it. A beat names where it is set in its `location`: a canonical location key, the place its location image is made of. A beat may travel: a shot set somewhere else names that place in its own `location` (a key, or a place in plain words), and the render follows it there."},"shots":{"type":"array","maxItems":8,"items":{"$ref":"#/components/schemas/TrailerShot"},"description":"THE BEAT'S CUT LIST — shots with HARD CUTS between them, performed inside the beat's render calls. Use it when a beat has to contain an EVENT rather than an image; leave it off for a beat that is one continuous shot. The cuts are FREE — cost is per rendered second, not per cut. A beat names where it is set in its `location`: a canonical location key, the place its location image is made of. A beat may travel: a shot set somewhere else names that place in its own `location` (a key, or a place in plain words), and the render follows it there. Pass [] to clear the list back to a single shot."},"arcStage":{"type":"string","enum":["setup","escalation","turn","closing_question"],"description":"The stage of the trailer's intent this beat serves: setup, escalation, turn or closing_question. The beats follow the arc in order, every stage is served, and the last beat is the closing_question — plan coverage refuses a plan that breaks it. Plan words: never makes a take stale."},"advancesQuestion":{"type":"string","minLength":1,"maxLength":400,"description":"One sentence: how this beat advances the intent's central question — what it raises, sharpens or makes costlier."},"whyItFollows":{"type":"string","maxLength":300,"description":"One line, never on screen: why this beat comes after the one before it — the link a viewer sees or hears and what it moves forward. Required on every beat after the first; \"\" clears it. Plan words: never makes a take stale."},"montage":{"type":"boolean","description":"true makes the beat a rapid intercut run — several brief shots (one to two seconds each) across the people and places its shots name, cut tight; its shots may run as short as 1 s. false makes it hold or travel. It changes how the beat compiles, so its takes go out of date."}}},"TrailerShot":{"type":"object","required":["shotType","storyBeat"],"additionalProperties":false,"description":"One shot inside one trailer beat's cut list. A beat names where it is set in its `location`: a canonical location key, the place its location image is made of. A beat may travel: a shot set somewhere else names that place in its own `location` (a key, or a place in plain words), and the render follows it there.","properties":{"shotType":{"type":"string","maxLength":240,"description":"THIS shot's framing — the point of a cut list is that the framing changes between entries. Shot 1's framing picks the plate rung the render opens on."},"storyBeat":{"type":"string","maxLength":600,"description":"What this shot delivers dramatically: the fragment of the build that happens here, not the beat's whole arc."},"cameraMove":{"type":"string","maxLength":300,"description":"What the camera does in THIS shot: holds, pushes in, travels, cranes, follows. Keep the move inside this shot: the next one starts on a hard cut."},"characterAction":{"type":"string","maxLength":600,"description":"What the figure physically does during this shot."},"visibleCharacters":{"type":"array","maxItems":3,"items":{"type":"string"},"description":"Exact project-canon cast names visible in THIS shot (max 3)."},"spokenLine":{"type":"string","maxLength":300,"description":"Exact words a VISIBLE character speaks during this shot."},"spokenLineCharacter":{"type":"string","maxLength":160,"description":"Who says `spokenLine` in this shot."},"location":{"type":"string","maxLength":160,"description":"Where THIS shot is, when it is not the beat's own place. A beat names where it is set in its `location`: a canonical location key, the place its location image is made of. A beat may travel: a shot set somewhere else names that place in its own `location` (a key, or a place in plain words), and the render follows it there."},"durationSeconds":{"type":"number","minimum":1,"maximum":20,"description":"How long this shot holds, in seconds (at least 2; as short as 1 in a montage beat). Optional: the fitter rescales written rhythms to the beat, and an all-unwritten list is split evenly."}}},"TrailerBeatPreview":{"type":"object","additionalProperties":true,"description":"The saved beat plus a shareable link to preview it.","properties":{"success":{"type":"boolean"},"needsRerender":{"type":"boolean","description":"True when the beat's rendered takes (or its text layer) are now out of date. They stay playable; render the beat again (priced) to bring it in line."},"planCoverage":{"type":"string","nullable":true,"enum":["queued","current","failed",null],"description":"On a draft plan the edit changed: its plan coverage — `queued` (one run per burst of edits, started once the plan has been left alone for three minutes; the plan approval starts it at once), `current` (this revision is already scored or being scored) or `failed` (it could not be queued; the approval says what is missing). Null outside draft, or when the edit changed nothing."},"previewUrl":{"type":"string","description":"Shareable link to watch this beat's current cut."}}},"VideoIntentArcEntry":{"type":"object","required":["stage","purpose","temperature","intensity"],"additionalProperties":false,"description":"One stage of the intent's arc: why it is there and the emotional temperature it should hit.","properties":{"stage":{"type":"string","enum":["setup","escalation","turn","closing_question"],"description":"Which stage: the arc is exactly setup, escalation, turn, closing_question, in that order."},"purpose":{"type":"string","minLength":1,"maxLength":400,"description":"What this stage does for a first-time viewer."},"temperature":{"type":"string","minLength":1,"maxLength":160,"description":"The emotional temperature, in words."},"intensity":{"type":"integer","minimum":1,"maximum":10,"description":"The same temperature on a 1–10 scale. It rises to the turn: the escalation runs hotter than the setup, and the turn is the peak of the first three (the closing question may fall quiet)."}}},"VideoStoryIntent":{"type":"object","required":["purpose","logline","theme","centralQuestion","stakes","viewerPromise","arc","ending"],"additionalProperties":false,"description":"What a deck or trailer is FOR. Every deck and trailer is planned from an explicit intent — what the video is for, its logline, theme, central question, stakes, what a first-time viewer is promised, a four-stage arc (setup, escalation, turn, closing question) and how the ending lands the question. Every chapter or beat names the arc stage it serves (`arcStage`), how it advances the central question (`advancesQuestion`) and, after the first, why it follows the one before it (`whyItFollows`: the visible link — a cause, an answer, a contrast, a match cut, a look, an object, a sound — and what it moves forward; one line, never shown on screen). Plan coverage first writes what a first-time viewer would take away from the plan without seeing the intent (`viewerTakeaway`, with how much of the story the words recited rather than let them work out: `toldVersusShown`, and where they lost the thread: `lostTheThread`), then compares it with the intent: intent clarity, the central question, the buildup and whether the theme is carried each fail the plan on their own, and so do subtext — a plan that recites its plot instead of posing its question — and transitions — a part that follows the one before it for no reason a first-time viewer can feel (`weakTransitions` names the weakest, each with its fix); voices (do the characters speak, rather than the narrator carrying everything) is scored beside them.","properties":{"purpose":{"type":"string","minLength":1,"maxLength":400,"description":"What this video is for: a pitch sells the series, a showcase shows the world AS the story, a trailer hooks the episode."},"audience":{"type":"string","maxLength":400,"description":"Who it is for (a pitch's buyer, a trailer's audience). Optional."},"logline":{"type":"string","minLength":1,"maxLength":400,"description":"What this show is about, in one sentence."},"theme":{"type":"string","minLength":1,"maxLength":400,"description":"What it is really about underneath."},"centralQuestion":{"type":"string","minLength":1,"maxLength":300,"description":"The dramatic question the video poses and leaves open."},"stakes":{"type":"string","minLength":1,"maxLength":600,"description":"What is at risk, and for whom."},"viewerPromise":{"type":"string","minLength":1,"maxLength":600,"description":"What a first-time viewer should feel and want by the end."},"arc":{"type":"array","minItems":4,"maxItems":4,"items":{"$ref":"#/components/schemas/VideoIntentArcEntry"},"description":"The four stages, in order: setup, escalation, turn, closing_question."},"ending":{"type":"string","minLength":1,"maxLength":600,"description":"How the final moment lands the central question — posed, sharpened, and left open."}}},"VideoViewerTakeaway":{"type":"object","required":["aboutTheShow","centralQuestion","whereItBuilds","toldVersusShown"],"additionalProperties":false,"description":"What a first-time viewer takes away from the plan, written by plan coverage BEFORE it was shown the intent — the read the story checks compare with the intent.","properties":{"aboutTheShow":{"type":"string","description":"What the show is about, as a stranger would say it after one viewing."},"centralQuestion":{"type":"string","description":"The question the stranger is left holding, or a plain statement that none is posed."},"whereItBuilds":{"type":"string","description":"Where the cut builds: where tension rises, peaks, and plateaus or falls back."},"toldVersusShown":{"type":"object","required":["verdict","told","shown"],"additionalProperties":false,"description":"How much of the story the stranger was TOLD in words rather than SHOWN — what `subtext` is judged against.","properties":{"verdict":{"type":"string","enum":["mostly_told","balanced","mostly_shown"],"description":"`mostly_told` (the words recited the story), `balanced`, or `mostly_shown` (worked out from images, the characters' own lines and juxtaposition)."},"told":{"type":"string","description":"The story facts the cut put into words that the pictures or the characters could have carried, quoted."},"shown":{"type":"string","description":"What the stranger worked out for themselves."}}},"lostTheThread":{"type":"string","description":"Each cut where the stranger could not tell why a part came after the one before it, by name, or \"I never lost it\" — what `transitions` is judged against. Empty on a read written before it was asked for."}}},"VideoIntentArcProblem":{"type":"object","required":["code","units","detail"],"additionalProperties":false,"description":"Where the plan breaks its intent's arc. Each is a blocking plan defect: fix it with the chapter or beat refine (`arcStage`, `advancesQuestion`, `whyItFollows`), free.","properties":{"code":{"type":"string","enum":["arc_stage_missing","question_not_advanced","transition_unexplained","arc_out_of_order","arc_stage_unserved","ending_off_question"]},"units":{"type":"array","items":{"type":"integer","minimum":1},"description":"The 1-based places in the cut the problem is about (empty for a whole-plan problem)."},"stage":{"type":"string","enum":["setup","escalation","turn","closing_question"],"description":"The arc stage it is about, when it is about one."},"detail":{"type":"string","description":"The problem and its fix, in one sentence."}}},"VideoIntent":{"type":"object","required":["planKind","jobId","deckMode","status","editable","intent","units","arcProblems","coverage"],"additionalProperties":false,"description":"A deck's or trailer's intent beside what its plan says. Every deck and trailer is planned from an explicit intent — what the video is for, its logline, theme, central question, stakes, what a first-time viewer is promised, a four-stage arc (setup, escalation, turn, closing question) and how the ending lands the question. Every chapter or beat names the arc stage it serves (`arcStage`), how it advances the central question (`advancesQuestion`) and, after the first, why it follows the one before it (`whyItFollows`: the visible link — a cause, an answer, a contrast, a match cut, a look, an object, a sound — and what it moves forward; one line, never shown on screen). Plan coverage first writes what a first-time viewer would take away from the plan without seeing the intent (`viewerTakeaway`, with how much of the story the words recited rather than let them work out: `toldVersusShown`, and where they lost the thread: `lostTheThread`), then compares it with the intent: intent clarity, the central question, the buildup and whether the theme is carried each fail the plan on their own, and so do subtext — a plan that recites its plot instead of posing its question — and transitions — a part that follows the one before it for no reason a first-time viewer can feel (`weakTransitions` names the weakest, each with its fix); voices (do the characters speak, rather than the narrator carrying everything) is scored beside them.","properties":{"planKind":{"type":"string","enum":["pitch_deck","trailer"]},"jobId":{"type":"string"},"deckMode":{"type":"string","nullable":true,"enum":["pitch","showcase",null],"description":"A deck's mode (`pitch` or `showcase`); null for a trailer."},"status":{"type":"string"},"editable":{"type":"boolean","description":"Whether the intent can be edited now: only while the plan is a draft (approving the plan approves its intent)."},"intent":{"allOf":[{"$ref":"#/components/schemas/VideoStoryIntent"}],"nullable":true,"description":"The plan's intent, or null when it was planned before intents existed — an edit with a complete intent sets one."},"units":{"type":"array","description":"Each chapter (deck) or beat (trailer), in cut order, with the arc stage it serves, how it advances the central question and why it follows the one before it.","items":{"type":"object","required":["key","number","title","arcStage","advancesQuestion","whyItFollows"],"additionalProperties":false,"properties":{"key":{"type":"integer","minimum":0,"description":"The unit's key: a deck chapter's `chapterIndex`, a trailer beat's `beatIndex` — what the refine takes."},"number":{"type":"integer","minimum":1,"description":"Its place in the cut, from 1."},"title":{"type":"string"},"arcStage":{"type":"string","nullable":true,"enum":["setup","escalation","turn","closing_question",null]},"advancesQuestion":{"type":"string","nullable":true},"whyItFollows":{"type":"string","nullable":true,"description":"Why it follows the unit before it (the visible link, and what it moves forward); null when the plan says none."}}}},"arcProblems":{"type":"array","items":{"$ref":"#/components/schemas/VideoIntentArcProblem"},"description":"Where the units break the intent's arc (empty when they follow it, or when the plan has no intent)."},"coverage":{"type":"object","required":["status","current","gatePassed","gateThreshold","viewerTakeaway","opening","storyVetoes","weakTransitions"],"additionalProperties":false,"description":"What plan coverage says about the plan against its intent.","properties":{"status":{"type":"string","enum":["missing","queued","generating","complete","failed"]},"current":{"type":"boolean","description":"The report graded the plan as it stands now."},"gatePassed":{"type":"boolean"},"gateThreshold":{"type":"number","minimum":0,"maximum":10},"viewerTakeaway":{"allOf":[{"$ref":"#/components/schemas/VideoViewerTakeaway"}],"nullable":true,"description":"Null until a current report exists."},"opening":{"allOf":[{"$ref":"#/components/schemas/StoryboardCoverageOpening"}],"nullable":true,"description":"The judge's note on the deck's opening title sequence. Null until a current report writes one."},"storyVetoes":{"type":"array","description":"The story checks: the vetoes (intentClarity, centralQuestion, buildup, themeCarried, subtext, transitions), each of which fails the plan on its own in every mode, then voices (do the characters speak), scored and not a veto.","items":{"type":"object","required":["dimension","label","veto","score","finding","passed"],"additionalProperties":false,"properties":{"dimension":{"type":"string","enum":["intentClarity","centralQuestion","buildup","themeCarried","subtext","transitions","voices"]},"label":{"type":"string"},"veto":{"type":"boolean","description":"Whether a score under the gate fails the plan on its own (every check but voices)."},"score":{"type":"number","nullable":true,"minimum":0,"maximum":10},"finding":{"type":"string","nullable":true},"passed":{"type":"boolean","nullable":true,"description":"Whether it clears the gate on its own; null while no current report exists."}}}},"weakTransitions":{"type":"array","description":"The weakest cuts plan coverage named, worst first, each with its fix (empty until a current report names one).","items":{"type":"object","required":["toKey","toNumber","fromKey","fromNumber","problem","fix"],"additionalProperties":false,"properties":{"toKey":{"type":"integer","minimum":0,"description":"The key of the unit the cut goes into — what the refine takes."},"toNumber":{"type":"integer","minimum":1},"fromKey":{"type":"integer","minimum":0},"fromNumber":{"type":"integer","minimum":1},"problem":{"type":"string","description":"Why a first-time viewer would not know why this part follows the one before it."},"fix":{"type":"string","description":"The link to give it (a look, an object, a sound, a cause, a contrast) or the reorder."}}}}}}}},"VideoIntentEditRequest":{"type":"object","additionalProperties":false,"description":"An edit to a draft's intent: each field given replaces the stored one (`arc` replaces the whole arc; an `audience` of \"\" clears it), and the merged intent must be complete and build. Editing the intent is free and only while the plan is a draft (approving the plan approves its intent): it saves the new intent and queues plan coverage again for the latest revision — a burst of edits is one coverage run, started once the plan has been left alone for three minutes. The chapters or beats are not re-planned; edit them (their `arcStage`, `advancesQuestion` and `whyItFollows` included) to follow the new intent.","properties":{"purpose":{"type":"string","minLength":1,"maxLength":400},"audience":{"type":"string","maxLength":400},"logline":{"type":"string","minLength":1,"maxLength":400},"theme":{"type":"string","minLength":1,"maxLength":400},"centralQuestion":{"type":"string","minLength":1,"maxLength":300},"stakes":{"type":"string","minLength":1,"maxLength":600},"viewerPromise":{"type":"string","minLength":1,"maxLength":600},"ending":{"type":"string","minLength":1,"maxLength":600},"arc":{"type":"array","minItems":4,"maxItems":4,"items":{"$ref":"#/components/schemas/VideoIntentArcEntry"}}}},"VideoIntentEditAck":{"type":"object","required":["success","planKind","jobId","intent","planCoverage","arcProblems"],"additionalProperties":false,"description":"The saved intent. Nothing was re-planned, rendered or charged.","properties":{"success":{"type":"boolean"},"planKind":{"type":"string","enum":["pitch_deck","trailer"]},"jobId":{"type":"string"},"intent":{"$ref":"#/components/schemas/VideoStoryIntent"},"planCoverage":{"type":"string","nullable":true,"enum":["queued","current","failed",null],"description":"The changed plan's coverage: `queued` for the latest edit (one run per burst of edits, started once the plan has been left alone for three minutes), `current` (already scored or being scored), `failed` (could not be queued). Null when the edit changed nothing."},"arcProblems":{"type":"array","items":{"$ref":"#/components/schemas/VideoIntentArcProblem"},"description":"Where the units now break the new intent's arc: fix them with the chapter or beat refine."}}},"TrailerPlanApproveRequest":{"type":"object","additionalProperties":false,"description":"Say whether this call is the price or the purchase.","properties":{"confirmed":{"type":"boolean","description":"Omit (or false) to get the 480p render quote WITHOUT spending anything — the beats are compiled and voiced (free to the writer), nothing is reserved. Send true only after the writer has seen that price and said go: it reserves exactly the quoted calls and starts the render."},"acknowledgePlanCoverage":{"$ref":"#/components/schemas/PlanCoverageAcknowledgement"}}},"TrailerRenderClipRequest":{"type":"object","required":["beatIndex"],"additionalProperties":false,"description":"Render one beat again (the writer's priced retake), and say whether this call is the price or the purchase.","properties":{"beatIndex":{"type":"integer","minimum":0,"description":"Zero-based index of the beat to render."},"callId":{"type":"string","description":"One of the beat's calls (from the beats read); omit to render a new take of every call of the beat. A beat whose plan changed is recompiled first, and a call id is then refused — render the whole beat."},"confirmed":{"type":"boolean","description":"Omit (or false) to get the price WITHOUT spending anything. Send true only after the writer has seen that price and said go."}}},"TrailerRenderAnswer":{"type":"object","required":["quotedOnly","jobId","credits"],"additionalProperties":true,"description":"Either the PRICE of a render (plan approval, or a beat rendered again) or the queued render itself — branch on `quotedOnly`. Without `confirmed` nothing is reserved; with `confirmed: true` exactly the quoted lines are held and the calls are queued.","properties":{"quotedOnly":{"type":"boolean","description":"True when this was a price and NOTHING was spent; false when the render was reserved and queued."},"queued":{"type":"boolean","description":"Confirmed call only."},"jobId":{"type":"string"},"credits":{"type":"number","description":"What a confirmed call reserves (quote), or what it reserved (confirmed)."},"costLabel":{"type":"string","nullable":true,"description":"What the whole quote buys, in one label — the shots, the beats they span, their seconds at 480p and the location plates made first, e.g. \"Trailer render: 7 shots across 5 beats (58s at 480p)\" (quote only)."},"lines":{"type":"array","description":"Quote only. Every line a confirmed call would reserve: one per call at its voiced seconds, one per location plate the renders make first, and — a plan approval — the automatic-fix allowance (kind `allowance`).","items":{"type":"object","additionalProperties":true,"properties":{"kind":{"type":"string","enum":["call","plate","allowance"],"description":"`allowance`: a plan approval's automatic-fix allowance (see `autoFixAllowance`)."},"idempotencyKey":{"type":"string"},"label":{"type":"string"},"credits":{"type":"number"},"callId":{"type":"string","nullable":true},"plateKey":{"type":"string","nullable":true},"seconds":{"type":"number","nullable":true}}}},"calls":{"description":"Quote: the priced calls `[{ beatIndex, callId, take, seconds, audioMode, credits }]`. Confirmed: how many calls were queued.","oneOf":[{"type":"array","items":{"type":"object","additionalProperties":true,"properties":{"beatIndex":{"type":"integer"},"callId":{"type":"string"},"take":{"type":"integer"},"seconds":{"type":"number"},"audioMode":{"type":"string","enum":["dialogue","ambient","silent"]},"credits":{"type":"number"}}}},{"type":"integer","minimum":0}]},"warnings":{"type":"array","items":{"type":"string"},"description":"Quote only. Compile warnings worth showing (advisory; nothing blocks on them)."},"balanceAfter":{"type":"integer","description":"Quote only. Studio Credits the writer would have left once this is spent: the balance available now (as Billing shows it) minus the price. Negative means they are that many short."},"sufficient":{"type":"boolean","description":"Quote only. True when the available balance covers the price. False means the writer must top up first (the note names the Billing link) — do not confirm; the reserve would refuse."},"note":{"type":"string","description":"The price in the writer's own words, what it would leave (or how short they are, with the top-up link), and how to proceed (quote only)."},"message":{"type":"string","description":"Confirmed call only: what was queued."},"previewUrl":{"type":"string","description":"A beat render only: a shareable link to watch the beat's current cut."},"autoFixAllowance":{"type":"object","nullable":true,"additionalProperties":false,"required":["credits","note"],"description":"A plan approval's quote only: the AUTOMATIC-FIX ALLOWANCE, part of the one approval and included in `credits`. A ceiling the engine draws on to retake the shots the review of the 480p preview flags (and render flagged on-screen text again), reserved with the render; only what the fixes draw is charged and the rest comes back when the loop stops. 0 when the balance covers only the render (the preview is still reviewed). The 720p finish is a separate approval.","properties":{"credits":{"type":"integer","minimum":0},"note":{"type":"string","description":"The allowance in the writer's words."}}},"planCoverageOverride":{"allOf":[{"$ref":"#/components/schemas/PlanCoverageOverride"}],"nullable":true,"description":"A plan approval over a failed plan gate (`acknowledgePlanCoverage`): what the price would override, or — confirmed — the override as recorded on the report (null when the record could not be written; the render still runs)."}}},"TakeDefect":{"type":"object","required":["kind","source","severity","startSeconds","endSeconds","description","removableInPlace"],"additionalProperties":true,"description":"One thing wrong with a take: what the take watcher saw, or what a check measured failing.","properties":{"kind":{"type":"string","enum":["anachronism","signage","stray_person","duplicate_person","crowd_unison","lighting","camera_path","speed_ramp","timing","invented_cut","identity","identity_swap","lip_sync","line_voice","other"]},"source":{"type":"string","enum":["watcher","playthrough","mouths","duplicates","opening","dissolves"],"description":"`watcher`: the take watcher saw it; the others are the checks that measured it."},"severity":{"type":"string","enum":["minor","major","critical"],"description":"`minor` is shown and never acted on; `major` and `critical` earn a remedy."},"startSeconds":{"type":"number","minimum":0,"description":"Seconds from the start of the take."},"endSeconds":{"type":"number","minimum":0},"description":{"type":"string"},"removableInPlace":{"type":"boolean","description":"True when it can be removed or replaced in place without a new take."},"replaceWith":{"type":"string","nullable":true,"description":"For a removable defect: what should be there instead, in plain words (never a word to write)."}}},"TakeWatch":{"type":"object","required":["status","defects","reason","watchedAt"],"additionalProperties":true,"description":"The take watcher's pass over one take. `unavailable`: the watch could not finish (after one retry with more room), so it proves nothing either way and the take is UNWATCHED — its decision reads `unwatched`, never clean, and `decision: rewatch` watches it again, free; `skipped` was not watched on purpose (the checks alone then decide).","properties":{"status":{"type":"string","enum":["watched","unavailable","skipped"]},"defects":{"type":"array","items":{"$ref":"#/components/schemas/TakeDefect"}},"reason":{"type":"string","nullable":true},"watchedAt":{"type":"string","format":"date-time"}}},"TakeRepairDecision":{"type":"object","required":["remedy","rule","reason","defects","automatic","heldBy","outcome","resultTake","outcomeMessage","decidedAt"],"additionalProperties":true,"description":"What the repair engine decided for one take, and why. Every rendered take is watched with its sound after its checks. A cut nobody asked for at the start or end of an otherwise good take is trimmed off first, before any new take: free, frame for frame, only where the cut is measured in the file, outside every spoken line (each kept whole with a breath after it), and only when what is left keeps its chapter or beat within a quarter of its planned length. When the shot's automatic new take is spent and the take it plays still has such a cut outside its line, that end is trimmed as a last resort: past the quarter, as long as the line stays whole, at least 2 seconds are left and the chapter or beat keeps half its planned length. A cut inside a spoken line is never trimmed. A speaker whose lips keep moving past the take's last line, where nothing is heard, is trimmed half a second after the line, free and frame for frame. Lips moving where no line is heard are never retaken on their own: the same staging brings them back, so the shot is offered for restaging (one face, framed closer), a trim, or your call. Any other defect is remedied by rule during processing, at our cost: first a new take told what to avoid; a repair in place when the same problem comes back after a new take, or when the take passed every other check and only something removable (a period anachronism, garbled lettering, a stray or doubled person) or the light is wrong. Camera movement and its speed (a move that rushes, or dies into a hold at the end), timing, cuts and a crowd moving as one (extras in step, everyone turning or looking down at once, repeated faces) are only ever fixed by a new take, or by the free trim when they sit only at an end of the take; spoken shots are not repaired in place. A line the speaker's mouth did not speak on two takes is no longer retaken on camera: the next take stages it as a voice-over, the speaker seen from behind, the line heard over the picture. Every take plays its own sound under its own picture, so the lips follow what is heard: a line said in the take's own reading is kept, and only a line not heard, or said in a voice too like another character's, is a defect (a new take). Where a speaker's mouth cannot be measured (or the face is too small to trust a miss), the watcher's own judgment of the lips decides the line: a mouth it saw not speak is a miss, one it saw speak is uncertain. Each shot gets at most one automatic new take, one automatic repair in place and one automatic voice-over re-stage, so a new take whose removable defect came back is repaired in the same pass; a repaired or trimmed take is never remedied again on its own. A repair that fails on the repair service's own side (never one declined at its content check) and cost nothing is sent again once; if that fails too, a new take instead. A repaired take keeps the take's own sound, is judged again, and is kept as a new take linked to the one it repairs; it plays only when it passes. A trimmed take plays over the take it came from when it passes, or when it shows only part of what that take showed, with the same speech and faces: it is judged by time window, so anything seen inside the span it keeps counts as that take's own footage too, and it wins when the trim cut something off.","properties":{"remedy":{"type":"string","enum":["none","wan_retake","aleph_repair","trim","stage_as_voice_over"],"description":"`none`; `trim`: a cut nobody asked for at the start or end of the take is cut off (free, frame for frame, never into a spoken line; see `trim`); `wan_retake`: a new take, told what to avoid; `stage_as_voice_over`: a new take that plays a line whose speaker's lips failed it on two takes as a voice-over (see `voiceOver`); `aleph_repair`: a repair in place."},"rule":{"type":"string","enum":["not_judged","clean","writer_kept","first_remedy_is_a_new_take","recurred_after_new_take","clean_but_removable","needs_a_new_render","spoken_shot_held_from_repair","repair_refused_before","repair_unavailable","writer_requested","trim_edge","writer_trimmed","repair_failed_at_host","line_revoiced_twice","mouth_failed_twice","writer_staged_voice_over","trim_last_resort","trim_overrun","unwatched"],"description":"The rule that fired. `unwatched`: the take's watch could not finish, so nothing is decided for it (never `clean`, never a remedy on its own); `defects` lists what its other checks measured, and `take-repair-override` with `decision: rewatch` watches it again, free."},"reason":{"type":"string","description":"The rule in plain words, with what was found."},"defects":{"type":"array","items":{"$ref":"#/components/schemas/TakeDefect"}},"automatic":{"type":"boolean","description":"True when the engine applied (or applies) it on its own, at our cost."},"heldBy":{"type":"string","nullable":true,"enum":["one_remedy_per_pass","new_take_budget_spent","repair_budget_spent","spend_gate","next_take_is_the_writers","writer_overrode","failed_repair_billed","voice_over_budget_spent","restage_needed",null],"description":"Why it was not applied automatically: it is offered to the writer instead."},"outcome":{"type":"string","enum":["applied","offered","failed","refused","pending","none"],"description":"`refused` only when the repair was declined at the content check (never sent again unchanged)."},"resultTake":{"type":"integer","nullable":true,"description":"The take the remedy made, once it exists."},"outcomeMessage":{"type":"string","nullable":true},"decidedAt":{"type":"string","format":"date-time"},"trim":{"allOf":[{"$ref":"#/components/schemas/TakeTrimPlan"}],"nullable":true,"description":"Remedy `trim` only: the span kept, and the cut or dissolve measured at each end it cuts off."},"voiceOver":{"allOf":[{"$ref":"#/components/schemas/TakeVoiceOver"}],"nullable":true,"description":"Remedy `stage_as_voice_over` only: the lines the new take plays as a voice-over."}}},"TakeVoiceOver":{"type":"object","required":["lineRefs","speakers"],"additionalProperties":true,"description":"Lines a take plays as a VOICE-OVER that its call says on camera: the speaker seen from behind, the line heard over the picture.","properties":{"lineRefs":{"type":"array","items":{"type":"integer"}},"speakers":{"type":"array","items":{"type":"string"}}}},"TakeVoiceOverRestage":{"type":"object","required":["available","reason","lineRefs","credits"],"additionalProperties":true,"description":"What a writer's voice-over re-stage of this take would stage — the call's on-camera lines whose speaker's lips failed them on a take of it, else every line it says on camera — and its price (a new take of the call).","properties":{"available":{"type":"boolean"},"reason":{"type":"string","nullable":true,"description":"Why it cannot be asked for right now."},"lineRefs":{"type":"array","items":{"type":"integer"}},"credits":{"type":"integer","nullable":true}}},"TakeLineage":{"type":"object","required":["repairedFrom","remedy","rule","automatic"],"additionalProperties":true,"description":"Where a take came from: the take it remedies, and how.","properties":{"repairedFrom":{"type":"integer","minimum":1},"remedy":{"type":"string","enum":["wan_retake","aleph_repair","trim","stage_as_voice_over"],"description":"`trim`: a trim of the take (free); `wan_retake`: a new take after it; `stage_as_voice_over`: a new take after it that plays its line as a voice-over; `aleph_repair`: a repair of it in place."},"rule":{"type":"string","enum":["not_judged","clean","writer_kept","first_remedy_is_a_new_take","recurred_after_new_take","clean_but_removable","needs_a_new_render","spoken_shot_held_from_repair","repair_refused_before","repair_unavailable","writer_requested","trim_edge","writer_trimmed","repair_failed_at_host","line_revoiced_twice","mouth_failed_twice","writer_staged_voice_over","trim_last_resort","trim_overrun","unwatched"]},"automatic":{"type":"boolean","description":"True for the engine's automatic remedy (at our cost)."}}},"TakeRepairCost":{"type":"object","required":["hostCredits","estimatedHostCredits","usd","billedSeconds"],"additionalProperties":true,"description":"What a repair cost on our ledger: the repair host's own billed units and their dollars.","properties":{"hostCredits":{"type":"number","nullable":true},"estimatedHostCredits":{"type":"number","nullable":true},"usd":{"type":"number","nullable":true},"billedSeconds":{"type":"number"}}},"TakeRepairQuoteLine":{"type":"object","required":["credits","seconds","available","reason"],"additionalProperties":true,"description":"What a writer-requested repair of this take would cost now: the price table's TAKE_REPAIR line (4 credits a second, a two-second minimum). `credits` is null when the take cannot be repaired in place (`reason` says why).","properties":{"credits":{"type":"integer","nullable":true},"seconds":{"type":"number","description":"The seconds a repair sends: the whole take, or only the shots its defect sits in."},"available":{"type":"boolean"},"reason":{"type":"string","nullable":true,"description":"Why it cannot be asked for right now."}}},"TakeRepairRequest":{"type":"object","required":["callId","take"],"additionalProperties":false,"description":"Repair one take of one call in place. TWO CALLS: without `confirmed` it is priced; with `confirmed: true` it is reserved and queued.","properties":{"callId":{"type":"string","description":"One of this unit's call ids (from the beats or chapters read)."},"take":{"type":"integer","minimum":1,"description":"The take to repair: an accepted take of the call."},"confirmed":{"type":"boolean","description":"Send true only once the writer has seen the price and said go."}}},"TakeRepairAnswer":{"type":"object","required":["quotedOnly","jobId","callId","take","credits"],"additionalProperties":true,"description":"The repair's price (`quotedOnly: true`; nothing reserved) or the queued repair (`quotedOnly: false`, `repairTake`).","properties":{"quotedOnly":{"type":"boolean"},"jobId":{"type":"string"},"callId":{"type":"string"},"take":{"type":"integer","minimum":1},"repairTake":{"type":"integer","minimum":1,"description":"Confirmed call only: the take number the repair is recorded as."},"seconds":{"type":"number","description":"Quote only: the seconds sent."},"credits":{"type":"integer"},"costLabel":{"type":"string","nullable":true},"summary":{"type":"string","description":"Quote only: what the repair will do, in plain words."},"note":{"type":"string","description":"Quote only: the price in the writer's words and how to proceed."},"balanceAfter":{"type":"integer","description":"Quote only. Studio Credits the writer would have left once this is spent: the balance available now (as Billing shows it) minus the price. Negative means they are that many short."},"sufficient":{"type":"boolean","description":"Quote only. True when the available balance covers the price. False means the writer must top up first (the note names the Billing link) — do not confirm; the reserve would refuse."},"queued":{"type":"boolean"},"message":{"type":"string"}}},"StageVoiceOverRequest":{"type":"object","required":["callId","take"],"additionalProperties":false,"description":"Stage a take's line as a voice-over in a new take of its call. TWO CALLS: without `confirmed` it is priced; with `confirmed: true` it is reserved and queued.","properties":{"callId":{"type":"string","description":"One of this unit's call ids (from the beats or chapters read)."},"take":{"type":"integer","minimum":1,"description":"The take whose line is staged: an accepted take of the call."},"confirmed":{"type":"boolean","description":"Send true only once the writer has seen the price and said go."}}},"StageVoiceOverAnswer":{"type":"object","required":["quotedOnly","jobId","callId","take","voiceOverTake","lineRefs","credits"],"additionalProperties":true,"description":"The new take's price (`quotedOnly: true`; nothing reserved) or the queued voice-over take (`quotedOnly: false`).","properties":{"quotedOnly":{"type":"boolean"},"jobId":{"type":"string"},"callId":{"type":"string"},"take":{"type":"integer","minimum":1},"voiceOverTake":{"type":"integer","minimum":1,"description":"The take number the voice-over is (or will be) recorded as."},"lineRefs":{"type":"array","items":{"type":"integer"},"description":"The lines staged as a voice-over."},"seconds":{"type":"number","description":"Quote only: the new take's seconds."},"credits":{"type":"integer"},"costLabel":{"type":"string","nullable":true},"summary":{"type":"string","description":"Quote only: what the new take will do, in plain words."},"note":{"type":"string","description":"Quote only: the price in the writer's words and how to proceed."},"balanceAfter":{"type":"integer","description":"Quote only. Studio Credits the writer would have left once this is spent: the balance available now (as Billing shows it) minus the price. Negative means they are that many short."},"sufficient":{"type":"boolean","description":"Quote only. True when the available balance covers the price. False means the writer must top up first (the note names the Billing link) — do not confirm; the reserve would refuse."},"queued":{"type":"boolean"},"message":{"type":"string"}}},"TakeTrimEdge":{"type":"object","required":["edge","transition","transitionStartSeconds","transitionEndSeconds"],"additionalProperties":true,"description":"One end of a take a trim cuts off, and the transition measured there.","properties":{"edge":{"type":"string","enum":["head","tail"]},"transition":{"type":"string","enum":["cut","dissolve","settle","writer"],"description":"`cut`: a hard cut (one frame, or a few the render smeared it over); `dissolve`: a blend over several frames, cut at its first blended frame; `settle`: the camera's move dying into a hold at the end, cut at the hold's first frame (nothing is slowed or padded); `writer`: the writer's own span (nothing measured)."},"transitionStartSeconds":{"type":"number"},"transitionEndSeconds":{"type":"number"}}},"TakeTrimPlan":{"type":"object","required":["keepStartSeconds","keepEndSeconds","keptSeconds","takeSeconds","edges"],"additionalProperties":true,"description":"A trim: the span of the take that is kept (frame-exact) and what was cut off at each end.","properties":{"keepStartSeconds":{"type":"number"},"keepEndSeconds":{"type":"number"},"keptSeconds":{"type":"number","description":"The kept span's length: what the trimmed take plays in its cut."},"takeSeconds":{"type":"number","description":"The take's length before the trim."},"edges":{"type":"array","items":{"$ref":"#/components/schemas/TakeTrimEdge"}},"basis":{"type":"string","enum":["outside_the_line","dying_move","last_resort_after_retakes","speaker_overrun"],"nullable":true,"description":"Why the engine trimmed it, recorded with the take's checks: `outside_the_line` (\"invented edge cut trimmed: outside the line\": the free trim before any new take, the invented edge cut lying outside every spoken line and the chapter or beat kept within a quarter of its plan), `dying_move` (\"camera move dying into a hold trimmed: outside the line\": the same free trim of a camera move that dies into a hold at the end, cut at the hold's first frame, never slowed or padded) or `last_resort_after_retakes` (\"invented edge cut trimmed: last resort after retakes\": the shot's automatic new take was spent, so the take it plays was trimmed past the quarter, the line kept whole, at least 2 s left, the chapter or beat at least half its plan). Null for the writer's own trim."},"droppedLineRefs":{"type":"array","items":{"type":"integer"},"nullable":true,"description":"The lines this trim cut OUT of the take (the writer cut the chapter's line out and kept the rest of its footage): the span kept is one side of them. Null for every other trim, which keeps every line whole."}}},"TakeTrimBounds":{"type":"object","required":["takeSeconds","speech","minimumSeconds","suggested","available","reason"],"additionalProperties":true,"description":"What a free trim of this take may keep. A trim never cuts into a spoken line.","properties":{"takeSeconds":{"type":"number"},"speech":{"type":"object","nullable":true,"required":["startSeconds","endSeconds"],"description":"Where the take's spoken lines sit (the first line's start, the last one's end); a trim must keep all of it. Null for a take that speaks nothing.","properties":{"startSeconds":{"type":"number"},"endSeconds":{"type":"number"}}},"minimumSeconds":{"type":"number","description":"The shortest a trimmed take may be."},"suggested":{"type":"object","nullable":true,"required":["keepStartSeconds","keepEndSeconds"],"description":"The engine's own trim of this take (a cut it measured at an end), or null.","properties":{"keepStartSeconds":{"type":"number"},"keepEndSeconds":{"type":"number"}}},"available":{"type":"boolean"},"reason":{"type":"string","nullable":true,"description":"Why it cannot be trimmed right now."}}},"PitchDeckDropLineRequest":{"type":"object","additionalProperties":false,"description":"Cut the line, keep the rest (free, one step): the chapter's spoken line leaves its plan (its words, its voice and how it is said), and each take that says it is trimmed to the footage on one side of it — after the line when the line opens the take, before it when it closes it — measured where the take says it, with a small pad, so nothing of the line is heard. The trimmed take is the same footage, so the chapter stays current: nothing is rendered or charged, the trimmed take plays as soon as it lands, and its sound is the take's own sound with the line cut out. A line with footage on both sides is refused until you choose keep before or keep after; so is a side shorter than 1.5 s, and a trim that would leave another chapter of its scene group with no shot. Name a chapter to move the line to and its words, speaker and delivery go there, to be rendered with that chapter's own staging; otherwise add it there with a refine.","properties":{"keep":{"type":"string","enum":["before","after"],"description":"Which side of the line each take keeps. Omit it to keep the side with footage; a line with footage on both sides needs it (`drop_line_in_middle`)."},"toChapterIndex":{"type":"integer","minimum":0,"description":"The chapter (its key from the chapters read) the line's words, speaker and delivery move into. Refused when it already has another line. Omit to only cut the line out."}}},"PitchDeckLineDropTrim":{"type":"object","required":["callId","take","keep","keepStartSeconds","keepEndSeconds","lineStartSeconds","lineEndSeconds","takeSeconds"],"additionalProperties":true,"description":"One take the line would be cut out of: the take the call plays now and the side of the line it keeps.","properties":{"callId":{"type":"string"},"take":{"type":"integer","minimum":1},"keep":{"type":"string","enum":["before","after"]},"keepStartSeconds":{"type":"number"},"keepEndSeconds":{"type":"number"},"lineStartSeconds":{"type":"number","description":"Where the take says the line, padded: cut out with it."},"lineEndSeconds":{"type":"number"},"takeSeconds":{"type":"number"}}},"PitchDeckLineDrop":{"type":"object","required":["available","reason","code","trims","sides"],"additionalProperties":true,"description":"What cutting a chapter's line out of its footage would do now.","properties":{"available":{"type":"boolean"},"reason":{"type":"string","nullable":true,"description":"Why not, in one plain sentence."},"code":{"type":"string","nullable":true,"description":"`drop_line_in_middle`, `drop_line_too_short`, `drop_line_empties_chapter`, `drop_line_cuts_other_line`, `drop_line_narrated`, `drop_line_not_rendered`, `drop_line_no_take`, `drop_line_unplaced`, `drop_line_no_file` or `drop_line_no_line`."},"trims":{"type":"array","items":{"$ref":"#/components/schemas/PitchDeckLineDropTrim"}},"sides":{"type":"array","description":"A line in the middle: the sides that could be kept instead (`keep`), per take.","items":{"type":"object","additionalProperties":true,"properties":{"callId":{"type":"string"},"take":{"type":"integer"},"keep":{"type":"string","enum":["before","after"]},"keepStartSeconds":{"type":"number"},"keepEndSeconds":{"type":"number"},"seconds":{"type":"number"}}}}}},"PitchDeckDroppedLine":{"type":"object","required":["speaker","text","lineDelivery","movedTo","at","trims"],"additionalProperties":true,"description":"A line cut out of a chapter's footage, with the rest kept.","properties":{"speaker":{"type":"string","nullable":true},"text":{"type":"string"},"lineDelivery":{"type":"string","nullable":true},"movedTo":{"type":"object","nullable":true,"properties":{"chapterIndex":{"type":"integer"},"chapterNumber":{"type":"integer"}}},"at":{"type":"string","format":"date-time"},"trims":{"type":"array","items":{"type":"object","additionalProperties":true,"properties":{"callId":{"type":"string"},"take":{"type":"integer","description":"The take the line was cut out of."},"trimTake":{"type":"integer","description":"The trimmed take that keeps the rest."},"keep":{"type":"string","enum":["before","after"]},"keepStartSeconds":{"type":"number"},"keepEndSeconds":{"type":"number"},"state":{"type":"string","enum":["pending","playing","landed","failed"]}}}}}},"PitchDeckDropLineAnswer":{"type":"object","required":["jobId","chapterIndex","chapterNumber","line","movedTo","trims","queued","resumed","current","message"],"additionalProperties":true,"description":"The line is cut out: what each take keeps (queued as trimmed takes), where the line went, and whether the chapter is current. Nothing was charged.","properties":{"jobId":{"type":"string"},"chapterIndex":{"type":"integer"},"chapterNumber":{"type":"integer"},"line":{"type":"object","properties":{"speaker":{"type":"string","nullable":true},"text":{"type":"string"},"lineDelivery":{"type":"string","nullable":true}}},"movedTo":{"type":"object","nullable":true,"properties":{"chapterIndex":{"type":"integer"},"chapterNumber":{"type":"integer"}}},"trims":{"type":"array","items":{"type":"object","additionalProperties":true,"properties":{"callId":{"type":"string"},"take":{"type":"integer"},"trimTake":{"type":"integer"},"keep":{"type":"string","enum":["before","after"]},"keepStartSeconds":{"type":"number"},"keepEndSeconds":{"type":"number"}}}},"queued":{"type":"boolean"},"resumed":{"type":"boolean","description":"The line was cut out before; only its trims that never landed were queued again."},"current":{"type":"boolean","description":"Whether the chapter's takes are current now (false: it was already out of date for another change)."},"message":{"type":"string"}}},"TrailerDropLineRequest":{"type":"object","additionalProperties":false,"description":"Cut the line, keep the rest (free, one step): one of the beat's spoken lines leaves its plan (its words and who says it), and each take that says it is trimmed to the footage on one side of it — after the line when the line opens the take, before it when it closes it — measured where the take says it, with a small pad, so nothing of the line is heard. The trimmed take is the same footage, so the beat stays current: nothing is rendered or charged, the trimmed take plays as soon as it lands, and its sound is the take's own sound with the line cut out. A beat with several lines names the one to cut (`line`, from 1, as its `lineDrops` list them). A line with footage on both sides is refused until you choose keep before or keep after; so is a side shorter than 1.5 s. Name a beat to move the line to and its words and speaker go there, to be rendered with that beat's own staging; otherwise add it there with a refine.","properties":{"line":{"type":"integer","minimum":1,"description":"Which of the beat's lines (from 1, as its `lineDrops` list them). Omit it when the beat says one line."},"keep":{"type":"string","enum":["before","after"],"description":"Which side of the line each take keeps. Omit it to keep the side with footage; a line with footage on both sides needs it (`drop_line_in_middle`)."},"toBeatIndex":{"type":"integer","minimum":0,"description":"The beat the line's words and speaker move into, as its own line. Refused when it already has another. Omit to only cut the line out."}}},"TrailerLineDrop":{"allOf":[{"$ref":"#/components/schemas/PitchDeckLineDrop"},{"type":"object","required":["line","speaker","text","shotNumber"],"properties":{"line":{"type":"integer","minimum":1,"description":"The line's number in the beat: what `line` names."},"speaker":{"type":"string"},"text":{"type":"string"},"shotNumber":{"type":"integer","nullable":true,"description":"The shot of the beat's cut list that carries it (from 1), or null: the beat's own line."}}}],"description":"One of a beat's spoken lines, and what cutting it out of its footage would do now."},"TrailerDroppedLine":{"type":"object","required":["speaker","text","movedTo","at","trims"],"additionalProperties":true,"description":"A line cut out of a beat's footage, with the rest kept.","properties":{"speaker":{"type":"string","nullable":true},"text":{"type":"string"},"movedTo":{"type":"object","nullable":true,"properties":{"beatIndex":{"type":"integer"},"beatNumber":{"type":"integer"}}},"at":{"type":"string","format":"date-time"},"trims":{"type":"array","items":{"type":"object","additionalProperties":true,"properties":{"callId":{"type":"string"},"take":{"type":"integer","description":"The take the line was cut out of."},"trimTake":{"type":"integer","description":"The trimmed take that keeps the rest."},"keep":{"type":"string","enum":["before","after"]},"keepStartSeconds":{"type":"number"},"keepEndSeconds":{"type":"number"},"state":{"type":"string","enum":["pending","playing","landed","failed"]}}}}}},"TrailerDropLineAnswer":{"type":"object","required":["jobId","beatIndex","beatNumber","line","movedTo","trims","queued","resumed","current","message"],"additionalProperties":true,"description":"The line is cut out: what each take keeps (queued as trimmed takes), where the line went, and whether the beat is current. Nothing was charged.","properties":{"jobId":{"type":"string"},"beatIndex":{"type":"integer"},"beatNumber":{"type":"integer"},"line":{"type":"object","properties":{"speaker":{"type":"string","nullable":true},"text":{"type":"string"}}},"movedTo":{"type":"object","nullable":true,"properties":{"beatIndex":{"type":"integer"},"beatNumber":{"type":"integer"}}},"trims":{"type":"array","items":{"type":"object","additionalProperties":true,"properties":{"callId":{"type":"string"},"take":{"type":"integer"},"trimTake":{"type":"integer"},"keep":{"type":"string","enum":["before","after"]},"keepStartSeconds":{"type":"number"},"keepEndSeconds":{"type":"number"}}}},"queued":{"type":"boolean"},"resumed":{"type":"boolean","description":"The line was cut out before; only its trims that never landed were queued again."},"current":{"type":"boolean","description":"Whether the beat's takes are current now (false: it was already out of date for another change)."},"message":{"type":"string"}}},"TakeTrimRequest":{"type":"object","required":["callId","take","keepStartSeconds","keepEndSeconds"],"additionalProperties":false,"description":"Trim one take of one call to a span (\"trim this take to <start>–<end>\"). Free: one call, nothing priced or reserved.","properties":{"callId":{"type":"string","description":"One of this unit's call ids (from the beats or chapters read)."},"take":{"type":"integer","minimum":1,"description":"The take to trim: an accepted take of the call."},"keepStartSeconds":{"type":"number","minimum":0,"description":"Where the kept span starts, in seconds of the take (snapped to its frames)."},"keepEndSeconds":{"type":"number","description":"Where the kept span ends, in seconds of the take; must keep every spoken line whole."}}},"TakeTrimQueued":{"type":"object","required":["jobId","callId","take","trimTake","keepStartSeconds","keepEndSeconds","queued","message"],"additionalProperties":true,"description":"The trim is queued as a new take. Nothing was charged; the trimmed take is checked again when it lands and plays only if it passes.","properties":{"jobId":{"type":"string"},"callId":{"type":"string"},"take":{"type":"integer","minimum":1},"trimTake":{"type":"integer","minimum":1,"description":"The take number the trim is recorded as."},"keepStartSeconds":{"type":"number"},"keepEndSeconds":{"type":"number"},"queued":{"type":"boolean"},"message":{"type":"string"}}},"TakeRepairOverrideRequest":{"type":"object","required":["callId","take","decision"],"additionalProperties":false,"description":"Override the repair engine on one take. Free.","properties":{"callId":{"type":"string"},"take":{"type":"integer","minimum":1},"decision":{"type":"string","enum":["keep","restore","rewatch"],"description":"`keep`: keep the take as it is (what was found no longer ranks it down or warns, and the engine never acts on it); `restore`: the engine's decision stands again; `rewatch`: watch a take that could not be watched (its decision `unwatched`) again, at our cost — it reads checks pending until the watch lands, then is decided like any take. A take already watched is refused (`take_already_watched`)."}}},"TakeRepairOverrideAck":{"type":"object","required":["jobId","callId","take","decision","repair","selectedTake","message"],"additionalProperties":true,"description":"The override is recorded. Nothing was rendered or charged; the cut re-assembles when its selection changed. A `rewatch` queues the take's watch (`repair` is null until it lands).","properties":{"jobId":{"type":"string"},"callId":{"type":"string"},"take":{"type":"integer","minimum":1},"decision":{"type":"string","enum":["keep","restore","rewatch"]},"repair":{"allOf":[{"$ref":"#/components/schemas/TakeRepairDecision"}],"nullable":true},"selectedTake":{"type":"integer","nullable":true,"description":"The take the call plays now."},"message":{"type":"string","description":"What happened, in the writer's words."}}},"TrailerSelectTakeRequest":{"type":"object","required":["callId","take"],"additionalProperties":false,"description":"Choose one take of one of this beat's calls. Free and reversible.","properties":{"callId":{"type":"string","description":"One of this beat's call ids (from the beats read). A call of another beat is refused."},"take":{"type":"integer","minimum":1,"description":"A take number from the call's `takes`. A take that produced no file cannot be selected."}}},"TrailerSelectTakeAck":{"type":"object","required":["jobId","beatIndex","callId","take","warning"],"additionalProperties":true,"description":"The take now selected. Nothing was rendered and nothing was charged; the beat's preview cut is re-assembled from it.","properties":{"jobId":{"type":"string"},"beatIndex":{"type":"integer","minimum":0},"callId":{"type":"string"},"take":{"type":"integer","minimum":1},"warning":{"type":"string","nullable":true,"description":"Set when the chosen take is flagged: the finish quote and the export carry this sentence."}}},"TrailerFinishRequest":{"type":"object","additionalProperties":false,"description":"Finish the cut at the delivery class, and say whether this call is the price or the purchase.","properties":{"deliveryResolution":{"type":"string","enum":["720p","1080p"],"description":"Omit to finish at the trailer's delivery class (720p unless chosen)."},"confirmed":{"type":"boolean","description":"Omit (or false) to get the finish price WITHOUT spending anything. Send true only after the writer has seen that price and said go."},"acknowledgeVideoCoverage":{"type":"boolean","description":"Finish although the master the writer is looking at FAILED its video coverage (otherwise refused with 409 `trailer_video_coverage_failed`, `details.videoCoverage` carrying the score, the bar and the findings). Send true ONLY after the writer has seen those findings and chosen, in their own words, to finish anyway — never on your own."}}},"TrailerFinishAnswer":{"type":"object","required":["quotedOnly","jobId","deliveryResolution","credits","warnings"],"additionalProperties":true,"description":"Either the PRICE of the finish or the running finish — branch on `quotedOnly`.","properties":{"quotedOnly":{"type":"boolean"},"queued":{"type":"boolean","description":"Confirmed call only."},"jobId":{"type":"string"},"deliveryResolution":{"type":"string","enum":["720p","1080p"]},"credits":{"type":"number"},"costLabel":{"type":"string","nullable":true,"description":"Quote only."},"lines":{"type":"array","description":"Quote only. One line per selected take upscaled (a take already finished at the class is not charged again) and one per beat's on-screen text layer.","items":{"type":"object","additionalProperties":true,"properties":{"idempotencyKey":{"type":"string"},"label":{"type":"string"},"credits":{"type":"number"},"kind":{"type":"string","enum":["finish","text"]},"callId":{"type":"string","nullable":true},"take":{"type":"integer","nullable":true},"beatIndex":{"type":"integer","nullable":true},"seconds":{"type":"number","nullable":true}}}},"takes":{"type":"integer","description":"Confirmed call only: how many selected takes the finish upscales."},"warnings":{"type":"array","items":{"type":"string"},"description":"One per selected take the writer kept although it was flagged (a line not heard or in another character's voice, an opening off its plan)."},"balanceAfter":{"type":"integer","description":"Quote only. Studio Credits the writer would have left once this is spent: the balance available now (as Billing shows it) minus the price. Negative means they are that many short."},"sufficient":{"type":"boolean","description":"Quote only. True when the available balance covers the price. False means the writer must top up first (the note names the Billing link) — do not confirm; the reserve would refuse."},"note":{"type":"string","description":"Quote only: the price in the writer's words and how to proceed."},"message":{"type":"string","description":"Confirmed call only."}}},"TrailerDeliveryResolution":{"type":"object","required":["jobId","deliveryResolution","isDefault","renderResolution","options","editable"],"additionalProperties":false,"description":"The class the finish delivers the cut at. Raising it upscales the approved takes at finish — it quotes a finish, never a render.","properties":{"jobId":{"type":"string"},"deliveryResolution":{"type":"string","enum":["720p","1080p"],"description":"The writer's choice, else 720p."},"isDefault":{"type":"boolean","description":"True when nobody has chosen, so the default applies."},"renderResolution":{"type":"string","enum":["480p"],"description":"Every take renders at this class first, whatever the delivery class."},"options":{"type":"array","items":{"type":"string","enum":["720p","1080p"]}},"editable":{"type":"boolean","description":"False while a render or finish pass is running."}}},"TrailerAspectView":{"type":"object","required":["aspectRatio","chosen","width","height"],"additionalProperties":false,"description":"The shape the cut renders at, the shape it delivers at the cut's delivery class, and the writer's own pick (null while the default holds). A shape change never re-renders on its own: on a cut that already rendered, every beat whose takes are at the old shape is flagged `clipStale` and named in `invalidatedBeats` (rendering it again recompiles at the new shape).","properties":{"aspectRatio":{"type":"string","enum":["9:16","16:9","1:1"],"description":"The shape the cut renders at — the writer's choice when chosen, otherwise the default."},"chosen":{"type":"string","nullable":true,"enum":["9:16","16:9","1:1",null],"description":"The raw job column: the writer's choice, or null while the default holds."},"width":{"type":"integer","description":"The frame this shape delivers at, in pixels, at the cut's delivery class."},"height":{"type":"integer"},"invalidatedBeats":{"type":"array","items":{"type":"integer"},"description":"POST only: rendered beats whose takes are at another shape — kept playable, flagged, re-rendered at the new one."}}},"TrailerExportAck":{"type":"object","required":["renderedVideoStatus","warnings"],"additionalProperties":true,"description":"The finished-master re-stitch, queued (or already queued / rendering).","properties":{"renderedVideoStatus":{"type":"string","enum":["queued","rendering"]},"warnings":{"type":"array","items":{"type":"string"},"description":"One per flagged take the writer kept."}}},"ReferenceRenderAction":{"type":"object","required":["kind"],"additionalProperties":false,"description":"The one action that unblocks a refused render. `recast_figure` (a person on camera has no body figure), `add_signature_items` (a video with two or more people needs each person's signature items), `recast_voice` (a speaker has no cast voice) each carry `character`.","properties":{"kind":{"type":"string","enum":["recast_figure","add_signature_items","recast_voice"]},"character":{"type":"string"}}},"ProjectWorkspaceStage":{"type":"string","enum":["premise","premise_coverage","planning","full_coverage","ready"],"description":"The project development stage, in order: develop the premise, pass premise coverage, plan the episode map, pass full Series Bible coverage, then `ready`. The mood board is not a stage: it opens after premise coverage and blocks nothing."},"ProjectPreconditionDetails":{"type":"object","required":["stage","missingFields","canPlan","canStartEpisode"],"additionalProperties":false,"description":"`error.details` on a 409 `project_precondition_failed`. Nothing was created, queued or charged. Finish `stage`, then call again — retrying before that refuses the same way.","properties":{"stage":{"$ref":"#/components/schemas/ProjectWorkspaceStage"},"missingFields":{"type":"array","items":{"type":"string"},"description":"What the stage still lacks, when the gate can name it (Series Bible fields, `Project`, `Mood board image model`). Often empty: a coverage or look stage is finished by an act, not a field."},"canPlan":{"type":"boolean","description":"Whether the project can plan episodes and cast now."},"canStartEpisode":{"type":"boolean","description":"Whether the project can draft episodes and generate project media now."}}},"TrailerRefusalDetails":{"type":"object","required":["refusal"],"additionalProperties":true,"description":"`error.details` on a refused trailer operation (409 trailer_state_invalid, 400 for a beat/call/take the trailer does not have, 402 for credits). Nothing was reserved.","properties":{"refusal":{"type":"object","required":["code","action","fix"],"additionalProperties":false,"properties":{"code":{"type":"string","description":"The engine's refusal code, e.g. `trailer_render_blocked`, `trailer_not_draft`, `trailer_not_reviewable`, `trailer_finish_missing_takes`, `trailer_narrator_missing`."},"action":{"allOf":[{"$ref":"#/components/schemas/ReferenceRenderAction"}],"nullable":true},"fix":{"type":"object","nullable":true,"additionalProperties":false,"description":"Where the action is taken, on every surface.","properties":{"kind":{"type":"string","enum":["recast_figure","add_signature_items","recast_voice"]},"subject":{"type":"string","description":"The character or plate the action is about."},"sentence":{"type":"string","description":"One sentence for the writer."},"mcpTool":{"type":"string"},"cliCommand":{"type":"string"}}}}},"videoCoverage":{"$ref":"#/components/schemas/VideoCoverageRefusalDetails"},"blocked":{"type":"array","description":"On `trailer_render_blocked`: every beat the compile could not render, with its issues and the actions that fix them.","items":{"type":"object","additionalProperties":true,"properties":{"beatIndex":{"type":"integer","minimum":0},"issues":{"type":"array","items":{"type":"object","additionalProperties":true}},"actions":{"type":"array","items":{"$ref":"#/components/schemas/ReferenceRenderAction"}}}}}}},"TrailerBeatIndexRequest":{"type":"object","required":["beatIndex"],"additionalProperties":false,"description":"Target one trailer beat by index, and say whether this call is the price or the purchase.","properties":{"beatIndex":{"type":"integer","minimum":0,"description":"Zero-based index of the beat to render."},"confirmed":{"type":"boolean","description":"Omit (or false) to get the price WITHOUT spending anything — nothing is reserved and nothing is queued. Send true only after the writer has seen that price and said go. It is optional rather than a required `true` because a field that must be true to validate cannot separate a confirmed call from an unconfirmed one."}}},"TrailerMoveBeatRequest":{"type":"object","required":["fromIndex","toIndex"],"additionalProperties":false,"description":"Move one trailer beat to another position in the cut.","properties":{"fromIndex":{"type":"integer","minimum":0,"description":"Zero-based index of the beat to move."},"toIndex":{"type":"integer","minimum":0,"description":"Zero-based position the beat ENDS UP at."}}},"TrailerInsertBeatRequest":{"type":"object","additionalProperties":false,"description":"Insert a new blank trailer beat at a seam. Give `atIndex` OR `afterBeatIndex`, or neither to append.","properties":{"atIndex":{"type":"integer","minimum":0,"description":"Zero-based position the new beat takes."},"afterBeatIndex":{"type":"integer","minimum":0,"description":"Insert just AFTER this existing beat."}}},"TrailerRemoveBeatRequest":{"type":"object","required":["beatIndex"],"additionalProperties":false,"description":"Remove one trailer beat, with everything already rendered on it.","properties":{"beatIndex":{"type":"integer","minimum":0,"description":"Zero-based index of the beat to remove."}}},"TrailerBeatOrderAck":{"type":"object","required":["success","beatCount","staleBeats"],"additionalProperties":true,"description":"The result of a beat-order edit. Costs no credits and renders nothing.","properties":{"success":{"type":"boolean"},"beatCount":{"type":"integer","description":"How many beats the cut has after the edit."},"staleBeats":{"type":"array","items":{"type":"integer","minimum":0},"description":"Beats whose FOLLOWING beat changed, so their seam direction is out of date. Their footage stays playable."},"movedTo":{"type":"integer","minimum":0,"description":"Where the moved beat ended up (move only)."},"insertedBeatIndex":{"type":"integer","minimum":0,"description":"Where the new blank beat landed (insert only)."}}},"TrailerRestoreTakeRequest":{"type":"object","required":["beatIndex","url"],"additionalProperties":false,"description":"Put one of a beat's earlier cuts back. Free and reversible.","properties":{"beatIndex":{"type":"integer","minimum":0,"description":"Zero-based index of the beat whose earlier cut to put back."},"url":{"type":"string","description":"The earlier cut's url, exactly as `storyboard/takes` returned it. A url this beat never played is refused."}}},"TrailerRestoreTakeAck":{"type":"object","required":["success","clipUrl","selection"],"additionalProperties":true,"description":"The cut now playing. Nothing was rendered and nothing was charged; the cut it replaced joined the beat's list, and the takes the restored cut was assembled from are selected again.","properties":{"success":{"type":"boolean"},"clipUrl":{"type":"string","description":"The cut now playing for that beat."},"selection":{"type":"array","nullable":true,"description":"The takes re-selected (one per call), so the finish delivers exactly what was restored. Null for a cut recorded without its selection.","items":{"type":"object","additionalProperties":false,"properties":{"callId":{"type":"string"},"take":{"type":"integer","minimum":1}}}}}},"TrailerBeatTake":{"type":"object","required":["url","replacedAt","durationMs"],"additionalProperties":true,"description":"One earlier cut of a beat, and when it stopped being the one playing.","properties":{"url":{"type":"string","description":"The earlier cut's storage url — pass this to `storyboard/restore-take`."},"replacedAt":{"type":"string","format":"date-time","description":"When this cut was replaced."},"durationMs":{"type":"integer","nullable":true,"description":"How long this cut runs, when it was recorded."}}},"TrailerBeatTakes":{"type":"object","required":["jobId","takesKept","beats","reviewUrl"],"additionalProperties":true,"description":"The cut history of a trailer: what each beat plays now, and the earlier cuts behind it. (The per-call takes are on the beats read.)","properties":{"jobId":{"type":"string"},"takesKept":{"type":"integer","description":"How many earlier cuts a beat remembers, so a caller can tell \"it has three\" from \"the oldest fell off the list\"."},"beats":{"type":"array","items":{"type":"object","required":["beatIndex","clipUrl","durationMs","takes"],"additionalProperties":true,"properties":{"beatIndex":{"type":"integer","minimum":0},"clipUrl":{"type":"string","nullable":true,"description":"The cut playing now; null when this beat has no footage yet."},"durationMs":{"type":"integer","nullable":true},"takes":{"type":"array","items":{"$ref":"#/components/schemas/TrailerBeatTake"},"description":"Earlier cuts, newest first. Empty when this beat has never been re-assembled."}}}},"reviewUrl":{"type":"string","nullable":true,"description":"Logged-in browser link to watch them — motion cannot play in a terminal."}}},"TrailerDirectTextMotionRequest":{"type":"object","additionalProperties":false,"description":"Say in plain words how one beat's on-screen text should move (`beatIndex` + `direction`) — or how one of the closing sequence's cards should look and move (`closingElement` + `direction`, or + `clear: true` to put it back in its own type). Exactly one of `beatIndex` and `closingElement`. Free — no `confirmed` field, because there is no spend to confirm. A trailer's closing sequence's words are directed in the writer's own words through the trailer's direct-text-motion call with a `closingElement` instead of a `beatIndex`: `quote`, `comingSoon` or `productionCredit` (\"Coming Soon wide and slow, a faint vignette breathing behind it\"). The same director compiles the sentence into a motion-text spec (face, colour, size, motion and a quiet background) and saves it with the sentence on the closing sequence (`textMotion`). The words stay the closing's: a direction says how they look, never what they say. `clear: true` puts a card back in its own type. Free: an LLM call, no credits; a rendered trailer's master is stitched again with it, and nothing else renders.","properties":{"beatIndex":{"type":"integer","minimum":0,"description":"Zero-based index of the beat whose on-screen text should move."},"closingElement":{"type":"string","enum":["quote","comingSoon","productionCredit"],"description":"Instead of a beat: one of the closing sequence's cards — quote (with where it is from), comingSoon, or productionCredit."},"direction":{"type":"string","minLength":1,"maxLength":2000,"description":"How the text should move, in the writer's own words — e.g. \"type it out letter by letter, then draw an underline beneath it\". Required unless `clear` is true."},"clear":{"type":"boolean","enum":[true],"description":"With `closingElement`: put the card back in its own type."}}},"TrailerDirectTextMotionAck":{"type":"object","required":["success","direction"],"additionalProperties":true,"description":"The direction now on the beat (or on one of the closing sequence's cards). Nothing was rendered and nothing was charged; a beat's text layer is re-rendered to see it move; a closing card is drawn by the master, which a rendered trailer stitches again (free; `reassembling`).","properties":{"success":{"type":"boolean"},"beatIndex":{"type":"integer","minimum":0,"description":"The beat directed (a beat direction)."},"direction":{"type":"string","nullable":true,"description":"The writer's own words, saved beside the compiled spec so the next direction is not authored blind; null when a closing card was put back in its own type."},"closingElement":{"type":"string","enum":["quote","comingSoon","productionCredit"],"description":"The closing card directed (a closing direction)."},"look":{"type":"string","nullable":true,"description":"What the master draws for a directed closing card, read from the stored spec. Null when it was put back in its own type."},"closingSequence":{"allOf":[{"$ref":"#/components/schemas/ClosingSequenceView"}],"nullable":true,"description":"The closing sequence as every read shows it, with the direction in `textMotion` (a closing direction)."},"reassembling":{"type":"boolean","description":"True when the trailer's master is stitched again with it (a rendered trailer; free — no beat renders)."}}},"TrailerTextRenderAnswer":{"type":"object","required":["previewUrl","quotedOnly"],"additionalProperties":true,"description":"Either the PRICE of a beat's on-screen text render or the queued render itself — branch on `quotedOnly`. Sent without `confirmed`, the call quotes and nothing is reserved; sent with `confirmed: true`, the credit is held and the render is queued.","properties":{"quotedOnly":{"type":"boolean","description":"True when this was a price and NOTHING was spent; false when the render was reserved and queued."},"previewUrl":{"type":"string","description":"Shareable link to watch this beat's current render."},"reviewUrl":{"type":"string","description":"Logged-in browser link to the trailer in the Videos tab. Present on a confirmed call."},"credits":{"type":"number","description":"Credits charged for this render, or — when `quotedOnly` is true — what a confirmed call would charge."},"costLabel":{"type":"string","nullable":true,"description":"The rate-card line the price came from (quote only)."},"balanceAfter":{"type":"integer","description":"Quote only. Studio Credits the writer would have left once this is spent: the balance available now (as Billing shows it) minus the price. Negative means they are that many short."},"sufficient":{"type":"boolean","description":"Quote only. True when the available balance covers the price. False means the writer must top up first (the note names the Billing link) — do not confirm; the reserve would refuse."},"note":{"type":"string","description":"The price in the writer's own words, what it would leave (or how short they are, with the top-up link), and how to proceed (quote only)."}}},"MyLibraryImages":{"type":"object","required":["images","nextCursor","total"],"additionalProperties":false,"description":"One page of the caller's reusable My-Library images (cached cast headshots), newest first.","properties":{"images":{"type":"array","description":"Reusable images, each with its `id`, the character it depicts and the script it came from.","items":{"type":"object","required":["id","imageUrl","figureUrl","character","scriptTitle","createdAt"],"additionalProperties":false,"properties":{"id":{"type":"string","description":"Library image id (use as libraryImageId)."},"imageUrl":{"type":"string","format":"uri","description":"The headshot portrait."},"figureUrl":{"type":["string","null"],"description":"The full-body theater figure (a transparent cutout); null when none was generated."},"character":{"type":"string","description":"The character the portrait depicts."},"scriptTitle":{"type":["string","null"]},"createdAt":{"type":"string","format":"date-time"}}}},"nextCursor":{"oneOf":[{"type":"string"},{"type":"null"}],"description":"Pass as `cursor` for the next page; null on the last page."},"total":{"type":"integer","minimum":0,"description":"How many images the whole library holds, across every page."}}},"MyLibraryAudioAsset":{"type":"object","required":["id","kind","label","prompt","soundUrl","tags","vocals","createdAt"],"additionalProperties":true,"description":"A durable reusable music, sound-effect, or soundscape asset in My Library.","properties":{"id":{"type":"string"},"kind":{"type":"string","enum":["music","sfx","soundscape"]},"label":{"type":"string"},"prompt":{"type":"string"},"soundUrl":{"type":"string","format":"uri"},"provider":{"type":["string","null"]},"scriptUploadId":{"type":["string","null"]},"scriptTitle":{"type":["string","null"]},"tags":{"type":"array","items":{"type":"string"}},"durationSeconds":{"type":["number","null"]},"vocals":{"type":"boolean"},"contentType":{"type":["string","null"]},"byteSize":{"type":["integer","null"]},"contentSha256":{"type":["string","null"]},"createdAt":{"type":"string","format":"date-time"}}},"MyLibraryAudio":{"type":"object","required":["audio","nextCursor","total"],"additionalProperties":false,"description":"One page of the caller's reusable audio, newest first.","properties":{"audio":{"type":"array","items":{"$ref":"#/components/schemas/MyLibraryAudioAsset"}},"nextCursor":{"oneOf":[{"type":"string"},{"type":"null"}],"description":"Pass as `cursor` (with the same filters) for the next page; null on the last page."},"total":{"type":"integer","minimum":0,"description":"How many assets match the filters, across every page."}}},"MyLibraryAudioImportRequest":{"type":"object","required":["kind","label"],"additionalProperties":false,"description":"Import metadata plus exactly one audio source: sourceUrl or audioBase64 with contentType.","properties":{"kind":{"type":"string","enum":["music","sfx","soundscape"]},"label":{"type":"string","minLength":1,"maxLength":160},"prompt":{"type":"string","maxLength":2000},"provider":{"type":"string","maxLength":120},"tags":{"type":"array","maxItems":40,"items":{"type":"string","maxLength":80}},"durationSeconds":{"type":"number","exclusiveMinimum":0,"maximum":3600},"vocals":{"type":"boolean"},"scriptUploadId":{"type":"string"},"sourceUrl":{"type":"string","format":"uri"},"audioBase64":{"type":"string","contentEncoding":"base64"},"contentType":{"type":"string","enum":["audio/mpeg","audio/mp3","audio/wav","audio/x-wav","audio/flac","audio/ogg","audio/mp4","audio/aac"]},"originalFilename":{"type":"string"}},"oneOf":[{"required":["sourceUrl"],"not":{"required":["audioBase64"]}},{"required":["audioBase64","contentType"],"not":{"required":["sourceUrl"]}}]},"MyLibraryAudioImportResponse":{"type":"object","required":["audio","deduplicated"],"additionalProperties":false,"properties":{"audio":{"$ref":"#/components/schemas/MyLibraryAudioAsset"},"deduplicated":{"type":"boolean"}}},"MyLibraryAudioRenameRequest":{"type":"object","required":["label"],"additionalProperties":false,"description":"The one field a rename changes.","properties":{"label":{"type":"string","minLength":1,"maxLength":120}}},"MyLibraryAudioRenameResponse":{"type":"object","required":["id","label","updatedAt"],"additionalProperties":false,"properties":{"id":{"type":"string"},"label":{"type":"string"},"updatedAt":{"type":"string","format":"date-time"}}},"ProVoiceCloneStatus":{"type":"object","required":["proClone"],"additionalProperties":true,"description":"Current status of a Pro voice clone job.","properties":{"proClone":{"type":"object","additionalProperties":true,"properties":{"status":{"type":"string","description":"collecting | training | ready | failed."},"totalAudioMs":{"type":"integer","description":"Audio collected so far, in milliseconds."},"minRequiredMs":{"type":"integer","description":"Minimum audio needed before training."},"trainable":{"type":"boolean","description":"Whether enough audio is collected to start training."},"voiceId":{"type":"string","description":"The resulting voice id once ready (else null)."},"error":{"type":"string","description":"Failure detail when status is failed (else null)."}}}}},"ProVoiceCloneScript":{"type":"object","required":["script"],"additionalProperties":true,"description":"The recording script the user reads aloud to collect Pro-clone audio.","properties":{"script":{"type":"object","additionalProperties":true,"properties":{"title":{"type":"string"},"intro":{"type":"string"},"totalEstSeconds":{"type":"number"},"passages":{"type":"array","items":{"type":"object","additionalProperties":true},"description":"Ordered passages: { label, direction, kind, text }."}}}}},"ProVoiceCloneTrainRequest":{"type":"object","required":["jobId"],"additionalProperties":false,"description":"Start training for a Pro voice clone.","properties":{"jobId":{"type":"string","description":"The Pro voice clone job id."}}},"ProVoiceCloneTrainResult":{"type":"object","required":["status"],"additionalProperties":true,"description":"Training acknowledgement.","properties":{"status":{"type":"string","description":"training. A short balance is a 402 insufficient_credits error instead."},"required":{"type":"integer","description":"Credits required (present when insufficient)."},"available":{"type":"integer","description":"Credits available (present when insufficient)."}}},"TheaterCanvasBeat":{"type":"object","required":["entryIndex","compositionKind"],"additionalProperties":false,"description":"One motion-graphic beat in a theater scene canvas.","properties":{"entryIndex":{"type":"integer","minimum":0,"description":"Zero-based script entry (spoken line) this beat occupies."},"compositionKind":{"type":"string","enum":["motion-only","motion-over-clip","performance-motion-overlay"]},"captionText":{"type":"string","description":"On-screen copy the motion layers render."},"direction":{"type":"string","description":"Full-canvas motion direction (where elements sit + how the text moves)."},"durationSeconds":{"type":"number","minimum":0.5,"maximum":30,"description":"This beat's render duration in seconds."},"backgroundClipPrompt":{"type":"string","description":"For a motion-over-clip beat: the background clip look (generated separately)."}}},"TheaterCanvasRequest":{"type":"object","required":["sceneIndex","beats"],"additionalProperties":false,"description":"A whole theater scene canvas: the scene plus its ordered motion-graphic beats.","properties":{"sceneIndex":{"type":"integer","minimum":0,"description":"Zero-based scene index whose canvas this commits."},"beats":{"type":"array","minItems":1,"maxItems":12,"items":{"$ref":"#/components/schemas/TheaterCanvasBeat"},"description":"Ordered beats composing the scene canvas."}}},"ArtifactRenderStatus":{"type":"object","required":["artifactId"],"additionalProperties":true,"description":"Current durable render status (theater MP4 / full-mix MP3) for an artifact, plus the shareable link to watch or listen.","properties":{"artifactId":{"type":"string"},"renderedVideoStatus":{"type":"string","description":"idle | queued | rendering | complete | stale | failed | needs_recording. A render that stopped with no job behind it reads `failed`, or `stale` over an earlier MP4, with `renderedVideoError` saying so: POST render-video again."},"renderedAudioStatus":{"type":"string","description":"idle | queued | rendering | complete | stale | failed | needs_recording. A render that stopped with no job behind it reads `failed`, or `stale` over an earlier MP3, with `renderedAudioError` saying so."},"shareUrl":{"type":"string","description":"Shareable link to open/listen/watch the render."}}},"ArtifactPublishState":{"type":"object","required":["artifactId","publishable","renderStatus","recordingReady","inFeed"],"additionalProperties":false,"description":"The publish/render state of one table-read render: whether it has been sent to Publishing, the durable full-mix render's status, and whether a live RSS feed backs it.","properties":{"artifactId":{"type":"string","nullable":true,"description":"The Story artifact behind the render."},"publishable":{"type":"boolean","description":"True once the render has been sent to Publishing."},"renderStatus":{"type":"string","enum":["complete","rendering","needs_recording","not_finalized","failed"],"description":"Normalized render status driving the publishing button's spinner/tooltip."},"recordingReady":{"type":"boolean","description":"True once the durable full-mix MP3 is ready to publish."},"inFeed":{"type":"boolean","description":"True when a live/published release in an RSS feed backs this render."}}},"ArtifactPublishStateRequest":{"type":"object","required":["action"],"additionalProperties":false,"description":"Set a render's publish state. `mark` sends it to Publishing (ensuring its durable render exists); `unmark` removes it.","properties":{"action":{"type":"string","enum":["mark","unmark"],"description":"mark | unmark."},"confirm":{"type":"boolean","description":"Only read for `action: \"unmark\"`. Without it, a render a live/published RSS release backs comes back `{ requiresConfirm: true, inFeed: true }` instead of being removed."}}},"ArtifactPublishStateResult":{"type":"object","required":["inFeed"],"additionalProperties":false,"description":"The render's publish state after the change. A `mark` also carries `renderRequest` (`complete` when the durable render already existed, `rendering` when it was just queued). An `unmark` a live feed backs carries `requiresConfirm: true` and `inFeed: true` and the remaining state fields may be absent.","properties":{"artifactId":{"type":"string","nullable":true,"description":"The Story artifact behind the render."},"publishable":{"type":"boolean","description":"True once the render has been sent to Publishing."},"renderStatus":{"type":"string","enum":["complete","rendering","needs_recording","not_finalized","failed"]},"recordingReady":{"type":"boolean"},"inFeed":{"type":"boolean","description":"True when a live/published release in an RSS feed backs this render."},"renderRequest":{"type":"string","enum":["complete","rendering"],"description":"Present on a `mark` that also ensured the durable render: `complete` when it already existed, `rendering` when it was just queued."},"requiresConfirm":{"type":"boolean","description":"Present (true) on an `unmark` a live feed backs, until `confirm: true` is sent."}}},"WriterScriptVersion":{"type":"object","additionalProperties":false,"required":["scriptUploadId","versionNumber","title","status","parseStatus","slug","wordCount","pageCount","format","createdAt","counts"],"properties":{"scriptUploadId":{"type":"string","description":"The version's id: what coverage, overrides, table reads and exports take."},"versionNumber":{"type":"integer","minimum":1},"title":{"type":"string"},"status":{"type":"string","description":"`draft` while it is being written; otherwise a locked version."},"parseStatus":{"type":["string","null"],"description":"Where the screenplay parse stands (`queued`, `parsing`, `complete`, `failed`); null when none ran."},"slug":{"type":["string","null"]},"wordCount":{"type":"integer","minimum":0},"pageCount":{"type":["integer","null"]},"format":{"type":"string","description":"How it was written: text, fountain or fdx."},"createdAt":{"type":"string","format":"date-time"},"counts":{"type":"object","additionalProperties":false,"required":["coverageReports","tableReadJobs","pitchDeckJobs","visualizerJobs"],"properties":{"coverageReports":{"type":"integer","minimum":0},"tableReadJobs":{"type":"integer","minimum":0},"pitchDeckJobs":{"type":"integer","minimum":0},"visualizerJobs":{"type":"integer","minimum":0}},"description":"How much has been made from this version."}}},"WriterScript":{"type":"object","additionalProperties":false,"required":["scriptId","routeId","projectId","title","slug","logline","mediaType","genre","seasonNumber","createdAt","updatedAt","workspacePath","versions"],"properties":{"scriptId":{"type":"string"},"routeId":{"type":"string","description":"The id in its Writers Lab URL."},"projectId":{"type":["string","null"],"description":"Its project, or null for a script in no project."},"title":{"type":"string"},"slug":{"type":["string","null"],"description":"Its URL slug."},"logline":{"type":["string","null"],"description":"The writer's one-sentence logline — the same one Slug and the Writers Lab read and write."},"mediaType":{"type":["string","null"],"description":"`film` or `tv`."},"genre":{"type":["string","null"],"description":"Free text, in the writer's words."},"seasonNumber":{"type":"integer","minimum":1,"maximum":99},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"workspacePath":{"type":"string","description":"Where it opens in the Writers Lab, relative to the site origin."},"versions":{"type":"array","items":{"$ref":"#/components/schemas/WriterScriptVersion"},"description":"Newest first."}}},"WriterScriptEnvelope":{"type":"object","additionalProperties":false,"required":["script"],"properties":{"script":{"$ref":"#/components/schemas/WriterScript"}}},"ProjectEpisode":{"type":"object","additionalProperties":false,"required":["scriptId","routeId","projectId","title","slug","logline","mediaType","genre","seasonNumber","createdAt","updatedAt","workspacePath","versions","release","supersededBy"],"properties":{"scriptId":{"type":"string"},"routeId":{"type":"string","description":"The id in its Writers Lab URL."},"projectId":{"type":["string","null"],"description":"Its project, or null for a script in no project."},"title":{"type":"string"},"slug":{"type":["string","null"],"description":"Its URL slug."},"logline":{"type":["string","null"],"description":"The writer's one-sentence logline — the same one Slug and the Writers Lab read and write."},"mediaType":{"type":["string","null"],"description":"`film` or `tv`."},"genre":{"type":["string","null"],"description":"Free text, in the writer's words."},"seasonNumber":{"type":"integer","minimum":1,"maximum":99},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"workspacePath":{"type":"string","description":"Where it opens in the Writers Lab, relative to the site origin."},"versions":{"type":"array","items":{"$ref":"#/components/schemas/WriterScriptVersion"},"description":"Newest first."},"release":{"type":["object","null"],"additionalProperties":false,"required":["status","episodeNumber"],"properties":{"status":{"type":"string"},"episodeNumber":{"type":["integer","null"]}},"description":"The feed release of the plan that generated it, when there is one."},"supersededBy":{"type":["string","null"],"description":"Set when this is an earlier or failed attempt at an episode another script now stands for: that script's id."}}},"ProjectEpisodesResponse":{"type":"object","additionalProperties":false,"required":["project","seasons","episodes","plannedEpisodes","videos","videosNote","workspacePath"],"properties":{"project":{"type":"object","additionalProperties":false,"required":["id","routeId","name"],"properties":{"id":{"type":"string"},"routeId":{"type":"string"},"name":{"type":"string"}}},"seasons":{"type":"array","items":{"type":"object","additionalProperties":false,"required":["number","name","episodeCount"],"properties":{"number":{"type":"integer","minimum":1},"name":{"type":"string"},"episodeCount":{"type":"integer","minimum":0,"description":"Episodes in it, earlier attempts left out."}}}},"episodes":{"type":"array","items":{"$ref":"#/components/schemas/ProjectEpisode"},"description":"Oldest first within each season."},"plannedEpisodes":{"type":"array","description":"The Series Bible's episode map, joined against what is written.","items":{"type":"object","additionalProperties":false,"required":["episodeId","label","title","summary","status","seasonNumber","planIndex","writtenScriptId"],"properties":{"episodeId":{"type":"string"},"label":{"type":"string"},"title":{"type":"string"},"summary":{"type":"string"},"status":{"type":"string"},"seasonNumber":{"type":"integer","minimum":1},"planIndex":{"type":"integer","minimum":0},"writtenScriptId":{"type":["string","null"],"description":"The script that covers it; null when it has not been written."}}}},"videos":{"type":"array","items":{"type":"object","additionalProperties":false,"required":["id","kind","title","kindLabel","description","durationSeconds","status","scope","scopeKind","seasonNumber","videoUrl","workspacePath","createdAt","updatedAt"],"properties":{"id":{"type":"string"},"kind":{"type":"string","enum":["trailer","pitch-deck","video"]},"title":{"type":"string"},"kindLabel":{"type":["string","null"]},"description":{"type":["string","null"]},"durationSeconds":{"type":["number","null"]},"status":{"type":"string"},"scope":{"type":"string","description":"What it covers: an episode's title, \"Season 2\", \"Project\"."},"scopeKind":{"type":["string","null"],"enum":["episode","season","project",null]},"seasonNumber":{"type":["integer","null"]},"videoUrl":{"type":["string","null"]},"workspacePath":{"type":["string","null"]},"createdAt":{"type":["string","null"],"format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}}},"videosNote":{"type":["string","null"],"description":"Why `videos` is empty while the project has not finished development."},"workspacePath":{"type":"string"}}},"ScriptUploadRequest":{"type":"object","additionalProperties":false,"required":["title","content"],"properties":{"title":{"type":"string","minLength":1,"maxLength":200},"content":{"type":"string","minLength":1,"maxLength":500000,"description":"The screenplay text."},"format":{"type":"string","enum":["text","fountain","fdx"],"description":"How the text is written (default `text`)."},"projectId":{"type":"string","minLength":1,"description":"The project to file the script in; without it a new project is named after the script."}}},"ScriptVersionUploadRequest":{"type":"object","additionalProperties":false,"description":"Exactly one of `content`, `sourceId` and `fileUrl`. A new version can come from a file (a PDF, a Final Draft .fdx, or a Fountain or plain-text script) instead of its text. The file is read as a source of the script's project: `fileUrl` (a link to the file) adds it and answers `waiting` with its `sourceId`; check that source until it is READY, then send `sourceId` and its text becomes the new version. A source that is already READY becomes the version at once, and the same source makes its version once (sending it again answers that version). A script in no project has nowhere to keep the file: send its text instead.","properties":{"content":{"type":"string","minLength":1,"maxLength":500000,"description":"The new version's screenplay text."},"sourceId":{"type":"string","minLength":1,"description":"A source of the script's project whose text becomes the version once it is READY."},"fileUrl":{"type":"string","format":"uri","maxLength":4000,"description":"A link to the file (PDF, .fdx, .fountain or .txt), read in the background as a source of the script's project."},"fileName":{"type":"string","maxLength":200,"description":"The file's name; its extension says what it is."},"format":{"type":"string","enum":["text","fountain","fdx"],"description":"How the text is written (default: the file's own, else `text`)."}}},"ScriptVersionWaiting":{"type":"object","additionalProperties":false,"required":["waiting","scriptId","projectId","sourceId","sourceStatus","message"],"description":"The file is still being read: no version is made yet. Check the source until it is READY, then send `sourceId`.","properties":{"waiting":{"type":"boolean","enum":[true]},"scriptId":{"type":"string"},"projectId":{"type":"string","description":"The project the source is in (GET /story-projects/{projectId}/sources/{sourceId})."},"sourceId":{"type":"string","description":"Send it as `sourceId` once the source is READY."},"sourceStatus":{"type":"string","enum":["PENDING","EXTRACTING"]},"message":{"type":"string","description":"What to tell the writer, and how to go on."}}},"ScriptUploadResponse":{"type":"object","additionalProperties":false,"required":["script","scriptUploadId","versionNumber"],"properties":{"script":{"$ref":"#/components/schemas/WriterScript"},"scriptUploadId":{"type":"string","description":"The version this call created."},"versionNumber":{"type":"integer","minimum":1}}},"ScriptMetadataPatch":{"type":"object","additionalProperties":false,"minProperties":1,"description":"Only the fields to change; the rest are untouched.","properties":{"title":{"type":"string","minLength":1,"maxLength":200,"description":"Renames the latest version too."},"slug":{"type":"string","minLength":1,"maxLength":64,"description":"Its URL slug; punctuation becomes dashes. Refused when reserved or already one of yours."},"logline":{"type":["string","null"],"maxLength":500,"description":"The writer's one-sentence logline; null or blank clears it."},"mediaType":{"type":["string","null"],"enum":["film","tv",null],"description":"Null clears it."},"genre":{"type":["string","null"],"maxLength":80,"description":"Free text, in the writer's words (suggestions: Drama, Comedy, Thriller, Horror, Sci-Fi, Action, Romance, Crime, Fantasy, Animation, Musical, Mystery, Western, Other; a suggestion in any case is stored in its casing). Null or blank clears it."},"seasonNumber":{"type":"integer","minimum":1,"maximum":99,"description":"Moves an episode into that season of its project, adding the season when it is new. A script in no project is refused."}}},"ScriptVersionDeleteResponse":{"type":"object","additionalProperties":false,"required":["deleted","scriptId","scriptUploadId","versionNumber","remainingVersions"],"properties":{"deleted":{"type":"boolean","enum":[true]},"scriptId":{"type":"string"},"scriptUploadId":{"type":"string"},"versionNumber":{"type":"integer"},"remainingVersions":{"type":"integer","minimum":1}}},"ScriptExportResponse":{"type":"object","additionalProperties":false,"required":["scriptUploadId","format","filename","contentType","encoding","content"],"properties":{"scriptUploadId":{"type":"string","description":"The version exported."},"format":{"type":"string","enum":["fountain","fdx","pdf"]},"filename":{"type":"string"},"contentType":{"type":"string"},"encoding":{"type":"string","enum":["utf8","base64"],"description":"`utf8` for fountain and fdx (the text itself), `base64` for a PDF."},"content":{"type":"string"}}},"ProjectSeasonCreateRequest":{"type":"object","additionalProperties":false,"properties":{"name":{"type":"string","maxLength":80,"description":"Leave out for \"Season N\"."}}},"ProjectSeasonCreateResponse":{"type":"object","additionalProperties":false,"required":["season","seasons"],"properties":{"season":{"type":"object","additionalProperties":false,"required":["number","name"],"properties":{"number":{"type":"integer","minimum":1},"name":{"type":"string"}}},"seasons":{"type":"array","items":{"type":"object","additionalProperties":false,"required":["number","name"],"properties":{"number":{"type":"integer","minimum":1},"name":{"type":"string"}}}}}},"StartPlannedEpisodeRequest":{"type":"object","additionalProperties":false,"required":["episodeId"],"properties":{"episodeId":{"type":"string","minLength":1,"maxLength":200,"description":"The Series Bible episode's id: `plannedEpisodes[].episodeId` in the episode listing."}}},"StartPlannedEpisodeResponse":{"type":"object","additionalProperties":false,"required":["created","script","scriptUploadId"],"properties":{"created":{"type":"boolean","description":"False when the episode was already written: the script that covers it comes back."},"script":{"$ref":"#/components/schemas/WriterScript"},"scriptUploadId":{"type":"string","description":"The draft to write into (for an episode already written, its latest version)."}}},"ScriptContentWriteRequest":{"type":"object","additionalProperties":false,"required":["content"],"properties":{"content":{"type":"string","maxLength":500000,"description":"The whole screenplay text the version should now hold."},"expectedContent":{"type":"string","maxLength":500000,"description":"The text you last read: the write lands only if the version still holds exactly this."}}},"ScriptContentWriteResponse":{"type":"object","additionalProperties":false,"required":["scriptUploadId","status","wordCount","pageCount"],"properties":{"scriptUploadId":{"type":"string"},"status":{"type":"string","description":"`draft`, or the locked version's status."},"wordCount":{"type":"integer","minimum":0},"pageCount":{"type":["integer","null"]}}},"FinishDraftRequest":{"type":"object","additionalProperties":false,"properties":{"coverage":{"type":"string","enum":["adopt","fresh"],"description":"`adopt` carries the last passing coverage forward; `fresh` runs new coverage before the screenplay is prepared. Leave it out for the recommendation only."}}},"FinishDraftResponse":{"oneOf":[{"type":"object","additionalProperties":false,"required":["status","scriptUploadId","carryover"],"properties":{"status":{"type":"string","enum":["judged"]},"scriptUploadId":{"type":"string"},"carryover":{"type":"object","additionalProperties":false,"required":["available","base","change","recommendation","reason"],"properties":{"available":{"type":"boolean","description":"Whether an earlier version has passing coverage to carry forward."},"base":{"type":["object","null"],"description":"That report: `reportId`, `scriptUploadId`, `versionNumber`, `overallScore`, `gateThreshold`."},"change":{"type":["object","null"],"description":"How much changed since: `addedLines`, `removedLines`, `changedFraction`."},"recommendation":{"type":"string","enum":["adopt","rerun"]},"reason":{"type":"string"}}}}},{"type":"object","additionalProperties":false,"required":["status","scriptUploadId","versionStatus","coverage","script"],"properties":{"status":{"type":"string","enum":["finished"]},"scriptUploadId":{"type":"string"},"versionStatus":{"type":"string","description":"The locked version's status, e.g. `v2`."},"coverage":{"type":"string","enum":["adopt","fresh"]},"script":{"$ref":"#/components/schemas/WriterScript"}}}]},"CoverageVersionsResponse":{"type":"object","additionalProperties":false,"required":["artifactId","scriptUploadId","versions"],"properties":{"artifactId":{"type":"string","description":"The id you addressed."},"scriptUploadId":{"type":"string","description":"The version it resolved to."},"versions":{"type":"array","description":"Newest first, across every version of the screenplay.","items":{"type":"object","additionalProperties":false,"required":["id","version","status","focusPrompt","overallScore","scriptUploadId","scriptVersionNumber","coversCurrentVersion","createdAt"],"properties":{"id":{"type":"string","description":"The report id: read it with GET /artifacts/{id}/coverage?reportId=."},"version":{"type":"integer"},"status":{"type":"string"},"focusPrompt":{"type":["string","null"]},"overallScore":{"type":["number","null"]},"scriptUploadId":{"type":["string","null"]},"scriptVersionNumber":{"type":["integer","null"]},"coversCurrentVersion":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"}}}}}},"BlackListAnalysisResponse":{"type":"object","additionalProperties":false,"required":["artifactId","coverageReportId","blackList"],"properties":{"artifactId":{"type":"string","description":"The id you addressed."},"coverageReportId":{"type":["string","null"],"description":"The coverage report analysed: the one asked for, else the latest complete one; null when none."},"blackList":{"type":["object","null"],"description":"Null until it has run, or when professional coverage add-ons are not available for this episode.","properties":{"id":{"type":"string"},"status":{"type":"string","description":"analyzing, complete or failed."},"progressDetail":{"type":["string","null"]},"failureMessage":{"type":["string","null"]},"report":{"type":["object","null"],"additionalProperties":true},"createdAt":{"type":"string","format":"date-time"}}}}}}},"x-status":"foundation","x-api-version":"v1","x-scopes":{"story:read":"Read story projects, plans, jobs, artifacts, and shared discovery surfaces.","story:write":"Create or modify story projects, plans, and jobs.","source:read":"Read source packs and extraction metadata.","source:write":"Create or modify sources and trigger extraction.","artifact:read":"Read generated artifacts, coverage, adaptive soundtrack, sound effects, and durable render status.","artifact:publish":"Mutating + billable artifact ops: refine table reads, recast voices, regenerate adaptive soundtrack, manage sound-effect cues, and finalize/render durable MP3/MP4 outputs.","publishing:read":"Read publishing series, destinations, releases, assets, and target status.","publishing:write":"Create and update publishing series and releases, including durable asset promotion handoffs.","publishing:publish":"Schedule, cancel, and publish promoted releases to configured destinations.","publishing:connect":"Create or update publishing provider configuration and destinations.","publishing:analytics":"Read publishing analytics snapshots.","credits:read":"Read Studio Credit balance and quota.","webhook:write":"Manage webhook endpoints and signing secrets.","account:read":"account:read","account:write":"account:write"},"x-error-codes":[{"code":"authentication_required","description":"No bearer API key was supplied in the Authorization header."},{"code":"invalid_api_key","description":"The API key is malformed, unknown, or does not match the stored secret."},{"code":"api_key_revoked","description":"The API key has been revoked by an account owner."},{"code":"api_key_expired","description":"The API key has passed its configured expiration timestamp."},{"code":"account_disabled","description":"The account that owns this API key has been disabled."},{"code":"ip_not_allowed","description":"The request originated from an IP that is not in the key allowlist."},{"code":"insufficient_scope","description":"The credential does not include the scope required for this endpoint. The endpoint exists — `error.details.missingScopes` names what is missing. On an OAuth connector, disconnect and sign in again approving that access; on an `sh_` key, mint a new key with those scopes."},{"code":"rate_limited","description":"The per-key + per-IP rate limit for this route family was exceeded."},{"code":"idempotency_key_required","description":"The endpoint requires an Idempotency-Key header for safe retries."},{"code":"idempotency_conflict","description":"A request with the supplied Idempotency-Key is already processing or used incompatible parameters."},{"code":"validation_failed","description":"The request payload failed validation. The error message names the offending field."},{"code":"project_not_found","description":"The story project does not exist, was deleted, or is owned by another account."},{"code":"source_not_found","description":"The story source does not exist, was deleted, or is owned by another account."},{"code":"source_type_unsupported","description":"The requested source type is not yet supported by this API phase. Check `/api/v1/capabilities`."},{"code":"source_too_large","description":"The source payload exceeds the documented size limit. See `details.maxBytes`."},{"code":"source_incomplete","description":"A source declared as full context failed its producer-specific completeness proof and was rejected before generation."},{"code":"source_fetch_failed","description":"A URL or PDF source could not be safely fetched (SSRF block, redirect limit, size limit, content-type mismatch, or upstream error)."},{"code":"story_plan_not_found","description":"The story plan does not exist, was deleted, or is owned by another account."},{"code":"story_plan_state_invalid","description":"The story plan is in a state that does not allow the requested operation. See `details.status` and `details.allowed`."},{"code":"story_plan_failed","description":"StoryPlan generation or refinement failed. See `details` for the underlying reason when available."},{"code":"season_run_not_found","description":"The season run does not exist, was deleted, or is owned by another account."},{"code":"season_run_state_invalid","description":"The season run or episode is not at the required lifecycle state. Reload it and follow `nextAction` before retrying."},{"code":"story_job_not_found","description":"The story job does not exist, was deleted, or is owned by another account."},{"code":"story_job_state_invalid","description":"The story job is in a state that does not allow the requested operation (e.g. canceling a job that has already reached a terminal state). See `details.status` and `details.allowed`."},{"code":"artifact_not_found","description":"The story artifact does not exist or is owned by another account."},{"code":"artifact_not_ready","description":"The artifact is not in a state where this operation is valid yet (e.g. requesting an MP4 render before the share link exists). See `details.status`."},{"code":"artifact_generation_failed","description":"The artifact adapter failed to produce a shareable output. The job and artifact rows stay inspectable; see the artifact `failureCode` / `failureMessage`."},{"code":"publishing_series_not_found","description":"The publishing series does not exist, was deleted, or is owned by another account."},{"code":"publishing_destination_not_found","description":"The publishing destination does not exist, was deleted, or is owned by another account."},{"code":"publishing_release_not_found","description":"The publishing release does not exist, was deleted, or is owned by another account."},{"code":"publishing_release_state_invalid","description":"The publishing release is in a state that does not allow the requested operation. See `details.status` and `details.allowed`."},{"code":"publishing_asset_not_ready","description":"The release does not have a finalized, ready asset for the requested publish operation."},{"code":"publishing_asset_stale","description":"The source artifact changed after the release asset was promoted. Finalize the current source before publishing."},{"code":"project_precondition_failed","description":"The project has not reached the development stage this call needs, so nothing was created, queued or charged. `details.stage` names the stage it has not finished (`premise`, `premise_coverage`, `planning`, `full_coverage`; `ready` when the stage is passed and a narrower precondition refused, such as a mood-board render with no image model bound), `details.missingFields` what that stage still lacks, and `details.canPlan` / `details.canStartEpisode` what the project can do now. Finish the named stage — do not retry: the identical call refuses the same way until it is done. `GET /story-projects/{projectId}` reports the same position as `project.workspaceGate` without attempting anything — read it first."},{"code":"spend_subject_unresolved","description":"The project gate could not find the artifact, screenplay version, episode or project this paid call is for under your account, so it refused instead of guessing — nothing was created, queued or charged. `details.subject` is the kind it looked for (`artifact`, `script_upload`, `script`, `project`) and `details.id` the id. Check the id; retrying the same one refuses the same way."},{"code":"insufficient_credits","description":"The account does not have enough Studio Credits to reserve this job. See `details.required` and `details.available`. A story job's refusal also carries `details.jobId`: null when the job was refused before it was created, or the id of the one job the Idempotency-Key is bound to — retrying that key after topping up resumes that job, it never creates another."},{"code":"internal_error","description":"The Sleeper Hit Story API failed unexpectedly. Retrying with the same Idempotency-Key is safe."},{"code":"trailer_not_found","description":"No trailer with that id, or it belongs to another account."},{"code":"trailer_state_invalid","description":"The trailer exists and the request is well-formed, but the trailer, its beat, its cast or its plates are not in a state that allows it — a plan approved outside `draft`, a take chosen or a beat rendered outside `review`/`complete`, an export before the trailer is finished, a finish while a call has no selected take, a narrated cut with no narrator, a re-voiced take selected (it is never played), a beat with no caption or footage-only when text was asked for, or a render the compile blocked. Nothing was reserved. A render refusal carries `details.refusal` = `{ code, action, fix }` — `action` is what unblocks it (`recast_figure`, `add_signature_items`, `recast_voice` or `remake_location_plate`) and `fix` names the MCP tool and CLI command that take it — plus `details.blocked[]`, one entry per beat the compile could not render. The identical request succeeds once the trailer is put right."},{"code":"pitch_deck_not_found","description":"No pitch deck with that id, or it belongs to another account."},{"code":"pitch_deck_state_invalid","description":"The deck exists and the request is well-formed, but the deck, its chapter, its cast or its plates are not in a state that allows it; `details.refusal` names the fix. State, not payload."},{"code":"pitch_deck_version_not_found","description":"The deck has no version with that number. Read its versions (`GET /pitch-decks/{jobId}/versions`) for the numbers it has; a version is kept each time the deck's master is published."},{"code":"trailer_version_not_found","description":"The trailer has no version with that number. Read its versions (`GET /trailers/{jobId}/versions`) for the numbers it has; a version is kept each time the trailer's master is published."},{"code":"story_job_not_resumable","description":"The job exists but has no resumable checkpoint — it either finished or failed past the point a resume can start from."},{"code":"script_not_found","description":"No screenplay with that id, or it belongs to another account."},{"code":"cast_precondition_failed","description":"A cast is not ready, so nothing was created, quoted or charged. On the casting step: nobody castable, nobody with a description, or no image model bound. On a table_read job: the project cast canon (+ the request's voiceMap) does not cover every character (`details.reason`: `no_cast_canon`, `cast_canon_collision`, `cast_canon_incomplete`), an adopted `scriptUploadId` is a draft or not cast (`script_not_finalized`, `script_not_cast_ready`), or a resumed job's own screenplay voices someone uncast (`episode_cast_incomplete`); `details.missing` lists each member and what it lacks (`voice`, and unless `details.audioOnly`, `face`, `body`, `sheet`). Complete the cast — do not retry: the identical call refuses the same way until it is done. `GET /story-projects/{projectId}` reports the canon half as `project.tableReadReadiness`."},{"code":"table_read_not_prepared","description":"The read's table read is not paid for yet, so nothing was started, rendered or charged. A live read, a durable audio render (finalize) and the read's sound effects and music wait until it is PREPARED — charged once per screenplay version: `POST /scripts/{scriptUploadId}/table-read/prepare` (MCP `prepare_table_read`, CLI `script table-read prepare`), or Continue to Table Read in the Writing Lab. A read made by a Story API job is paid by that job. Prepare it, then retry."},{"code":"table_read_not_failed","description":"The read's creative analysis is not in a FAILED state, so there is nothing to retry. Only a read whose analysis failed can be re-run (`POST /scripts/{scriptUploadId}/table-read/retry-analysis`, MCP `retry_creative_analysis`, CLI `script table-read retry-analysis`, or the Writing Lab failed card's Try Again). Nothing was changed or charged."},{"code":"voice_clone_not_found","description":"No voice clone with that id, or it belongs to another account."},{"code":"mood_board_full","description":"The mood board already holds the maximum number of frames that can bind a render. Drop one before keeping another."},{"code":"fal_account_locked","description":"The upstream generation provider has locked the studio account, usually for billing. Nothing was charged."},{"code":"location_plate_not_found","description":"The script stores no location plate at that key. Read the location plates (`GET /artifacts/{artifactId}/location-plates`) for the keys it does store; a plate the script has not made yet is made when a video renders."},{"code":"location_plate_precondition_failed","description":"The plate cannot be remade yet, and nothing was priced or charged: it is not ready, a remake is already running for it, the plate it is cropped from is missing, or no image model is bound. The message says which; the identical call succeeds once the plate is put right."},{"code":"voice_not_found","description":"No voice with that id in your library (`GET /voices` lists the ones you own), or a voice you may not use: another writer's cloned or designed voice is never previewed, cast, pinned or migrated onto. A catalog voice can be previewed and assigned, but only your own voices can be renamed or deleted."},{"code":"voice_design_not_found","description":"No cast voice design with that id in this project. `designId` comes from the answer of `POST /story-projects/{projectId}/cast-voice/design` for the same project."},{"code":"voice_in_use","description":"The voice is still pinned in a cast canon or spoken in a read, so it was not deleted. `details.uses` names where; repoint those first, or send the delete again with `confirmed: true` to delete it anyway (those characters will have no voice until they are recast)."},{"code":"cast_voice_precondition_failed","description":"The read or project is not ready for this cast voice operation, and nothing was priced or charged: the character has no lines to speak, there is no earlier voice to restore, or the read has no finished recording to re-voice. The message says which and what to do first."},{"code":"script_in_use","description":"The script was not deleted: something live still depends on it. `details.uses` names each (a published or scheduled release, a running job, a season-run slot, a pitch deck or a trailer); unpublish, cancel or delete those first, then delete the script again."},{"code":"script_state_invalid","description":"The script's state refuses this change: the most common is deleting a script's only version, which leaves no script — delete the whole script instead (DELETE /scripts/{scriptId}). The message says which."},{"code":"plan_limit_reached","description":"Your plan does not allow this: an upload that would take you past the plan's Writers Lab page allowance (the message gives the pages in use and the pages this upload adds) or a second version of a script on the free plan, or a second screenplay coverage report on the free plan (it includes one). `details` carries the plan and the numbers. Upgrade the plan, or delete scripts you no longer need, then try again."},{"code":"usage_limit_reached","description":"This month's cap on your coverage reports (screenplay, Series Bible premise and full, outline) or table-read generations (scene music, scene atmosphere, backdrop prompts) is used up, so nothing was queued or charged. `details` carries `feature` (`COVERAGE` or `TABLE_READ`), `limit`, `used` and `resetsAt` (the first of next month, 00:00 UTC). It is state, not a rate: do not retry before `resetsAt` — the same call refuses the same way until then."},{"code":"cast_voice_casting_coverage_failed","description":"The candidate voice you chose FAILED its casting coverage (a model listened to the candidates and scored each against the brief), so nothing was cloned or pinned. `details.castingCoverage` carries its score, the bar, why (`flags`, `gateReasons`), the candidate it recommends instead and, when every candidate failed, a revised brief to design again from. Choose the recommendation, design again, or send `acknowledgeCastingCoverage: true` only once the writer has heard the failure and chosen it anyway."},{"code":"cast_portrait_casting_coverage_failed","description":"The headshot you chose FAILED its casting coverage (a model looked at the candidates and scored each against the look and the Series Bible), so nothing was pinned. `details.castingCoverage` carries its score, the bar, why (`flags`, `gateReasons`), the headshot it recommends instead and, when every one failed, a revised look to draw again from. Choose the recommendation, draw again, or send `acknowledgeCastingCoverage: true` only once the writer has seen the failure and chosen it anyway."},{"code":"voice_retired","description":"A voice its provider has retired (it can no longer speak) is in the way, and nothing was queued, charged or changed. A finalize, export, render, live read, re-voice or preview that would speak it refuses before anything is synthesized; a cast, pin or choose refuses to add it; a migration refuses a replacement whose provider is retiring or retired. `details.voices` names each voice (`character`, `voiceId`, `voiceName`, `provider`, `retiresAt`). Migrate those voices to a new voice (`POST /voice-migrations`; MCP `migrate_voice`; CLI `voices migrate`), then retry."},{"code":"voice_consent_required","description":"A CLONED voice has no consent on record from the person whose voice it is (it was made before consent records began), so nothing was cast, pinned, previewed, spoken or charged. `details.voices` names each (`voiceId`, `name`). Mint a consent link (`POST /voices/{voiceId}/consent-link`; MCP `create_voice_consent_link`; CLI `voices consent <voiceId>`) and give it to that person; once they confirm, retry. A Pro clone with no consent from any recording session cannot be trained."},{"code":"voice_consent_revoked","description":"The speaker of a cloned voice revoked their consent, so the voice is never used for anything new (work already published stays up). Nothing was cast, pinned, previewed, spoken, recorded or charged. `details.voices` names each (`voiceId`, `name`, `revokedAt`). Choose another voice; revoking is final."},{"code":"account_email_mismatch","description":"`confirmEmail` is not this account's email address, so no deletion was scheduled. Ask the account owner for the address on the account."},{"code":"account_deletion_refused","description":"This account cannot delete itself (`message` says why): it is an admin account, it hosts workshop cohorts other members belong to, or it owns a live organization (transfer or delete it first). Also returned when cancelling a deletion whose purge has already started. Nothing was changed."},{"code":"project_not_restorable","description":"The project cannot be restored: it is not deleted, its 30-day restore window has passed, or its purge has started (`message` says which). Its episodes are already in the library as private scripts."},{"code":"content_refused","description":"The request asks for, or what would be published or shared contains, content our rules never allow (sexual content involving anyone under 18, or a real person, however long ago they lived, shown in a sexual or intimate way), so nothing was made, saved, published, shared or charged. It is not retried. Rephrase or remove that content."},{"code":"safety_hold","description":"Publishing and sharing are paused on this account while a review is open, so nothing was published or shared. Writing is not affected. Write to help@sleeperhit.studio if you think this is a mistake."},{"code":"safety_check_unavailable","description":"The content safety check that runs before anything is published or shared could not run, so nothing was published or shared. Retry in a few minutes; a queued publish retries on its own."},{"code":"likeness_consent_required","description":"The picture or video would show a real, identifiable person who is living or died within the last 70 years (`details.people` names them). A real person's likeness needs their approval, or their estate's, so nothing was made or charged. Mint a link for them to approve (POST /likeness-consents/links; the account holder approves their own likeness in the dashboard), then retry; or describe a fictional character instead. Fictional characters, including ones who share a name with real people, and long-dead historical figures are not affected, and nobody real is ever shown in a sexual or intimate way."},{"code":"reference_image_not_owned","description":"A face or likeness reference (a portrait or body reference, a cast canon picture, a roster portrait) must be a picture stored in your own account: one you uploaded, or one made in your own projects. A link to anywhere else, or to another writer's picture, is refused (`details.field` names it), and nothing was changed or charged. Use a picture from your library or one of your cast's portraits."},{"code":"likeness_consent_not_found","description":"No likeness approval with that id belongs to this account. List them with GET /likeness-consents."},{"code":"character_not_found","description":"Nobody in the project's cast canon or Series Bible answers to that name (any name or alias counts). Read `GET /story-projects/{projectId}/cast-look` for the people the project has, or add the character to the Series Bible first. Nothing was changed or charged."},{"code":"portrait_design_not_found","description":"No portrait design with that id in this project. `designId` comes from `POST /story-projects/{projectId}/cast-portraits/design` (confirmed) or `GET .../cast-portraits/designs` for the same project."},{"code":"series_bible_version_conflict","description":"The Series Bible changed since the version this edit was based on (`details.expectedVersion` vs `details.currentVersion`), so nothing was saved. Read the character again and send the edit with the current version, or omit `expectedVersion` to edit whatever is saved now."},{"code":"show_cap_reached","description":"A recurring show's next paid step would cross one of its spending caps, so it was not taken: `details.scope` is `episode` (the episode cap), `month` (the monthly cap, in the series' timezone) or `unset` (the show has no caps, so it spends nothing). `details.cap`, `details.credits` (the step's quote) and `details.wouldSpend` give the numbers. Raise the cap with the creator (`PATCH /publishing-series/{seriesId}` `episodeCreditCap` / `monthlyCreditCap`) or wait for the next month; do not retry — the same step refuses the same way."},{"code":"not_a_sign_in_key","description":"A sign-out (`DELETE /device/credential`) was made with a credential no browser sign-in minted — a key made on the API page, or a connector's OAuth grant — so nothing was revoked and the credential keeps working. Revoke a key, or disconnect a connector, on the API page (`/dashboard/api`)."},{"code":"not_found","description":"No endpoint at that path. This is a routing problem, not an authentication or method one — check the path against the API reference."},{"code":"method_not_allowed","description":"The path exists but does not accept that HTTP method. The `Allow` response header lists the methods it does accept."}],"x-rate-limits":{"default":{"requestsPerMinute":120,"scope":"per API key + IP + route family"},"elevated":{"requestsPerMinute":600,"scope":"per API key + IP + route family"}}}