# Narration Desk > Paste one narration script and take it to the point of recording with a synthetic voice. Four > lanes over the same script: prepare the read, cast the voices, cue the sound, brief the music. Site: https://narration-desk.skillsafe.ai/ API tutorial: https://narration-desk.skillsafe.ai/api.html Token panel: https://narration-desk.skillsafe.ai/tokens.html (noindex) Platform: SkillSafe. Model: `gpt-terra` (currently `gpt-5.6-terra`). Publisher markup: 1000 bps (10%). ## What it is for A written script and a finished recording are separated by a set of decisions nobody writes down: which acronyms get spelled out, which numbers get expanded, which of the two pronunciations of "read" is meant here, who reads which line, how stable the voice should be, what the two sound effects are and where the music gets out of the way. Narration Desk makes those decisions explicit, in the order a producer makes them, over one pasted script. It does **not** synthesise anything. No audio is generated, no voice is auditioned, no music is rendered, and nothing is sent to any voice provider. The output is a plan to execute in whichever tool the user already pays for. ## The four lanes Every run carries a `task` field naming one lane. The lanes are the four stages of one sitting and each later lane can be run on an earlier lane's output. | `task` | What it produces | | --- | --- | | `script` | The read prepared: the whole script normalised and split into synthesis blocks that never end mid-sentence, plus a pronunciation table, a normalisation table and a list of everything removed from the spoken text. Artifact: `prepared-read.md`. | | `cast` | The voices cast: characteristics a person can shop for, an engine family with its trade-off named, and stability / similarity / style / speed set per voice with a reason each, plus per-block direction. Artifact: `voice-settings.json`. | | `sfx` | The sound cued: only the effects the script actually implies, each with a generation prompt, a duration, a level in dB and a placement, plus ambience beds and mix notes. Artifact: `cue-sheet.csv`. | | `music` | The music briefed: one bed as a generation prompt, sectioned to the script, with a ducking plan, one genuine alternate direction and a list of what to avoid. Artifact: `music-brief.md`. | ## Input contract ```json { "task": "script | cast | sfx | music", "script": "the narration script itself - the one work object every lane operates on", "medium": "audiobook | explainer | podcast | advertisement | elearning | documentary | game | other", "voice_count": "one | two | few | full", "pace": "slow | measured | brisk", "language": "English, or \"unstated\"", "notes": "production context - brand, platform, deadline, constraints. Optional.", "prescan": {"flags": [], "resources": []}, "clip_note": "present only when the script was too long to send whole", "carried_from": "present only after a lane handoff" } ``` `voice_count` is a hard constraint on the `cast` lane, not a hint: `one` means one voice however many speaker labels the script has, and the labels that collapse into it are named. ## Output contract One JSON object. The envelope is identical across all four lanes; only `body` differs. ```json { "lane": "script", "lane_inferred": false, "title": "...", "posture": "ready | needs-work | blocked", "verdict": "one sentence naming the single thing that decides the posture", "medium": "audiobook", "speakers": ["NARRATOR"], "est_seconds": 96, "summary": "...", "assumptions": [], "open_questions": [], "findings": [{"id": "ND-001", "title": "...", "severity": "critical | high | medium | low", "area": "pronunciation | normalization | pacing | structure | casting | delivery | sound | music | rights | mix", "line": 12, "evidence": "...", "why": "...", "fix": "..."}], "coverage_check": [{"flag_id": "NS-ACRONYM", "status": "confirmed | set-aside | superseded", "finding_id": "ND-002", "note": "..."}], "artifact": {"kind": "none | markdown | json | csv", "filename": "...", "content": "..."}, "next_lane": {"lane": "cast", "reason": "..."}, "body": {} } ``` ## The free in-browser script reader Before any model runs, the page parses the script locally. It is a real parser, not a keyword search: - Speaker labels are resolved per line and only accepted when short and not a sentence, so `So he said:` does not become a character while `DR. REYES (clipped):` does — and the parenthetical is kept as a delivery note rather than discarded. - Bracketed and parenthesised stage directions are separated from the spoken text, so the character count is the count of what a voice would actually be sent. - Sentences are split on terminal punctuation while protecting abbreviations, single-letter initials and decimals: `Dr. Reyes paid $1.5m.` is one sentence, not three. - Paragraphs are chunked into synthesis blocks under a 4800-character request cap, cut on sentence boundaries; only a single sentence longer than the cap is cut on a word boundary, and it says so. - Duration is estimated from the word count at 130, 150 or 170 words per minute. Twenty-seven deterministic rules fire, deduplicated by rule id and carrying the first line, the total occurrence count and the actual sample tokens: `NS-DIRECTION`, `NS-MARKUP`, `NS-SSML`, `NS-ACRONYM`, `NS-NUMBER`, `NS-CURRENCY`, `NS-PERCENT`, `NS-DATE`, `NS-TIME`, `NS-RANGE`, `NS-UNIT`, `NS-ABBREV`, `NS-ROMAN`, `NS-URL`, `NS-HETERONYM`, `NS-LONG-SENTENCE`, `NS-LONG-PARAGRAPH`, `NS-OVER-CAP`, `NS-CAPS-SHOUT`, `NS-REPEAT-PUNCT`, `NS-ELLIPSIS`, `NS-EMOJI`, `NS-FOREIGN`, `NS-NUMBERED-LIST`, `NS-NO-SPEAKER`, `NS-SPEAKER-DRIFT`, `NS-CREDENTIAL`. Every flag is sent to the model, which must return exactly one `coverage_check` entry per flag id — `confirmed`, `set-aside` (with the reason) or `superseded` (naming the larger finding). The page then compares the two lists and names any flag the model dropped, and any id it reconciled that was never sent. The free lane exists to make the paid lane accountable. ## Grounding rules the prompt enforces - Every finding quotes or names something actually in the pasted script. - Nothing is heard: the output never claims a voice was auditioned or a bed rendered. - Voices are described by characteristics, never by a library ID the model cannot verify. - A voice imitating a real, identifiable person is named as needing that person's recorded consent, and is not designed around. - Music is briefed as original work; no copyrighted track, artist or library is named. - Anything that looks like a credential is referred to by line and never repeated. - The deterministic character, word, sentence, block and duration figures are not restated differently; where the model's own duration differs, it says why as a separate named number. ## Persistence Runs are stored in the declared `reads` collection on the user's SkillSafe account (`acl_read: owner`, `acl_write: user`), mirrored in `localStorage` for instant paint only. The embed set is `title`, `verdict`, `speakers`, `medium`, which is what makes semantic search over past runs work — "the chapter with the two doctors" finds a run whose title says neither word. There is no vector backfill, so that set was chosen before the app had users. ## Cost Metered. `ss.estimate(input)` is free and is re-taken on every lane switch, because the hold differs per lane. The reservation shown next to the run button is a hold, not a price; the charge is what the run actually used and is usually far lower. Both bundled examples replay a saved run in all four lanes for free, with no account. ## Attribution Derived from four agent skills published by ElevenLabs in `@elevenlabs/skills`: `@elevenlabs/text-to-speech`, `@elevenlabs/speech-engine`, `@elevenlabs/sound-effects` and `@elevenlabs/music`. This is a derived work. It is not affiliated with, endorsed by, or maintained by ElevenLabs, and it calls no ElevenLabs API.