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

# 项目配音

> 使用你自己的翻译 SRT 为已转录的项目配音

# 项目配音

根据你提供的翻译 SRT，为已有项目生成语音并混合输出。VoiceCheap 在此接口不做任何翻译：没有翻译引擎、没有 SmartSync 改写、没有术语表。每条字幕都会在项目转录的对应片段上逐字朗读，然后与原始背景、字幕和可选的口型同步混合，与常规翻译相同。

当译文在别处完成（人工译者、你自己的工具、自动化流程），只需要 VoiceCheap 生成语音时使用。运行是异步的：轮询[获取翻译状态](/docs/zh/api-reference/translation-status)，或监听 [Webhooks](/docs/zh/api-reference/webhooks)。

## 前提条件

* 项目必须存在，并且转录必须完成：用[创建项目](/docs/zh/api-reference/create-project)（仅转录）或[开始翻译](/docs/zh/api-reference/translate)创建，然后等待 `transcriptionStatus: "success"`。
* 你的 SRT 必须覆盖项目转录：每个转录片段至少需要一条字幕，每条字幕都必须落在某个片段上。用[导出项目转录](/docs/zh/api-reference/project-transcript)导出转录，并逐条翻译以保留时间码。
* 该请求按同一媒体的翻译计费，并进行相同的额度检查。口型同步模式会照常消耗额外额度。

## 并发限制

创建项目、翻译和配音都会检查账户的进行中翻译上限（最多 10 个，实际限制可能更低）。只有进行中的翻译或配音任务占用名额；仅转录的项目不占用。达到上限时，API 返回 `CONCURRENT_TRANSLATION_LIMIT_REACHED`（HTTP 429）。实际限制见 `details.limit`。

同一项目和目标语言已有任务运行时，第二次运行也会被拒绝（`TRANSLATION_ALREADY_IN_PROGRESS`，HTTP 409）。

## 请求

此接口接受 JSON 请求体（`Content-Type: application/json`）。布尔和数值选项以 JSON 值发送，而不是字符串。

### 标头

<ParamField header="x-api-key" type="string" required>
  您的 VoiceCheap API 密钥。请从 [app.voicecheap.ai/page-api](https://app.voicecheap.ai/page-api) 获取。
</ParamField>

### 路径参数

<ParamField path="projectId" type="string" required>
  [创建项目](/docs/zh/api-reference/create-project)或[开始翻译](/docs/zh/api-reference/translate)返回的项目标识。
</ParamField>

### 正文参数

<ParamField body="targetLanguage" type="string" required>
  `translatedSrt` 的语言，小写，与[开始翻译](/docs/zh/api-reference/translate)使用同一列表。如果项目还没有该语言的工作，会创建一个，并把该语言加入项目。
</ParamField>

<ParamField body="translatedSrt" type="string" required>
  最终的目标语言 SRT 内容（字幕编号、时间行、文本）。最多 1,000,000 个字符。字幕条按时间戳匹配到转录片段：在片段开始后 50 毫秒内开始的字幕条会分配给该片段，否则分配给重叠最多的片段。同一片段上的多条字幕用空格连接。每条朗读片段的时间、说话人和顺序始终来自项目转录；你的时间码只用于匹配。
</ParamField>

<ParamField body="webhookUrl" type="string">
  一个 https 端点，从此接收此项目的[Webhook 事件](/docs/zh/api-reference/webhooks)，并覆盖账户上配置的端点。

  **默认值：** 先使用项目中已保存的端点，然后是账户的 Webhook 端点。
</ParamField>

<ParamField body="keepBackgroundMusic" type="boolean">
  在生成的语音下方保留原始背景音频（音乐、环境声）。

  **默认值：** `true`
</ParamField>

<ParamField body="keepOriginalVoice" type="boolean">
  在生成的语音下方小声保留原始说话人。

  **默认值：** `false`
</ParamField>

<ParamField body="originalVoiceVolume" type="number">
  启用 `keepOriginalVoice` 时原始语音的音量，范围从 `1` 到 `70`。

  **默认值：** `30`
</ParamField>

<ParamField body="voiceMode" type="string">
  音色策略：`cloning`（克隆项目中说话人的音色）或 `custom`（使用目录中的音色或您保存的自定义音色）。

  **默认值：** `cloning`
</ParamField>

<ParamField body="voiceId" type="string">
  当 `voiceMode` 为 `custom` 时必填。请使用 `dubbing_voices_list` 中的
  ID：目录中的音色或经过身份验证的账户保存的自定义音色。团队计费不会授予访问其他成员已保存音色的权限。
</ParamField>

<ParamField body="voiceIsolatorOption" type="string">
  启用 `keepBackgroundMusic` 时的人声分离模式：`studio` 或 `realistic`。

  **默认值：** `studio`
</ParamField>

<ParamField body="subtitles" type="boolean">
  把字幕烧录到输出视频中。需要视频项目。

  **默认值：** `false`
</ParamField>

<ParamField body="subtitlesSource" type="string">
  启用 `subtitles` 时的字幕文本：`translated`（你的 SRT）或 `original`（源转录）。

  **默认值：** `translated`
</ParamField>

<ParamField body="lipSyncMode" type="string">
  配音准备好后运行口型同步：`standard`、`pro` 或 `studio`。方案、时长和额度规则与[开始翻译](/docs/zh/api-reference/translate)相同。需要视频项目。

  **默认值：** 省略此字段可跳过口型同步。
</ParamField>

<ParamField body="activeSpeakerDetectionEnabled" type="boolean">
  为请求的口型同步运行启用当前说话人检测。

  **默认值：** `false`
</ParamField>

<ParamField body="voiceCloningSettings" type="object">
  语音克隆微调，JSON 对象包含 `stability` 和 `similarity`，取值在 `0` 到 `1` 之间。仅在 `voiceMode: "cloning"` 时可用。各值说明见[开始翻译](/docs/zh/api-reference/translate)。
</ParamField>

<ParamField body="translationTimeSkips" type="array">
  保留为原始语言的源时间范围，格式为 `[{ "startTime": 10.2, "endTime": 12.8 }]`。需要视频项目和 `keepBackgroundMusic: true`，不能与 `keepOriginalVoice` 同时使用，也不能与转录片段重叠。
</ParamField>

## 响应

<ResponseField name="success" type="boolean" required>
  请求成功时始终为 `true`。
</ResponseField>

<ResponseField name="message" type="string" required>
  可供人阅读的确认信息。
</ResponseField>

<ResponseField name="projectId" type="string" required>
  正在配音的项目。
</ResponseField>

<ResponseField name="workId" type="string" required>
  用于 `targetLanguage` 的翻译工作。它与状态、详情和转录接口中的 `workId` 一致。
</ResponseField>

<ResponseField name="estimatedDuration" type="number" required>
  大致处理时间估计（分钟）：源媒体每开始一分钟，处理约五分钟。
</ResponseField>

```json Response theme={null}
{
  "success": true,
  "message": "Dubbing started successfully from the provided translation. Use the status endpoint to track progress.",
  "projectId": "project_123",
  "workId": "work_456",
  "estimatedDuration": 10
}
```

## 跟踪运行

配音一开始，项目就会切换到 `translation` 流程，因此[获取翻译状态](/docs/zh/api-reference/translation-status)会跟踪这次新运行：`status` 从 `processing` 变为 `success`（带有 `translatedVideoUrl` 和 `translatedAudioUrl`）或 `failed`（带有 `error`）。`project.translation.completed` 和 `project.translation.failed` Webhook 会带着相同的 `targetLanguage` 发送。在[获取项目详情](/docs/zh/api-reference/project-details)中，翻译版本会报告 `settings.dubbingMode: "dub-only"` 和 `settings.smartSync: false`。

POST 请求被接受后，对齐错误才会异步报告。状态接口返回带有 `error.code` 和 `error.message` 的 `status: "failed"`；`project.translation.failed` Webhook 携带相同的代码和消息。`TRANSLATED_SRT_UNMATCHED_CUES` 的消息中包含未匹配的字幕条编号，`TRANSLATED_SRT_MISSING_SEGMENTS` 的消息中包含没有字幕条的片段索引。这些异步响应不包含结构化的 `details`。修正 SRT 后再次调用接口：失败的版本仍保留在项目历史中，并创建新版本。

### 检查语速

用[导出项目转录](/docs/zh/api-reference/project-transcript)把翻译转录导出为 JSON。每个片段都带有 `finalSpeedFactor`，即生成语音与片段窗口的比值。缩短大约高于 `1.3` 的字幕条，再用修订后的 SRT 重新配音。

## 示例流程

典型的自动化（例如 n8n 或 Make 场景）按以下方式串联这些接口：

1. 使用媒体、`targetLanguage` 和明确的 `originalLanguage` 调用 `POST /v1/projects`（仅转录）。
2. 轮询 `GET /v1/translate/{projectId}/status`，直到 `transcriptionStatus` 为 `success`，或等待 `project.created` Webhook。
3. 使用 `{ "source": "original", "outputFormat": "srt" }` 调用 `POST /v1/projects/{projectId}/transcript`，翻译字幕条并保留编号和时间码。
4. 使用翻译后的 SRT 调用 `POST /v1/projects/{projectId}/dub`。
5. 轮询状态接口，直到 `status` 为 `success`，或等待 `project.translation.completed`。
6. 下载 `translatedVideoUrl`（如果请求了口型同步，则下载 `lipSync.videoUrl`）。
7. 输出保存在你这边之后，调用 `DELETE /v1/translate/{projectId}`。

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST "https://api.voicecheap.ai/v1/projects/project_123/dub" \
    -H "x-api-key: vc_your-api-key" \
    -H "Content-Type: application/json" \
    -d '{
      "targetLanguage": "english",
      "translatedSrt": "1\n00:00:00,000 --> 00:00:02,400\nWelcome to this lesson.\n\n2\n00:00:02,400 --> 00:00:05,100\nToday we talk about fractions.",
      "keepBackgroundMusic": true,
      "subtitles": true,
      "subtitlesSource": "translated"
    }'
  ```

  ```typescript TypeScript theme={null}
  interface DubProjectResponse {
    success: boolean;
    message: string;
    projectId: string;
    workId: string;
    estimatedDuration: number;
  }

  const API_BASE_URL = 'https://api.voicecheap.ai/v1';
  const headers = { 'x-api-key': 'vc_your-api-key', 'Content-Type': 'application/json' };

  async function waitForStatus(projectId: string, isReady: (status: any) => boolean): Promise<any> {
    while (true) {
      const status = await fetch(`${API_BASE_URL}/translate/${projectId}/status`, { headers }).then((response) => response.json());
      if (status.status === 'failed') throw new Error(`${status.error?.code}: ${status.error?.message}`);
      if (isReady(status)) return status;
      await new Promise((resolve) => setTimeout(resolve, 15_000));
    }
  }

  async function dubExistingProject(projectId: string, translatedSrt: string): Promise<string> {
    // 1. The project was created earlier with POST /v1/projects; wait for its transcript.
    await waitForStatus(projectId, (status) => status.transcriptionStatus === 'success');

    // 2. Send the translation you produced from the exported transcript.
    const dubResponse = await fetch(`${API_BASE_URL}/projects/${projectId}/dub`, {
      method: 'POST',
      headers,
      body: JSON.stringify({ targetLanguage: 'english', translatedSrt, keepBackgroundMusic: true }),
    });
    if (!dubResponse.ok) {
      const error = await dubResponse.json();
      throw new Error(`${error.code}: ${error.message}`);
    }
    const { workId } = (await dubResponse.json()) as DubProjectResponse;
    console.log('Dubbing started on work', workId);

    // 3. Wait for the dubbed output.
    const completed = await waitForStatus(projectId, (status) => status.status === 'success');
    return completed.translatedVideoUrl as string;
  }
  ```

  ```python Python theme={null}
  import time
  import requests

  API_BASE_URL = 'https://api.voicecheap.ai/v1'
  HEADERS = {'x-api-key': 'vc_your-api-key'}


  def wait_for_status(project_id, is_ready):
      while True:
          status = requests.get(f'{API_BASE_URL}/translate/{project_id}/status', headers=HEADERS).json()
          if status['status'] == 'failed':
              raise RuntimeError(f"{status['error']['code']}: {status['error']['message']}")
          if is_ready(status):
              return status
          time.sleep(15)


  def dub_existing_project(project_id, translated_srt):
      wait_for_status(project_id, lambda status: status['transcriptionStatus'] == 'success')

      response = requests.post(
          f'{API_BASE_URL}/projects/{project_id}/dub',
          headers=HEADERS,
          json={'targetLanguage': 'english', 'translatedSrt': translated_srt, 'keepBackgroundMusic': True},
      )
      response.raise_for_status()
      print('Dubbing started on work', response.json()['workId'])

      completed = wait_for_status(project_id, lambda status: status['status'] == 'success')
      return completed['translatedVideoUrl']
  ```
</CodeGroup>

## 错误

| 状态 | 代码 | 描述 |
| - | - | - |
| 400 | `VALIDATION_ERROR` | 请求体字段缺失或无效（`details` 列出每个字段） |
| 400 | `INVALID_TRANSLATED_SRT` | translatedSrt 不包含有效的 SRT 字幕条 |
| 400 | `INVALID_BOOLEAN_VALUE` | 布尔选项未以 JSON 布尔值发送 |
| 400 | `INVALID_NUMBER_VALUE` | 数值选项未以 JSON 数字发送 |
| 400 | `DURATION_DETECTION_FAILED` | 项目中保存的媒体时长不可用 |
| 400 | `VOICE_ID_REQUIRED` | 选择了自定义语音模式但未提供语音 ID |
| 400 | `VOICE_ID_NOT_ALLOWED_WITH_CLONING` | 语音 ID 与语音克隆结合使用 |
| 400 | `VOICE_CLONING_SETTINGS_NOT_ALLOWED` | 克隆设置与自定义语音模式结合使用 |
| 400 | `TRANSLATION_TIME_SKIP_CONFLICT` | 时间跳过与其他音频选项冲突 |
| 400 | `TRANSLATION_TIME_SKIP_REQUIRES_BACKGROUND` | 时间跳过需要背景音频 |
| 400 | `TRANSLATION_TIME_SKIP_OUT_OF_RANGE` | 时间跳过超出了媒体时长 |
| 400 | `SUBTITLES_NOT_AVAILABLE_FOR_AUDIO` | 音频输入请求了硬编码字幕 |
| 400 | `LIPSYNC_NOT_AVAILABLE_FOR_AUDIO` | 音频输入请求了口型同步 |
| 400 | `TRANSLATION_TIME_SKIP_NOT_AVAILABLE_FOR_AUDIO` | 音频输入请求了时间跳过 |
| 400 | `LIPSYNC_VIDEO_TOO_LONG` | 超过支持时长的媒体请求了口型同步 |
| 401 | `MISSING_API_KEY` | 需要 API 密钥 |
| 401 | `INVALID_API_KEY_FORMAT` | API 密钥必须以 `vc_` 开头 |
| 401 | `INVALID_API_KEY` | 提供的 API 密钥无效 |
| 403 | `SUBSCRIPTION_REQUIRED` | API 访问需要 Starter 或更高级别的计划 |
| 403 | `FORBIDDEN` | 您没有访问该项目的权限 |
| 403 | `INSUFFICIENT_CREDITS` | 没有足够的额度为此项目配音 |
| 403 | `CUSTOM_VOICE_ACCESS_DENIED` | 所选音色既不是目录中的音色，也不是经过身份验证的账户保存的自定义音色。 |
| 403 | `LIPSYNC_MODE_ACCESS_DENIED` | 当前套餐不支持高级口型同步模式 |
| 404 | `PROJECT_NOT_FOUND` | 项目不存在 |
| 409 | `DUB_ONLY_REQUIRES_TRANSCRIPTION` | 项目转录仍在处理中或已失败 |
| 409 | `TRANSLATION_ALREADY_IN_PROGRESS` | 该项目中此语言的配音已在进行中 |
| 429 | `RATE_LIMIT_EXCEEDED` | 请求过多（限制：每分钟 10 个请求） |
| 429 | `CONCURRENT_TRANSLATION_LIMIT_REACHED` | 已达到账户并发翻译上限；参见 `details.limit` |
| 500 | `INTERNAL_ERROR` | 意外的服务器错误 |

运行过程中发现的对齐失败（`TRANSLATED_SRT_UNMATCHED_CUES`、`TRANSLATED_SRT_MISSING_SEGMENTS`）由状态接口和失败 Webhook 返回，而不是由本次请求返回。


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