Guides
Studio concepts: projects, sections, documents, and workflows
How LyricWinter Studio models stories as projects, sections, versioned documents, speakers, and asynchronous workflows, and how to edit them safely over the API.
BetaUpdated
LyricWinter Studio models a story as a project made of ordered sections. Each section has a versioned document of narration, dialogue, and sound-effect blocks, each spoken line belongs to a project-wide speaker, and long-running work such as generating audio happens in asynchronous workflows. This page explains each concept and how to change them safely through the LyricWinter API.
Projects and unassigned stories#
A project is a named container for related sections, such as the chapters of a book. All sections in a project share one cast, so a character keeps the same voice from chapter to chapter.
A standalone story is a single section that is not yet part of a named project. POST /studio/sections creates one. GET /studio/projects returns named projects in projects and standalone stories in unassigned_sections. A standalone story still has a project_id, and every project-scoped endpoint works with it. To turn a standalone story into a named project, call PATCH /studio/projects/{projectId} with a name.
Before deleting a project or standalone story, request its deletion-info?consent_version=1 and review the returned contents and display_name. This name is the current project name, or the current section title for a standalone story. Confirm with schema_version: 2, the reviewed project kind and expected_deletion_revision. Retain that exact revision when a deletion response is interrupted. If saved contents change, deletion returns 409 PROJECT_DELETION_REVIEW_REQUIRED: review them and confirm again. Never replace an earlier revision automatically to retry deletion. 409 PROJECT_DELETION_BUSY leaves the original consent unchanged; a user may retry later, but clients must not retry automatically. The generic /projects/{projectId} deletion modes are for Standard-only projects, not Studio work.
Sections#
A section is one chapter, scene, or episode of story text. Sections in a project are ordered. Add one with POST /studio/projects/{projectId}/sections, move one with PATCH /studio/projects/{projectId}/sections/{sectionId}, and delete one with DELETE. Deletions cannot be undone through the API, so call the matching deletion-info endpoint first to see what will be removed.
Automatic section splitting is included on every plan, including ZeroWinter. It preserves exact text and prefers paragraph, line, sentence, then word boundaries. By default, parts fit the current plan's preparation limit. Opting in with split_strategy: "largest_plan" instead divides only text exceeding the largest offered section cap, currently 150,000 UTF-16 units, or your larger current allowance. For Free, a 589,000-character section becomes parts up to 150,000 units in one split. A 65,000- or 120,000-unit section is not automatically divided: prepare it with a suitable paid plan or edit it manually. Resulting parts are not automatically refined to smaller plan caps. Splitting does not change plan allowances or preparation billing. Only raw, unprepared sections without a pending Source draft can be split. Parts retain their original position and use numbered titles.
Project import previews preserve author chapters and scenes, dividing only oversized chapters into ordered parts before confirmation. By default they split at your current cap. With split_strategy: "largest_plan" in AO3 JSON, or ?split_strategy=largest_plan on EPUB/document uploads, they retain chapters up to the largest offered cap, currently 150,000 UTF-16 units, or your larger current allowance. A Free import can therefore retain a 120,000-character chapter without dividing it; preparing audio may require an upgrade or manual editing. Each generated part has a nonblocking SECTION_SPLIT_TO_PLAN_LIMIT warning naming the selected cap. Displayed character counts use Unicode code points, while preparation limits use UTF-16 units, so use that warning rather than character counts alone to identify divided chapters. Review titles and section count before importing. Previews allow up to 500 sections after splitting; chapters over the 8 MiB raw section limit remain blocked. Importing the original file or link as one section is separate and unchanged.
For a recoverable manual split, save optional split_character_limit (a safe integer of at least 2 UTF-16 units) with the exact strategy, mutation ID and base versions. It pins the chosen section size, not an account allowance. A new split must fit the current strategy allowance; verified partial or completed work can finish with its saved bound after an upgrade or downgrade. Keep an interrupted request unchanged. A higher saved bound with no verified committed work is rejected without starting a new split; retain the request because an earlier call may still be in flight. Requests without this field retain their existing behavior.
Documents and blocks#
Every section has a document: the canonical, structured script of the section. Read it with GET /studio/sections/{sectionId}/document. A document contains:
document_version, which increases on every change.speakers, the project's current speaker registry, andspeaker_registry_version.blocks, the script in order. Each block has akind:
| Block kind | What it is |
|---|---|
narration | Narrated prose, voiced by the narrator. |
speech | A line of dialogue attributed to a speaker. |
sfx | A sound effect placed in the timeline. |
note | A note kept in the script but not voiced. |
excluded | Source text kept in the script but intentionally left out of the audio. |
unparsed | Text that has not been structured into narration or dialogue yet. It is not voiced until it is. |
After the initial full load, poll GET /studio/sections/{sectionId}/document?since=<document_version>&speaker_registry_version=<speaker_registry_version> with the versions you last applied. A kind: "delta" response contains current changed blocks, deleted block IDs, group-relative predecessor positions, section settings and versions, and the complete speaker registry only when its version changed. Apply changed rows in response order and preserve unchanged blocks' relative order before projecting sound-effect anchors. A kind: "reset" response means the server cannot safely reconstruct the interval, or a saved Source draft needs recovery; load the full document once without these parameters. Keep local unsaved edits separate from the saved server document and advance the cursor only after a delta is fully applied.
A new section starts as unstructured text. A prepare_sections or create_audio workflow turns it into narration and speech blocks.
For a finished speaker-labeled script, POST /studio/sections/{sectionId}/script creates speech blocks directly, without a prepare workflow or a hand-built AST. Use one SPEAKER: spoken text line per turn. A [direction] tag at the start of a line steers the whole turn, and a tag mid-line, as in TORVALD: [softly] Go, Ingvild. [whispers] Please., anchors a direction where it appears; both become tracked directions and are removed from the spoken text. A separate (silence 4s) or (silence 4000ms) line inserts exact silence after the preceding turn. Supply a speakers map whose keys exactly match every label and whose values contain voice_id and a complete generation_profile. For example, MOM: [quiet, unsteady] I saw his face.\n(silence 4s)\nMOM: I know. compiles into two speech blocks separated by an excluded newline block, with a timed-silence point on the first speech block. Newline separators preserve line breaks in Write and add no audio or timing. The returned document includes these separators, so select spoken lines by kind: "speech" and retain their block IDs instead of treating every block position as a line. Direction uses the selected model's supported control, such as an Inworld turn instruction or an ElevenLabs audio tag; models without a suitable whole-turn or mid-line control reject the direction. The route replaces the section's script, preserves its title and section settings, and returns the canonical document. A matching speaker name reuses and recasts that speaker across the project. It makes no generation or provider calls. Unmatched lines, missing speaker mappings, and unsupported directions fail with 400 rather than being dropped.
Unescaped [directions] in typed, pasted, or imported text are performance controls, not spoken words. To keep bracketed words as ordinary story text, escape the opening bracket: \[literal words]. The saved source keeps that spelling; synthesis receives literal words, without the escape marker or brackets. An odd number of backslashes before [ escapes it; preceding paired backslashes stay literal. Directions use the selected engine's supported syntax and placement.
Speakers and casting#
A speaker named Narrator starts with Male Narrator A when you have never
chosen a pin for that name. You can pin another voice or choose no pin. Choosing
no pin persists for your account and sends Narrator through the same voice
matching and AI-voice creation as every other character. GET /voice-pins
returns data.pins and data.unpinned together; unpinned contains lowercased
names. DELETE /voice-pins/{characterName} records a no-pin choice even when
there was no pin; PUT saves a pin and clears that choice. PATCH renames either
kind of preference, returning data.pin: null for a no-pin choice.
A speaker is a character or narrator in a project. The speaker registry is shared by every section in the project and has its own speaker_registry_version. Each speaker has a voice_id and a generation profile that controls how its lines are performed. GET /studio/projects/{projectId}/speakers lists speakers with how often each one appears; read a speaker's id, speaker_version, voice_id and generation_profile from the speakers array of GET /studio/sections/{sectionId}/document. Create Audio casts only speakers that have no voice yet, so a voice you assign stays. To choose a speaker's voice yourself, assign it with the casting route below (before the first Create Audio run, or after it and then run Create Audio again to regenerate the lines), or recast in the Studio app. To make a new voice from a description, POST /ai-voices with a name and a description of 20 to 500 characters returns its id for casting.
Profiles and provider options#
Use PATCH /studio/projects/{projectId}/speakers/{speakerId}/casting to assign
a speaker's voice and complete generation profile together. The request schema
types options by the provider/model pair. For example:
{
"provider": "elevenlabs",
"model": "eleven_v4",
"fallback": "strict",
"controls": "best_effort",
"options": {}
}These are the implemented option keys for connected Studio models. All keys are optional; omit a key to use the provider default. Unsupported keys and invalid values fail before a new profile is saved or paid work starts.
| Model | Provider | Accepted options |
|---|---|---|
eleven_v4 | elevenlabs | stability 0-1; integer seed 1-4294967295. Empty {} preserves defaults. |
inworld-2 | inworld | apply_text_normalization: APPLY_TEXT_NORMALIZATION_UNSPECIFIED, ON, or OFF. |
inworld-1.5-max, inworld-1.5-mini | inworld | The normalization option above; speaking_rate 0.5-1.5; temperature greater than 0 and at most 2. |
cartesia-sonic-3.6, cartesia-sonic-3.5 | cartesia | speed 0.6-1.5; volume 0.5-2. |
fishaudio, fishaudio-s2.1-pro, fishaudio-drama-3 | fish_audio | temperature and top_p 0-1; latency: low, normal, or balanced; integer chunk_length 100-300; boolean normalize and condition_on_previous_chunks. |
lightning_v3.1 | smallest_ai | {} only. |
gpt-4o-mini-tts, tts-1-hd, tts-1 | openai | {} only. |
f5tts | f5tts | {} only. |
stable-audio-3-medium (generated SFX only) | stable_audio | Integer steps 1-50. |
ElevenLabs seed is best-effort, not a guarantee of deterministic audio or a
remedy for repeated words. The connected dialogue transport does not expose
similarity_boost; v4 does not support speed. Unsupported keys are rejected
instead of silently ignored. For example, options: {"stability": 0.5, "seed": 42}
sets only those two supported controls.
Use the listed keys for provider options. Delivery directions and expressive controls belong in delivery control sets. A partial block override inherits its missing fields from the speaker's effective profile; its options are checked in that resolved context. Changing an inherited model can make an existing override invalid. Historical document profiles remain readable.
Catalog-only profiles such as inworld-2-experimental, chatterbox_fal,
zyphra, and misotts can still be saved for compatibility, but their Studio
generation is not connected. A saved profile does not promise usable audio:
check generation availability for the assigned voice and then /media for
actual audio readiness.
Phonon uses provider: "phonon", model: "phonon-31m-en", and options: {}.
Its built-in English presets and explicitly selected custom voices support
natural speech without delivery settings or inline instructions. Saved delivery
sets use format phonon-31m-en-1. Custom voices require a completed sample for
the current acoustic revision. Create Audio and engine previews prepare the
voice when needed; manual preparation uses materialization_kind: "provider_asset",
provider: "phonon", model: "phonon-31m-en" and the saved expected_source_voice_id.
Preparation exports a reusable voice file; speech and previews then run on our
self-hosted Phonon service. Generation keeps the ordinary 1× words rate.
Selecting Phonon does not change automatic casting defaults or the AI new-voice
engine preference.
Fish Audio Drama 3 is an explicit engine choice: use provider: "fish_audio"
and model: "fishaudio-drama-3". It reuses the same Fish voice reference as
the other Fish engines and uses 2× words for generated speech and engine
samples.
A completed sample for the voice's current revision
can prepare that reference automatically during Create Audio. Manual Fish
voice preparation continues to use model: "fishaudio-s2.1-pro"; it does not
change the speaker's selected synthesis model.
Drama 3 accepts open-vocabulary [directions] before controlled speech,
inline sounds and pauses, and paired <whisper>, <emphasis>, <soft>,
<fast>, <slow>, <stress>, and <forceful> tags around specific words.
Existing Fish phoneme pronunciation controls also apply. Adjacent directions
are retained; Studio omits malformed cues and terminal directions with no
following speech. Saved source text remains unchanged.
Preparing a speaker for ElevenLabs v4#
ElevenLabs v4 requires a paid plan. Free accounts can use other engines, and existing audio remains playable. Requests to select v4 or create new v4 audio without an active paid plan return 403 VOICE_MODEL_SUBSCRIPTION_REQUIRED.
New custom ElevenLabs voices require UltraWinter and are available only within a dynamic shared allocation and live provider capacity. MyWinter has no new-clone allowance, except a few accounts with a temporary allowance whose end date plan_notice gives, but prepared current-revision voices remain usable on eligible paid plans. There is no guaranteed fixed clone allowance or provider reset date. If preparation is unavailable, choose a prepared voice or explicitly select another engine, or wait and recheck availability. Do not switch engines silently or automatically resubmit quota failures.
Casting and voice preparation are separate. A voice's default_model or completed ElevenLabs engine sample does not change a Studio speaker's saved generation_profile. There is currently no project-level default generation profile; when no speaker or block override selects a model, Studio uses inworld-2. Select provider: "elevenlabs" and model: "eleven_v4" in the speaker's casting update (or a block override) before checking its readiness.
- Read
GET /studio/sections/{sectionId}/generation-availabilityand find the speech or narration block. Itsgeneration.requested_profileidentifies the effective model.generation.statusisavailable,materialization_required, orunavailable;generation.optionsreports alternatives. - If ElevenLabs v4 is
materialization_required, prepare the assigned voice throughPOST /studio/projects/{projectId}/speakers/{speakerId}/voice-materialization-jobswithschema_version: 1, a newclient_mutation_id,materialization_kind: "provider_asset",expected_source_voice_idset to the speaker's savedvoice_id,provider: "elevenlabs", andmodel: "eleven_v4". PollGET /studio/voice-materialization-jobs/{jobId}until it completes, then read generation availability again. - If you also want to audition the voice,
POST /voices/{voiceId}/engine-sampleswithmodel: "eleven_v4"and the voice's currentexpected_acoustic_revisionsynthesizes the fixed comparison phrase. A successfully completed sample can publish a reusable ElevenLabs provider asset, so it may make Studio ready too. The sample's cached audio is not itself Studio's readiness authority: Studio requires an active provider asset for the voice's current acoustic revision. Always re-read generation availability before generating.
For any new provider_asset preparation, expected_source_voice_id pins the intended assigned voice. First admission rejects a different current cast rather than preparing the replacement voice. Once accepted, an identical request returns the original job even if casting later changes. Save the exact request and client_mutation_id; if acceptance is uncertain, retry both unchanged instead of adding the assertion to an existing request or generating a new mutation ID. Legacy requests without the assertion remain supported. This field does not apply to AI voice design.
For a materializable voice, generation.reason_code and the matching option's reason_code distinguish provider_asset_missing from acoustic_revision_changed when a prior-revision provider asset exists. Editing or redesigning a voice can advance its acoustic revision; the old asset then cannot generate new Studio audio. An asset can also be retired or evicted. If your plan and current shared capacity allow new preparation, prepare the current revision and recheck. Otherwise, choose another prepared voice or explicitly select another engine, or wait and recheck availability. Other unavailable conditions use the human-readable reason and diagnostics; absence of reason_code does not mean the block is ready.
POST /ai-voices currently creates Inworld-designed voices. It has no ElevenLabs Voice Design selector. Preparing such a voice for ElevenLabs v4 changes its synthesis asset, not the design provider or the speaker's selected profile.
Voice-choice previews use POST /voices/{voiceId}/engine-samples with schema_version: 2,
purpose: "picker_preview", the voice's current expected_acoustic_revision, and the
engine the voice will speak in: a speaker's own engine, or the voice's default model from
the engine-sample catalog when no speaker is involved. Signed in, any engine the voice
supports and the account may use is accepted. On the first request the server writes
one original audition line from the voice's public name, description and tags, then
generates and shares the sample. The requester pays no Snowcoins. Changing that
metadata replaces the line on the next preview; story text and uploaded recordings
are never preview text. Clients cannot supply the line. Poll the returned sample ID
until completed and play its signed audio URL. Ordinary comparisons omit purpose
and use schema_version: 1; they keep the fixed sentence and existing prices.
Version 1 picker requests remain supported for installed clients.
ElevenLabs picker previews never add a voice to the ElevenLabs library. A shared
cached ElevenLabs sample remains playable; generating a new preview requires a
current prepared ElevenLabs voice. Otherwise the request returns
409 VOICE_PREVIEW_ENGINE_NOT_PREPARED with the message "This voice isn't on
ElevenLabs yet." Studio uses the catalog's Inworld TTS 2 option instead, or its
non-ElevenLabs default if that option is absent, and shows a notice naming that
engine. If neither option is offered, the error remains. This restriction applies
only to ElevenLabs picker previews. Paid comparisons without purpose and story
generation can still prepare voices.
Public voices, including presets and public library voices, can be previewed without signing in. Signed-out catalog reads expose only the default engine, and signed-out creation accepts only version 2 picker previews. They use the same shared line and audio as signed-in previews and cost no Snowcoins. Private voices, other engines, and fixed-phrase comparisons require sign-in and the usual access checks. Public preview reads, creation and polling share a limit of 120 requests per voice and 240 per client in five minutes; a rate-limited request returns HTTP 429.
Versions and optimistic concurrency#
Studio never silently overwrites newer work. Every mutable resource carries a version:
| Version | Protects | Where to read it |
|---|---|---|
document_version | One section's document | GET /studio/sections/{sectionId}/document |
speaker_registry_version | The project's speakers and casting | The same document response |
section_order_version | The order of sections in a project | GET /studio/projects/{projectId}/sections |
mastering_version | Loudness and mastering settings | GET /studio/projects/{projectId}/mastering-profile |
When you change something, send the version you last read. If someone else changed it first, the request fails with 409 CONFLICT instead of overwriting their work. Read the resource again, reapply your change, and retry. Playback compilation and new exports also validate these versions. Versions do not identify audio takes: a retake can replace audio without editing the document or cast. An existing export is an immutable historical file, not a request to compile the newest takes.
Section pacing#
Use set_section_pacing in PATCH /studio/sections/{sectionId}/document
to change the silence between consecutive audible passages without replacing
the script. Both values are integer milliseconds from 0 through 1000:
{
"kind": "set_section_pacing",
"same_speaker_gap_ms": 100,
"different_speaker_gap_ms": 250
}Wrap the command in the normal document-save request with current
base_document_version, base_speaker_registry_version, session and mutation
IDs, and target-version maps. Section-only changes use empty
target_block_versions and target_speaker_versions maps. Read the returned
document version before your next edit or export. Existing audio takes remain
intact; playback and new exports use the new spacing.
Mutation IDs and actor sessions#
Studio write requests include two UUIDs in the JSON body:
actor_session_ididentifies the client session making changes. Generate one when your process starts and reuse it for all requests from that process.client_mutation_ididentifies one intended change. Generate a new one for each new change, and reuse it only when retrying the exact same request after a timeout or network error.
Replaying a client_mutation_id with the same body returns the original result. Some creation endpoints return 200 with idempotent_replay: true instead of 201. Reusing it with a different body returns 409. See Errors and retries.
Workflows#
A workflow is a durable, asynchronous job over one or more sections. Start one with POST /studio/projects/{projectId}/workflows and poll GET /studio/workflows/{workflowRunId}. Creation, polling, cancellation, and resume return data: { run, steps }; read the ID and status from data.run, not directly from data. The latest-project-workflow endpoint is separately nullable when no workflow exists. There are two kinds:
workflow_kind | What it does | Cost | Sections per run |
|---|---|---|---|
prepare_sections | Structures text into narration and dialogue blocks. Use it to review the script before generating audio. | No audio is generated. | Up to 100 |
create_audio | Runs the full pipeline: prepare, assign speakers, cast voices, direct emotion and delivery, place sounds, and generate audio. | Consumes words. | Up to 12 with both sound options on, 14 with one, 16 with none |
A Create Audio run moves through these current_stage values: preparing_sections, assigning_speakers, casting_voices, planning_emotion, directing_delivery, placing_project_sounds, directing_sfx, preparing_voices, and generating_audio. Stages that are turned off are skipped.
A run's status is one of queued, running, canceling, completed, partially_completed, failed, or cancelled. The last four are terminal. Cancel a run with POST /studio/workflows/{workflowRunId}/cancel. Cancellation is cooperative, so work that already started may finish first. When resumable is true on a failed or partially completed run, POST /studio/workflows/{workflowRunId}/resume starts a new linked run that picks up from each section's first failed stage instead of redoing finished work. If the balance runs out, the audio step fails with error_code STUDIO_INSUFFICIENT_BALANCE; add words, then resume.
A nonterminal run can wait for a free assistant or voice-creation slot. Poll
run.progress.capacity_wait_code: STUDIO_ASSISTANT_LIMIT means assistant
capacity is occupied, and STUDIO_VOICE_CREATION_BUSY means voice creation is
occupied. The run resumes automatically when capacity frees; no manual retry is
needed. The field disappears after the workflow can advance again.
Workflow progress counts summarize the run. To inspect a particular block's
current audio, read GET /studio/sections/{sectionId}/media and join by
block_id. That endpoint exposes freshness, generation activity, errors, and
playback URLs. GET /studio/sections/{sectionId}/generation-availability
answers generation eligibility instead; available is not a current-audio
status.
Create Audio has three independent options in schema_version 2:
include_generated_sfxadds AI-generated sound effects where the story calls for them.place_project_soundsplaces sounds from your sound library that are linked to the project.direct_emotionruns the automatic emotion step for spoken blocks without saved controls for their selected engine. It can supplement directions already written in story text, without repeating or contradicting them. Set it tofalseto skip the step.
All three default to true. Direction text sent to the provider is billed, which for ElevenLabs v4 means the audio tags are counted. A run with direct_emotion on can therefore cost slightly more than the text alone, and the charge is the text actually sent.
Checking the balance#
GET /me/balance with account:read returns the current settled account
balance in data.balance.snowcoins and the amount available after holds in
data.balance.available_snowcoins. This is account funding only. For Studio
promotional credits too, call GET /studio/account-capabilities with
studio:read and use data.studio_funding.total_available_snowcoins or
total_available_words. Its as_of identifies the snapshot. This endpoint
can claim an eligible account reward before reading its balance; repeated reads
do not award the same reward again. Subscription state describes the plan,
not the remaining generation balance. Capability reads are advisory, not live
provider-health checks; the server reserves credits atomically when paid work
starts. Billing activity is a historical ledger, not a source of per-event
resulting balances.
Media, playback, and exports#
Speech and narration whose included source contains no Unicode letter (category
L) or number (category N) need no voice generation. Classification uses the
runtime's Unicode properties. Visible punctuation, such as ..., plays as a 100 ms
silent beat; excluded or blank text adds no beat. Explicit sound events, custom
engine input and authored pauses keep their existing behavior.
Version 3 generation requests may set skip_silent_targets: true to omit targets
that need no speech. The response lists only the actual generation targets;
sound effects and sound-bearing controls remain targets. Without this opt-in,
a mixed request containing such text is rejected before generation starts, so
successful responses keep the requested target count. Requests containing only
text that needs no audio are rejected. Preview requests have no opt-in: omit
unspoken text when requesting a synthesis preview.
- Generation eligibility.
GET /studio/sections/{sectionId}/generation-availabilitychecks plan access, the effective model, voice preparation, and compiler constraints. ElevenLabs v4 is unavailable on free plans, including for already prepared voices. New custom ElevenLabs voices require UltraWinter and a dynamic shared allocation, subject to live provider capacity. MyWinter has no new-clone allowance; prepared current-revision voices remain usable on eligible paid plans. New or renewed clones consume preparation capacity; reusing a current clone and other engines do not.generation.status: availabledoes not mean audio exists or guarantee synthesis quota or funding.GET /voices/preparation-statusprovides a read-only preparation check before selecting or creating a voice; signed-in requests check account eligibility and current allocation.unknownmeans preparation capacity could not be verified and does not permit new preparation. Recheck availability rather than assuming a fixed quota or provider reset date. - Media.
GET /studio/sections/{sectionId}/mediareturns one entry per block with itsfreshness(absent,current,stale, orunsupported), independentactivity(idle,generating, orfailed), latest generation/importerror, and a short-lived signedplayback_urlfor active audio. Usefreshness: currentplus a non-null playback URL for ready current audio. A failed retake can leave valid current audio available; do not turnactivity: failedinto an assertion that nothing can play.
Pinned project_sound blocks use an uploaded library revision, not Stable Audio
synthesis. They return generation: null, as do other blocks to which provider
generation does not apply. This is not an assertion that the sound exists or
is playable. Read /media to check materialization and freshness; do not require
every block to have generation.status: available before playback or export.
Boundary pause handling#
Request GET /studio/sections/{sectionId}/media?schema_version=2 for each active
take's boundary_silence_handling. The default response is unchanged.
timeline: leading/trailing timed silences are assembled outside the clip. Editing those boundary pauses does not require regenerating its speech.fixed_in_asset: the older generated take includes its authored boundary timing in the audio. Changing those points makes the take stale withstale_reasons: ["timing"]and requires regeneration for current export.unsupported: imported or non-speech media has no supported boundary-pause classification. Do not infer editability from its creation timestamp.unavailable: there is no active take to classify.
Internal timed silences remain part of the generated asset even in timeline
mode. Editing an internal pause can still require regeneration. /media
reports the resulting freshness; a stale historical take can remain playable.
Playback and export identity#
- Playback manifest.
GET /studio/sections/{sectionId}/playback-manifestcompiles the section's audio, gaps, and sound effects into one timeline for gapless playback. Pass thedocument_versionandspeaker_registry_versionyou read from the document. - Exports.
POST /studio/exportsrenders a section intomp3,wav,m4b, synchronizedepub, orsrt. PollGET /studio/exports/{exportId}and downloadartifact.download_urlwhenstatusiscompleted.
For synchronized playback of an exported file, use
GET /studio/exports/{exportId}/manifest. Its immutable manifest.hash and
block timings describe that file, even after a retake or script edit. Do not
pair an older file with the current section playback manifest. Block durations
include authored silence within the block but not the gaps between blocks. The
export manifest's timing_basis is
media_and_intra_block_silence_excluding_inter_block_spacing.
To compare an export's media with current playback, request
include_playback_hash=true. The optional manifest.playback_hash uses the same
canonical hash with only the spatial descriptor removed; it equals manifest.hash
for a descriptor-free export. Compare it with playback's manifest_hash, falling
back to manifest.hash when absent. Also require the export's authority.is_current,
which checks saved spatial changes. The default response shape remains unchanged
for older strict readers.
The playback manifest's sequential_units distinguish unit_type: "asset"
(the audio asset's own duration), unit_type: "silence" (an authored pause
assembled on the timeline), and unit_type: "spacing" (an inter-block gap).
Sound effects appear in overlay_units. For speech and narration, an export
block's duration_ms is the sum of its contiguous asset and silence unit
durations; it excludes spacing.
Thus an asset unit and its export block can share start_ms but have different
durations when that block has a trailing pause. Do not replace either duration
with the other or normalize the block to its audio asset. Join block-level
records by stable block_id, and per-unit records by unit_id, not by start
time or array position. A partial manifest does not contain audio for every
document block.
authority.is_current on the export status is a legacy version comparison:
it checks the document, cast, and mastering versions, not audio-take identity.
After a retake, create a new export with a new Idempotency-Key. Retrying the
old key intentionally returns its original export. Newly requested exports
compile the current media before content-addressed artifact reuse is checked.
Signed URLs expire. Fetch fresh ones from the API instead of storing them.
Project-wide exports#
POST /studio/projects/{projectId}/exports creates one parent job with a child
export for each selected section. Send an Idempotency-Key header and:
{
"schema_version": 1,
"format": "mp3",
"pin": "current"
}The supported formats are mp3, wav, m4b, synchronized epub, and srt.
Omit sections to export all active sections in project order, up to 100.
For a subset, add sections with 1-100 unique section UUIDs belonging to that
project; the children still follow project order, not the order of your array.
Empty selections, duplicates, and selections over 100 are invalid.
For a project with more than 100 active sections, explicitly select a subset
of up to 100 per request; omitting sections does not truncate the project.
The parent selection and version plan are captured together and cannot change.
pin: "current" captures the saved document, cast, and mastering versions when
the request is enqueued. It does not capture all audio takes or media
revisions atomically at that moment: each child selects and pins its exact
media when it compiles against those versions. Save your edits and finish
generating the intended audio before requesting an export. This export renders
existing media; it does not run TTS or automatically retry failed paid
generation.
Creation returns 202 for a new job or 200 for a database replay. A retry
handled by HTTP idempotency can replay the original 202 and body. Reuse the
same key only for the same intended request, then poll for current state;
reusing a key with a different body returns 409.
Reordering the same explicit section IDs is the same selection and can replay
the original request; changing the selected IDs is a different request.
- Read the parent ID from
data.idand pollGET /studio/projects/{projectId}/exports/{exportId}while its status isqueuedorrunning. Dispatch is asynchronous; queued children are recovered if needed, but there is no fixed completion deadline. - Inspect
data.sections. Each entry has asection_id, capturedtitle, childexport_id, capturedauthority(document_version,speaker_registry_version, andmastering_version), nullable ordinaryexportpayload, and nullableerror. Parentcompletedmeans every child succeeded; terminalpartial_failedmeans some succeeded; terminalfailedmeans none succeeded.percentaverages child progress and counts terminal failures and unavailable children as finished, so 100 percent is not a success guarantee. Check the section error before reading a non-null child's status, error, and artifact. - Download each available completed child's
export.artifact.download_urlwhen its artifact is non-null. For timings matching that file, readGET /studio/exports/{childExportId}/manifest; addschema_version=2for provider word timing sidecars. Cancel a child withPOST /studio/exports/{childExportId}/cancel. Use the child ID, not the parent ID, for these operations. There is no combined ZIP or project-level file:archiveis always null.
The parent preserves its captured plan without preventing section transfers or
deletion. If a child export moves to another project or is deleted, its section
entry keeps the original identity, title, export ID, and authority versions,
but returns export: null and
error.code: "STUDIO_PROJECT_EXPORT_SECTION_UNAVAILABLE". That entry counts
as a failure, not a success, in the parent summary. The original child's status,
manifest, cancellation, and download access are not guaranteed to remain
available after transfer or cleanup.
To retrieve earlier parent jobs, use
GET /studio/projects/{projectId}/exports. Optional format filters the list;
cursor is a nonnegative integer offset, default 0, and limit is 1-10,
default 1. Pass the returned string next_cursor as the next offset, keeping
the same filter, until it is null.
Export version comparisons and selected re-export#
GET /studio/projects/{projectId}/export-freshness?format=mp3 returns active
sections in project order, excluding deleted sections. Omit format to use
mp3; projects with more than 100 active sections return 400 rather than a
truncated freshness list. Each section reports its
current document_version, speaker_registry_version, mastering_version,
and latest_export for that format. With no export, latest_export and
matches_current_authority are both null.
The basis is document_cast_mastering_versions.
matches_current_authority: true means only that these three saved versions
match. It does not prove that the export contains the latest takes, completed
successfully, contains all media, or has a valid unexpired artifact. Inspect
latest_export.status and latest_export.artifact separately, and use the
child manifest's completeness when checking timeline completeness. A retake
can leave the version comparison true while the historical file contains
older audio.
An existing parent never automatically re-exports after edits or retakes. To
export updated sections, explicitly create a new parent with a new
Idempotency-Key and a sections array containing only the sections you want
to render again. For missing or stale audio, repair or regenerate that audio
through its normal workflow first; reading freshness or creating a parent is
not an automatic paid retry. Keep successful child files from the earlier job
and download the new children separately.
Provider word timings#
For measured SFX mixing, request the private playback manifest with schema_version=3.
The response retains the v1 manifest, hash, mastering version, and signed assets and adds
mix_envelopes, with at most one stored measurement per referenced asset. Each entry
contains asset_id, envelope_version: "studio-energy-envelope-1", block_ms: 100,
duration_ms, and integer k_weighted_cdb values from -12000 to 2400. Overlay assets
also carry same-length unweighted_cdb; speech-only assets carry null. These are
centi-dB energy measurements of the original asset, before timeline placement or gain.
For N blocks, duration is between (N-1)100 and N100 ms, inclusive. Missing measurements
are omitted without placeholders. If measurements cannot be read, the v3 request fails.
Shared section links, project-section links, and Stories chapter playback also accept
?schema_version=3 or no version. Their outer payload remains version 1; only
playback switches to version 3. The snapshot identity continues to use the existing
manifest hash, so requesting envelopes does not create a different snapshot. Deploy
v3 server support before enabling a client that requests it. Requests without a version
retain the existing payload; private v2 word timings remain a separate opt-in.
Token listening links request schema_version=4 to also receive spatial: { plan },
using a studio-spatial-plan-1 plan, when the owner has enabled spatial audio and
saved placements. Their nested playback stays version 3; the outer version stays 1.
Token requests without a version or with version 3 omit spatial entirely, preserving
older readers. Stories chapter playback always includes the optional spatial sidecar.
A null or absent spatial plays flat. Readers apply placements only to matching block
versions; the owner's section setting controls every public listening link.
With a v3 playback manifest, add include_word_timings=true to receive optional
unit_word_timings alongside the measured envelopes. This works on owner, shared
section, shared project-section, and Stories chapter playback reads. Token query
version 4 also supports it and retains its existing spatial response. The flag
reads stored asset-bound alignment for the opening 16-unit window. This limit also
applies to playback v2. The window counts sequential units first, then overlays;
silence and spacing count toward the 16 units. Only units in that window can
receive timing sidecars. Later units have no sidecar in this playback response.
The flag does not create alignment. False or absent keeps the existing v3 response
shape. The outer versions, envelopes, manifest and hash remain unchanged. V1 and
v2 ignore this flag; v2 still uses the same opening-window cap.
Add schema_version=2 to the private playback-manifest or export-manifest GET
request for unit_word_timings. Playback still requires both saved version
parameters. The default V1 responses remain unchanged. The 16-unit playback cap
does not apply to export timing reads: they continue to cover the pinned export,
subject to the existing 2 MiB provider timing payload budget.
Each sidecar identifies its unit_id, block_id, and immutable asset_id.
word_timing: "provider" includes words with text, start_ms, and end_ms
in section-relative milliseconds, including preceding assembled pauses and gaps.
Join by unit ID, not by word or block index. These sidecars do not change the
content-addressed audio manifest or its hash. Export reads use only the assets
and hashes pinned to that export, never a newer retake's alignment.
New Cartesia speech captures normalized spoken-word timestamps in the same
synthesis request, with no separate transcription charge. Provider normalization
can change word text from the original script; these are not original-text
character offsets. SFX, missing alignment, or invalid alignment report word_timing: "unavailable"
without preventing playback. Manifest reads never start alignment; missing
speech timings are requested explicitly when the listener plays.
Alignment is bounded to 10,000 words and 512 KiB per asset. The read's provider timing payload budget is 2 MiB; units beyond it report unavailable. Base manifest and unavailable-unit identity data are separate from that budget.
Block identity and indexes#
Join documents, media, workflow issues, and playback using stable block_id
UUIDs. A block_index is zero-based in the full captured document block order,
including sound effects and non-audio blocks. It is not a speech-only count.
The legacy workflow block_number is the corresponding one-based display
label. Positions can change after editing; they are not stable identifiers.
For indexed failure details, request
GET /studio/workflows/{workflowRunId}/progress?schema_version=2. It returns
the normal ready/total counts plus block_issues with section_id, block_id,
block_index, and block_number. The index records the order when the issue
was captured. Use the ID to find the current block after an edit. Omitting the
query keeps the original counts-only response for existing clients. Provider
text chunks are generation units; never equate their counters with document
block positions.
Sound uploads and long ambience#
Uploaded project sounds accept files up to 256 MiB and decoded audio up to
65 minutes. The completed sound's current_revision.duration_ms and
file_size_bytes describe the verified, sanitized audio; the sanitized WAV may
be larger than the original compressed upload, up to 720 MiB. Upload
metadata is not proof of duration: processing rejects audio outside the limits.
An uploaded ambience track uses an ordinary SFX block with a pinned
content.source with kind: "project_sound", seed: null, and the source revision's
verified duration. To start it with the section, use a placement anchor
{ "kind": "section", "position": "start" } and offset_ms: 0. The overlay
plays for the pinned revision's verified duration; it does not loop, trim, or
automatically grow to fit later edits. A long bed is not a separate block type.
content.duration_ms describes the immutable source, not a requested playback
length. Changing it on an existing pinned block returns
400 STUDIO_PROJECT_SOUND_DURATION_MISMATCH, with expected_duration_ms and
requested_duration_ms in the error details. Placement gain and fades remain
editable, but a fade does not shorten the section timeline. To use a shorter
bed, upload and place a shorter source revision. There is no placement trim or
play_duration_ms field in this contract.
Generated SFX, which omit content.source, remain limited to five minutes.
Do not use the uploaded-sound limit to request longer provider-generated SFX.
Sharing#
In shared Studio, the eye hides speaker chips, sound-effect tags and saved performance directions without changing the published manuscript. E shows saved directions when they are available. Desktop readers also see the read-only Characters and Lines panel. Character cards show public voice names, or Private voice for private assignments, and the engine label; they reveal no private voice name, voice ID or samples. Their play buttons use existing chapter audio and never generate a preview.
These display details travel in an optional studio_view sidecar on public
chapter text and playback responses. Direction labels contain only their public
block and UTF-16 position; private notes, excluded text, synthesis controls and
silences are not included. Older token playback requests omit the sidecar;
schema_version=4 opts in. Readers ignore metadata whose section or saved
versions do not match the displayed text.
The headphones button appears only when the owner has enabled and placed spatial audio. A reader can switch spatial rendering off or on for themselves; this local preference never updates the owner's saved settings.
Audio embeds use older listening links and are available only before Stories
publishing opens for the account. After that, share the story link instead;
the audio-embed API returns 409 LIBRARY_AUDIO_EMBED_UNAVAILABLE.
Share a section with POST /studio/sections/{sectionId}/share or a whole project with POST /studio/projects/{projectId}/share. Both return the reading link in data.share.url. Stories shares also return data.share.studio_url, the same story under /studio/: owners open their chapter in the editor, and other visitors see Studio's shared view with Make a copy. Chapter links support the same path change. Allow copies defaults to on for Unlisted and Public stories; turning it off makes data.share.studio_url null and sends visitors to Stories. Both links enforce the same visibility and sign-in requirements.
When Stories publishing is available to your account, the link is the story's /stories/<id>-<slug> page. Sharing a section publishes that chapter; sharing a project publishes its current chapters with saved story text and skips blank drafts. Existing chapter publications stay as they are; a project with no words publishes nothing and returns not shared. A Private story becomes Unlisted, so anyone with the link can read and listen while it stays out of Stories and search. An existing Unlisted or Public story keeps its visibility and metadata. Chapters must have saved text to be published.
New chapters start as drafts, including chapters added to an already shared project. Share the new chapter, share the project again, or publish it in Studio when it is ready. DELETE on a section share unpublishes that chapter; DELETE on a project share unpublishes its current chapters. These actions keep the story's visibility and link identity, so sharing again restores the same story link.
Before Stories publishing opens for your account, sharing keeps using the older unlisted listening links and DELETE revokes them. GET keeps showing an older link's controls until that link is converted. Converted links at /studio/shared?token=..., /studio/s/..., and /studio/p/... redirect to the story page; unconverted links keep working. See the Sharing API.
Windowed word timings (first-party preview)#
For speech beyond the playback response's opening 16-unit timing window, use the separate timing read below with unit IDs from the current playback manifest. Request only units whose words you have not cached. Export timing reads keep their pinned-export behavior.
When the listener presses Play and visible speech lacks timings, request
POST /studio/sections/{sectionId}/word-timings/request. The response contains
pending_units; repeated requests are safe.
It does not replace the playing audio.
Read GET /studio/sections/{sectionId}/word-timings?units=<untimed unit IDs>
for a window of 1–16 comma-separated unit IDs from the current manifest. The
response is { schema_version: 1, units }. Each returned unit identifies
unit_id, block_id and asset_id, with word_timing of provider, pending or
unavailable. Here, provider words are relative to the audio asset: add the
unit's current start_ms from the manifest. Legacy manifest timings remain
section-relative and never add the pending variant.
Send only unit IDs whose timings are not already cached. There is no version
cursor. Queued or running alignment work reports pending; unrequested speech
and final failures report unavailable. Check again at Play or a unit boundary, with at most two delayed
checks while visible units are pending; do not poll the full playback manifest.
The same /word-timings and /word-timings/request suffixes work on section
listening links, project-section listening links and Stories chapters. They retain
those links' access rules, including the Stories sign-up wall. Responses are
private and uncached. Anonymous requests have a per-section quota.