# Sleeper Hit Studio > A story-creation and production workspace for developing, writing, performing, packaging, and publishing narrative work. Start from an initial premise, notes, research, or an existing screenplay. An existing screenplay is not required. Status: production-stable identity, auth, credits, discovery, source ingestion, project Series Bibles, conversational season runs, StoryPlans, story jobs, live table reads, pitch decks, and post-creation artifact mutations. Remaining artifact adapters and webhooks ship in follow-on phases. Always read `/api/v1/capabilities` before integrating a flow. ## Capability manifest - [`https://sleeperhit.studio/capabilities.json`](https://sleeperhit.studio/capabilities.json) — the single authoritative machine-readable manifest for the MCP connector (authenticated + public), the Story API, and the CLI. Versioned, unauthenticated, and it states the auth requirement and scopes for every capability. Read it first. ## No Sleeper Hit account yet? Without an account you can: - Search opted-in creator profiles, project summaries, and creator-approved public assets through the public MCP server or the public discovery API. - Read every published contract: the capability manifest, OpenAPI spec, docs site, llms.txt, and the CLI and MCP tool maps. - Open public share links (table reads, pitch decks, trailers) a creator has already published. An account is required to: - Create story projects, attach source material, and develop a project Series Bible. - Plan, generate, and revise screenplay installments, table reads, pitch decks, and trailers. - Spend Studio Credits on generation, rendering, casting, scoring, and finalization. - Publish durable releases to the first-party RSS feed and connected destinations. Anyone 18 or older can sign up at https://sleeperhit.studio/signup, where they confirm their age and accept the Terms of Service (https://sleeperhit.studio/terms) and Privacy Policy (https://sleeperhit.studio/privacy). An agent cannot create an account or accept the Terms for a user. Hand the human the link that matches them, then stop: - Business and brand storytelling (Companies, agencies, and brand or marketing teams producing narrative work.) — https://sleeperhit.studio/signup?intent=business - Independent creator (Solo creators and small studios publishing their own shows, podcasts, and short-form video.) — https://sleeperhit.studio/signup?intent=creator - Screenwriter (Screenwriters developing scripts, pilots, features, and pitch material.) — https://sleeperhit.studio/signup?intent=writer ## MCP endpoints - **Authenticated creation and production:** `https://sleeperhit.studio/mcp` — OAuth-protected Streamable HTTP. This is the preferred remote connector for Codex and other supported MCP clients. - **Public discovery:** `https://sleeperhit.studio/api/mcp` — unauthenticated and read-only. It exposes only opted-in creator profiles and creator-approved public assets. - Do not use `/api/mcp` for private projects or generation; it intentionally has no creation tools. ### If you are an agent connecting a user to Sleeper Hit You cannot complete the sign-in from inside a chat or a sandboxed shell: the browser cannot reach your sandbox's callback, so your user approves and no token arrives. Do not run the login yourself, and do not retry it. Ask your user to do the one-time sign-in in their app: - **Codex:** They run `codex mcp login sleeperhit` in their own terminal, outside the agent sandbox, approve in the browser, then start a new Codex session so the tools load. - **Claude Code:** They run `/mcp`, choose sleeperhit, then Authenticate. - **ChatGPT:** They choose Connect on the app, then Authorize. After that the app renews access by itself. The app loads its tool list when a session starts, so if the tools don't appear, ask your user to start a new session. If your user can't do a browser sign-in, they can create an API key at https://sleeperhit.studio/dashboard/api (Dashboard → Developers), set it as SLEEPERHIT_API_KEY and add it as a bearer token: `codex mcp add sleeperhit --url https://sleeperhit.studio/mcp --bearer-token-env-var SLEEPERHIT_API_KEY`. Never ask your user to paste a key into the chat. Claude Code takes the same key as a header: `claude mcp add --transport http sleeperhit https://sleeperhit.studio/mcp --header "Authorization: Bearer $SLEEPERHIT_API_KEY"`. Every client's setup: https://sleeperhit.studio/mcp#api-key Verify with the `whoami` tool. It answers `source: "oauth"`, or `source: "api_key"` with the key's scopes; to create and generate, a key needs story:write and source:write. ### Codex remote OAuth ```bash codex mcp add sleeperhit --url https://sleeperhit.studio/mcp codex mcp login sleeperhit ``` - `codex mcp add` registers the server. `codex mcp login` starts OAuth and opens or directs the user to browser authorization. The user runs it in their own terminal, outside any agent sandbox. - Complete authorization in the browser. Authentication success and tool-roster refresh are separate events: Codex loads a server's tools when a session starts, so start a new Codex session if Sleeper Hit tools do not appear. - Inspect configuration with `codex mcp get sleeperhit --json` or `codex mcp list --json`. - After connection, call `whoami`, `get_agent_guidance`, and `get_credits` before committing credits. ### Authenticated workflow groups - **Orientation and account:** Confirm the connected identity and load live backend guidance before choosing a workflow. Start with `whoami` → `get_agent_guidance`. - **Credits and usage:** Check the available Studio Credit balance before any generation or rendering work. Start with `get_credits`. - **Your data: download it, or delete the account:** Only when the writer asks: build "Download my data" and hand them the link; or show what deleting removes and the credit balance, schedule the deletion, or cancel it. Start with `get_account_export` → `get_account_deletion`. - **Projects and source material:** Create or resume a project and ground it in notes, URLs, PDFs, research, or an existing script. Start with `list_projects` → `create_project`. - **Series Bible and development:** Establish canon, format, characters, world, episode architecture, and foundational coverage. Start with `get_series_bible` → `get_mood_board` → `get_cast_canon`. - **Planning and screenwriting:** Create a reviewable plan, generate an installment, inspect real pages, and revise the same artifact. Start with `create_plan` → `get_plan`. - **Pitch deck rendering:** Approve a pitch deck plan into a priced 480p render, review and choose takes chapter by chapter, then finish the deck at 720p (or 1080p) and export it. Start with `get_pitch_deck_chapters` → `approve_pitch_deck_plan`. - **Trailer rendering:** Approve a trailer plan into a priced 480p render, review and choose takes, then finish the cut at 720p (or 1080p) and export it. Start with `get_trailer_beats` → `approve_trailer_plan`. - **Table reads and audio:** Cast, perform, score, add effects, and create durable audio from a screenplay-backed table read. Start with `get_cast` → `get_table_read_script`. - **Video rendering:** Direct theater motion and render a durable video from a reviewed artifact. Start with `get_artifact` → `direct_theater_motion_beat`. - **Coverage and revision:** Read existing coverage first, request analysis when needed, and revise before final packaging. Start with `get_coverage` → `get_brand_coverage`. - **Seasons and automation:** Operate controlled multi-installment season workflows. Start with `list_season_runs`. - **Recurring shows:** Set up and run a recurring podcast or show: a cast and format, a source it reads (a feed, a site, a repository, Hacker News), a schedule, spending caps the user agrees, one preview episode the user hears, then live — each episode made unattended from new source material and credited in its show notes. The user owns the show, its episodes and its feed. Start with `create_project` → `save_series_bible` → `create_publishing_series` → `preview_show_sources` → `get_show_quote` → `produce_show_episode` → `update_publishing_series`. - **Publishing and analytics:** Prepare destinations and releases, then publish only after the user approves the exact externally visible action. Start with `list_publishing_series` → `list_publishing_releases`. ## Discovery - [Capability manifest](https://sleeperhit.studio/capabilities.json): authoritative versioned map of every MCP / Story API / CLI capability with its auth requirement and scopes. No account needed to read it. - [OpenAPI 3.1 spec](https://sleeperhit.studio/api/v1/openapi.json): full machine-readable contract — operation IDs, schemas, security, error model. - [Live feature availability](https://sleeperhit.studio/api/v1/capabilities): which artifacts, sources, and docs surfaces are live right now. Needs an API key (`story:read`). - [Agent guidance](https://sleeperhit.studio/api/v1/agent-guidance): backend-managed workflow guidance for Story API, CLI, and MCP agents, including MCP server instructions plus tool/resource descriptions. Needs an API key. Read this before creating or refining table reads. - [Examples](https://sleeperhit.studio/api/v1/examples): cURL + Node recipes for the polling-only happy path, error recovery, and idempotent POSTs. - [Docs site](https://sleeperhit.studio/docs): crawlable human docs plus API / CLI / MCP maps. - [Agent docs index](https://sleeperhit.studio/agent-index.json): page, section, operation, UI, CLI, and MCP discovery index for agents. - [Four-surface contract](https://sleeperhit.studio/docs/surface-contract.json): generated UI / Story API / CLI / MCP contract snapshot. - [Agent integration guide](https://sleeperhit.studio/llms-full.txt): long-form prose for agents — auth, idempotency, rate limits, error recovery, what is and is not live. ## Authentication - Remote MCP clients should prefer OAuth at `/mcp`, run by the client app. An agent asks its user to sign in from their app and never runs the login itself (see "If you are an agent connecting a user to Sleeper Hit"); a user who can't do a browser sign-in sends a Sleeper Hit API key to `/mcp` as the bearer token instead. - Create production API keys at https://sleeperhit.studio/dashboard/api. Tokens are shown once. - Send `Authorization: Bearer sh__` on every request. - Keys are scoped. Initial scopes: `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`. - Provider keys (e.g. OpenAI) are managed separately by Sleeper Hit and are not exposed to API customers. ## Conventions - Errors use a stable envelope: `{ error: { code, message, requestId } }`. Match on `code`, not `message`. - Per-request `X-Request-Id` is echoed in response headers and inside error envelopes — quote it when reporting issues. - Rate limit headers (`RateLimit-Limit`, `RateLimit-Remaining`, `RateLimit-Reset`) are returned on every response. `429` responses include `Retry-After`. - Idempotent POSTs require `Idempotency-Key`. Retry-safe with the same key; the API replays the original response. - Studio Credits are the single shared meter. Read balance from `/api/v1/credits` before triggering a job. - Development flow warnings are non-blocking for one-off artifacts: complete the full project Series Bible before screenplay generation and review coverage afterward. Season runs add hard gates: all imported episode briefs and the complete map require human confirmation; every episode plan, failed-coverage revision, and RSS publish action requires a later explicit human decision; durable audio is held until coverage passes. - Anything that reaches the first-party RSS feed is a human boundary, whichever route you take: `POST /season-runs/{runId}/advance` with `{ action: "publish" }`, `POST /publishing-releases/{releaseId}/publish`, `POST /publishing-releases/{releaseId}/schedule`, and — because a date there enqueues the same publish — `POST /publishing-series/{seriesId}/releases` or `PATCH /publishing-releases/{releaseId}` carrying a non-null `scheduledAt`. Each requires `userConfirmed: true` after the user approves that exact release. Scheduling counts because it publishes later, unattended; clearing a schedule (`scheduledAt: null`) does not. The `publishing:publish` scope authorizes the act; it does not stand in for the user agreeing to it. - The one thing that does stand in for it is a show's STANDING APPROVAL, granted once by the owner (Publishing tab, or `PATCH /publishing-series/{seriesId}` with `{ standingApprovalKeyId, userConfirmed: true }`; arming a cadence grants one too). It is bound to ONE API key (never an OAuth connection) and covers only that show: `POST /story-plans/{planId}/approve` for a table-read plan in the show's project, and `POST /publishing-releases/{releaseId}/publish` for one of its audio releases — each created AFTER the grant, never a season run, never while the show is paused or archived. Every other caller, and every other route above, still needs `userConfirmed: true`. `standingApprovalKeyId: null` revokes it and turns the cadence off. A call no grant covers is a 400 `validation_failed` with `details.reason` (`season_run | not_table_read | not_audio | no_standing_approval | not_bound_key | series_not_exercisable | created_before_grant`) — none clears on retry, so stop and tell the owner. Every use is recorded (the plan's `approvedVia`, the release's `publishedVia` / `publishedByKeyId`), and a job from a standing-approved plan may request only `table_read`. Recurring series are audio-only. - A recurring show spends only within its CAPS, agreed with the owner: `episodeCreditCap` (one episode) and `monthlyCreditCap` (a calendar month's episodes, in the series' timezone), set on `PATCH /publishing-series/{seriesId}` with `userConfirmed: true`. Arming a cadence is refused without both, and refused while the project cannot make an episode (its Bible gate, its cast canon). `GET /publishing-series/{seriesId}/quote` gives one episode's quote from the pricing library and the caps suggested from it (quote + 25%; the cadence's episodes a month × that). Each paid step is priced by the same library and charged through the same ledger as any other spend; a step that would cross a cap is not taken (`show_cap_reached`), a reroll the caps would refuse is not made, and a failed step refunds. `publishPolicy: hold` leaves each episode `ready` for review instead of publishing it. Every episode that is not made notifies the owner; three in a row pause the show (`status: paused`, `pausedReason`) until the owner resumes it. - A show goes live only after a PREVIEW (owner, 2026-10-09): `POST /publishing-series/{seriesId}/episodes` makes one episode now — quoted without `userConfirmed`, charged and queued with it — held for the owner to hear and never published; arming refuses until one is `ready`. So: create the show unarmed, `POST /publishing-series/{seriesId}/source-preview` to read its sources now (free, guarded fetch; what an episode would get, what is already covered, which sources fail), set caps from the quote, make the preview, let the owner listen, then `PATCH` `cadenceEnabled: true` with `userConfirmed: true`. `GET /publishing-series/{seriesId}/episodes` is the run history: every episode with how it ended, what it cost and what it covered. ## Endpoints (live today) - `GET /api/v1/capabilities` — requires scope `story:read`. Capability manifest. - `GET /api/v1/agent-guidance` — requires scope `story:read`. Current backend-managed agent workflow guidance. - `GET /api/v1/credits` — requires scope `credits:read`. Studio Credit summary, plus `costs`: what each operation costs, the list the Billing page shows (a rate names its unit in `per`; a pitch deck and a trailer are priced per second; `ops` names the ledger op keys a line's charges carry). - `GET|POST /api/v1/account/export` — `account:read`. "Download my data": POST (with `Idempotency-Key`) builds one zip of everything the account holds (scripts as Fountain and Final Draft, projects' documents as JSON and Markdown, coverage as JSON, Markdown and PDF, links to every audio and video master, shows and feeds, consent records, credits and payments, voices, chats, forum posts); GET says where it stands and, when `ready`, gives `download.url`, a signed link that works for an hour (read again for a new one). The archive is deleted 7 days after it is built. Free. - `GET /api/v1/account/deletion` (`account:read`), `POST /api/v1/account/deletion` and `DELETE /api/v1/account/deletion` (`account:write`). Delete the account and everything it made (scripts, projects, reads, decks, trailers, shows and feeds, voices here and at the voice provider, stored files, keys, connected apps). POST needs `confirmEmail` (the account's email; 400 `account_email_mismatch`) and `Idempotency-Key`, and schedules the purge 7 days out; the owner is emailed and can cancel with DELETE until it starts. Unused credits are forfeited at the purge, so GET first: it shows the balance and any top-up still inside its 14-day refund window (request the refund first). Payment records are kept without a name or email. 409 `account_deletion_refused` for an admin, a workshop host or an organization owner. Free. - `POST /api/v1/story-projects` — `story:write`. Create a project workspace. - `GET /api/v1/story-projects` — `story:read`. Cursor-paginated project list. - `GET|PATCH|DELETE /api/v1/story-projects/{id}` — read, update, delete a project. DELETE hides it at once and keeps it restorable for 30 days (`restorableUntil`; `GET /api/v1/story-projects/deleted`, `POST /api/v1/story-projects/{id}/restore`, 409 `project_not_restorable` after), then purges it with its Bible, cast, mood board, sources, plans, season runs, show, decks and trailers; its episodes go with it: private during the window, restored with the project or deleted at the purge (`episodesAtPurge`). PATCH `seasonCadence: "monthly"` files every episode made from then on into the season named for the month (UTC) its source was made (`originatedAt` on the source, else when it was added, else when the episode was made), adding the month after the last season when it is new; null goes back to numbered seasons. It keeps the rest of metadata (the seasons list lives there) and is not combined with `metadata`. - `GET|PATCH|POST /api/v1/story-projects/{id}/series-bible` — read, save, or sidecar-generate the project-level Series Bible used as durable canon across UI/API/CLI/MCP. Preferred first gate before screenplay generation. - `GET|POST /api/v1/story-projects/{id}/series-bible/coverage` — read or queue a versioned Series Bible-rubric report in the shared coverage lifecycle, calibrated against 72 provenance-backed craft profiles. Series script coverage waits for a current 7.0+ report. - `GET|POST /api/v1/story-projects/{id}/season-runs` — list or create a persistent season run from the current Series Bible episode map. POST requires `story:write` + `publishing:write` because it may create the audio series/RSS destination. Creation imports unconfirmed briefs and never starts generation. - `GET /api/v1/season-runs/{runId}` — resume the conversation from durable `nextAction`, linked episode state, planning attempt count, and coverage-attempt audit history. - `PATCH /api/v1/season-runs/{runId}/episodes/{episodeId}` — save one discussed episode brief; every edit requires the latest `expectedMapVersion`, and `confirmed=true` is reserved for explicit user agreement. - `POST /api/v1/season-runs/{runId}/approve` — human approval that locks the exact reviewed `expectedMapVersion`; stale versions are rejected. - `POST /api/v1/season-runs/{runId}/advance` — requires `story:write` + `artifact:publish` + `publishing:write` + `publishing:publish`; performs one durable orchestration step and stops for plan approval, an approved replan/same-report coverage retry/revision, and RSS publication. - `GET|PATCH /api/v1/story-projects/{id}/cast-canon` — read or save pinned project character portrait, body, character-sheet, voice, `aliases` and `signatureItems` canon (pinned assets are never re-rendered). Declare a script cue as an alias of the canon person it names (or answer `sameAs` / `newPersonName` on `PATCH /story-projects/{id}/cast` when a save names the candidates); videos with two or more people need signature items for each. Both answers carry `identity` warnings (one name reaching two people, a shared body figure, no Series Bible appearance, and `unresolvedBibleNames`: a Series Bible name no canon person answers to, like "Foma Stepanovich" beside FOMA — add it to that person's `aliases`). Project-wide character design and visual language live in the Series Bible. - `POST /api/v1/story-projects/{id}/cast-voice/design` — `artifact:publish`. Design a NEW voice for one named cast member (canon name, alias or script cue) from `description`, or from the Series Bible's voice line for them when omitted; up to four candidates, each voiced on the character's OWN screenplay lines (preview urls). METERED, TWO CALLS: without `confirmed` it prices the design (one casting voice per candidate) and draws nothing; with `confirmed: true` it draws and answers `designId` + `candidates`. Every candidate drawn is charged, chosen or not. Requires `Idempotency-Key`. Anyone on the project cast is reached, in an episode or not: a person with no screenplay lines is designed on `sampleText` (the line a deck gives them), else on a passage the designer writes. 409 `cast_voice_precondition_failed` with no description and no Bible voice line. - `POST /api/v1/story-projects/{id}/cast-voice/choose` — `story:write`. Pin one candidate (`{ designId, candidate, voiceName? }`) to the cast canon and the voice library; `reads` lists the finished reads still speaking the old voice: re-voice the character in each with `POST /api/v1/artifacts/{artifactId}/revoice` (priced, two calls; the old recording stays restorable). Free. Requires `Idempotency-Key`. `GET .../cast-voice/designs?character=` (`story:read`) lists the project's designs with their previews and each design's casting coverage. - `PATCH /api/v1/story-projects/{id}/cast-voice` — `story:write`. Pin an EXISTING voice (`{ character, voiceId, voiceName?, provider?, gender? }`) on anyone on the project cast, in an episode or not (a person only 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. Free; `previous` is the voice it replaced, `reads` the finished reads still on another voice. A provider retiring its voices is refused (409 `voice_retired`). - CASTING COVERAGE — `GET /api/v1/story-projects/{id}/cast-voice/designs/{designId}/coverage` (`story:read`) and `POST` the same path (`story:write`, `Idempotency-Key`, optional `{ focusPrompt }`). A model LISTENS to a design's candidate takes blind, beside the voices already cast in the project, and scores each against the brief and the Series Bible (emblematic fit, truthful performance, period register, accent honesty, clean audio, word accuracy from a blind transcript, distinctness from the cast, also MEASURED by speaker embedding against every chosen voice — at or above `distinctnessThreshold` a candidate fails); it ranks them, `recommended` names the best that cleared the bar, `cast.pick` the candidate the cast plan takes so no two of the project's voices measure as the same speaker, and when every one failed `report.redesign.revisedBrief` is the brief to design again from. It runs by itself, free, when a KEY character's design is drawn; POST judges again with the cast as it stands now (free). Choosing a candidate in `failingCandidates` is refused (409 `cast_voice_casting_coverage_failed`) unless `acknowledgeCastingCoverage: true` — only once the writer has heard it and chosen it anyway. - `POST /api/v1/story-projects/{id}/cast-portraits/design` — `artifact:publish`. Draw N HEADSHOT candidates (default 3, at most 4) for one named cast member, from the Series Bible's appearance with the person's LOOK after it (`direction`, else their cast canon look direction; `age` likewise). METERED, TWO CALLS: without `confirmed` it prices them (one cast headshot each) and draws nothing; with `confirmed: true` it reserves and queues the draw and answers `designId`. Poll `GET .../cast-portraits/designs?designId=` (`story:read`) until `status` is `complete`; each candidate has a stable `candidateId` and `imageUrl`. Every candidate drawn is charged, chosen or not. `keepFace: true` restyles the person KEEPING THEIR FACE: each candidate is their current headshot edited to the look (hair, grooming, wardrobe, styling) on the model that keeps a face (`restyledFrom` names it); same price, judged and chosen the same; 409 `cast_face_missing` when they have no headshot yet. Requires `Idempotency-Key`. - `POST /api/v1/story-projects/{id}/cast-portraits/choose` — `story:write`. Pin one candidate (`{ designId, candidate }`) as the person's face on the cast canon. A new face clears the canon body shot and sheet drawn from the old one (`cleared`); draw them again with `POST .../cast` for that character. Free. Requires `Idempotency-Key`. A headshot its casting coverage FAILED is refused (409 `cast_portrait_casting_coverage_failed`, `details.castingCoverage`) unless `acknowledgeCastingCoverage: true` — only once the writer has seen it and chosen it anyway. - PORTRAIT CASTING COVERAGE — `GET /api/v1/story-projects/{id}/cast-portraits/designs/{designId}/coverage` (`story:read`) and `POST` the same path (`story:write`, `Idempotency-Key`, optional `{ focusPrompt }`). A model LOOKS at a design's drawn headshots blind, beside the faces already chosen for the cast, and scores each on emblematic fit, period truth (no modern faces or styling), naturalness (a photograph, not a generated image) and distinctness; it ranks them, `recommended` names the best that cleared the bar, and when every one failed `report.redesign.revisedDirection` is the look to draw again from. It runs by itself, free, when a KEY character's headshots are drawn; POST looks again with the cast as it stands now (free). - `GET|PATCH /api/v1/story-projects/{id}/cast-look` — `story:read` / `story:write`. A person's LOOK at project level on the cast canon: `{ character, lookDirection?, age? }` (null clears a field). Every headshot and portrait candidate drawn for them reads it, outranking the Series Bible. Free; it does not re-open Bible coverage. - `GET|POST /api/v1/story-projects/{id}/cast` with `scriptUploadId` — an episode's cast. POST `images` (`face` / `body` / `sheet`) with `force` and `characters` redraws only those pictures and no voices (the Cast page's Redraw: a body shot or sheet drawn from the chosen headshot and checked against it), priced per picture, two calls like any episode cast. GET's `episode.read.behindCanon` names each character the episode's finished read still speaks with another voice than their cast canon voice (the voice the Cast page plays): re-voice them in `episode.read.artifactId` with `POST /api/v1/artifacts/{artifactId}/revoice`. - `POST /api/v1/story-projects/{id}/cast` with `images` and NO `scriptUploadId` — the project cast canon's Redraw: `body` and/or `sheet` for canon people (`characters`), including people no episode names, drawn again from the chosen headshot (kept; `face` is a 400 — draw headshot candidates instead) and written onto the canon. Priced per picture, two calls: confirm with the quote's `quoteId`. GET the cast until `redraw.status` is `complete`; `redraw.pictures` names each new picture and its check against the headshot. - `GET|PATCH /api/v1/story-projects/{id}/series-bible/characters` — `story:read` / `story:write`. One Series Bible character (`?character=` id or name): PATCH `{ character, content: { appearance?, wardrobe?, ... }, expectedVersion?, create? }` changes only those fields and leaves every other character as stored; `expectedVersion` is a compare-and-swap (409 `series_bible_version_conflict`). - `POST /api/v1/story-projects/{id}/sources` — `source:write`. Attach inline text, markdown, URL, or PDF. Set `metadata.sourceContextMode="full"` when the planner and final artifact writer must receive the complete hash-verified retained text; full mode fails instead of clipping. Send `originatedAt` (ISO 8601) with when the material itself was made — a thread's posting time — and a monthly project files the episode by that month. A producer sending one source per item adds `producer` + `externalId`: the project keeps one live source per item and a repeat returns it with `deduplicated: true`, even while it is still PENDING/EXTRACTING (delete it to send the item again, or when it never becomes READY). Producer items are episode material, not canon — the Bible never reads them, and a season pins only items curated into it (never ones sent here). - `GET /api/v1/story-projects/{id}/sources` — list sources for a project. - `GET|DELETE /api/v1/story-projects/{id}/sources/{sourceId}` — read or soft-delete a single source. - `POST /api/v1/story-projects/{id}/story-plans` — `story:write`. Generate a StoryPlan from sources + target + artifact requests; for screenplay/table_read work, read or generate the Series Bible first and review the plan against the canon auto-loaded by the planner. - `GET /api/v1/story-projects/{id}/story-plans` — list plans. - `GET /api/v1/story-plans/{planId}` — read a single plan. - `POST /api/v1/story-plans/{planId}/approve` and `/reject` — `story:write`. Approval is a mandatory human boundary and requires `{ userConfirmed: true }` after review of the exact plan; rejection accepts an optional reason. - `POST /api/v1/story-jobs` — `story:write`. Generate artifacts from an APPROVED plan; reserves Studio Credits and enqueues the runner. - `GET /api/v1/story-jobs` and `GET /api/v1/story-jobs/{jobId}` — `story:read`. List + poll jobs (status, progress, artifacts). - `POST /api/v1/story-jobs/{jobId}/cancel` — `story:write`. Cancel a job that has not started (`PENDING`, `RESERVED`, or `FAILED` before it ever started, which also stops it being resumed) and release reserved credits. A `RUNNING` job, and a `FAILED` job that had started generating, cannot be canceled. Requires `Idempotency-Key`. - `GET /api/v1/story-jobs/{jobId}/artifacts` and `GET /api/v1/artifacts/{artifactId}` — `artifact:read`. Read generated artifacts + their manifest URLs. Add `?revision=N` to fetch a prior revision. - `POST /api/v1/artifacts/{artifactId}/render-video` — `artifact:publish`. Opt-in MP4 render for a table read (free). Requires `Idempotency-Key`. - `POST /api/v1/artifacts/{artifactId}/refine` — `artifact:publish`. Conversational in-place revision. Requires `Idempotency-Key`. Body: `{ instruction, scope?, confirmed? }`. METERED, TWO CALLS: without `confirmed` it prices and changes nothing; with `confirmed: true` it reserves exactly the quote and returns `{ storyJob }` to poll. TABLE_READ_REFINE (8 credits) is reserved only when confirmed. Artifact id + share URLs stay stable. - `POST /api/v1/artifacts/{artifactId}/voice` — `artifact:publish`. Recast one character's voice in place (no new revision, no charge). Requires `Idempotency-Key`. Body: `{ character, voiceId, voiceName, gender?, provider?, pinToCanon? }`. The cast canon person follows (decks and trailers speak with it) unless `pinToCanon: false`; the previous voice and recording are kept (`restore`). Returns the updated `voiceMap`; the cast is readable on `manifest.audio.voiceMap`. - `POST /api/v1/artifacts/{artifactId}/voice/restore` — `artifact:publish`. Undo the newest recast of `character` (or any): the previous voice comes back with the recording the read had before it, and the canon voice while the canon still holds the recast one. Free; restoring again flips back. Requires `Idempotency-Key`. `GET /artifacts/{artifactId}/cast` lists what is `restorable`. - `POST /api/v1/artifacts/{artifactId}/revoice` — `artifact:publish`. Re-voice ONE character in a finished read (`{ character, voiceId?, voiceName?, provider? }`; default: their cast canon voice): only their lines are spoken again, every other line is kept, then the read is mixed again. METERED, TWO CALLS: without `confirmed` it prices (`lines`, `keptLines`) and changes nothing; with `confirmed: true` it reserves and queues the finalize — poll the artifact until `manifest.audio.finalize.status` is `complete`. The previous recording stays restorable. Requires `Idempotency-Key`. - `POST /api/v1/artifacts/{artifactId}/revoice-line` — `artifact:publish`. Re-voice ONE line at a new speed, with a new acting direction, or both (`{ entryIndex, speed?, actingDirection? }`): the line's voice's provider speaks it again — the speed at its own speed setting (Cartesia 0.6–1.5, ElevenLabs 0.7–1.2, Deepgram 0.7–1.5; OpenAI and Hume voices are refused, and so are Eleven v3, Cartesia before sonic-3, a Deepgram Aura-1 voice or a Cartesia professional clone, which have no speed setting), never a time-stretch; the direction (free text; empty or null takes it off) through its own expressive control (OpenAI `instructions`; ElevenLabs Eleven v3 and later, as a leading audio tag; on Cartesia, sonic-3 and newer, as one emotion mapped from the direction's words — Cartesia before sonic-3, Deepgram, Hume and earlier ElevenLabs models read none and are refused) — and every other line is kept, then the read is mixed again. FREE (0 credits), still TWO CALLS: without `confirmed` it quotes and changes nothing (`mode`: `revoice` the line spoken again, `restore_take` a take the line had with that speed and direction brought back, `save` saved for a read with no recording yet); with `confirmed: true` it does it. Requires `Idempotency-Key`. - `POST /api/v1/artifacts/{artifactId}/update-recording` — `artifact:publish`. Update a read's finished recording with the lines changed since it was made (body: `{ confirmed? }`): every line whose words, voice or delivery (emotion, speed, volume, acting direction) changed is spoken again, every other line is kept byte for byte, a line whose only change is the pause after it is stitched again without being spoken, and the read is mixed again. FREE (0 credits, however many lines change), still TWO CALLS: without `confirmed` it quotes and changes nothing (`mode`: `respeak`, `restitch`, `remix`, `record` or `current`; `changedLines`, `respokenLines`, `pauseChanges`, `keptLines`, the changed `lines`); with `confirmed: true` it does it. Requires `Idempotency-Key`. - `POST /api/v1/voices/preview` — `story:write`. Hear any voice say `text`, or a character's first line in a read (`artifactId` + `character`), before assigning it; answers `preview.url`. Free; the same words in the same voice are voiced once (`cached`). Requires `Idempotency-Key`. - `PATCH|DELETE /api/v1/voices/{voiceId}` — `story:write`. Rename one of your library voices (`{ name }`; cast canon people pinned to it carry the new name), or delete it. A delete of a voice still pinned in a cast canon or spoken in a read is refused with 409 `voice_in_use` (`details.uses`) until sent with `?confirmed=true`. - `POST /api/v1/voices/{voiceId}/consent-link` and `POST /api/v1/voices/{voiceId}/revoke-consent` — `story:write`. A cloned voice speaks only with its subject's recorded consent (`consent` in `GET /voices`). `consent-link` mints a 7-day link the person whose voice it is opens to confirm (they read a statement aloud and agree); until then the clone is refused with 409 `voice_consent_required`. `revoke-consent` withdraws it: no new use from then on (409 `voice_consent_revoked`), provider copy deleted within 7 days, published work stays up; final. Both free; require `Idempotency-Key`. - `POST /api/v1/voice-migrations` — `story:write`. Migrate a voice (`{ fromVoiceId, toVoiceId, toProvider?, toVoiceName?, projectId?, character? }`): every cast canon pin (so a recurring show's pinned voices), trailer and deck narrator, and read with no finished recording moves to the replacement; finished reads keep their recording and are listed in `skipped` (re-voice them, priced). Free, TWO CALLS: without `confirmed` it answers `applied: false` with what would change; with `confirmed: true` it migrates and records `migrationId`. `GET /api/v1/voice-migrations` lists them; `POST /api/v1/voice-migrations/{migrationId}/undo` reverses one (uses changed since are kept). A replacement on a retiring or retired provider is refused with 409 `voice_retired`. Requires `Idempotency-Key`. A library voice on a retiring provider carries `retiresAt` in `GET /voices`; once retired, anything that would speak it refuses with 409 `voice_retired` (`details.voices`) until it is migrated. - `POST /api/v1/artifacts/{artifactId}/avatar` — `artifact:publish`. Regenerate ONE character's avatar portrait in place (no new revision, no charge). Requires `Idempotency-Key`. Body: `{ character, refinement?, style?, referenceImageUrl? }`; `style` is saved as the episode `avatar` override and holds at most 8000 characters, as that override does. Async + queue-backed: returns `{ artifactId, character, status, chatToolJobId }`; poll `GET /api/v1/artifacts/{artifactId}/cast` and watch that character's `avatarStatus` (`queued` → `rendering` → `ready`). - `POST /api/v1/artifacts/{artifactId}/cast` — `artifact:publish`. Batch-update the cast in one call (no new revision, no charge). Requires `Idempotency-Key`. Body: `{ entries?: [{ character, voiceId?, voiceName?, gender?, provider?, avatarRefinement?, regenerateAvatar? }], avatarStyle? }` — provide at least one `entries` item OR a top-level `avatarStyle` (which restyles ALL portraits; saved as the episode `avatar` override, at most 8000 characters). Voice reassignments apply immediately; returns the updated `voiceMap` plus one async avatar op `{ character, status, chatToolJobId }` per queued render — poll `/cast` until each `avatarStatus` is `ready`. - `GET /api/v1/artifacts/{artifactId}/cast` — `artifact:read`. Read the enriched cast: each character's voice (`voiceId` / `voiceName` / `gender` / `provider`) merged with its avatar (`avatarUrl` / `avatarStyle` / `avatarStatus`), plus the resolved cast-wide `avatarStyle`. `avatarStatus` is `none` | `queued` | `rendering` | `ready` | `failed` — poll after recast_avatar / update_cast until in-flight renders reach `ready`. - `GET /api/v1/scripts/{scriptUploadId}/overrides` — `story:read`. Read the five episode overrides on a script version — `avatar`, `theater`, `soundscape`, `pitchDeck`, `locations` — each the writer's per-episode direction over the Series Bible canon (null when unset; a null soundscape falls back to `soundscapeDefault`). Works for any Writers Lab episode, with or without an artifact. Free. - `PATCH /api/v1/scripts/{scriptUploadId}/overrides` — `story:write`. Partially change them: text sets one (at most 8000 characters), null or blank text clears it, keys left out are untouched; an unknown key or a non-text or over-long value is 400 `validation_failed`. Free, renders nothing, no `Idempotency-Key`. To re-render existing portraits in a new style now, use the cast's `avatarStyle` (metered). - `GET /api/v1/story-projects/{projectId}/episodes` — `story:read`. A project's seasons, every episode (uploaded, started from the Series Bible or written in the app included) with its logline, format, genre, season and every version (`scriptUploadId`, status, parse status, pages, what has been made from it), the Series Bible's planned episodes and the project's videos. `?season=N` narrows it. Free. - `POST /api/v1/story-projects/{projectId}/seasons` — `story:write`. Name a new season (`{ name? }`): the lowest free number, "Season N" when unnamed; 409 `project_precondition_failed` until the project can plan. Requires `Idempotency-Key`. Free. - `POST /api/v1/scripts` — `story:write`. Upload a screenplay (`{ title, content, format? }`, format text|fountain|fdx) as a new script in no project, parsed in the background; wrap it with `POST /story-projects/from-script`. 403 `plan_limit_reached` past the page allowance. Requires `Idempotency-Key`. Free. - `GET|PATCH /api/v1/scripts/{scriptId}` — `story:read` / `story:write`. Read one script (any id: the script's, its Writers Lab URL id, or a version's) with every version, or change its `title`, `slug`, `logline` (at most 500 characters; the writer's own, the one Slug reads), `mediaType`, `genre` (free text) and `seasonNumber` (moves an episode into that season). Free; PATCH needs no `Idempotency-Key`. - `POST /api/v1/scripts/{scriptId}/versions` — `story:write`. Upload the next version (`{ content, format? }`); the previous version's episode overrides are copied forward. 403 `plan_limit_reached` on the free plan. Requires `Idempotency-Key`. Free. - `DELETE /api/v1/scripts/{scriptId}/versions/{scriptUploadId}` — `story:write`. Delete ONE version with `confirmed: true`; 409 `script_in_use` (`details.uses`) while a release, running job, draft, season-run slot, deck or trailer depends on it, 409 `script_state_invalid` for the only version. Free. - `GET /api/v1/scripts/{scriptUploadId}/export?format=fountain|fdx|pdf` — `story:read`. Export a version (or a script's latest): `includeTitlePage`, `fromPage`/`toPage`, a PDF's `font`. Fountain and fdx come back as text, a PDF base64. Free. - `POST /api/v1/story-projects/{projectId}/episodes` — `story:write`. Start a Series Bible episode (`{ episodeId }` from `plannedEpisodes`) as an empty draft, or get the script that already covers it (`created: false`). Write the returned `scriptUploadId` with `/screenplay-drafts` or the content write. Requires `Idempotency-Key`. Free. - `PATCH /api/v1/scripts/{scriptUploadId}/content` — `story:write`. Write a draft's or version's whole text (`{ content, expectedContent? }`); with `expectedContent` it lands only if the version still holds it (else 409 `script_state_invalid`). Free, no `Idempotency-Key`. - `POST /api/v1/scripts/{scriptUploadId}/finish` — `story:write`. Finish a draft: without `coverage` the carry-over recommendation (`status: judged`); `coverage: adopt` copies the last passing coverage and prepares the screenplay; `coverage: fresh` runs new coverage first. Requires `Idempotency-Key`. - `GET /api/v1/artifacts/{artifactId}/coverage/versions` and `/coverage/blacklist?reportId=` — `artifact:read`. A screenplay's coverage history, and a report's Black List analysis (null until it has run). The coverage and script reads under `/artifacts/{artifactId}/` also take a writer's own episode: its script id, Writers Lab URL id or a version's `scriptUploadId`. - `POST /api/v1/scripts/{scriptUploadId}/table-read/prepare` — `artifact:publish`. Prepare a cast episode's table read: charges its table-read credit (`TABLE_READ_SHORT` up to 60 pages, `TABLE_READ_LONG` above — the price a job adopting the screenplay reserves) once per screenplay version and adds its sound effects and music; the Writers Lab's "Continue to Table Read" on Cast is the same operation. Requires `Idempotency-Key`. Two calls: without `confirmed` it prices it (`status: quoted`); with `confirmed: true` it charges and starts (`status: queued`). Already prepared on any surface: `status: prepared`, nothing charged; media in flight: `preparing`. 409 `cast_precondition_failed` when the cast is incomplete; 402 `insufficient_credits` changes nothing. - `POST /api/v1/artifacts/{artifactId}/voice-modification` — `artifact:publish`. Apply a voice effect (autotune) to a contiguous range of dialogue entries in place (no new revision, no charge). Requires `Idempotency-Key`. Body: `{ startEntryIndex, endEntryIndex, effect?, params? }`; the autotune `params` are optional/partial and merged over the defaults. Returns `{ artifactId, modificationId, status }`; rendered audio is projected onto the read once `status` is `ready`. - `POST /api/v1/artifacts/{artifactId}/performance` — `artifact:publish`. Refine the delivery (emotion / speed / volume / cadence) of ALL of one character's lines in place (no new revision, no charge). `character` is a name, `"NARRATOR"`, or `"ALL"`. Use `speedFactor`/`volumeFactor` (per-line multipliers, e.g. 1.2 = +20%) for relative asks like "speed the narrator up by 20%"; `speed`/`volume` are the absolute equivalents. Cadence = `speed` + `pauseAfterMs`; flatten with `emotions:["neutral"]` + `overrideExisting:true`. Requires `Idempotency-Key`. Invalidates durable media — re-finalize to hear it. - `POST /api/v1/artifacts/{artifactId}/finalize` — `artifact:publish`. Durable render. Requires `Idempotency-Key`. Body: `{ mode? }` — `"audio"` (default, the MP3) or `"video"` (the MP4); both free. Replay-safe. Poll GET artifact for `manifest.audio.finalize.status` (audio) or `manifest.video.status` (video). - `POST /api/v1/artifacts/{artifactId}/coverage` — `story:write`. Generate a coverage round for the screenplay backing a table read when `GET /coverage` returns none or the user wants another pass. Requires `Idempotency-Key`. Body: `{ focusPrompt? }`. Returns `{ reportId, status }`; poll `GET /api/v1/artifacts/{artifactId}/coverage`. Free plan: one report. - `GET /api/v1/artifacts/{artifactId}/coverage` — `artifact:read`. Read the latest coverage report (or `?reportId=`). The report carries a `status` (`generating` → `complete` / `failed`); `report` holds the structured payload on completion. `overallScore` is the gated score: near the bar the script is judged again (up to three judgments) and it is the MEDIAN of them — `summary` says so, `judging` records each judgment; a report the platform queues by itself (the coverage gate, a season run) on an identical script, rubric and Series Bible reuses an earlier report's judgments (`judging.reusedFrom`), and one you request is judged afresh. - `POST /api/v1/artifacts/{artifactId}/music` — `artifact:publish`. Scene music (mode is always `defined_clips` — durable per-scene beds): edit one defined clip with `{ sceneIndex, clip: { prompt?, vocals?, disabled?, volume? } }` (vocals are an explicit opt-in — instrumental otherwise) or re-render clips with `{ regenerateScenes }`. Requires `Idempotency-Key`. Poll GET `/music` until `music.status` is `ready`. - `POST /api/v1/artifacts/{artifactId}/sfx` — `artifact:publish`. Add, update, mute, retime, regenerate, or remove line-level sound-effect cues in place. Requires `Idempotency-Key`. Add body: `{ op: "add", entryIndex, label, prompt, volume?, triggerOffset?, offsetMs?, generatedDurationS?, enabled? }`; update body: `{ op: "update", id, entryIndex?, label?, prompt?, volume?, triggerOffset?, offsetMs?, generatedDurationS?, enabled?, regenerate? }`; remove body: `{ op: "remove", id }`. - `GET /api/v1/artifacts/{artifactId}/sfx` — `artifact:read`. List timed sound-effect cues. - `GET /api/v1/artifacts/{artifactId}/location-plates` — `artifact:read`. Read the location plates of the script behind a table read: every room, each lighting variant (day and night of one room are ONE room), and per variant the master, medium and close plate in both pools with `status` (`ready` or `missing` — made when a video renders), `imageUrl`, `description` (the room as the plate shows it), the judge's advisory `note`, and any `remake` in flight. `duplicates` reports rooms stored under two spellings. Free; never makes a plate. Plates are never approved. - `POST /api/v1/artifacts/{artifactId}/location-plates/{plateKey}/remake` — `artifact:publish`. Remake ONE plate that looks wrong. METERED, TWO CALLS: without `confirmed` it prices the remake (every attempt the cap allows) and makes nothing; with `confirmed: true` it holds one credit line per attempt and queues one capped round that replaces the plate when it ends — only the attempts made are charged, and there is no approval step. Requires `Idempotency-Key`. 404 `location_plate_not_found` for an unknown key; 409 `location_plate_precondition_failed` (not ready, already being remade, source plate missing, no image model) with nothing priced. ## Source ingestion semantics - `text` and `markdown`: extract synchronously, return `status: READY` with `extractedTextPreview`, `contentHash`, `byteSize`, `tokenEstimate`. - `url`: validated synchronously; fetch + extract runs async in a pg-boss worker. Returns `status: PENDING` immediately. Poll the source until `status: READY` (or `FAILED` with `failureCode` / `failureMessage`). The fetcher enforces http/https only, blocks private / link-local / cloud-metadata IPs, caps redirects (3) and body size (10 MB), and accepts content types `text/html`, `text/plain`, `text/markdown`, `application/json`, `application/xhtml+xml`. - `pdf`: same SSRF + redirect + size guards as `url`. Requires upstream `Content-Type: application/pdf`. Worker extracts text via pdfjs-dist and records `metadata.pageCount`. Poll until terminal. ## Source digest Once a source reaches `status: READY` and has extracted text, the API queues an LLM digest pass behind the DB sidecar `story_api_source_digest`. The digest is structured-output only — no freeform JSON from the model. - Poll `digestStatus` on the source. States: `NOT_STARTED`, `PENDING`, `RUNNING`, `READY`, `FAILED`, `SKIPPED`. - A `PENDING` digest with `digestFailureCode: provider_capacity_backoff` already has a core-owned delayed retry. Keep polling; no client provider retry loop is needed. - When `digestStatus: READY`, `digest` carries `{ summary, thesis, claims[], evidence[], gaps[], rightsNotes[] }`. Claims are typed (`fact`/`estimate`/`opinion`/`forecast`/`quotation`/`definition`) and may carry an `attribution` string. The lists are capped (at most 12 claims, 20 evidence entries, 10 gaps and 8 rights notes); a longer reply is fitted to the caps rather than failed, and every evidence entry points at a claim that is present. - A reply that still does not fit the digest (a missing field, an unknown claim kind, output cut off) is retried once immediately and then lands `FAILED` with `digestFailureCode: digest_failed` and the fields that missed in `digestFailureMessage`. It is never `provider_capacity_backoff`: the provider answered, so there is nothing to wait for. - `SKIPPED` is the expected state when the sidecar/provider is not bound in your environment, when retention is `METADATA_ONLY`, or when the extracted text is too short — the underlying source is still usable. - A full-context producer item (`producer` set, `metadata.sourceContextMode: "full"`) is `SKIPPED` by design (`digestFailureMessage: "Used in full by its episode plan."`): its plan reads every character, so no digest is paid for. `SKIPPED` is terminal and never a fault — stop polling. - `FAILED` keeps the source available but signals the digest itself errored. Inspect `digestFailureCode` + `digestFailureMessage`. ## StoryPlan The StoryPlan is the canonical source-grounded planning abstraction for the Story API. It is generated asynchronously via DB sidecar `story_api_plan` with structured Zod-validated output — never freeform JSON from the model. For multi-installment and screenplay-backed work, use the project Series Bible before each StoryPlan, including the first screenplay. GET/PATCH/POST `/story-projects/{id}/series-bible` keeps format, audience, canon, episode map, characters, visual style, typography, and audio direction in one project-level document shared by the web UI, Story API, CLI, and MCP. POST generation is backed by the DB sidecar `story_api_series_bible`. Then GET/POST `/story-projects/{id}/series-bible/coverage`: the report is tied to the exact Bible revision, compares the premise and series engine against 72 provenance-backed analytical profiles, and must be current with `gatePassed=true` before series script coverage dispatches. Every Bible response includes `document.readiness.development` and `document.readiness.moodBoard`. Storyboard creation and rendering must stop until `document.readiness.moodBoard.ready` is true. It also includes `document.renderContext.periodNegatives`, read-only: the period world (derived from the Bible's period, place and tone once per Bible version; empty for a contemporary series) — `indoors` and `outdoors`, the world as it is, which every video render is told, and `negatives`, what it never has, which every take is checked for. To change it, change the Bible: the next render derives it again. - Create with POST `/story-projects/{id}/story-plans`, body `{ target, artifactRequests, sourceIds?, styleConstraints?, autoApprove? }`. Any `sourceIds` must already have `status: READY`. For `table_read`, `artifactRequests[].narrationPolicy` may be `auto`, `include`, or `suppress`; ask/preserve this when narrator/no-narrator matters instead of inferring it from mode alone. - Poll `status` on the plan. States: `PENDING`, `RUNNING`, `REQUIRES_APPROVAL`, `READY`, `APPROVED`, `REJECTED`, `FAILED`, `SUPERSEDED`. - `table_read` plans always land at `REQUIRES_APPROVAL`: show the exact plan to the user, then POST `/approve` with `{ userConfirmed: true }` only after explicit acceptance. `autoApprove: true` may land eligible non-table plans at `APPROVED`; it is rejected for table reads. - `plan` field carries the canonical envelope: `{ version: 1, projectId, title, target, sourceDigest, thesis, journey: { beats }, visualSystem, audioSystem?, artifacts }`. Beats use the role enum `hook | context | conflict | proof | turn | payoff | cta`. - `quote` carries a deterministic Studio Credits estimate. Planning and source digest line items are included/unmetered with zero credits; artifact and add-on line items are reserved and settled when jobs/refines/finalizes run. - Source provenance is preserved: `sourceIds[]` on the plan record + `sourceDigest.evidence[].sourceId` inside the envelope. - Provider capacity/policy failures are owned by Sleeper Hit: the shared provider circuit skips cooling-down primaries, traverses configured fallbacks, and leaves the plan `PENDING` with `failureCode: provider_capacity_backoff` when a delayed retry is needed. Keep polling; do not parse provider messages or call resume to build a client-side backoff loop. A completed response that cannot parse or satisfy the schema still lands terminally at `FAILED/story_plan_failed` with a concise field diagnostic. ## Story jobs + Artifacts A story job runs queued artifact generation against an APPROVED StoryPlan. Table reads, pitch decks and trailers are live adapters; check `/api/v1/capabilities.artifacts` for the current inventory. - Create with POST `/story-jobs`, body `{ storyPlanId, artifactRequests?, projectId? }`. The plan must be `APPROVED`. `artifactRequests` defaults to the plan's own requests; if supplied it overrides them (max 6). For table reads, preserve or set `narrationPolicy` (`auto | include | suppress`) when overriding the request. A table read performs the project cast and never makes one: from a plan it starts only when the project cast canon covers every character (a voice for everyone and, unless the project is audio-only, a headshot, body shot and sheet; a narrated read also needs a narrator voice), else `409 cast_precondition_failed` with `details.reason` and `details.missing` — GET `/story-projects/{projectId}` reports it as `project.tableReadReadiness`. For voices the canon lacks, first GET `/voices`, then put a `voiceMap` on the single table_read request (`character -> { voiceId, voiceName?, gender?, provider? }`). To perform a finalized, cast-ready screenplay as written, set `scriptUploadId` on the table_read request instead. Studio Credits for the artifact line items are reserved at create time — a `402 insufficient_credits` (with `details.required` / `details.available`) means the reservation could not be covered. - A job spends only what its plan was approved for: `artifactRequests` may narrow or refine the plan's requests, but an artifact type the approved plan does not contain is refused (400 `validation_failed`, `details.reason: "artifact_type_not_in_plan"`) — that work was never quoted or approved, so make and approve a plan for it. - Poll `status` on the job. States: `PENDING`, `RESERVED`, `RUNNING`, `PARTIAL`, `READY`, `FAILED`, `CANCELED`. A `RESERVED` job with `progress.stage: provider_backoff` is healthy, nonterminal recovery: Sleeper Hit retained its credits and durable checkpoint and already scheduled the next attempt. Clients only keep polling. `PARTIAL` means at least one artifact is READY while others are still running. Credits settle when an artifact becomes shareable. - A `RUNNING` job with `runStale: true` lost its worker mid-run (no heartbeat for five minutes: its server died or was replaced). POST `/story-jobs/{jobId}/resume` sends it on from its last checkpoint at no new cost; Sleeper Hit also does that itself when its workers restart. `runStale` is `false` for every job that is still working, and for every job that is not a table read. - Cancel a job that has not started — `PENDING`, `RESERVED`, or `FAILED` before it ever started (a refused reservation or a lost enqueue; canceling that one means it can no longer be resumed) — with POST `/story-jobs/{jobId}/cancel`; reserved-but-unsettled credits are released. A `RUNNING` job, and a `FAILED` job that had started generating, cannot be canceled. `CANCELED` is final: the job reads `failureClass: null`, `retryable: false`. Requires `Idempotency-Key`. - Read artifacts via GET `/story-jobs/{jobId}/artifacts` or GET `/artifacts/{artifactId}`. Artifact states: `PENDING`, `RUNNING`, `READY`, `FAILED`. A failed adapter surfaces `artifact_generation_failed` with the artifact `failureCode` / `failureMessage`. Add `?revision=N` to GET `/artifacts/{artifactId}` to fetch a prior revision. - Refine with POST `/artifacts/{artifactId}/refine` (scope `artifact:publish`, Idempotency-Key required). Body: `{ instruction, scope?, confirmed? }`. METERED, TWO CALLS: without `confirmed` it returns the price and reserves and enqueues NOTHING; with `confirmed: true` it reserves exactly the quoted TABLE_READ_REFINE (8 credits) and returns `{ storyJob }` to poll. The artifact id, share token, and player URLs are stable across refines. - Finalize with POST `/artifacts/{artifactId}/finalize` (scope `artifact:publish`, Idempotency-Key required). Body: `{ mode? }` — `"audio"` (default, the MP3) or `"video"` (the MP4); both free. Replay-safe. Poll GET artifact for `manifest.audio.finalize.status` (audio) or `manifest.video.status` (video). ### Pitch deck render flow A `pitch_deck` artifact request plans a chapter plan and moves through `draft → rendering → review → finishing → complete` (or `failed`) under `/pitch-decks/{jobId}`. There are no stills and no storyboard or draft approval: each chapter is a few short shots in one room, and its poster frame is taken from its takes. 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 ), 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 story job completes READY at `progress.stage: plan_ready` with `progress.pitchDeckJobId` and `progress.nextStep: approve_pitch_deck_plan`, its quote covers the planning only (0 credits), and the plan is scored for free; approving THAT job renders the deck that was scored. When the finish completes, the deck becomes that story job's pitch_deck artifact. - Price: 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 deck request's `deckMode` says what the deck is for: `pitch` (the default) tells the approved story and is judged as a pitch; `showcase` is the show's world shown as its story — the intent's arc carried by a narrator and the characters' own lines, world chapters (`chapterFocus: "world"`: vistas, crowds, life, no cast on screen) interleaved with cast chapters (`chapterFocus: "cast"`: the characters in that world doing things) — and its pre-render coverage judges it as a showcase, never as a pitch of the approved story. `showcaseCharacters` names who it must feature, in the order they first appear (exact canon names; omit to let the planner choose). The mode is set when the deck is planned and never changes; GET `/pitch-decks/{jobId}/chapters` reports it as `deckMode`. - Approve the plan with POST `/pitch-decks/{jobId}/plan/approve` (scope `story:write`, Idempotency-Key). TWO CALLS: without `confirmed` it runs the coverage gate, compiles and voices every footage chapter (free to the writer) and returns the 480p render quote (`quotedOnly: true`, `lines` — one per call and one per location plate the render makes first — `calls`, `warnings`, `credits`, `balanceAfter`, `sufficient`, `note`); send `{ "confirmed": true }` only after the writer said go, and it reserves exactly the quoted calls and starts the render. - PLAN COVERAGE (GET `/pitch-decks/{jobId}/storyboard/coverage`, the gate the approval runs): One judgment is noisy near the bar, so when a plan revision first lands near it (the overall, or a veto dimension, within the band of its line) the SAME revision is judged again — up to the cap, a third only when two disagree, free to the writer — and the gate decides on the MEDIAN of each score, vetoes included; a plan edit starts a fresh set. The coverage read carries `judging`: each judgment with its own score, the medians the gate read, and `decidedOnMedian`. While another judgment is pending the status stays `generating` with `judging.pending` true, and approving the plan waits for the median. Near the bar a revision is never decided on one judgment: a further judgment that cannot run is queued again, and if the median still cannot be completed the report is `failed` with `judgingIncomplete` (and `judging.stoppedBecause: judging_incomplete`) — "Plan coverage couldn't finish judging — Score again": neither a pass nor a veto; score the plan again. 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. - RENDERING OVER THE PLAN GATE: 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 `acknowledgePlanCoverage: { reason }` on POST `/pitch-decks/{jobId}/plan/approve` (the price and the confirm alike; CLI `sleeperhit pitch-deck approve-plan --acknowledge-plan-coverage ""`). A refusal says whether it may be overridden (`details.overridable`, `details.overrideWhy`); the coverage read carries `gateOverride` (the same answer) and `writerOverride` (the recorded override: reason, who, when, report, revision, score and reasons), and the answer carries `planCoverageOverride`. - A chapter's line is spoken by a cast member in their own recorded voice when its `narrationVoice` names one (a V.O. inside the cut), otherwise by the narrator over the cut. Everyone on camera needs a body figure. A take that replaces a line with its own reading, or opens off its planned framing, is retaken ONCE automatically at our cost; after that a new take is the writer's priced retake. A moderation refusal is released (nothing is charged) and never retried. - Read the review with GET `/pitch-decks/{jobId}/chapters` (scope `story:read`): each chapter's `chapterIndex` (its key) and `chapterNumber` (its place in the deck, from 1), its calls, every take with its playthrough and opening checks, the selection, the cut and poster, the master (a 480p preview until finished), the delivery class and, in draft and review, `videoModelPreflight` (what each chapter's render needs and what is missing). A read never compiles, renders or makes a plate. - Naming a 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`. - `shotDensity` (a pitch_deck or trailer request): 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. The deck's and the trailer's reads report it back. - EDITING GRAMMAR: 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. Mark it with `chapters/refine` (`montage: true|false`; a trailer beat with `beats/refine`). 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. GET `/pitch-decks/{jobId}/chapters` returns them as `interludes` (in cut order, with what the master made of each); replace them with POST `/pitch-decks/{jobId}/interludes` `{ interludes: [{ afterChapterIndex, seconds, line?, speaker?, sound? }] }` (scope `story:write`, Idempotency-Key; `[]` removes them all). - HOW A CHAPTER'S LINE IS SAID: 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). Set it with `chapters/refine` (`lineDelivery: "broken, crying, quieter"`; `""` clears it); GET `/pitch-decks/{jobId}/chapters` reads it back as `lineDelivery`. CLI: `sleeperhit pitch-deck refine-chapter --chapter --line-delivery ""` (`--line-delivery ""` clears it). MCP: refine_pitch_deck_chapter with `lineDelivery`. How the face moves while it is said belongs in `motionPlan`. - HOW A CHAPTER HANDS INTO THE NEXT: 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. Set it with `chapters/refine` (`cutIntoNext: "cut"`, or `"dip"` to put the dip back; the answer's `restitching` says the master is being stitched again); GET `/pitch-decks/{jobId}/chapters` reads it back as `cutIntoNext`. CLI: `sleeperhit pitch-deck refine-chapter --chapter --cut-into-next cut|dip`. MCP: refine_pitch_deck_chapter with `cutIntoNext`. - THE VIDEO MODEL: 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. Read it with GET `/pitch-decks/{jobId}/video-model` (also `videoModel` on GET `/pitch-decks/{jobId}/chapters`, and on each take); set it with POST `/pitch-decks/{jobId}/video-model` `{ videoModel: "wan3-prime" | "vidu-q4" }`. CLI: `sleeperhit pitch-deck video-model [--model wan3-prime|vidu-q4]`. MCP: get_pitch_deck_video_model / set_pitch_deck_video_model. To compare models like for like, duplicate the deck and set the copy. - VERSIONS AND RAW CLIPS: 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//versions/-/chapters/-/), 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. List them with GET `/pitch-decks/{jobId}/versions` (newest first); read one with GET `/pitch-decks/{jobId}/versions/{number}` (every chapter's cut before the film finish and the take each shot played, with its trim, its `path` in the folder and its `archivedUrl`, plus `files`); ask for its zip with POST `/pitch-decks/{jobId}/versions/{number}/zip` (free; `zip.url` is a signed link once `zip.status` is `ready`). CLI: `sleeperhit pitch-deck versions <jobId>`, `sleeperhit pitch-deck version-clips <jobId> <number> [--download <dir>] [--zip]`. MCP: list_pitch_deck_versions / get_pitch_deck_version / request_pitch_deck_version_zip. - A CHAPTER'S OWN SOUND: 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. Mark it with `chapters/refine` (`keepsSceneSound: true|false`; the answer's `sceneSound` says `reassembling` or `at_finish`). An effect laid on a chapter is the audio timeline's SFX lane instead, the lane the master mixes: POST `/pitch-decks/{jobId}/storyboard/audio-timeline` `{ action: "generate-sfx-cue", sceneIndex, offsetMs, label, prompt, kind?, level?, lengthMs?, endSceneIndex?, fadeInMs?, fadeOutMs? }` (MCP generate_pitch_deck_timeline_sfx; Slug add_scene_sound_effect, or add_scene_sound_effects for a whole pass) once the deck has rendered: a draft has no rendered chapters to lay it on. A `spot` (a door, a hoof) sounds its own length; an `ambience` bed (a room's murmur, wind) with no `lengthMs` fills its chapter (or through `endSceneIndex`), repeating the effect, at 0.2 with short fades. `update-sfx-cue` moves, relevels, reshapes or mutes one (`muted: true`). The master plays them at its next stitch. Place them from the SOUND MAP: GET `/pitch-decks/{jobId}/storyboard/sound-map` (`?sceneIndex=` for one chapter; MCP get_pitch_deck_sound_map; CLI `sleeperhit pitch-deck sound-map <jobId> [--chapter N]`; Slug get_scene_sound_map) gives each chapter's length, its shots and spoken lines by the second, what its cut already carries (`heard`) and its room tone, and the cues and music already over it. - THE TITLE SEQUENCE: 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. GET `/pitch-decks/{jobId}/chapters` returns it as `titleSequence` (with `playing`, `durationMs`, what each flash cut resolved to: a moving close from chapter N, a close reused from another deck of the project, a motion close, a still portrait the writer asked for, or missing with what to do — plus `motionCloses` and `motionClosesNeeded` — and `supers`: each person's name super as it reads now, or null; and `beatTimeline` on the bell beat grid: the beat, the derived seconds, and each intro's and the title's beat and start in the sequence and the master); set it with POST `/pitch-decks/{jobId}/title-sequence` `{ titleSequence: { enabled, title, cast: [{ name, inShadow?, role?, label?, flashSource? }], seconds, epigraph?: { text, attribution? }, sourceCredit?, nameSupers?, beatGrid?, beatSeconds?, titleBeats? } | null }` (scope `story:write`, Idempotency-Key; free). 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. A cast entry may carry `flashSource` (deck | project | motion | portrait). Price, then make, the moving closes with POST `/pitch-decks/{jobId}/title-sequence/motion-closes` `{ names?, confirmed? }` (scope `artifact:publish`, Idempotency-Key; two calls, priced first): 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). CLI: `sleeperhit pitch-deck title-motion-closes <jobId> [--name <name>]… [--confirm]`, `--flash-source "Grushenka=project"` on title-sequence. CLI: `sleeperhit pitch-deck title-sequence <jobId> --name-supers on --role "Fyodor Karamazov=the father" --label "Grushenka=GRUSHENKA" --beat-grid on --beat-seconds 2 --title-beats 2` (on the grid each intro holds one beat and the title `titleBeats` beats, 2 by default). On the grid, `GET /pitch-decks/{jobId}/score-to-picture` returns the score check (`beatAlignment`): every intro and the title is a required bell strike, measured against the laid score, shifted onto the cuts when the bell is steady but out of phase, else a warning with the offsets. - THE TITLE SEQUENCE'S TYPE: 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). POST `/pitch-decks/{jobId}/storyboard/direct-text-motion` `{ titleElement: "title", direction: "KARAMAZOV huge, blood red, a stark sans, the ends cut off by the frame" }` (or `{ titleElement, clear: true }`); the answer carries the title sequence with `textMotion` and `reassembling`. CLI: `sleeperhit pitch-deck direct-text-motion <jobId> --title-element epigraph --direction "a subtle background behind the verse"`. MCP: direct_pitch_deck_text_motion with `titleElement`. - THE 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. GET `/pitch-decks/{jobId}/chapters` returns it as `visualGrammar`; replace it with POST `/pitch-decks/{jobId}/visual-grammar` `{ visualGrammar: "…" }` (scope `story:write`, Idempotency-Key; free). On a draft plan the changed plan is queued for plan coverage again (`planCoverage`); in review or complete it is saved and nothing else happens (`edited` says which). A plan coverage fix that names the deck's camera language is made here, never by re-planning the deck. - THE CLOSING SEQUENCE: 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. GET `/pitch-decks/{jobId}/chapters` returns it as `closingSequence` (with `playing`, `durationMs` — what it adds to the deck — `startSeconds`, `onBeatGrid`, each card's `seconds`, start, bell `beat` and the look it wears, `look`/`lookFrom`, and `fadeOut`: where the music and the bell fall to silence); set it with POST `/pitch-decks/{jobId}/closing-sequence` `{ closingSequence: { enabled, quote?: { text, attribution? }, comingSoon?, productionCredit? } | null }` (scope `story:write`, Idempotency-Key; free). CLI: `sleeperhit pitch-deck closing-sequence <jobId> --on --quote "<words>" --quote-attribution "<source>"`. Its cards' look: 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. POST `/pitch-decks/{jobId}/storyboard/direct-text-motion` with `{ closingElement, direction }` or `{ closingElement, clear: true }`; CLI `--closing-element quote|coming-soon|production-credit`. - BELL ON THE BEAT: on the grid, one bell sound is struck on every beat by the deck's bell lane — frame-exact, one lane on the audio timeline, free. POST `/pitch-decks/{jobId}/bell-lane` `{ assetId? | prompt?, every?: 1|2, fromSeconds?, toSeconds?, skipSceneIndexes?, accent?, level?, duckUnderVoice? }` (scope `story:write`, Idempotency-Key; fields left out keep the lane's), GET it for the strikes, the `parts` it can skip, start/end `points` and `check` (every strike against its beat in the master after assembly), POST `/pitch-decks/{jobId}/bell-lane/remove` to take it off. A rendered deck re-stitches its master for its sound; a sketch waiting for its master is mixed again with the bell; a score to picture composed with a lane leaves room for it. CLI: `sleeperhit pitch-deck bell <jobId> strike --prompt "a single low tolling church bell strike" --every 1`. - THE SAME BELL AS ANOTHER DECK: GET `/story-projects/{projectId}/sounds?kind=bell` (scope `story:read`) lists every sound the project's decks and trailers use, named as it was used and by its numbered deck ("Deck 2"), each with `bellSound`; POST the bell lane with `{ sound: { fromJobId } }` to strike that deck's bell here, exactly as it struck there (only the sound; this deck's timing stays). CLI: `sleeperhit projects sounds <projectId> --kind bell`, then `sleeperhit pitch-deck bell <jobId> strike --from-deck <otherJobId>`. - THE FINISH: 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. Read it with GET `/pitch-decks/{jobId}/look` (each effect's level in it as `effects`) and set it with POST `/pitch-decks/{jobId}/look` `{ finish: { look: "camera" | "period-35mm" | "night-interior" | "none", intensity?, adjust? } }` (scope `story:write`, Idempotency-Key; free; the whole finish — what it leaves out is the default). - THE 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. Read it with GET `/pitch-decks/{jobId}/narrator` (scope `story:read`; `source` says where the voice comes from) and cast it with POST `/pitch-decks/{jobId}/narrator` `{ voiceId: string | null, provider? }` (scope `story:write`, Idempotency-Key). - THE INTENT: 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. Read it with GET `/pitch-decks/{jobId}/intent` (scope `story:read`): the intent, each chapter's `arcStage`, `advancesQuestion` and `whyItFollows`, `arcProblems` (where the chapters break the arc, each a blocking plan defect) and `coverage.viewerTakeaway` (with `lostTheThread`: where a first-time viewer could not tell why one chapter came after the last) with the story checks (`storyVetoes`: the vetoes, transitions among them, then voices) and `coverage.weakTransitions` (the weakest cuts, each with its fix). Edit it with POST `/pitch-decks/{jobId}/intent` (scope `story:write`, Idempotency-Key; any of `purpose`, `audience`, `logline`, `theme`, `centralQuestion`, `stakes`, `viewerPromise`, `ending`, `arc`). 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 chapter's `arcStage`, `advancesQuestion` and `whyItFollows` are edited with `chapters/refine`. - Edit a chapter with POST `/pitch-decks/{jobId}/chapters/refine` (free; `title` — plan words, never stale a take — `storyBeat`, `visualPlan`, `shotType` — its shot size, which must name a scale on the shot ladder (establishing, wide, medium or close; the scale word decides, so "medium close" is a medium) and which the render is told; the answer's `shotLadderRung` names the rung — `motionPlan` — what its people physically do, the business its shots stage — `location` — its ONE canonical location key — `chapterFocus` — `world` (the place itself, no cast on camera) or `cast` — cast, words, duration and continuity — `propContinuity`, `wardrobeContinuity` and `screenDirection`, which the render is told ("Props: …"), and `continuityNotes`, which it is not; there is no `imagePrompt`) while the deck is a draft plan, in review or complete. On a draft nothing goes stale: the next plan approval recompiles just that chapter, and the answer's `planCoverage` says the changed plan was queued for coverage (`queued`: one run per burst of edits, on the latest revision, started once the plan has been left alone for three minutes; the approval starts it at once and waits for its score). A chapter the plan approval refused (`pitch_deck_render_blocked`, for example a sentence too long for one render) is fixed this way — refine it and price the approval again; the deck is not planned again. Every refine answer names the version it changed — `edited: 'draft_plan'` or `'rendered_deck'`, with the deck's `jobId` and `status` — so tell the writer which one changed. - Choose a take with POST `/pitch-decks/{jobId}/chapters/{chapterIndex}/select-take` `{ callId, take }` (free; a re-voiced take is never selected). Stage a re-voiced line as a voice-over in a new take with POST `/pitch-decks/{jobId}/chapters/{chapterIndex}/stage-voice-over` `{ callId, take, confirmed? }` (priced like a new take: quote, then confirm). Every take is watched with its sound and remedied by rule at our cost (each take's `watch`, `repair`, `lineage`); repair one take in place with POST `/pitch-decks/{jobId}/chapters/{chapterIndex}/repair-take` `{ callId, take, confirmed? }` (scope `artifact:publish`, TWO CALLS), trim it to a span with POST `/pitch-decks/{jobId}/chapters/{chapterIndex}/trim-take` `{ callId, take, keepStartSeconds, keepEndSeconds }` (free; never into a spoken line — each take's `trimBounds`), or keep it as it is with POST `/pitch-decks/{jobId}/chapters/{chapterIndex}/take-repair-override` `{ callId, take, decision: keep | restore | rewatch }` (free; `rewatch` watches again a take whose watch could not finish — its decision reads `unwatched`, never clean). Cut a chapter's line out of its footage and keep the rest with POST `/pitch-decks/{jobId}/chapters/{chapterIndex}/drop-line` `{ keep?: before | after, toChapterIndex? }` (free; the line leaves the chapter, each take that says it keeps one side of it, the chapter stays current, and `toChapterIndex` moves the line's words, speaker and delivery — each chapter's `lineDrop` says what it would keep now). Add or remove a chapter with POST `/pitch-decks/{jobId}/chapters/insert` and `/chapters/remove` (free). Put the chapters in a new order with POST `/pitch-decks/{jobId}/chapters/reorder` `{ order }` or `{ move: { chapterIndex, afterChapterIndex } }` (free; a scene group moves as one block, and a rendered deck's video is stitched again). Render a chapter again with POST `/pitch-decks/{jobId}/chapters/render-clip` `{ chapterIndex, callId?, confirmed? }` (scope `artifact:publish`, TWO CALLS). - Choose the delivery class with POST `/pitch-decks/{jobId}/resolution` `{ resolution: "720p" | "1080p" | null }` (free; 720p by default). Raising it upscales the selected takes at finish — it quotes a finish, never a render. - VIDEO COVERAGE: every stitched master — the 480p preview, then the finished master — is watched with its sound, by itself and free to the writer, 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) and on whether each cut follows the part before it (transitions — never whether the story is worth telling) against a bar. Read it with GET `/pitch-decks/{jobId}/video-coverage` (scope `story:read`; also `videoCoverage` on the chapters read): `overallScore` against `gateThreshold`, the `vetoDimensions` that fail it on their own, `gateReasons`, and `report.findings[]`, each with its `timecode`, the `unit` (chapter), `callId` and `shotIndex` it fell in, and its `remedy` (`retake_call` — render that call again — `rerender_text`, `rescore` — the score laid under the master is measured silent there, so score the cut to picture again — or `revise_plan` — a cut that does not follow because the plan gives it no link: give the unit its `whyItFollows` and stage the link with the refine). `report.transitions` judges every cut in order (`verdict` follows, weak or lost; whether the plan's link was seen; the fix and remedy). Cover the current master again with POST `/pitch-decks/{jobId}/video-coverage` `{ focusPrompt? }` (scope `story:write`, Idempotency-Key, free: no quote and no `confirmed`). One watch is noisy near the bar, so a first watch within `report.judging.nearGateMargin` of it is followed by a second, two that disagree by a third, and every score is the median of its watches (`report.judging`: each watch, each score's `scores` and `median`); an automatic report on a master whose picture is frame-identical to an earlier judged one carries that report's picture scores (`carriedFrom`) and judges only lip sync, audio sync and transitions again. - AUTOMATIC FIXES: the plan approval's quote carries an automatic-fix allowance beside the 480p render (`autoFixAllowance`, a `lines` entry of kind `allowance`, included in `credits`), reserved with it. After the preview is stitched the engine watches it, retakes the shots its review flags (each new take told what the review saw, a shot at most twice) and renders flagged on-screen text again, inside that allowance, then stitches and watches again — until the preview is clean, the allowance or its rounds are spent, or nothing left is fixable; what it did not draw comes back. Lead with `verdict` on GET `/pitch-decks/{jobId}/chapters` (`progress`, one paragraph naming what is left by chapter, and at most two `choices`), and follow `autoFix` (`state`: waiting, reviewing, fixing, clean or stopped; `allowanceCredits`, `drawnCredits`, `heldCredits`, `rounds[].fixes[].why`). While it is reviewing or fixing do NOT retake or finish by hand — poll. Once it stopped short: finish anyway (`acknowledgeVideoCoverage: true`, the writer's choice) or POST `/pitch-decks/{jobId}/more-fixes` (scope `artifact:publish`, priced: without `confirmed` it quotes one more grant, `confirmed: true` reserves it and the loop runs again) when `autoFix.moreFixes` is not null. `earlyLook` lists the chapters that can be watched before the preview exists (each take's clip) and `musicPlan` the music the deck will carry, known before the stitch. The 720p finish is always its own approval. - Finish with POST `/pitch-decks/{jobId}/finish` `{ deliveryResolution?, confirmed?, acknowledgeVideoCoverage? }` (scope `artifact:publish`, TWO CALLS): the selected takes are upscaled to the delivery class, the text layers laid over the finished cuts, the finished master stitched, and the PDF laid out too when the request carried `includePdf` (off by default). Publishing is never part of the finish: a deck is unpublished until the writer publishes it. Refused while a call has no selected take or a footage chapter has not rendered, and while the deck's video coverage holds it (`blocksFinish`: 409 `pitch_deck_video_coverage_failed`, `details.videoCoverage` with the score, the bar and the findings) — fix the shots it names, or send `acknowledgeVideoCoverage: true` ONLY after the writer has seen the findings and chosen to finish anyway. Coverage still running, one that could not run, or one of an earlier master never holds the finish. Only a `complete` deck exports (POST `/pitch-decks/{jobId}/video-export`). - A deck that reads `failed` with `reassembly.available: true` (on GET `/pitch-decks/{jobId}/chapters`) failed while it was being ASSEMBLED — the 480p preview (`assemble`) or the finished master (`finish_assemble`) — and lost nothing it paid for. Put it back together with POST `/pitch-decks/{jobId}/reassemble` (scope `story:write`, free, no `confirmed`): that stage re-runs from the takes the deck already has, and nothing is rendered, upscaled or reserved. Any other failure is refused with a 409 and its reason (`reassembly.refusal`). - STOP a deck's render in flight with POST `/pitch-decks/{jobId}/cancel-render` (scope `artifact:publish`, free, no `confirmed`): its first pass, its finish, or a retake or repair. Nothing more is sent to a provider, the writer's reservations for work not done are released, every accepted take is kept, and a shot already out at the provider is stopped and not charged. It lands in `review` with what rendered, or back at `draft` when nothing did. With nothing rendering it answers `stopped: false`. - LET ONE TAKE GO with POST `/pitch-decks/{jobId}/chapters/{chapterIndex}/abandon-take` `{ callId, take }` (scope `artifact:publish`, free, no `confirmed`): a take still out at the provider is let go — its claim off the shot, the take recorded failed with a plain notice, the writer's credits held for it given back (`releasedCredits`), the automatic fixes carried on. The provider may still bill it, at our cost, never the writer's. A take not out at a provider answers `abandoned: false`; one being polled right now is a `409` (`reference_render_take_busy`: try again in a minute). The engine lets a take go on its own when its polls run out, and rings the writer's bell. - A deck in `review` or `complete` has its master REBUILT by the same POST `/pitch-decks/{jobId}/reassemble` (free, no `confirmed`): a FRESH master from the takes the writer chose, with the current sound rules. In review the preview assembly runs (`stage: assemble`) and the deck stays in review; complete, the finished master is stitched again at its class (`stage: restitch`) and the deck stays complete. The selected takes never change. Refused with a 409 while a master is already being assembled for the deck or a shot has a take in flight — `reassembly.available` / `reassembly.refusal` on the chapters read say so first. - A refused render is a `409 pitch_deck_state_invalid` whose `details.refusal` is `{ code, action, fix }`: `action.kind` is `recast_figure`, `add_signature_items` or `recast_voice`, and `fix` names the MCP tool and CLI command that take it. A blocked compile adds `details.blocked[]`, one entry per chapter with its `chapterIndex` and `chapterNumber`. Nothing is reserved on a refusal. ### Pitch deck artifact shape A `pitch_deck` artifact is minted when the deck's finish completes. When `status: READY`, the artifact `manifest` carries: - `pitchDeckJobId` — the underlying pitch deck job. - `share.url` — the public pitch-deck page. - `video.status`, `video.url`, `video.durationMs` — the finished pitch-deck video. - `pdf.status`, `pdf.url` — the optional PDF companion deck, laid out from poster frames of the finished takes. - `audio.voiceMap` — table-read character voices reused for narration. ### Trailer render flow A `trailer` artifact request plans a beat plan and moves through `draft → rendering → review → finishing → complete` (or `failed`) under `/trailers/{jobId}`. 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 story job completes READY at `progress.stage: plan_ready` with `progress.trailerJobId` and `progress.nextStep: approve_trailer_plan`, its quote covers the planning only (0 credits), and the plan is scored for free; approving THAT job renders the cut that was scored. When the finish completes, the cut becomes that story job's trailer artifact. - Price: 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. - Approve the plan with POST `/trailers/{jobId}/plan/approve` (scope `story:write`, Idempotency-Key). TWO CALLS: without `confirmed` it runs the coverage gate, compiles and voices every footage beat (free to the writer) and returns the 480p render quote (`quotedOnly: true`, `lines`, `calls`, `warnings`, `credits`, `balanceAfter`, `sufficient`, `note`); send `{ "confirmed": true }` only after the writer said go, and it reserves exactly the quoted calls and starts the render. - PLAN COVERAGE (GET `/trailers/{jobId}/storyboard/coverage`, the gate the approval runs): One judgment is noisy near the bar, so when a plan revision first lands near it (the overall, or a veto dimension, within the band of its line) the SAME revision is judged again — up to the cap, a third only when two disagree, free to the writer — and the gate decides on the MEDIAN of each score, vetoes included; a plan edit starts a fresh set. The coverage read carries `judging`: each judgment with its own score, the medians the gate read, and `decidedOnMedian`. While another judgment is pending the status stays `generating` with `judging.pending` true, and approving the plan waits for the median. Near the bar a revision is never decided on one judgment: a further judgment that cannot run is queued again, and if the median still cannot be completed the report is `failed` with `judgingIncomplete` (and `judging.stoppedBecause: judging_incomplete`) — "Plan coverage couldn't finish judging — Score again": neither a pass nor a veto; score the plan again. - RENDERING OVER THE PLAN GATE: 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 `acknowledgePlanCoverage: { reason }` on POST `/trailers/{jobId}/plan/approve` (the price and the confirm alike; CLI `sleeperhit trailer approve-plan <jobId> --acknowledge-plan-coverage "<reason>"`). A refusal says whether it may be overridden (`details.overridable`, `details.overrideWhy`); the coverage read carries `gateOverride` (the same answer) and `writerOverride` (the recorded override: reason, who, when, report, revision, score and reasons), and the answer carries `planCoverageOverride`. - Every call renders at 480p with the cast's own recorded lines; every named character on camera is in the beat's cast (an identity reference each), and unnamed people — a crowd, passers-by — may fill the world. A take that replaces a line with its own reading, or opens off its planned framing, is retaken ONCE automatically at our cost; after that a new take is the writer's priced retake. A moderation refusal is released (nothing is charged) and never retried. - Read the review with GET `/trailers/{jobId}/beats` (scope `story:read`): each beat's calls, every take with its playthrough and opening checks, the selection, the cut and poster, the master (a 480p preview until finished) and the delivery class. A read never compiles, renders or makes a plate. - Choose a take with POST `/trailers/{jobId}/beats/{beatIndex}/select-take` `{ callId, take }` (free; a re-voiced take is never selected). Stage a re-voiced line as a voice-over in a new take with POST `/trailers/{jobId}/beats/{beatIndex}/stage-voice-over` `{ callId, take, confirmed? }` (priced like a new take: quote, then confirm). Every take is watched with its sound and remedied by rule at our cost (each take's `watch`, `repair`, `lineage`); repair one take in place with POST `/trailers/{jobId}/beats/{beatIndex}/repair-take` `{ callId, take, confirmed? }` (scope `artifact:publish`, TWO CALLS), trim it to a span with POST `/trailers/{jobId}/beats/{beatIndex}/trim-take` `{ callId, take, keepStartSeconds, keepEndSeconds }` (free; never into a spoken line — each take's `trimBounds`), or keep it as it is with POST `/trailers/{jobId}/beats/{beatIndex}/take-repair-override` `{ callId, take, decision: keep | restore | rewatch }` (free; `rewatch` watches again a take whose watch could not finish — its decision reads `unwatched`, never clean). Edit a beat with POST `/trailers/{jobId}/beats/refine` (free; `storyBeat`, `shotType`, `shots`, `characterAction` and `stagingNotes` — what a beat held as one shot is filmed from — `visibleCharacters`, `location`, caption and seams). A beat's `location` is where it is set, stored as a canonical location key: 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. Render a beat again with POST `/trailers/{jobId}/beats/render-clip` `{ beatIndex, callId?, confirmed? }` (scope `artifact:publish`, TWO CALLS). - THE INTENT: 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. Read it with GET `/trailers/{jobId}/intent` (scope `story:read`) and edit a draft's with POST `/trailers/{jobId}/intent` (scope `story:write`, Idempotency-Key). 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 beat's `arcStage`, `advancesQuestion` and `whyItFollows` are edited with `beats/refine`. - Choose the delivery class with POST `/trailers/{jobId}/resolution` `{ resolution: "720p" | "1080p" | null }` (free; 720p by default). Raising it upscales the approved takes at finish — it quotes a finish, never a render. - The finish: 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. Read it with GET `/trailers/{jobId}/look` and set it with POST `/trailers/{jobId}/look` `{ finish: { look: "camera" | "period-35mm" | "night-interior" | "none", intensity?, adjust? } }` (scope `story:write`, Idempotency-Key; free; the whole finish — what it leaves out is the default). - VIDEO COVERAGE: every stitched master — the 480p preview, then the finished master — is watched with its sound, by itself and free to the writer, 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) and on whether each cut follows the part before it (transitions — never whether the story is worth telling) against a bar. Read it with GET `/trailers/{jobId}/video-coverage` (scope `story:read`; also `videoCoverage` on the beats read): `overallScore` against `gateThreshold`, the `vetoDimensions` that fail it on their own, `gateReasons`, and `report.findings[]`, each with its `timecode`, the `unit` (beat), `callId` and `shotIndex` it fell in, and its `remedy` (`retake_call` — render that call again — `rerender_text`, `rescore` — the score laid under the master is measured silent there, so score the cut to picture again — or `revise_plan` — a cut that does not follow because the plan gives it no link: give the unit its `whyItFollows` and stage the link with the refine). `report.transitions` judges every cut in order (`verdict` follows, weak or lost; whether the plan's link was seen; the fix and remedy). Cover the current master again with POST `/trailers/{jobId}/video-coverage` `{ focusPrompt? }` (scope `story:write`, Idempotency-Key, free: no quote and no `confirmed`). One watch is noisy near the bar, so a first watch within `report.judging.nearGateMargin` of it is followed by a second, two that disagree by a third, and every score is the median of its watches (`report.judging`: each watch, each score's `scores` and `median`); an automatic report on a master whose picture is frame-identical to an earlier judged one carries that report's picture scores (`carriedFrom`) and judges only lip sync, audio sync and transitions again. - AUTOMATIC FIXES: the plan approval's quote carries an automatic-fix allowance beside the 480p render (`autoFixAllowance`, a `lines` entry of kind `allowance`, included in `credits`), reserved with it. After the preview is stitched the engine watches it, retakes the shots its review flags (each new take told what the review saw, a shot at most twice) and renders flagged on-screen text again, inside that allowance, then stitches and watches again — until the preview is clean, the allowance or its rounds are spent, or nothing left is fixable; what it did not draw comes back. Lead with `verdict` on GET `/trailers/{jobId}/beats` (`progress`, one paragraph naming what is left by beat, and at most two `choices`), and follow `autoFix` (`state`: waiting, reviewing, fixing, clean or stopped; `allowanceCredits`, `drawnCredits`, `heldCredits`, `rounds[].fixes[].why`). While it is reviewing or fixing do NOT retake or finish by hand — poll. Once it stopped short: finish anyway (`acknowledgeVideoCoverage: true`, the writer's choice) or POST `/trailers/{jobId}/more-fixes` (scope `artifact:publish`, priced: without `confirmed` it quotes one more grant, `confirmed: true` reserves it and the loop runs again) when `autoFix.moreFixes` is not null. `earlyLook` lists the beats that can be watched before the preview exists (each take's clip) and `musicPlan` the music the trailer will carry, known before the stitch. The 720p finish is always its own approval. - Finish with POST `/trailers/{jobId}/finish` `{ deliveryResolution?, confirmed?, acknowledgeVideoCoverage? }` (scope `artifact:publish`, TWO CALLS): the selected takes are upscaled to the delivery class, the text layers composited and the finished master stitched. Refused while a call has no selected take, and while the trailer's video coverage holds it (`blocksFinish`: 409 `trailer_video_coverage_failed`, `details.videoCoverage` with the score, the bar and the findings) — fix the shots it names, or send `acknowledgeVideoCoverage: true` ONLY after the writer has seen the findings and chosen to finish anyway. Coverage still running, one that could not run, or one of an earlier master never holds the finish. Only a `complete` trailer exports (POST `/trailers/{jobId}/video-export`). - A trailer that reads `failed` with `reassembly.available: true` (on GET `/trailers/{jobId}/beats`) failed while it was being ASSEMBLED — the 480p preview (`assemble`) or the finished master (`finish_assemble`) — and lost nothing it paid for. Put it back together with POST `/trailers/{jobId}/reassemble` (scope `story:write`, free, no `confirmed`): that stage re-runs from the takes the trailer already has, and nothing is rendered, upscaled or reserved. Any other failure is refused with a 409 and its reason (`reassembly.refusal`). - CUT A LINE OUT, KEEP THE REST: 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. POST `/trailers/{jobId}/beats/{beatIndex}/drop-line` `{ line?, keep?, toBeatIndex? }` (scope `story:write`, Idempotency-Key; free); GET `/trailers/{jobId}/beats` returns each beat's `lineDrops` (what it would do now, line by line) and `droppedLines`. CLI: `sleeperhit trailer drop-line <jobId> --beat <beatIndex> [--line <n>] [--keep before|after] [--to <beatIndex>]`. MCP: drop_trailer_beat_line. - BLACK 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. GET `/trailers/{jobId}/beats` returns them as `interludes` (in cut order, with what the master made of each); replace them with POST `/trailers/{jobId}/interludes` `{ interludes: [{ afterBeatIndex, seconds, line?, speaker?, sound? }] }` (scope `story:write`, Idempotency-Key; `[]` removes them all). CLI: `sleeperhit trailer interludes <jobId> --json <list> | --clear`. MCP: update_trailer_interludes. - THE QUIET TRACK: 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. GET `/trailers/{jobId}/quiet-track` reads it (with the renders it reaches and the takes already rendered with each track); POST it `{ quietTrack: 'silence' | 'room_tone' | null }` (scope `story:write`, Idempotency-Key; free). CLI: `sleeperhit trailer quiet-track <jobId> --track silence|room_tone | --off`. MCP: get_trailer_quiet_track, set_trailer_quiet_track. - THE CLOSING SEQUENCE: 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. GET `/trailers/{jobId}/beats` returns it as `closingSequence`; set it with POST `/trailers/{jobId}/closing-sequence` `{ closingSequence: { enabled, quote?: { text, attribution? }, comingSoon?, productionCredit? } | null }` (scope `story:write`, Idempotency-Key; free). Its cards' look: POST `/trailers/{jobId}/storyboard/direct-text-motion` with `{ closingElement, direction }` or `{ closingElement, clear: true }`. CLI: `sleeperhit trailer closing-sequence <jobId> --on [--quote "<words>" --quote-attribution "<source>"]`. MCP: update_trailer_closing_sequence. - DUPLICATE: 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>). POST `/trailers/{jobId}/duplicate` `{ title? }` (scope `story:write`, Idempotency-Key; free); the answer's `jobId` is the NEW draft trailer, which every later call takes. CLI: `sleeperhit trailer duplicate <jobId> [--title <text>]`. MCP: duplicate_trailer. - VERSIONS AND RAW CLIPS: 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. List them with GET `/trailers/{jobId}/versions` (newest first); read one with GET `/trailers/{jobId}/versions/{number}` (every beat's cut before the film finish and the take each shot played, with its trim, its `path` in the folder and its `archivedUrl`, plus `files`); ask for its zip with POST `/trailers/{jobId}/versions/{number}/zip` (free; `zip.url` is a signed link once `zip.status` is `ready`). CLI: `sleeperhit trailer versions <jobId>`, `sleeperhit trailer version-clips <jobId> <number> [--download <dir>] [--zip]`. MCP: list_trailer_versions / get_trailer_version / request_trailer_version_zip. - STOP a trailer's render in flight with POST `/trailers/{jobId}/cancel-render` (scope `artifact:publish`, free, no `confirmed`): its first pass, its finish, or a retake or repair. Nothing more is sent to a provider, the writer's reservations for work not done are released, every accepted take is kept, and a shot already out at the provider is stopped and not charged. It lands in `review` with what rendered, or back at `draft` when nothing did. With nothing rendering it answers `stopped: false`. - LET ONE TAKE GO with POST `/trailers/{jobId}/beats/{beatIndex}/abandon-take` `{ callId, take }` (scope `artifact:publish`, free, no `confirmed`): a take still out at the provider is let go — its claim off the shot, the take recorded failed with a plain notice, the writer's credits held for it given back (`releasedCredits`), the automatic fixes carried on. The provider may still bill it, at our cost, never the writer's. A take not out at a provider answers `abandoned: false`; one being polled right now is a `409` (`reference_render_take_busy`: try again in a minute). The engine lets a take go on its own when its polls run out, and rings the writer's bell. - A trailer in `review` or `complete` has its master REBUILT by the same POST `/trailers/{jobId}/reassemble` (free, no `confirmed`): a FRESH master from the takes the writer chose, with the current sound rules. In review the preview assembly runs (`stage: assemble`) and the trailer stays in review; complete, the finished master is stitched again at its class (`stage: restitch`) and the trailer stays complete. The selected takes never change. Refused with a 409 while a master is already being assembled for the trailer or a shot has a take in flight — `reassembly.available` / `reassembly.refusal` on the beats read say so first. - A refused render is a `409 trailer_state_invalid` whose `details.refusal` is `{ code, action, fix }`: `action.kind` is `recast_figure`, `add_signature_items` or `recast_voice`, and `fix` names the MCP tool and CLI command that take it. A blocked compile adds `details.blocked[]`, one entry per beat. Nothing is reserved on a refusal. ### Table read artifact shape A `table_read` artifact is a live, shareable performance — not a static file. Modes: `documentary | podcast | drama`; `narrationPolicy` controls whether a NARRATOR role is expected and is not implied by mode alone. When `status: READY`, the artifact `manifest` carries: Narration defaults to `auto` — let the genre and conversation decide rather than managing fixed modes. Auto can resolve to no narration (dialogue-led drama, sitcom, panel, interview), a NARRATOR voice (documentary, audiobook, storybook, casefile), or a named-character voice-over (memoir, retrospective, noir, where one character narrates their own story). Suggest the genre-fit posture and let the user confirm; only pin `include`/`suppress` when they are explicit. For a voice-over, cast that character and keep the narration as their own (V.O.) lines rather than a generic NARRATOR. - `shareToken` — an expiring JWT. This token (not your customer API key) authorizes the returned URLs, so you can embed them or hand them to end-users ("unlisted", like an unlisted video). - `theaterUrl` — the framed visual theater-mode page (`/share/table-read/{token}`), with attribution chrome. - `theaterFullscreenUrl` — the same theater stage rendered immersive and chrome-free (`?view=fullscreen`). Best for embeds, kiosk, or handing an end-user a distraction-free full-window experience. - `audio.liveUrl` — a minimal-chrome, audio-only player page (`/share/table-read-audio/{token}`). Serves the durable MP3 auto-rendered once the read is READY; shows a "Preparing…" state until the render is complete. - `audio.mode` — `"durable_recording"` for new artifacts; audio is auto-rendered as a durable MP3 on ready unless `deferAudioRender` was set. (Legacy artifacts may show `"live_on_demand"` — live playback has been retired; `audio.liveUrl` now serves the durable file.) - `audio.recordingUrl` and `audio.recordingDurationMs` — populated once the auto-render (or explicit finalize) is complete. `null` until then. Poll GET `/artifacts/{artifactId}` and watch `audio.finalize.status`. With `deferAudioRender` there is no auto-render: the read stays at `finalize.status: "not_finalized"` until you POST /artifacts/{id}/finalize yourself. - `audio.finalize` — tracks the durable MP3 render state. Fields: `{ status, finalizeUrl, error }`. Auto-triggered on ready; also callable explicitly via POST `/artifacts/{artifactId}/finalize` with `mode:"audio"`. Progresses through `not_finalized → queued → rendering → uploading → complete` (or `failed`). A render that stopped with nothing working on it (no progress for ten minutes and no job queued or running) reads `stalled`: POST `/artifacts/{artifactId}/finalize/retry` renders it again. Poll GET `/artifacts/{artifactId}` until `complete`, `failed` or `stalled`. - `video` — the OPT-IN MP4. `video.status` is `not_rendered` by default; trigger with POST `/artifacts/{artifactId}/render-video` or POST `/artifacts/{artifactId}/finalize` with `{ mode: "video" }`. On completion: `video.status: "complete"` and `video.url` populated. - `currentVersion` and `refineCount` — versioning fields on the artifact. v1 = initial generation; each refine increments. Fetch a prior revision with GET `/artifacts/{artifactId}?revision=N`. - `expiresAt` — when the share token (and thus the player URLs) expire. ### Refine (post-creation revision) POST `/artifacts/{artifactId}/refine` (scope `artifact:publish`, requires `Idempotency-Key`) Body: `{ "instruction": "<free-form>", "scope"?: "auto" | "screenplay" | "plan" }` (default scope `"auto"`). - `"screenplay"` — rewrites the screenplay + recasts voices in place (faster, most instructions land here). - `"plan"` — re-grounds the StoryPlan against sources then re-adapts (use for major structural or thematic changes). - `"auto"` — model self-routes. Returns `{ storyJob: { id, status, ... } }`. Poll GET `/story-jobs/{jobId}` until `READY` or `FAILED`, exactly like a standard artifact job. The artifact id, share token, and all three player URLs are STABLE — embed once and the updated read appears automatically. Without `confirmed` it returns the price (`quotedOnly: true`) and reserves and enqueues NOTHING; with `confirmed: true` it reserves exactly the quoted TABLE_READ_REFINE (8 Studio Credits) and creates the job. Every precondition is refused BEFORE the quote. ### Exact screenplay replacement POST `/artifacts/{artifactId}/script` versions supplied screenplay text without an AI rewrite. Body: `{ "screenplay": "<complete screenplay>", "instruction"?: "<audit note>", "narrationPolicy"?: "include" | "suppress", "deferMusic"?: boolean, "deferAudioRender"?: boolean }`. Set `deferMusic: true` when you need the revised script and cast to become READY before attaching exact library clips through POST `/artifacts/{artifactId}/music`; normal SFX detection/bake still dispatches, automatic music does not. Set `deferAudioRender: true` when you run your own post-production: the durable MP3 is NOT rendered on ready, so the whole read is not synthesised once for a mix you then invalidate and again on your finalize — you must POST `/artifacts/{artifactId}/finalize` yourself. Verify intended SFX and music are ready before finalizing. ### Finalize (durable render) POST `/artifacts/{artifactId}/finalize` (scope `artifact:publish`, requires `Idempotency-Key`) Body: `{ "mode"?: "audio" | "video" }` (default `"audio"`). - `"audio"` (free) — freezes the current completed Pipecat/Daily table-read recording into a durable MP3. If no completed recording exists yet, finalize returns `needs_recording`. Poll GET artifact: `manifest.audio.finalize.status` (`needs_recording`, or `queued → rendering → uploading → complete`). On complete: `manifest.audio.recordingUrl` (MP3) and `manifest.audio.recordingDurationMs`. - `"video"` (free) — queues the Remotion MP4 export, which renders only when asked. Poll GET artifact: `manifest.video.status` and `manifest.video.url`. Replay-safe: re-calling finalize when already complete or in-flight returns the current status without re-charging. ### Coverage (script analysis) POST `/artifacts/{artifactId}/coverage` (scope `story:write`, requires `Idempotency-Key`) Generates professional script coverage for the screenplay backing a `table_read` artifact: logline, premise, structure, characters, dialogue, pacing, market fit, an overall score, top fixes, strengths, comparables, and a recommendation. Table-read jobs queue the initial coverage pass automatically once pages are created; use this POST when GET returns no report or the user wants another pass. Generation is async + queued. Body: `{ "focusPrompt"?: "<free-form lens>" }`. Returns `{ artifactId, reportId, status, version }` with `status: "generating"`. Poll GET `/artifacts/{artifactId}/coverage` (scope `artifact:read`, optionally `?reportId=<id>`) until `status` is `complete` or `failed`. On `complete`, `report` carries the structured coverage payload; it is `null` while generating. `overallScore` is the gated score: a first judgment within `judging.nearGateBand` of `gateThreshold` is judged again, two that disagree on the bar a third time, and the score is the MEDIAN of the judgments (`summary` says "the median of 3 judgments"; `judging` records each, which one gave the median and why). GET returns `{ coverage: null }` when no coverage has been requested yet. Coverage produces a separate report and never changes the artifact, its revision, or its share URLs. Free-plan accounts may keep one coverage report; further requests return 403 `plan_limit_reached` until you upgrade. Coverage reports are also capped per month on every plan: past the cap a request returns 403 `usage_limit_reached` with `details.resetsAt` — do not retry before then. ### Scene music (defined clips + realtime scoring) POST `/artifacts/{artifactId}/music` (scope `artifact:publish`, requires `Idempotency-Key`) Scripts carry a top-level music mode, always `defined_clips` — durable per-scene music beds rendered once and played as assets. All mutations below act on the live read in place (no new revision; same artifact id and share URLs) and mark any finalized MP3/video stale until explicit finalize. Mode switch body: `{ "musicMode": "defined_clips" }` (the only mode; realtime streaming is deprecated). Switching INTO defined_clips auto-queues a baseline render covering roughly half the read, opening scene always included. Returns `{ artifactId, musicMode, status: "ok" }`. Soundtrack switch body: `{ "soundtrackEnabled": boolean }` — the read's "Scene Soundtrack Active" master switch, the same one the in-app theater settings show. `false` silences every scene's music everywhere (live playback, the MP3 and the MP4); banked clips and coverage are kept, and `true` brings them back. Free; it never waits on the project gate. Returns `{ artifactId, soundtrackEnabled, status: "ok" }`. Defined-clip edit body: `{ "sceneIndex": number, "clip": { "prompt"?: string, "vocals"?: boolean, "disabled"?: boolean, "volume"?: number, "fadeInMs"?: number, "fadeOutMs"?: number, "playMode"?: "once" | "loop", "soundUrl"?: string, "durationMs"?: number, "anchor"?: "start" | "end", "postRollMs"?: number } }`. VOCALS RULE: leave `vocals` unset/false unless the user EXPLICITLY asks for vocals — clips render instrumental otherwise. Editing `prompt` or `vocals` marks the clip `pending`; follow with `{ "regenerateScenes": [sceneIndex] }` to re-render. A banked `soundUrl` on files.sleeperhit.studio lands ready immediately; preserve its measured `durationMs`. For a final music-only tail, set `postRollMs` to the desired milliseconds after the final spoken line; a positive value automatically selects play-once and end anchoring. Returns `{ artifactId, sceneIndex, clip }`. Targeted re-render body: `{ "regenerateScenes": [0, 2] }`. Re-renders those scenes' clips with their current prompts. Returns `{ artifactId, status: "generating", sceneIndexes }`. Generation body: `{ "musicCoveragePercent"?: number }` (or empty). Empty queues the clip pass: the scenes the applied soundscape coverage keeps on, or a baseline covering roughly half the read when none was applied. `musicCoveragePercent` is SOUNDSCAPE COVERAGE — the same setting as the in-app coverage slider — a fraction (0..1) or whole percent (0..100), clamped into [0,1]: 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. Playback, the MP3 and the MP4 honour it. `0` silences every scene; banked clips are kept, so a later positive percent restores them without rendering again. Returns `{ artifactId, status: "generating", coveragePercent, totalScenes }`. Realtime directive edit body: `{ "scope"?: "screenplay"|"scene"|"entry", "mode"?: "merge"|"replace", "sceneIndex"?: number, "entryIndex"?: number, "prompt"?: string, "summary"?: string, "enabled"?: boolean, "weight"?: number }`. Use `mode: "merge"` (default) for additive steering and `mode: "replace"` to overwrite the targeted prompt palette. A scene-scope `enabled: false` silences that scene's music in the read (its clip is kept); `true` turns it back on. Returns `{ artifactId, status: "ready", scope, sceneIndex?, entryIndex?, directive, adaptiveSceneDirection? }`. Scene music direction body: `{ "sceneIndex": number, "direction": { "endTransition"?: "fadeOut"|"cut", "mix"?: number, "ducking"?: number, "bpm"?: number, "density"?: number, "brightness"?: number, "guidance"?: number, "temperature"?: number, "topK"?: number, "scale"?: string, "seed"?: number, "instruments"?: [{ "text": string, "weight": number }], "summary"?: string, "enabled"?: boolean } }`. Fields left out are kept; `instruments` replaces the scene's palette. The same save as the Table Read page's scene console: it drops the scene's rendered bed so the next render plays the new direction. GET returns each scene's direction as `music.sceneDirections`. CLI: `sleeperhit music update <artifactId> --scene N --end-transition cut --mix 0.4 --bpm 92`. MCP: update_music with `direction`. Poll GET `/artifacts/{artifactId}/music` (scope `artifact:read`) until `music.status` is `ready` (or `none` for intentional no-soundtrack reads). GET returns `{ music: { musicMode, status, soundtrackEnabled, scenesWithMusic, totalScenes, coveragePercent, missingClipSceneIndexes, definedClips, sceneDirections } }` (`sceneDirections`: each scene's music direction) — `soundtrackEnabled` is the master switch (false: nothing plays, `status` is `none`); `scenesWithMusic` counts the scenes whose music the read PLAYS (soundtrack on, scene on, rendered, unmuted); `coveragePercent` is the applied soundscape coverage (null when none); `missingClipSceneIndexes` are scenes coverage keeps on that have no clip yet; `totalScenes` comes from the parsed script even when scene settings are sparse; `definedClips` lists each durable clip `{ sceneIndex, heading, prompt, vocals, status: pending|rendering|ready|failed, soundUrl, error, disabled, fadeInMs, fadeOutMs, playMode: once|loop, durationMs, anchor: start|end|null, postRollMs, volume, sceneEnabled, playing }` — `soundUrl` is the scene's playable clip, `volume` the level it plays at, `sceneEnabled: false` a scene silenced by coverage or a scene mute, `playing` whether the read plays it. The artifact manifest `audio.adaptiveMusic` flag also reflects readiness. ### Sound effects POST `/artifacts/{artifactId}/sfx` (scope `artifact:publish`, requires `Idempotency-Key`) Adds, updates, or removes timed sound-effect cues on the live read backing a `table_read` artifact. This mutates the same read in place: no new artifact revision, same artifact id, same share URLs. Add body: `{ "op": "add", "entryIndex": number, "label": string, "prompt": string, "volume"?: number, "triggerOffset"?: "before" | "with" | "after", "offsetMs"?: number, "generatedDurationS"?: number, "enabled"?: boolean }`. Update body: `{ "op": "update", "id": string, "entryIndex"?: number, "label"?: string, "prompt"?: string, "volume"?: number, "triggerOffset"?: "before" | "with" | "after", "offsetMs"?: number, "generatedDurationS"?: number, "enabled"?: boolean, "regenerate"?: boolean }`. Remove body: `{ "op": "remove", "id": string }`. GET `/artifacts/{artifactId}/sfx` (scope `artifact:read`) returns `{ sfx: { artifactId, cues } }`. Each cue has `{ id, label, entryIndex, prompt, soundUrl, volume, triggerOffset, offsetMs, generatedDurationS, enabled, isDisabled, isDraft }`; `soundUrl` can be null while reusable sound generation is not ready. ### Theater presentation, noun imagery, soundscapes and animated portraits Every write below lands on the same core the Table Read page and Slug use, on the live read (no new revision, same share URLs). - GET `/artifacts/{artifactId}/theater` (`artifact:read`): the whole read's presentation, every style set, and each scene's theater. POST (`artifact:publish`, Idempotency-Key) a partial update: `{ theaterModeDefault?, backgroundAnimationEnabled?, adaptiveStyleEnabled?, applyStyleSetId?, styleSets?, fullscreenChrome?, globalSceneTheme?, scriptPaneTheme?, fontTheme?, titleScreen?, nounImagery?: { displayMode: "insets"|"replace_backdrop"|"off" } }`. Free. CLI `sleeperhit theater get|update`; MCP get_theater_settings / update_theater_settings. - POST `/artifacts/{artifactId}/theater-scene`: one scene's `{ sceneIndex, textRevealStyle?, visualStyle?, summary?, backdropPrompt?, mediaPreference?, transitionType?, transitionDurationMs?, transitionPrompt?, fxCues? }` (a list replaces the cues; `[]` clears them). Free. CLI `sleeperhit theater scene`; MCP update_scene_theater. - POST `/artifacts/{artifactId}/theater-fx-cues` `{ sceneIndex }` writes a scene's atmospheric FX cues from its lines; POST `/artifacts/{artifactId}/theater-backdrop-prompts` `{ sceneIndexes?, force? }` writes backdrop prompts for scenes with none. Both wait on the project gate (409 `project_precondition_failed`). CLI `sleeperhit theater fx-cues|backdrop-prompts`; MCP generate_scene_fx_cues / write_scene_backdrop_prompts. - GET `/artifacts/{artifactId}/noun-imagery`: how noun images appear and each noun's image and status. POST `{}` / `{ force: true }` builds the library (one image per depictable noun, reused on every mention); `{ noun: { normalizedNoun, displayLabel?, kind? } }` draws one noun again. Queued; waits on the project gate. CLI `sleeperhit noun-imagery get|generate`; MCP get_noun_imagery / generate_noun_imagery. - GET `/artifacts/{artifactId}/soundscapes`: every scene bed and custom region. POST one edit by `op`: `update_bed` `{ sceneIndex, label?, startEntryOffset?, endEntryOffset?, volume?, fadeInMs?, fadeOutMs?, playMode?: "loop"|"stretch" }`, `add_region` `{ startEntry, endEntry, assetId?, …levels }`, `update_region` `{ regionId, startEntry?, endEntry?, …levels }`, `attach` `{ target: "scene_bed"|"region", sceneIndex|regionId, assetId }` (library audio from GET `/library/audio`), `remove` `{ target, sceneIndex|regionId }`. Free; marks a finalized MP3/MP4 stale. CLI `sleeperhit soundscapes list|update-bed|add|update|attach|remove`; MCP get_soundscapes / update_soundscape / remove_soundscape. - POST `/artifacts/{artifactId}/avatar-video` `{ character, description? }`: a short animated talking portrait from the character's portrait (never the narrator). Queued; waits on the project gate. Poll GET `/artifacts/{artifactId}/cast` for `avatarVideoStatus` and `avatarVideoUrl`. CLI `sleeperhit cast animate`; MCP animate_avatar. ### Voice modification POST `/artifacts/{artifactId}/voice-modification` (scope `artifact:publish`, requires `Idempotency-Key`) Applies a voice effect (currently only `autotune`) to a contiguous range of dialogue entries on the live read backing a `table_read` artifact. This mutates the same read in place: no new artifact revision, same artifact id, same share URLs. Body: `{ "startEntryIndex": number, "endEntryIndex": number, "effect"?: "autotune", "params"?: { "key"?, "scale"?, "strength"?, "smooth"?, "reverb"? } }`. Indices are inclusive integers >= 0. `effect` defaults to `"autotune"`. The autotune `params` 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`. Allowed values: `key` ∈ `C C# D D# E F F# G G# A A# B`; `scale` ∈ `major | minor | majpent | minpent | chromatic`; `strength` is a number `0..1`; `smooth` is an integer `>= 1`; `reverb` ∈ `none | light | chapel`. Async + queued: returns `{ artifactId, modificationId, status }` where `status` is `queued | rendering | ready | failed`. The rendered audio is projected onto the read's entries once the modification reaches `ready`. ## Endpoints (planned) - Webhooks: signed deliveries for job and artifact lifecycle events. ## Optional - [Product docs](https://sleeperhit.studio/docs) - [API key management](https://sleeperhit.studio/dashboard/api)