> ## 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.

# Create a project from your own transcript

> Create a Rask API project from your own SRT subtitles instead of speech recognition, see what is charged, replace the transcript later and bring the dub up to date.

When you already have subtitles for a video, send them with the project. Rask then dubs your text instead of what speech recognition
would hear. The subtitles are SRT, sent inline in the request. Sending them at creation needs the plan feature `subtitle_import`,
and replacing the transcript later needs `subtitle_reimport`.

## What you can send

`subtitles` on [createProject](https://docs.api.rask.ai/operations/createProject.md) takes a `source` file, a `target` file, or both:

| You send | What Rask does with the text |
| - | - |
| `source` | Uses your source text, finds the speakers, and translates it |
| `source` and `target` | Uses both as written and finds the speakers. No speech is recognized and nothing is translated |
| `target` only | Uses your translation, and recognizes the source speech inside the times of your cues |

The rules for a file:

* Each file is `{"format": "srt", "content": "..."}`, where `content` is the whole SRT file as one JSON string, at most 2 MiB and
  5000 cues.
* `source_language` is required with `source`.
* With both files, the cues must match: the same number, with the same times and, where both files label a cue, the same speaker.
* A cue may start with a speaker label such as `[SPEAKER_00]:`. Label every cue or none.
* Every cue starts before it ends, and ends within the media.

A file that breaks a rule answers 422 [`subtitles_invalid`](https://docs.api.rask.ai/errors/subtitles_invalid), and its `errors` say which rule and where.
A `content` longer than 2,097,152 characters (2 MiB) answers 422 [`validation_failed`](https://docs.api.rask.ai/errors/validation_failed) instead.

## 1. Estimate

The price does not depend on the subtitles. A project with your own text costs what any project costs: the project itself, and the
auto-dub, which is priced on the length of the media. So the estimate can leave the subtitles out, which also keeps it small: an
estimate takes 1 MiB at most, while [createProject](https://docs.api.rask.ai/operations/createProject.md) takes 5 MiB. Without them, the estimate does not
check the files or the plan feature `subtitle_import`: only createProject does. When the request with them fits in 1 MiB, send
them to the estimate as well to check them first.

```typescript TypeScript theme={null}
const { data: estimate } = await rask.projects.estimateProject({
  body: { media_id: 'MEDIA_ID', source_language: 'en', target_language: 'es-es' },
});
```

```python Python theme={null}
estimate = client.projects.estimate_project(media_id="MEDIA_ID", source_language="en", target_language="es-es")
```

```go Go theme={null}
estimate, err := client.Projects.EstimateProject(ctx, api.EstimateProjectRequest{
	MediaId:        "MEDIA_ID",
	SourceLanguage: rask.Ptr("en"),
	TargetLanguage: "es-es",
})
if err != nil {
	return err
}
```

```java Java theme={null}
Estimate estimate = rask.projects().estimateProject(EstimateProjectRequest.builder()
        .mediaId("MEDIA_ID")
        .sourceLanguage("en")
        .targetLanguage("es-es")
        .build()).data();
```

```php PHP theme={null}
$estimate = $rask->projects->estimateProject([
    'media_id' => 'MEDIA_ID',
    'source_language' => 'en',
    'target_language' => 'es-es',
]);
```

```bash cURL theme={null}
curl -X POST https://api.rask.ai/v3/projects/estimate \
  -H "Authorization: Bearer $RASK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"media_id": "MEDIA_ID", "source_language": "en", "target_language": "es-es"}'
```

Keep `total_minutes`: it is the price cap you send next.

## 2. Create the project

```typescript TypeScript theme={null}
const { data: job } = await rask.projects.createProject({
  body: {
    media_id: 'MEDIA_ID',
    source_language: 'en',
    target_language: 'es-es',
    subtitles: {
      source: { format: 'srt', content: '1\n00:00:00,000 --> 00:00:03,200\n[SPEAKER_00]: Welcome to our webinar.\n' },
      target: { format: 'srt', content: '1\n00:00:00,000 --> 00:00:03,200\n[SPEAKER_00]: Bienvenidos al webinar.\n' },
    },
    expected_cost_minutes: 1,
  },
  idempotencyKey: createKey,
});
```

```python Python theme={null}
job = client.projects.create_project(
    media_id="MEDIA_ID",
    source_language="en",
    target_language="es-es",
    subtitles={
        "source": {
            "format": "srt",
            "content": "1\n00:00:00,000 --> 00:00:03,200\n[SPEAKER_00]: Welcome to our webinar.\n",
        },
        "target": {
            "format": "srt",
            "content": "1\n00:00:00,000 --> 00:00:03,200\n[SPEAKER_00]: Bienvenidos al webinar.\n",
        },
    },
    expected_cost_minutes=1,
    idempotency_key=create_key,
)
```

```go Go theme={null}
job, err := client.Projects.CreateProject(ctx, api.CreateProjectRequest{
	MediaId:        "MEDIA_ID",
	SourceLanguage: rask.Ptr("en"),
	TargetLanguage: "es-es",
	Subtitles: &api.SubtitlesInput{
		Source: &api.SrtFileInput{
			Format:  api.SrtFileInputFormatSrt,
			Content: "1\n00:00:00,000 --> 00:00:03,200\n[SPEAKER_00]: Welcome to our webinar.\n",
		},
		Target: &api.SrtFileInput{
			Format:  api.SrtFileInputFormatSrt,
			Content: "1\n00:00:00,000 --> 00:00:03,200\n[SPEAKER_00]: Bienvenidos al webinar.\n",
		},
	},
	ExpectedCostMinutes: rask.Ptr(int32(1)),
}, rask.WithIdempotencyKey(createKey))
if err != nil {
	return err
}
```

```java Java theme={null}
Job job = rask.projects().createProject(
        CreateProjectRequest.builder()
                .mediaId("MEDIA_ID")
                .sourceLanguage("en")
                .targetLanguage("es-es")
                .subtitles(SubtitlesInput.builder()
                        .source(SrtFileInput.builder()
                                .format("srt")
                                .content("1\n00:00:00,000 --> 00:00:03,200\n[SPEAKER_00]: Welcome to our webinar.\n")
                                .build())
                        .target(SrtFileInput.builder()
                                .format("srt")
                                .content("1\n00:00:00,000 --> 00:00:03,200\n[SPEAKER_00]: Bienvenidos al webinar.\n")
                                .build())
                        .build())
                .expectedCostMinutes(1)
                .build(),
        RequestOptions.builder().idempotencyKey(createKey).build()).data();
```

```php PHP theme={null}
$job = $rask->projects->createProject(
    [
        'media_id' => 'MEDIA_ID',
        'source_language' => 'en',
        'target_language' => 'es-es',
        'subtitles' => [
            'source' => [
                'format' => 'srt',
                'content' => "1\n00:00:00,000 --> 00:00:03,200\n[SPEAKER_00]: Welcome to our webinar.\n",
            ],
            'target' => [
                'format' => 'srt',
                'content' => "1\n00:00:00,000 --> 00:00:03,200\n[SPEAKER_00]: Bienvenidos al webinar.\n",
            ],
        ],
        'expected_cost_minutes' => 1,
    ],
    ['idempotency_key' => $createKey],
);
```

```bash cURL theme={null}
CREATE_KEY=$(uuidgen)
curl -X POST https://api.rask.ai/v3/projects \
  -H "Authorization: Bearer $RASK_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $CREATE_KEY" \
  -d '{"media_id": "MEDIA_ID", "source_language": "en", "target_language": "es-es", "subtitles": {"source": {"format": "srt", "content": "1\n00:00:00,000 --> 00:00:03,200\n[SPEAKER_00]: Welcome to our webinar.\n"}, "target": {"format": "srt", "content": "1\n00:00:00,000 --> 00:00:03,200\n[SPEAKER_00]: Bienvenidos al webinar.\n"}}, "expected_cost_minutes": 1}'
```

The answer is a `project_processing` job with the new `project_id`, as in the [Quickstart](https://docs.api.rask.ai/v3/quickstart.md). The auto-dub follows
it as a `dub` job of its own.

Your own text is [paid text](https://docs.api.rask.ai/v3/concepts/paid-text.md) like any other. Until the dub of the project is charged, reading the
transcript back answers 409 [`dub_payment_required`](https://docs.api.rask.ai/errors/dub_payment_required).

## 3. Replace the transcript later

[importTranscript](https://docs.api.rask.ai/operations/importTranscript.md) puts new subtitles into a project that already exists, for example after a
translator has edited the file. It needs the scope `transcripts:write`, and it is free.

```typescript TypeScript theme={null}
const { data: job } = await rask.transcript.importTranscript({
  path: { project_id: projectId },
  body: {
    target: {
      format: 'srt',
      content: '1\n00:00:00,000 --> 00:00:03,200\n[SPEAKER_00]: Les damos la bienvenida al seminario.\n',
    },
  },
});
```

```python Python theme={null}
job = client.transcript.import_transcript(
    project_id,
    target={
        "format": "srt",
        "content": "1\n00:00:00,000 --> 00:00:03,200\n[SPEAKER_00]: Les damos la bienvenida al seminario.\n",
    },
)
```

```go Go theme={null}
job, err := client.Transcript.ImportTranscript(ctx, projectID, api.ImportTranscriptRequest{
	Target: &api.SrtFileInput{
		Format:  api.SrtFileInputFormatSrt,
		Content: "1\n00:00:00,000 --> 00:00:03,200\n[SPEAKER_00]: Les damos la bienvenida al seminario.\n",
	},
})
if err != nil {
	return err
}
```

```java Java theme={null}
Job job = rask.transcript().importTranscript(projectId, ImportTranscriptRequest.builder()
        .target(SrtFileInput.builder()
                .format("srt")
                .content("1\n00:00:00,000 --> 00:00:03,200\n[SPEAKER_00]: Les damos la bienvenida al seminario.\n")
                .build())
        .build()).data();
```

```php PHP theme={null}
$job = $rask->transcript->importTranscript($projectId, [
    'target' => [
        'format' => 'srt',
        'content' => "1\n00:00:00,000 --> 00:00:03,200\n[SPEAKER_00]: Les damos la bienvenida al seminario.\n",
    ],
]);
```

```bash cURL theme={null}
curl -X POST "https://api.rask.ai/v3/projects/$PROJECT_ID/transcript/import" \
  -H "Authorization: Bearer $RASK_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"target": {"format": "srt", "content": "1\n00:00:00,000 --> 00:00:03,200\n[SPEAKER_00]: Les damos la bienvenida al seminario.\n"}}'
```

The answer is a `project_processing` job. Wait for it as for any [job](https://docs.api.rask.ai/v3/concepts/jobs.md). What to know before you send it:

* **The whole transcript is replaced.** Every segment gets a new id, and a `listSegments` cursor from before answers 409
  [`transcript_replaced`](https://docs.api.rask.ai/errors/transcript_replaced).
* **The side you leave out is made again.** With `source` only, the translation is made again by machine. With `target` only, the
  source text is recognized again inside your cue times. Send both files to keep both texts as you wrote them.
* **The old transcript is not lost.** A version is saved first, and if the job fails the old transcript comes back. See
  [listTranscriptVersions](https://docs.api.rask.ai/operations/listTranscriptVersions.md).
* **It does not dub.** The import ends when the text is ready.
* **The first dub must have been charged.** Before that, on a project created through this API, the import answers 409
  [`dub_payment_required`](https://docs.api.rask.ai/errors/dub_payment_required), like every other operation on the text.
* **The project must be at rest.** While it is processing, dubbing, translating or lip-syncing, the import answers 409
  [`project_not_ready`](https://docs.api.rask.ai/errors/project_not_ready), [`dub_in_progress`](https://docs.api.rask.ai/errors/dub_in_progress) or
  [`transcript_busy`](https://docs.api.rask.ai/errors/transcript_busy), and while another import runs,
  [`subtitle_import_in_progress`](https://docs.api.rask.ai/errors/subtitle_import_in_progress).
* **It counts against the hourly translation budget**, by its number of cues (`translation-hour` in
  [Rate limits](https://docs.api.rask.ai/v3/concepts/capabilities.md#rate-limits)). Over the budget it answers 429
  [`rate_limited`](https://docs.api.rask.ai/errors/rate_limited), and a file with more cues than the whole budget answers 422
  [`translation_batch_too_large`](https://docs.api.rask.ai/errors/translation_batch_too_large).

## 4. Bring the dub up to date

After the import, the dub no longer matches the text:

* every segment reads `dub_state: outdated`, and `counts.needs_redub` of [getTranscript](https://docs.api.rask.ai/operations/getTranscript.md) counts them;
* the dubbed outputs of [listProjectOutputs](https://docs.api.rask.ai/operations/listProjectOutputs.md) read `outdated: true`.

Dub again, after an estimate. Every line is new, so the whole project is dubbed, and the redub is charged for the speech in it,
in whole minutes.

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

const { data: job } = await rask.dubbing.createDub({
  path: { project_id: projectId },
  body: { expected_cost_minutes: 1 },
  idempotencyKey: dubKey,
});
```

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

job = client.dubbing.create_dub(project_id, expected_cost_minutes=1, idempotency_key=dub_key)
```

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

job, err := client.Dubbing.CreateDub(ctx, projectID, api.CreateDubRequest{
	ExpectedCostMinutes: 1,
}, rask.WithIdempotencyKey(dubKey))
if err != nil {
	return err
}
```

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

Job job = rask.dubbing().createDub(
        projectId,
        CreateDubRequest.builder().expectedCostMinutes(1).build(),
        RequestOptions.builder().idempotencyKey(dubKey).build()).data();
```

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

$job = $rask->dubbing->createDub($projectId, ['expected_cost_minutes' => 1], ['idempotency_key' => $dubKey]);
```

```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 '{}'

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 '{"expected_cost_minutes": 1}'
```

A new dub also leaves behind what was built on the old one. The lip-synced video is removed (`available: false`,
`unavailable_reason: not_produced`), and a video with burned-in subtitles or translated on-screen text reads `outdated: true`. Order
those again, each at its own price: see [Lip-sync](https://docs.api.rask.ai/v3/recipes/lipsync.md), [On-screen text](https://docs.api.rask.ai/v3/recipes/onscreen.md) and
[Downloads](https://docs.api.rask.ai/v3/recipes/downloads.md).


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