> ## Documentation Index
> Fetch the complete documentation index at: https://docs.breezeblue.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Manage Studio projects

> Import scripts with saved voices, edit chapters and clips safely, and arrange background audio.

The `/v1/studio` endpoints create and edit Studio projects. They do not generate speech, charge generation credits, or export audio; open the project in [Studio](https://breezeblue.ai/app/studio) for that. Creating or importing a project requires a paid Studio subscription and a free saved-project slot.

## Project structure

| Resource | Contains |
| - | - |
| Project | Title, TTS settings, and defaults for voice, instructions, and silence |
| Chapter | An ordered list of clips |
| Clip | One paragraph of script with its voice, instructions, playback, and trailing silence |
| Track | `narration` (read-only, built from clips) or an audio track for music and effects |
| Instance | One placement of an audio file on an audio track |
| File | An uploaded MP3 or WAV file |

Timeline positions, trims, and lengths are integer frames at **48,000 Hz**. Silence is in milliseconds and fades are in seconds. The SDKs provide `seconds_to_frames` / `secondsToFrames` and `frames_to_seconds` / `framesToSeconds`.

## Edit tokens and operation IDs

Every read and write returns the project's current `edit_token`. Each project write needs two values:

* `expected_edit_token`: the latest `edit_token` you received. If someone changed the project since, the write returns 409 with `meta.latest_edit_token`. Read the project again and reconcile before sending a new write; do not resend blindly.
* `operation_id`: a new UUID for each write. If a write times out, resend the same body with the same `operation_id`; the server returns the original result instead of applying it twice. Reusing an `operation_id` with a different body returns 409.

Project creation and import take `operation_id` only. File rename and delete use the file's `revision` as `expected_revision` instead of an edit token.

## Import a script

Import creates a complete project in one step: up to 20 chapters with up to 400 clips each. Every `voice_id` is checked first; if any is unavailable, nothing is created. Label chapters and clips with `client_ref` to find their IDs in `client_refs`.

```python theme={null}
from uuid import uuid4
from breeze_blue import BreezeBlue

client = BreezeBlue()
voice_id = client.voices.search(search="calm narrator")["voices"][0]["voice_id"]

created = client.studio.projects.import_project({
    "operation_id": str(uuid4()),
    "title": "An evening story",
    "default_voice_id": voice_id,
    "default_instructions": "Speak calmly and naturally.",
    "default_silence_ms": 350,
    "chapters": [
        {"client_ref": "opening", "title": "Opening", "clips": [
            {"client_ref": "opening.line1", "script": "The lights came on.",
             "instructions": "A quiet sense of wonder."},
        ]},
        {"title": "Ending", "clips": [
            {"script": "Tomorrow would be different."},
        ]},
    ],
})
project_id = created["project"]["id"]
clip_id = created["client_refs"]["opening.line1"]
edit_token = created["edit_token"]
```

Clips without `voice_id` use `default_voice_id`. To start from an empty project instead, [create a project](/api-reference/studio/create-a-project); it contains one chapter with one empty clip.

## Edit clips

Omitted fields keep their values. Empty `instructions` and a null `trailing_silence_ms` inherit the project defaults; a `trailing_silence_ms` of 0 means no pause.

```python theme={null}
changed = client.studio.clips.update(project_id, clip_id, {
    "operation_id": str(uuid4()),
    "expected_edit_token": edit_token,
    "instructions": "",
    "trailing_silence_ms": None,
    "playback": {"speed": 1.0, "volume": 0.9},
})
edit_token = changed["edit_token"]

clip = client.studio.clips.get(project_id, clip_id)["clip"]
print(clip["effective_instructions"], clip["effective_trailing_silence_ms"])
```

A clip script is one paragraph; line breaks inside it become spaces. Changing the project's `default_silence_ms` also makes clips with an explicit 0 ms silence inherit the new default. Assigning a voice to a clip never changes the voice itself; `voice.available` is `false` when a voice was deleted or is no longer accessible.

Batch endpoints apply atomically: create or delete up to 400 clips, or update up to 100 clips, in one write.

## Organize chapters

* Deleting a chapter keeps it restorable. Pass `move_to_chapter_id` to move its clips to another chapter first; otherwise they stay with the deleted chapter. A project keeps at least one active chapter and one clip.
* Reorder requests list every active chapter, or every clip in a chapter, exactly once.
* A project holds up to 20 active chapters with 400 clips each.

## Arrange background audio

Upload a file, add an audio track, then place the file on the track as an instance.

```python theme={null}
from breeze_blue import seconds_to_frames

file = client.studio.files.upload(
    "background.mp3", operation_id=str(uuid4()), project_id=project_id
)

track = client.studio.tracks.create(project_id, {
    "operation_id": str(uuid4()),
    "expected_edit_token": edit_token,
    "name": "Background music",
    "volume": 0.3,
})
track_id = track["created_ids"][0]

arranged = client.studio.instances.create(project_id, track_id, {
    "operation_id": str(uuid4()),
    "expected_edit_token": track["edit_token"],
    "file_id": file["id"],
    "position": {"mode": "fixed", "frame": 0},
    "trim_end": min(file["frames"], seconds_to_frames(10)),
    "length": seconds_to_frames(30),
    "effects": {"volume": 0.6, "fade_in_seconds": 0.5, "fade_out_seconds": 1},
})
print(f"https://breezeblue.ai/app/studio/{project_id}")
```

`files.upload` registers the file, uploads it to a signed URL without your API key, completes the upload, and waits until `status` is `ready`. Reusing the same `operation_id` resumes an interrupted upload. Without `project_id`, the file goes to your personal library. If processing fails, inspect `error_code` and `error_message` with `files.get`, then call `files.retry`.

Instance placement:

* `length` longer than the trimmed audio repeats it to fill the region.
* `{"mode": "follow", "clip_id": "clp_…", "edge": "start", "offset": 0}` starts the instance relative to a clip. When the clip moves to another chapter, audio that follows it on a chapter track moves too. If the clip is deleted, the instance reports `anchor_missing` in `issues`.
* Instances on the same track cannot overlap.
* Set `chapter_id` on a track to limit it to one chapter; omit it to span the whole project.

Deleting a track or instance keeps the audio file. A file that instances still use cannot be deleted.

## TypeScript

The TypeScript SDK uses camelCase fields and method names.

```typescript theme={null}
import { BreezeBlueClient } from "@breeze.blue/sdk";

const client = new BreezeBlueClient();
const created = await client.studio.projects.importProject({
  operationId: crypto.randomUUID(),
  title: "A short story",
  chapters: [{ title: "Opening", clips: [{ script: "A new day." }] }],
});
await client.studio.projects.update(created.project.id, {
  operationId: crypto.randomUUID(),
  expectedEditToken: created.editToken,
  defaultSilenceMs: 300,
});
```

## CLI

```bash theme={null}
breeze studio projects import --input script.json --agent
breeze studio clips update <project_id> <clip_id> --input update.json --agent
breeze studio files upload --file background.mp3 --project-id <project_id> --agent
```

The CLI uses the API's snake\_case JSON and generates a missing `operation_id`. See [Studio CLI](/cli/reference/studio) for every command.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.