> ## 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/ko/api-reference/translation-status)를 폴링하거나 [웹훅](/docs/ko/api-reference/webhooks)을 수신하세요.

## 사전 조건

* 프로젝트가 존재하고 전사가 완료되어 있어야 합니다. [프로젝트 생성](/docs/ko/api-reference/create-project)(전사만) 또는 [번역 시작](/docs/ko/api-reference/translate)로 만든 뒤 `transcriptionStatus: "success"`을 기다리세요.
* SRT는 프로젝트 전사를 모두 덮어야 합니다. 모든 전사 세그먼트에는 큐가 하나 이상 필요하고, 모든 큐는 어떤 세그먼트 위에 있어야 합니다. [프로젝트 대본 내보내기](/docs/ko/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/ko/api-reference/create-project) 또는 [번역 시작](/docs/ko/api-reference/translate)가 반환하는 프로젝트 식별자입니다.
</ParamField>

### 본문 매개변수

<ParamField body="targetLanguage" type="string" required>
  `translatedSrt`의 언어입니다. 소문자이며 [번역 시작](/docs/ko/api-reference/translate)와 같은 목록에서 고릅니다. 프로젝트에 이 언어의 작업이 아직 없으면 하나가 만들어지고 언어가 프로젝트에 추가됩니다.
</ParamField>

<ParamField body="translatedSrt" type="string" required>
  최종 대상 언어 SRT 내용입니다(큐 번호, 타이밍 줄, 텍스트). 최대 1,000,000자입니다. 큐는 타임스탬프로 전사 세그먼트에 맞춰집니다. 세그먼트 시작 후 50ms 이내에 시작하는 큐는 그 세그먼트에, 그렇지 않으면 겹침이 가장 큰 세그먼트에 할당됩니다. 한 세그먼트의 여러 큐는 공백으로 이어집니다. 읽히는 각 세그먼트의 타이밍, 화자, 순서는 항상 프로젝트 전사에서 옵니다. 타임코드는 맞추는 데만 쓰입니다.
</ParamField>

<ParamField body="webhookUrl" type="string">
  이제부터 이 프로젝트의 [웹훅 이벤트](/docs/ko/api-reference/webhooks)를 받는 https 엔드포인트입니다. 계정에 설정된 엔드포인트를 덮어씁니다.

  **기본값:** 프로젝트에 이미 저장된 엔드포인트, 그다음 계정의 웹훅 엔드포인트.
</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/ko/api-reference/translate)와 같습니다. 영상 프로젝트가 필요합니다.

  **기본값:** 립싱크를 건너뛰려면 이 필드를 생략하세요.
</ParamField>

<ParamField body="activeSpeakerDetectionEnabled" type="boolean">
  요청한 립싱크 실행에서 활성 화자 감지를 켭니다.

  **기본값:** `false`
</ParamField>

<ParamField body="voiceCloningSettings" type="object">
  음성 복제 미세 조정입니다. `stability`와 `similarity`를 `0`에서 `1` 사이로 둔 JSON 객체입니다. `voiceMode: "cloning"`일 때만 사용합니다. 각 값의 안내는 [번역 시작](/docs/ko/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>
  처리 시간의 대략적인 추정치(분)입니다. 원본 미디어의 시작된 1분당 처리 5분입니다.
</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/ko/api-reference/translation-status)가 새 실행을 따릅니다. `status`는 `processing`에서 `success`(`translatedVideoUrl` 및 `translatedAudioUrl` 포함) 또는 `failed`(`error` 포함)로 이동합니다. `project.translation.completed` 및 `project.translation.failed` 웹훅은 같은 `targetLanguage`와 함께 전송됩니다. [프로젝트 세부 정보 가져오기](/docs/ko/api-reference/project-details)에서 번역본은 `settings.dubbingMode: "dub-only"`와 `settings.smartSync: false`를 보고합니다.

POST 요청이 수락된 뒤 정렬 오류가 비동기적으로 보고됩니다. 상태 엔드포인트는 `error.code`와 `error.message`가 포함된 `status: "failed"`를 반환하고, `project.translation.failed` 웹훅에도 같은 코드와 메시지가 들어 있습니다. `TRANSLATED_SRT_UNMATCHED_CUES`의 메시지에는 일치하지 않은 큐 번호가, `TRANSLATED_SRT_MISSING_SEGMENTS`의 메시지에는 큐가 없는 세그먼트 인덱스가 표시됩니다. 이 비동기 응답에는 구조화된 `details`가 포함되지 않습니다. SRT를 수정하고 엔드포인트를 다시 호출하세요. 실패한 버전은 기록에 남고 새 버전이 생성됩니다.

### 속도 확인

[프로젝트 대본 내보내기](/docs/ko/api-reference/project-transcript)로 번역 전사를 JSON으로 내보내세요. 각 세그먼트에는 생성된 음성과 세그먼트 구간의 비율 `finalSpeedFactor`가 있습니다. 대략 `1.3`을 넘는 큐를 줄인 뒤 수정한 SRT로 더빙을 다시 실행하세요.

## 예시 흐름

일반적인 자동화(예를 들어 n8n 또는 Make 시나리오)는 엔드포인트를 다음과 같이 잇습니다.

1. 미디어, `targetLanguage`, 명시적인 `originalLanguage`와 함께 `POST /v1/projects`(전사만).
2. `transcriptionStatus`가 `success`가 될 때까지 `GET /v1/translate/{projectId}/status`를 폴링하거나 `project.created` 웹훅을 기다립니다.
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`)는 이 요청이 아니라 상태 엔드포인트와 실패 웹훅이 반환합니다.


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