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

# Voice chosen segments, then assemble with a dub

> Pay to voice chosen segments with the Rask API and download each clip without rebuilding the video, then assemble them with createDub.

[generateSegments](https://docs.api.rask.ai/operations/generateSegments.md) voices the segments you choose into one downloadable clip per segment. It does not
touch the video. Use it to check or download chosen lines. When you want them in the video, a dub assembles them.

You need the scopes `dubbing:write` and `credits:spend`, and the plan feature `audio_download`. The project needs a transcript and voices.
Listing segments needs `transcripts:read`, and on a project created through the API, its text must be open, which usually happens once a
dub is charged: see [Paid text](https://docs.api.rask.ai/v3/concepts/paid-text.md).

## 1. Choose the segments

```typescript TypeScript theme={null}
const { data: segmentList } = await rask.segments.listSegments({ path: { project_id: projectId } });
```

```python Python theme={null}
segment_list = client.segments.list_segments(project_id)
```

```go Go theme={null}
segmentList, err := client.Segments.ListSegments(ctx, projectID, nil)
if err != nil {
	return err
}
```

```java Java theme={null}
SegmentList segmentList = rask.segments().listSegments(projectId).data();
```

```php PHP theme={null}
$segmentList = $rask->segments->listSegments($projectId);
```

```bash cURL theme={null}
curl "https://api.rask.ai/v3/projects/$PROJECT_ID/segments" \
  -H "Authorization: Bearer $RASK_API_KEY"
```

## 2. Estimate

A batch holds 1 to 100 segments. The price is `max(1, round(s/60))` minutes (half to even), where `s` is the speech seconds of the chosen
segments. A free dubbing variant (`free_dubbing`) costs 0.

```typescript TypeScript theme={null}
const { data: estimate } = await rask.dubbing.estimateSegmentGeneration({
  path: { project_id: projectId },
  body: { segment_ids: ['5f0e2c7a-1b3d-4e6f-8a9c-2d4e6f8a0b11', '8b3d1e9f-6a2c-4d7e-b5f1-3c9e7a1d5b22'] },
});
```

```python Python theme={null}
estimate = client.dubbing.estimate_segment_generation(
    project_id, segment_ids=["5f0e2c7a-1b3d-4e6f-8a9c-2d4e6f8a0b11", "8b3d1e9f-6a2c-4d7e-b5f1-3c9e7a1d5b22"]
)
```

```go Go theme={null}
estimate, err := client.Dubbing.EstimateSegmentGeneration(ctx, projectID, api.EstimateSegmentGenerationRequest{
	SegmentIds: []string{"5f0e2c7a-1b3d-4e6f-8a9c-2d4e6f8a0b11", "8b3d1e9f-6a2c-4d7e-b5f1-3c9e7a1d5b22"},
})
if err != nil {
	return err
}
```

```java Java theme={null}
Estimate estimate = rask.dubbing().estimateSegmentGeneration(projectId, EstimateSegmentGenerationRequest.builder()
        .segmentIds(List.of("5f0e2c7a-1b3d-4e6f-8a9c-2d4e6f8a0b11", "8b3d1e9f-6a2c-4d7e-b5f1-3c9e7a1d5b22"))
        .build()).data();
```

```php PHP theme={null}
$estimate = $rask->dubbing->estimateSegmentGeneration($projectId, [
    'segment_ids' => ['5f0e2c7a-1b3d-4e6f-8a9c-2d4e6f8a0b11', '8b3d1e9f-6a2c-4d7e-b5f1-3c9e7a1d5b22'],
]);
```

```bash cURL theme={null}
curl -X POST "https://api.rask.ai/v3/projects/$PROJECT_ID/segments/generate/estimate" \
  -H "Authorization: Bearer $RASK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"segment_ids": ["5f0e2c7a-1b3d-4e6f-8a9c-2d4e6f8a0b11", "8b3d1e9f-6a2c-4d7e-b5f1-3c9e7a1d5b22"]}'
```

`details` says what you pay for and what the engine does:

| Field | Meaning |
| - | - |
| `details.segment_ids_to_voice` | The segments the job voices |
| `details.speech_seconds`, `details.billable_minutes` | The basis of the price |
| `details.processing_scope.synthesis` | `requested_segments`: speech is synthesized only for the chosen segments |
| `details.engine` | The engine that voices the segments: `meaning_first` or `legacy` |
| `details.processing_scope.voice_setup` | `requested_speakers`: only the speakers of the chosen segments are prepared. `all_cloned_speakers`: the `legacy` engine may read the project's speech and prepare every cloned speaker. That work is not charged |
| `details.text_adaptation` | `as_written`: the text is voiced exactly as written |

Two notices matter. `wider_voice_setup` repeats the voice setup above. `clips_not_reusable` says the project has no dub yet, so the first dub will voice and charge these segments again.

## 3. Generate

```typescript TypeScript theme={null}
const { data: job } = await rask.dubbing.generateSegments({
  path: { project_id: projectId },
  body: {
    segment_ids: ['5f0e2c7a-1b3d-4e6f-8a9c-2d4e6f8a0b11', '8b3d1e9f-6a2c-4d7e-b5f1-3c9e7a1d5b22'],
    expected_cost_minutes: 1,
  },
  idempotencyKey: generateKey,
});
```

```python Python theme={null}
job = client.dubbing.generate_segments(
    project_id,
    segment_ids=["5f0e2c7a-1b3d-4e6f-8a9c-2d4e6f8a0b11", "8b3d1e9f-6a2c-4d7e-b5f1-3c9e7a1d5b22"],
    expected_cost_minutes=1,
    idempotency_key=generate_key,
)
```

```go Go theme={null}
job, err := client.Dubbing.GenerateSegments(ctx, projectID, api.GenerateSegmentsRequest{
	SegmentIds:          []string{"5f0e2c7a-1b3d-4e6f-8a9c-2d4e6f8a0b11", "8b3d1e9f-6a2c-4d7e-b5f1-3c9e7a1d5b22"},
	ExpectedCostMinutes: 1,
}, rask.WithIdempotencyKey(generateKey))
if err != nil {
	return err
}
```

```java Java theme={null}
Job job = rask.dubbing().generateSegments(
        projectId,
        GenerateSegmentsRequest.builder()
                .segmentIds(List.of("5f0e2c7a-1b3d-4e6f-8a9c-2d4e6f8a0b11", "8b3d1e9f-6a2c-4d7e-b5f1-3c9e7a1d5b22"))
                .expectedCostMinutes(1)
                .build(),
        RequestOptions.builder().idempotencyKey(generateKey).build()).data();
```

```php PHP theme={null}
$job = $rask->dubbing->generateSegments(
    $projectId,
    [
        'segment_ids' => ['5f0e2c7a-1b3d-4e6f-8a9c-2d4e6f8a0b11', '8b3d1e9f-6a2c-4d7e-b5f1-3c9e7a1d5b22'],
        'expected_cost_minutes' => 1,
    ],
    ['idempotency_key' => $generateKey],
);
```

```bash cURL theme={null}
GENERATE_KEY=$(uuidgen)
curl -X POST "https://api.rask.ai/v3/projects/$PROJECT_ID/segments/generate" \
  -H "Authorization: Bearer $RASK_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $GENERATE_KEY" \
  -d '{"segment_ids": ["5f0e2c7a-1b3d-4e6f-8a9c-2d4e6f8a0b11", "8b3d1e9f-6a2c-4d7e-b5f1-3c9e7a1d5b22"], "expected_cost_minutes": 1}'
```

The answer is a `segment_generation` job. While it runs, its segments are `voicing`. A dub of the project answers 409
[`segment_busy`](https://docs.api.rask.ai/errors/segment_busy), and so does an edit that changes their speaker or translation, splits, merges or deletes them.

## 4. Read the clips

The finished job's `result.clips` holds one `SegmentClip` per segment:

| Field | Meaning |
| - | - |
| `segment_id`, `generation_id` | The segment, and the job that produced this take |
| `url`, `expires_at`, `format` | The download link, valid 1 hour after the read. Read the job again for a fresh one. `url` is null when the plan no longer includes `audio_download`, or once the project is deleted. `format` is the file type: mp3, flac or wav |
| `speaker_id`, `speaker_label` | The segment's speaker at synthesis |
| `start_seconds`, `end_seconds` | The segment's interval, frozen at synthesis |
| `audio_duration_seconds` | The clip's natural length. A clip is neither stretched nor cut |
| `fit`, `overrun_seconds` | `fits` or `overruns`, and by how much. In a later dub an overrun pushes the same speaker's next line later |
| `segment_etag` | The segment's ETag right after synthesis. If the current ETag differs, the take may no longer match the segment |

Any voiced clip counts as success, also when it overruns. A take is stored under its job and never overwritten, so an old job keeps serving its old
take. `Segment.audio` is different: it is the latest clip and each new take replaces it.

**If a segment fails, the job fails and is refunded whole.** A failed job usually has no clips, because the engine stores audio only after all
synthesis succeeded. Its segments go back to the state they had. Start again with a new `Idempotency-Key`.

After a successful job, the voiced segments' `dub_state` is `outdated` (`not_dubbed` when the project has no dub), and their ETags may have
changed. Use `clips[].segment_etag`, or read the segment again, before you edit it.

## 5. Assemble with a dub

A dub puts the clips into the video without voicing or charging them again only when it applies the `changed_segments` mode, which needs
a project that already has a dub. A first or full dub voices and charges every segment. The estimate's `details.mode_applied` says which
mode the dub applies: it is `full` when the project needs a full dub, even for `mode: changed_segments`.

```typescript TypeScript theme={null}
const { data: estimate } = await rask.dubbing.estimateDub({
  path: { project_id: projectId },
  body: { mode: 'changed_segments' },
});
```

```python Python theme={null}
estimate = client.dubbing.estimate_dub(project_id, mode="changed_segments")
```

```go Go theme={null}
estimate, err := client.Dubbing.EstimateDub(ctx, projectID, &api.EstimateDubRequest{
	Mode: rask.Ptr(api.DubModeChangedSegments),
})
if err != nil {
	return err
}
```

```java Java theme={null}
Estimate estimate = rask.dubbing().estimateDub(projectId, EstimateDubRequest.builder()
        .mode(DubMode.CHANGED_SEGMENTS)
        .build()).data();
```

```php PHP theme={null}
$estimate = $rask->dubbing->estimateDub($projectId, ['mode' => 'changed_segments']);
```

```bash cURL theme={null}
curl -X POST "https://api.rask.ai/v3/projects/$PROJECT_ID/dub/estimate" \
  -H "Authorization: Bearer $RASK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"mode": "changed_segments"}'
```

| Field | Meaning |
| - | - |
| `details.reused_clip_segment_ids` | Changed segments whose current clip is merged at no charge |
| `details.segment_ids_to_voice` | The segments the dub voices and charges; null for a full dub, which voices every segment |
| `details.assembly_anchor_segment_id` | See below; null when other segments are voiced anyway |

A clip counts while it is still the segment's latest clip, the segment's speaker, interval and translation are as they were when it was
voiced, and its voice has not changed since (a new voice or new clone settings). A change to any of these means the segment is voiced
again. Only clips of a succeeded job count, from the project's 200 most recent succeeded segment generations.
The estimate is the answer for your project: read `details.reused_clip_segment_ids` before you pay.

> [!WARNING]
>
> **The anchor segment.** A track can be rebuilt only when at least one segment is voiced. So when every changed segment already has a current clip,
> the shortest one is voiced again, at its own speech price with a minimum of 1 minute, and its clip in the track is a new take. The estimate discloses it
> in `assembly_anchor_segment_id` and the notice `assembly_anchor_voiced`.

```typescript TypeScript theme={null}
const { data: job } = await rask.dubbing.createDub({
  path: { project_id: projectId },
  body: { mode: 'changed_segments', expected_cost_minutes: 1 },
  idempotencyKey: dubKey,
});
```

```python Python theme={null}
job = client.dubbing.create_dub(project_id, mode="changed_segments", expected_cost_minutes=1, idempotency_key=dub_key)
```

```go Go theme={null}
job, err := client.Dubbing.CreateDub(ctx, projectID, api.CreateDubRequest{
	Mode:                rask.Ptr(api.DubModeChangedSegments),
	ExpectedCostMinutes: 1,
}, rask.WithIdempotencyKey(dubKey))
if err != nil {
	return err
}
```

```java Java theme={null}
Job job = rask.dubbing().createDub(
        projectId,
        CreateDubRequest.builder().mode(DubMode.CHANGED_SEGMENTS).expectedCostMinutes(1).build(),
        RequestOptions.builder().idempotencyKey(dubKey).build()).data();
```

```php PHP theme={null}
$job = $rask->dubbing->createDub(
    $projectId,
    ['mode' => 'changed_segments', 'expected_cost_minutes' => 1],
    ['idempotency_key' => $dubKey],
);
```

```bash cURL theme={null}
DUB_KEY=$(uuidgen)
curl -X POST "https://api.rask.ai/v3/projects/$PROJECT_ID/dub" \
  -H "Authorization: Bearer $RASK_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $DUB_KEY" \
  -d '{"mode": "changed_segments", "expected_cost_minutes": 1}'
```

The job's `result` reports `voiced_segment_count` and `reused_clip_count`. Then [download the outputs](https://docs.api.rask.ai/v3/recipes/downloads.md).

## The whole program

```python generate_segments.py theme={null}
"""Voice two segments, save the clips, then assemble: python generate_segments.py PROJECT_ID SEGMENT_ID SEGMENT_ID

Its keys are saved in segments-PROJECT_ID.json: run it again within 24 hours, and no paid work starts twice. Delete the file to
start anew.
"""
import sys

from rask_client import Progress, call, download, wait_for_job

project_id, segment_ids = sys.argv[1], sys.argv[2:]
progress = Progress(f"segments-{project_id}.json")
segments = " ".join(segment_ids)

estimate = call("POST", f"/projects/{project_id}/segments/generate/estimate", {"segment_ids": segment_ids})
if not estimate["eligible"]:
    sys.exit(f"Cannot start: {estimate['reasons']}")

job = call(
    "POST",
    f"/projects/{project_id}/segments/generate",
    {"segment_ids": segment_ids, "expected_cost_minutes": estimate["total_minutes"]},
    key=progress.key(f"generate {segments}"),
)
for clip in wait_for_job(job["id"])["result"]["clips"]:
    download(clip["url"], f"{clip['segment_id']}.{clip['format']}")
    if clip["fit"] == "overruns":
        print(f"{clip['segment_id']} runs {clip['overrun_seconds']} s past its interval")

estimate = call("POST", f"/projects/{project_id}/dub/estimate", {"mode": "changed_segments"})
if not estimate["eligible"]:
    sys.exit(f"Cannot assemble: {estimate['reasons']}")
details = estimate["details"]
print("Reused:", details["reused_clip_segment_ids"], "voiced again:", details["assembly_anchor_segment_id"])

dub = call(
    "POST",
    f"/projects/{project_id}/dub",
    {"mode": "changed_segments", "expected_cost_minutes": estimate["total_minutes"]},
    key=progress.key(f"dub after {segments}"),
)
print(wait_for_job(dub["id"])["result"]["reused_clip_count"], "clips reused")
```


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