# 보이스와 오디오

> Nodaro SDK로 TypeScript에서 보이스를 찾고, 바꾸고, 화자별로 교체하고, 새로 디자인하며, 동영상을 더빙하고, 오디오를 분리, 믹스, 텍스트로 변환합니다.

Source: https://nodaro.ai/ko/docs/developers/sdk/voices-and-audio

<strong>`client.voices`</strong>는 ElevenLabs 보이스를 다룹니다. 보이스를 나열하고 검색하며, 녹음 속 보이스를 바꾸고, 화자마다 새 보이스를 주고, 새 보이스를 디자인하고, 오디오와 동영상을 다른 언어로 더빙합니다. <strong>`client.audio`</strong>는 오디오의 구성 요소를 하나씩 따로 제공합니다. 분리, 음성 추출, 효과, 믹스, 볼륨, 합치기, 음성 텍스트 변환입니다. 모든 생성 메서드는 `jobId`를 반환하며, 이를 [`client.jobs.getStatus()`](https://nodaro.ai/docs/developers/sdk/jobs-and-executions)로 폴링하세요. 이 메서드들은 [보이스와 미디어 REST API](https://nodaro.ai/docs/developers/api/voice-and-media)를 호출합니다.

## 메서드
| 메서드 | 기능 |
| --- | --- |
| [`voices.list()`](#voiceslist) | 기본 제공 보이스 나열 |
| [`voices.searchLibrary(params?)`](#voicessearchlibraryparams) | 커뮤니티 보이스 라이브러리 검색 |
| [`voices.listClones()`](#voiceslistclones) | 내 기존 복제 보이스 나열 |
| [`voices.deleteClone(id)`](#voicesdeletecloneid) | 내 복제 보이스 하나 삭제 |
| [`voices.createClone()`과 `createCloneFromFile()`](#voicescreateclone-and-createclonefromfile) | 종료됨 |
| [`voices.change(input)`](#voiceschangeinput) | 녹음이나 동영상 속 보이스 바꾸기 |
| [`voices.recast(input)`](#voicesrecastinput) | 화자마다 다른 보이스 주기 |
| [`voices.analyze(input)`](#voicesanalyzeinput) | 보이스 교체 전에 화자 감지 |
| [`voices.exportMix(input)`](#voicesexportmixinput) | 믹스한 보이스 스템으로 동영상 렌더링 |
| [`voices.design(input)`](#voicesdesigninput) | 설명으로 새 보이스 만들기 |
| [`voices.remix(input)`](#voicesremixinput) | 설명한 보이스로 텍스트 읽기 |
| [`voices.dub(input)`](#voicesdubinput) | 오디오나 동영상을 다른 언어로 더빙 |
| [`voices.textToDialogue(input)`](#voicestexttodialogueinput) | 여러 화자의 스크립트를 오디오 파일 하나로 음성화 |
| [`audio.separate(input)`](#audioseparateinput) | 트랙을 스템으로 분리 |
| [`audio.isolate(input)`](#audioisolateinput) | 주 음성만 남기고 노이즈 제거 |
| [`audio.applyFx(input)`](#audioapplyfxinput) | 리버브, 에코, 전화기, 메가폰 효과 추가 |
| [`audio.mix(input)`](#audiomixinput) | 여러 트랙을 하나로 겹치기 |
| [`audio.adjustVolume(input)`](#audioadjustvolumeinput) | 레벨 변경, 노멀라이즈, 페이드 |
| [`audio.pitchTempo(input)`](#audiopitchtempoinput) | 피치 변경, 또는 피치를 바꾸지 않고 템포 변경 |
| [`audio.combine(input)`](#audiocombineinput) | 오디오 세그먼트를 이어 붙이기 |
| [`audio.transcribe(input)`](#audiotranscribeinput) | 음성을 단어 타이밍과 함께 텍스트로 변환 |

## client.voices: 보이스
### voices.list()
기본 제공 ElevenLabs 보이스를 나열합니다(`GET /v1/voices`). 서버에 ElevenLabs 키가 설정되어 있지 않으면 대신 엄선된 보이스 세트를 반환합니다.

```ts
list(): Promise<Voice[]>
```

```ts
const voices = await client.voices.list()
```

### voices.searchLibrary(params?)
커뮤니티 보이스 라이브러리를 검색합니다(`GET /v1/voices/library`). 모든 매개변수는 선택 사항이며, 빈 값은 빠지므로 서버 기본값이 적용됩니다. 응답의 `hasMore`는 다음 페이지가 있는지 알려 줍니다.

```ts
searchLibrary(params?: VoiceLibraryParams): Promise<{ voices: Voice[]; hasMore: boolean }>
```

<TypeTable
type={{
search: { type: 'string', description: "검색할 단어입니다." },
gender: { type: 'string', description: "성별로 필터링합니다." },
age: { type: 'string', description: "나이로 필터링합니다." },
accent: { type: 'string', description: "억양으로 필터링합니다." },
language: { type: 'string', description: "en 같은 언어 코드로 필터링합니다." },
category: { type: 'string', description: "카테고리로 필터링합니다." },
use_cases: { type: 'string', description: "용도로 필터링합니다." },
descriptives: { type: 'string', description: "묘사 태그로 필터링합니다." },
featured: { type: 'boolean', description: "추천 보이스만 표시합니다." },
sort: { type: 'string', description: "trending 같은 정렬 순서입니다." },
page: { type: 'number', default: '0', description: "0부터 세는 페이지입니다." },
page_size: { type: 'number', default: '30', description: "페이지당 보이스 수로, 1~100입니다." },
}}
/>

```ts
const { voices, hasMore } = await client.voices.searchLibrary({ search: "deep", language: "en" })
const voice = voices[0]

await client.nodes.run("text-to-speech", {
text: "Hello!",
voice: voice.voice_id,
voiceType: "library",
...(voice.recommendedProvider ? { provider: voice.recommendedProvider } : {}),
})
```

각 보이스에는 그 보이스가 검증된 음성 모델에 대한 힌트가 두 가지 담길 수 있습니다.

- <strong>`recommendedProvider`</strong>는 그 보이스에 가장 잘 맞는 모델입니다. 모델 선택기가 없는 앱은 이 값을 [텍스트 음성 변환](https://nodaro.ai/docs/nodes/audio/text-to-speech)에 `provider`로 보내야, 보이스가 라이브러리 미리 듣기와 같은 소리를 냅니다.
- <strong>`verifiedProviders`</strong>는 그 보이스가 검증된 모든 모델의 목록입니다. 모델 선택기가 있는 앱은 사용자가 고른 모델이 이 목록에 없을 때만 선택을 바꿔야 합니다.

두 힌트는 ElevenLabs v4, ElevenLabs v3, ElevenLabs v4 Turbo, Turbo v2.5, Multilingual v2 순서를 따릅니다. `recommendedProvider`는 `verifiedProviders`의 첫 항목입니다. 모델이 목록에 있는지 확인하고, 고정된 값 집합과 비교하지 마세요. 나중에 추가되는 음성 모델은 출시될 때 목록에 들어갑니다. 두 힌트는 설치에서 제공하는 모델만 가리킵니다. 그곳에서 검증된 모델이 없는 보이스에는 두 필드가 모두 없으므로, 사용자의 현재 모델을 유지하세요.

### voices.listClones()
내 복제 보이스를 나열합니다(`GET /v1/voice-clones`). 복제 기능이 종료되기 전에 만든 복제 보이스는 보이스를 받는 모든 곳에서 여전히 보이스 id로 쓸 수 있습니다.

```ts
listClones(): Promise<VoiceClone[]>
```

```ts
const clones = await client.voices.listClones()
```

### voices.deleteClone(id)
내 복제 보이스 하나를 삭제합니다(`DELETE /v1/voice-clones/:id`).

```ts
deleteClone(id: string): Promise<void>
```

<TypeTable
type={{
id: { type: 'string', required: true, description: "복제 보이스 id입니다." },
}}
/>

```ts
await client.voices.deleteClone(cloneId)
```

### voices.createClone()과 createCloneFromFile()
Nodaro는 더 이상 보이스 복제를 제공하지 않습니다. 이 기능은 2026년 9월에 종료되었습니다. 두 메서드는 클라이언트에 남아 있으며 사용 중단으로 표시되어 있습니다. 두 메서드는 `code`가 `voice_cloning_retired`이고 상태가 410인 `NodaroError`로 실패합니다. 새 사용자 지정 보이스가 필요하면 [`design()`](#voicesdesigninput)을 사용하세요.

## client.voices: 보이스 체인저
### voices.change(input)
[보이스 체인저](https://nodaro.ai/docs/nodes/audio/voice-changer) 노드처럼, 녹음이나 말하는 동영상 전체의 보이스를 다른 보이스로 바꿉니다(`POST /v1/voice-changer`). `videoUrl`을 주면 서버가 동영상에서 오디오를 꺼내 보이스를 바꾸고, 새 보이스를 원래 화면에 다시 입힙니다.

```ts
change(input: {
voiceId: string
audioUrl?: string
videoUrl?: string
model?: string
stability?: number
similarityBoost?: number
style?: number
useSpeakerBoost?: boolean
seed?: number
removeBackgroundNoise?: boolean
}): Promise<{ jobId: string }>
```

<TypeTable
type={{
voiceId: { type: 'string', required: true, description: "새 보이스입니다. 기본 제공 보이스 이름이나 ElevenLabs 보이스 id를 쓰세요." },
audioUrl: { type: 'string', description: "바꿀 녹음입니다. audioUrl이나 videoUrl을 주세요. 둘 다 보내면 동영상이 우선합니다." },
videoUrl: { type: 'string', description: "보이스를 바꿀 동영상입니다." },
model: { type: 'string', description: "음성 대 음성 모델을 직접 지정합니다." },
stability: { type: 'number', description: "보이스 안정성으로, 0~1입니다." },
similarityBoost: { type: 'number', description: "대상 보이스와 얼마나 가깝게 맞출지로, 0~1입니다." },
style: { type: 'number', default: '0', description: "스타일 과장 정도로, 0~1입니다. 0보다 크면 표현이 강해지지만 속도와 안정성이 조금 떨어집니다." },
useSpeakerBoost: { type: 'boolean', description: "대상 보이스와의 유사도를 높입니다. 조금 느려집니다." },
seed: { type: 'number', description: "출력을 재현할 수 있게 하는 정수입니다." },
removeBackgroundNoise: { type: 'boolean', description: "true이면 깨끗한 보이스만 반환합니다. false이면 새 보이스 아래에 음악과 효과음을 유지합니다." },
}}
/>

```ts
const { jobId } = await client.voices.change({
videoUrl: "https://example.com/talking.mp4",
voiceId: "Aria",
})
// the finished job's output_data has videoUrl and audioUrl
```

### voices.recast(input)
[보이스 체인저 Pro](https://nodaro.ai/docs/nodes/audio/voice-changer-pro) 노드처럼, 녹음에서 감지된 화자마다 다른 보이스를 줍니다(`POST /v1/voice-changer-pro`). Nodaro Cloud에서 실행되고, 크레딧이 들며, 작업으로 실행됩니다.

```ts
recast(input: VoiceChangerProInput): Promise<{ jobId: string }>
```

<TypeTable
type={{
orderedVoices: { type: 'Array<VoiceChangerProVoice | null>', required: true, description: "감지된 화자 순서대로 화자당 하나씩, 1~8개 항목입니다. 각 항목은 보이스 id, 보이스별 설정을 담은 객체, 또는 그 화자의 원래 목소리를 유지하는 null입니다. 적어도 한 항목은 null이 아니어야 합니다." },
audioUrl: { type: 'string', description: "녹음입니다. audioUrl이나 videoUrl을 주세요." },
videoUrl: { type: 'string', description: "화자의 보이스를 바꿀 동영상입니다. 새 보이스는 원래 화면에 다시 입혀집니다." },
model: { type: 'string', description: "음성 대 음성 모델을 직접 지정합니다." },
preserveBackground: { type: 'boolean', default: 'true', description: "새 보이스 아래에 음악과 효과음을 다시 믹스합니다. false이면 보이스만 반환합니다." },
separationQuality: { type: '"fast" | "best"', default: '"fast"', description: "fast는 더 빠르고 보이스를 더 많이 보존합니다. best는 보이스와 음악을 더 정밀하게 분리합니다." },
removeBackgroundNoise: { type: 'boolean', description: "결과에서 노이즈도 제거합니다." },
musicVolumeMode: { type: '"match" | "normalize" | "manual"', default: '"match"', description: "유지되는 배경음의 레벨입니다. 원래 레벨, 노멀라이즈, 또는 musicVolume 값입니다." },
musicVolume: { type: 'number', description: "musicVolumeMode가 manual일 때 쓰는 배경음 레벨로, 퍼센트 단위 0~200입니다." },
voiceFx: { type: '{ preset, wetDryMix?, delayMs?, decay? }', description: "모든 새 보이스에 거는 효과로, 배경음이 돌아오기 전에 적용됩니다." },
output: { type: '"video" | "stems"', default: '"video"', description: "video는 완성된 결과를 렌더링합니다. stems는 믹스하지 않은 개별 트랙을 반환하므로, 직접 믹스한 다음 exportMix()로 렌더링하세요." },
analysis: { type: 'VcpAnalysis', description: "이전 analyze() 작업의 결과로, 화자 감지를 건너뜁니다." },
}}
/>

`orderedVoices`의 각 항목은 다음 중 하나입니다.

- **보이스 id**: `"Rachel"` 같은 기본 제공 보이스 이름이나 ElevenLabs 보이스 id입니다.
- **`null`**: 유지 슬롯입니다. 그 화자는 원래 목소리를 유지하고, 뒤의 화자는 계속 보이스가 바뀝니다. 보이스를 바꾸는 화자만 가격이 매겨지므로 유지 슬롯은 비용이 들지 않습니다.
- **객체**: `voiceId`와 그 화자의 설정을 담습니다. 0~1 범위의 `stability`, `similarityBoost`, `style`, 그리고 `useSpeakerBoost`, 0~4,294,967,295 범위의 `seed`입니다. `volumeMode`는 `"match"`(기본값, 원래 화자의 레벨), `"normalize"`, 또는 퍼센트 단위 0~200의 `volume`을 쓰는 `"manual"`입니다. `engine: "v3"`는 녹음된 음성을 변환하는 대신, 스크립트를 바탕으로 ElevenLabs v3로 `[audio tags]`를 지원하며 대사를 새로 읽습니다. `engine: "v4"`는 같은 일을 ElevenLabs v4로 하며, `stability`로 0~1 사이의 어떤 값이든 받고 `similarityBoost`를 적용합니다.

화자 0은 `orderedVoices[0]`을, 화자 1은 `orderedVoices[1]`을 받는 식입니다. 마지막 항목 뒤의 화자는 원래 목소리를 유지합니다. 보이스와 음악은 항상 먼저 분리됩니다. `preserveBackground`는 음악을 다시 넣을지만 결정합니다.

`voiceFx.preset`은 리버브 공간(`room`, `bathroom`, `car`, `hall`, `concert-hall`, `church`, `cave`, `arena`, `outdoor`), `telephone`, `megaphone`, `echo`, `custom` 중 하나입니다. 리버브 프리셋은 0~100 범위의 `wetDryMix`를 씁니다. `echo`와 `custom`은 20~2,000 범위의 `delayMs`와 0~1 범위의 `decay`를 씁니다.

```ts
// Recast speakers 0 and 2, and keep speaker 1's own voice
const { jobId } = await client.voices.recast({
audioUrl: "https://example.com/panel.mp3",
orderedVoices: ["Rachel", null, "Aria"],
})

// Repeatable voices, a hall reverb, and finer separation
const { jobId: tuned } = await client.voices.recast({
audioUrl: "https://example.com/dialogue.mp3",
orderedVoices: [
{ voiceId: "Rachel", seed: 12345, stability: 0.6 },
{ voiceId: "Aria", seed: 67890, volumeMode: "manual", volume: 120 },
],
voiceFx: { preset: "hall", wetDryMix: 35 },
separationQuality: "best",
})
```

동영상 모드에서는 완료된 작업의 `output_data`에 `videoUrl`과 `audioUrl`이 들어 있습니다.

### voices.analyze(input)
클립의 화자를 **보이스를 바꾸지 않고** 감지합니다(`POST /v1/voice-changer-pro/analyze`). 보이스와 음악을 한 번 분리하고, 누가 언제 말하는지 찾습니다. Nodaro Cloud에서 고정 가격으로, 작업으로 실행됩니다.

```ts
analyze(input: {
audioUrl?: string
videoUrl?: string
separationQuality?: "fast" | "best"
suggestTitle?: boolean
}): Promise<{ jobId: string }>
```

<TypeTable
type={{
audioUrl: { type: 'string', description: "녹음입니다. audioUrl과 videoUrl 중 정확히 하나만 주세요." },
videoUrl: { type: 'string', description: "분석할 동영상입니다." },
separationQuality: { type: '"fast" | "best"', description: "recast()와 같은 분리 품질입니다." },
suggestTitle: { type: 'boolean', description: "클립 제목도 제안합니다." },
}}
/>

완료된 작업의 `output_data`는 `VcpAnalysis`입니다. 분리된 `vocalsUrl`과 `backgroundUrl`, 감지된 `speakers`(각각 `id`, 시간 `segments`, `firstStartSec`, `wordCount`, 텍스트 `snippet` 포함), `languageCode`와 `languageProbability`, 그리고 요청했을 때의 `suggestedTitle`이 담깁니다. 이후의 모든 `recast()`에 이것을 `analysis`로 전달하세요. 그러면 보이스 교체가 분리된 트랙을 재사용하고 감지 비용을 다시 내지 않습니다.

### voices.exportMix(input)
믹스한 스템 세트로 최종 동영상을 렌더링합니다(`POST /v1/voice-changer-pro/export`). `recast({ output: "stems" })` 다음의 마지막 단계입니다. Nodaro Cloud에서 고정 가격으로, 작업으로 실행됩니다.

```ts
exportMix(input: {
videoUrl: string
tracks: Array<{ url: string; gain: number; muted: boolean; kind?: "voice" | "background" }>
voiceFx?: { preset: AudioFxPreset; wetDryMix?: number; delayMs?: number; decay?: number }
}): Promise<{ jobId: string }>
```

<TypeTable
type={{
videoUrl: { type: 'string', required: true, description: "동영상입니다. 화면은 다시 인코딩하지 않고 복사됩니다." },
tracks: { type: 'VcpExportTrack[]', required: true, description: "최대 16개 트랙으로, 적어도 하나는 음소거되지 않아야 합니다. 각 트랙에는 url, 0~200 범위의 gain, muted, 선택 사항인 kind가 있습니다." },
voiceFx: { type: '{ preset, wetDryMix?, delayMs?, decay? }', description: "보이스 트랙에만 거는 효과이며, 배경 트랙에는 적용되지 않습니다." },
}}
/>

대화형 흐름은 한 번 분석하고, 스템으로 보이스를 바꾼 다음, 믹스하고 렌더링합니다. 작은 폴링 헬퍼를 쓴 예는 다음과 같습니다.

```ts

async function outputOf(jobId: string): Promise<any> {
for (;;) {
const { data } = await client.jobs.getStatus(jobId)
if (data.status === "completed") return data.output_data
if (data.status === "failed" || data.status === "cancelled") throw new Error(data.error_message ?? data.status)
await new Promise((resolve) => setTimeout(resolve, 2_000))
}
}

const { jobId: analyzeJob } = await client.voices.analyze({ videoUrl })
const analysis = (await outputOf(analyzeJob)) as VcpAnalysis

const { jobId: recastJob } = await client.voices.recast({
videoUrl,
orderedVoices: ["Rachel", null, "Aria"],
output: "stems",
analysis,
})
const stems = await outputOf(recastJob)

const { jobId: exportJob } = await client.voices.exportMix({
videoUrl,
tracks: [
{ url: stems.tracks[0].url, gain: 100, muted: false },
{ url: stems.tracks[1].url, gain: 90, muted: false },
{ url: stems.backgroundUrl, gain: 70, muted: false, kind: "background" },
],
voiceFx: { preset: "hall", wetDryMix: 25 },
})
const { videoUrl: finalVideo } = await outputOf(exportJob)
```

게인, 음소거, 효과는 동영상을 렌더링할 때 적용되고, 화면은 그대로 복사됩니다. 따라서 내보낸 결과는 미리보기와 일치하며, 내보내기 전에 믹스를 원하는 만큼 바꿀 수 있습니다. 모든 트랙을 음소거한 믹스는 400으로 거부됩니다.

## client.voices: 만들기와 번역
### voices.design(input)
[보이스 디자인](https://nodaro.ai/docs/nodes/audio/voice-design) 노드처럼, 텍스트 설명으로 새 합성 보이스를 만듭니다(`POST /v1/voice-design`). 완료된 작업에는 오디오 미리 듣기와 새 보이스의 id가 담기며, 이 id는 보이스를 받는 모든 곳에서 쓸 수 있습니다.

```ts
design(input: {
text: string
voiceDescription: string
model?: string
loudness?: number
guidanceScale?: number
seed?: number
quality?: number
shouldEnhance?: boolean
userPrompt?: string
}): Promise<{ jobId: string }>
```

<TypeTable
type={{
text: { type: 'string', required: true, description: "미리 듣기에서 읽을 대사로, 100~1,000자입니다." },
voiceDescription: { type: 'string', required: true, description: "원하는 보이스입니다. 예를 들어 나이 든 내레이터의 따뜻하고 낮은 목소리처럼 씁니다." },
model: { type: 'string', description: "보이스 디자인 모델을 직접 지정합니다." },
loudness: { type: 'number', description: "음량으로, -1~1입니다." },
guidanceScale: { type: 'number', description: "설명을 얼마나 충실히 따를지로, 0~100입니다." },
seed: { type: 'number', description: "결과를 재현하기 위한 시드입니다." },
quality: { type: 'number', description: "품질 설정입니다." },
shouldEnhance: { type: 'boolean', description: "모델이 설명을 보강하게 합니다." },
userPrompt: { type: 'string', description: "원래 요청으로, 작업과 함께 보관됩니다." },
}}
/>

```ts
const { jobId } = await client.voices.design({
text: "Welcome back. Tonight we follow the river north, into the mountains where the story began.",
voiceDescription: "A calm, deep voice of an older male narrator with a slight British accent",
})
```

### voices.remix(input)
[보이스 리믹스](https://nodaro.ai/docs/nodes/audio/voice-remix) 노드처럼, 보이스를 만들지 않고 평범한 말로 설명한 보이스로 텍스트를 읽습니다(`POST /v1/voice-remix`).

```ts
remix(input: { text: string; voiceDescription: string; userPrompt?: string }): Promise<{ jobId: string }>
```

<TypeTable
type={{
text: { type: 'string', required: true, description: "읽을 텍스트로, 1~5,000자입니다." },
voiceDescription: { type: 'string', required: true, description: "말로 설명한, 사용할 보이스입니다." },
userPrompt: { type: 'string', description: "원래 요청으로, 작업과 함께 보관됩니다." },
}}
/>

```ts
const { jobId } = await client.voices.remix({
text: "Your order is on its way.",
voiceDescription: "A cheerful young woman, fast and upbeat",
})
```

### voices.dub(input)
[더빙](https://nodaro.ai/docs/nodes/audio/dubbing) 노드처럼, 각 화자의 목소리를 유지하면서 오디오나 동영상 전체를 다른 언어로 더빙합니다(`POST /v1/dubbing`). 동영상 더빙은 더빙된 클립인 `output_data.videoUrl`과, 더빙된 트랙만 담은 `output_data.audioUrl`을 반환합니다.

```ts
dub(input: DubbingInput): Promise<{ jobId: string }>
```

<TypeTable
type={{
targetLanguage: { type: 'string', required: true, description: "더빙할 언어로, es나 pt-BR 같은 코드로 씁니다." },
audioUrl: { type: 'string', description: "오디오 소스입니다. audioUrl, videoUrl, sourceUrl 중 정확히 하나만 주세요." },
videoUrl: { type: 'string', description: "동영상 소스입니다. 결과는 더빙된 동영상입니다." },
sourceUrl: { type: 'string', description: "공개 YouTube, TikTok 링크나 직접 링크로, 대신 가져옵니다." },
sourceLanguage: { type: 'string', description: "말하는 언어입니다. 생략하면 감지합니다." },
numSpeakers: { type: 'number', default: '0', description: "화자 수로, 1~20이며 0이면 감지합니다. 화자 수를 알려 주면 분리가 더 좋아집니다." },
disableVoiceCloning: { type: 'boolean', description: "각 화자의 목소리 대신 기본 보이스를 사용합니다." },
dropBackgroundAudio: { type: 'boolean', description: "음악과 효과음을 뺍니다." },
startTime: { type: 'number', description: "이 지점부터만 더빙합니다(초)." },
endTime: { type: 'number', description: "이 지점까지만 더빙합니다(초)." },
highestResolution: { type: 'boolean', description: "동영상 더빙에서 소스 해상도를 유지합니다." },
useProfanityFilter: { type: 'boolean', description: "비속어를 걸러 냅니다." },
targetAccent: { type: 'string', description: "더빙의 억양입니다. 실험 기능입니다." },
watermark: { type: 'boolean', description: "동영상 더빙에 모델 제작사의 워터마크를 넣습니다." },
}}
/>

```ts
const { jobId } = await client.voices.dub({
videoUrl: "https://example.com/interview.mp4",
targetLanguage: "es",
numSpeakers: 2,
})
```

가격은 더빙 구간의 분 단위 길이에 따라 정해지며, 최소 1분입니다. 한 구간은 최대 30분이므로, 더 긴 소스에는 `startTime`과 `endTime`을 쓰세요.

### voices.textToDialogue(input)
[텍스트로 대화 만들기](https://nodaro.ai/docs/nodes/audio/text-to-dialogue) 노드가 [ElevenLabs Dialogue v3](https://nodaro.ai/docs/models/audio/elevenlabs-dialogue-v3) 또는 [ElevenLabs Dialogue v4](https://nodaro.ai/docs/models/audio/elevenlabs-dialogue-v4)로 하듯이, 여러 화자의 스크립트를 **하나의** 오디오 파일로 음성화합니다(`POST /v1/text-to-dialogue`).

```ts
textToDialogue(input: {
dialogue: Array<{ text: string; voice: string }>
provider?: "elevenlabs-dialogue" | "elevenlabs-dialogue-v4"
stability?: number
similarityBoost?: number
languageCode?: string
seed?: number
applyTextNormalization?: "auto" | "on" | "off"
}): Promise<{ jobId: string }>
```

<TypeTable
type={{
dialogue: { type: 'Array<{ text: string; voice: string }>', required: true, description: "말하는 순서대로 나열한 대사입니다. voice는 기본 제공 보이스 이름이나 ElevenLabs 보이스 id입니다. 텍스트에는 [laughs] 같은 오디오 태그를 넣을 수 있습니다." },
provider: { type: '"elevenlabs-dialogue" | "elevenlabs-dialogue-v4"', default: '"elevenlabs-dialogue"', description: "대화 모델입니다. 기본값인 Dialogue v3 또는 Dialogue v4입니다." },
stability: { type: 'number', description: "전달의 안정성입니다. Dialogue v3는 0, 0.5, 1만 받습니다. Dialogue v4는 0~1 사이의 어떤 값이든 받습니다." },
similarityBoost: { type: 'number', description: "Dialogue v4 전용입니다. 각 대사가 보이스의 특성을 얼마나 유지할지 0~1로 정합니다." },
languageCode: { type: 'string', description: "ISO 639-1 언어 힌트입니다. 생략하면 감지합니다." },
seed: { type: 'number', description: "결과를 재현하려면 0~4,294,967,295를 쓰세요. 생략하면 무작위입니다." },
applyTextNormalization: { type: '"auto" | "on" | "off"', description: "숫자와 약어를 풀어서 읽을지 여부입니다." },
}}
/>

```ts
const { jobId } = await client.voices.textToDialogue({
dialogue: [
{ text: "Did you hear that?", voice: "Rachel" },
{ text: "[whispers] Stay behind me.", voice: "Callum" },
],
})
```

스크립트는 Dialogue v3에서 최대 5,000자, Dialogue v4에서 최대 10,000자까지 담을 수 있으며, 2,000자 미만일 때 품질이 가장 좋습니다. 서로 다른 보이스는 최대 10개까지 쓸 수 있습니다. 라이브러리 보이스, 디자인한 보이스, 기존 복제 보이스를 원하는 대로 섞어 쓸 수 있습니다. 완료된 작업의 `output_data.audioUrl`이 그 파일입니다. 두 모델 모두 `output_data.transcript`에 타이밍이 들어 있습니다. `startMs`, `endMs`, `speaker`가 있는 `words[]`와 대사마다 하나씩인 `segments[]` 항목이며, `speaker`는 그 대사의 `voice`입니다. `transcript.words`를 `autoTranscribe: false`와 함께 `addCaptions`에 넘기면 다시 받아쓰지 않고 대화에 자막을 넣을 수 있습니다.

## client.audio
보이스 체인저 Pro가 내부에서 쓰는 오디오 구성 요소를 하나씩 쓸 수 있습니다. 모든 메서드는 `jobId`를 반환합니다.

### audio.separate(input)
[음원 분리](https://nodaro.ai/docs/nodes/audio/audio-separation) 노드처럼, 트랙을 스템으로 나눕니다(`POST /v1/audio-separation`).

```ts
separate(input: { audioUrl: string; mode?: "vocal_instrumental" | "stems"; quality?: "auto" | "fast" | "best" }): Promise<{ jobId: string }>
```

<TypeTable
type={{
audioUrl: { type: 'string', required: true, description: "나눌 트랙입니다." },
mode: { type: '"vocal_instrumental" | "stems"', default: '"vocal_instrumental"', description: "vocal_instrumental은 보이스를 음악과 효과음에서 분리합니다. stems는 드럼, 베이스, 나머지 파트를 반환합니다." },
quality: { type: '"auto" | "fast" | "best"', description: "분리 품질입니다." },
}}
/>

```ts
const { jobId } = await client.audio.separate({ audioUrl: songUrl })
// output: vocalUrl and instrumentalUrl, or one URL per stem in stems mode
```

### audio.isolate(input)
[음성 추출](https://nodaro.ai/docs/nodes/audio/voice-extractor) 노드처럼, 주 음성만 남기고 배경 노이즈를 제거합니다(`POST /v1/audio-isolation`).

```ts
isolate(input: { audioUrl: string }): Promise<{ jobId: string }>
```

<TypeTable
type={{
audioUrl: { type: 'string', required: true, description: "정리할 녹음입니다." },
}}
/>

```ts
const { jobId } = await client.audio.isolate({ audioUrl: interviewUrl })
```

### audio.applyFx(input)
[오디오 효과](https://nodaro.ai/docs/nodes/audio/audio-fx) 노드처럼, 리버브, 에코, 전화기, 메가폰 효과를 추가합니다(`POST /v1/audio-fx`). 프리셋은 보이스 체인저의 `voiceFx`와 같습니다.

```ts
applyFx(input: {
audioUrl: string
preset?: AudioFxPreset
mix?: number
delayMs?: number
decay?: number
eqLow?: number
eqHigh?: number
}): Promise<{ jobId: string }>
```

<TypeTable
type={{
audioUrl: { type: 'string', required: true, description: "처리할 트랙입니다." },
preset: { type: 'AudioFxPreset', description: "room, hall, church 같은 리버브 공간, 또는 telephone, megaphone, echo, custom 중 하나입니다." },
mix: { type: 'number', description: "리버브의 원음과 효과음 비율로, 0~100입니다." },
delayMs: { type: 'number', description: "에코 지연 시간으로, echo와 custom에서 씁니다." },
decay: { type: 'number', description: "에코 감쇠로, echo와 custom에서 씁니다." },
eqLow: { type: 'number', description: "저음 감소나 증폭(dB)으로, telephone과 megaphone에서 씁니다." },
eqHigh: { type: 'number', description: "고음 감소나 증폭(dB)으로, telephone과 megaphone에서 씁니다." },
}}
/>

```ts
const { jobId } = await client.audio.applyFx({ audioUrl: lineUrl, preset: "telephone" })
```

### audio.mix(input)
[오디오 믹스](https://nodaro.ai/docs/nodes/audio/mix-audio) 노드처럼, 여러 트랙을 하나로 겹칩니다(`POST /v1/mix-audio`).

```ts
mix(input: {
audioUrls: string[]
trackVolumes?: number[]
duck?: { under: number; amount?: number; thresholdDb?: number; ratio?: number; attackMs?: number; releaseMs?: number }
endAt?: "longest" | "voice"
}): Promise<{ jobId: string }>
```

<TypeTable
type={{
audioUrls: { type: 'string[]', required: true, description: "함께 재생할 트랙 2~20개입니다." },
trackVolumes: { type: 'number[]', description: "같은 순서로 트랙마다 지정하는 레벨로, 퍼센트 단위 0~200입니다." },
duck: { type: 'object', description: "audioUrls[under]의 소리가 클 때 나머지 모든 트랙의 볼륨이 낮아집니다. under는 음성 트랙의 0부터 시작하는 인덱스입니다. amount는 0~100이며 기본값은 75입니다. thresholdDb(-60~0, 기본값 -30), attackMs(1~2000, 기본값 20), releaseMs(10~9000, 기본값 500), ratio(1~20, amount보다 우선)는 선택 항목입니다. 요금은 볼륨 낮추기가 있든 없든 같습니다." },
endAt: { type: '"longest" | "voice"', description: "믹스가 끝나는 지점입니다. \"longest\"(기본값)는 가장 긴 트랙과 함께 끝납니다. \"voice\"는 duck.under 트랙이 끝날 때 끝나므로, 더 긴 배경 음악은 그 지점에서 잘립니다. \"voice\"에는 duck이 필요하며, 없으면 요청이 400 validation_error로 거부됩니다.", default: '"longest"' },
}}
/>

```ts
const { jobId } = await client.audio.mix({ audioUrls: [voiceUrl, musicUrl], trackVolumes: [100, 35] })

// The music dips under the voice (track 0) while it speaks.
const ducked = await client.audio.mix({ audioUrls: [voiceUrl, musicUrl], duck: { under: 0, amount: 80 } })
```

### audio.adjustVolume(input)
[볼륨 조절](https://nodaro.ai/docs/nodes/audio/adjust-volume) 노드처럼, 오디오 파일이나 동영상 오디오의 레벨을 바꿉니다(`POST /v1/adjust-volume`).

```ts
adjustVolume(input: {
audioUrl?: string
videoUrl?: string
volume?: number
normalize?: boolean
fadeIn?: number
fadeOut?: number
}): Promise<{ jobId: string }>
```

<TypeTable
type={{
audioUrl: { type: 'string', description: "오디오 파일입니다. audioUrl이나 videoUrl을 주세요." },
videoUrl: { type: 'string', description: "오디오를 바꿀 동영상입니다." },
volume: { type: 'number', default: '100', description: "퍼센트 단위의 레벨입니다." },
normalize: { type: 'boolean', description: "음량을 표준 레벨로 맞춥니다." },
fadeIn: { type: 'number', description: "페이드 인 길이(초)입니다." },
fadeOut: { type: 'number', description: "페이드 아웃 길이(초)입니다." },
}}
/>

```ts
const { jobId } = await client.audio.adjustVolume({ audioUrl: musicUrl, volume: 60, fadeOut: 3 })
```

### audio.pitchTempo(input)
[피치 및 템포](https://nodaro.ai/docs/nodes/audio/pitch-tempo) 노드처럼, 피치를 옮기거나 피치를 바꾸지 않고 템포를 바꿉니다(`POST /v1/pitch-tempo`). `audioUrl`과 `videoUrl` 중 정확히 하나를 주세요. 동영상은 화면을 그대로 유지하며 템포는 1로 고정됩니다.

```ts
pitchTempo(input: {
audioUrl?: string
videoUrl?: string
semitones?: number
tempo?: number
preserveFormants?: boolean
}): Promise<{ jobId: string }>
```

<TypeTable
type={{
audioUrl: { type: 'string', description: "처리할 오디오입니다. audioUrl이나 videoUrl을 주세요." },
videoUrl: { type: 'string', description: "소리를 처리할 동영상입니다. 화면은 그대로 복사됩니다." },
semitones: { type: 'number', description: "피치 변경 폭으로, -12~12입니다. 12는 한 옥타브 위입니다.", default: '0' },
tempo: { type: 'number', description: "피치를 유지한 속도로, 0.5~2입니다. 동영상에서는 무시됩니다.", default: '1' },
preserveFormants: { type: 'boolean', description: "피치가 바뀔 때 목소리의 자연스러운 성격을 유지합니다.", default: 'true' },
}}
/>

실행 비용은 입력의 시작된 1분마다 22크레딧이며, 최대 30분입니다.

```ts
const { jobId } = await client.audio.pitchTempo({ audioUrl: voiceUrl, semitones: -3 })
```

### audio.combine(input)
[오디오 합치기](https://nodaro.ai/docs/nodes/audio/combine-audio) 노드처럼, 오디오 세그먼트를 이어 붙입니다(`POST /v1/combine-audio`).

```ts
combine(input: { segments: Array<{ url: string; startTime?: number; endTime?: number }> }): Promise<{ jobId: string }>
```

<TypeTable
type={{
segments: { type: 'Array<{ url: string; startTime?: number; endTime?: number }>', required: true, description: "순서대로 나열한 세그먼트입니다. 각 세그먼트는 파일의 일부만, startTime부터 endTime까지(초) 쓸 수 있습니다." },
}}
/>

```ts
const { jobId } = await client.audio.combine({
segments: [{ url: introUrl }, { url: episodeUrl, startTime: 4 }, { url: outroUrl }],
})
```

### audio.transcribe(input)
[음성 텍스트 변환](https://nodaro.ai/docs/nodes/audio/transcribe) 노드처럼, 오디오나 동영상 파일 속 음성을 텍스트로 바꿉니다(`POST /v1/transcribe`).

```ts
transcribe(input: {
audioUrl: string
provider?: "elevenlabs-stt" | "incredibly-fast-whisper" | "whisper"
language?: string
diarize?: boolean
tagAudioEvents?: boolean
wordTimestamps?: boolean
}): Promise<{ jobId: string }>
```

<TypeTable
type={{
audioUrl: { type: 'string', required: true, description: "오디오 파일이나 동영상 파일입니다." },
provider: { type: '"elevenlabs-stt" | "incredibly-fast-whisper" | "whisper"', default: '"whisper"', description: "엔진입니다. 아래 표를 참고하세요." },
language: { type: 'string', description: "언어를 강제로 지정합니다. 생략하면 언어를 감지합니다." },
diarize: { type: 'boolean', description: "단어마다 말한 사람을 표시합니다. elevenlabs-stt 전용입니다." },
tagAudioEvents: { type: 'boolean', description: "웃음, 박수 등의 소리에 태그를 붙입니다. elevenlabs-stt 전용입니다." },
wordTimestamps: { type: 'boolean', description: "단어별 타이밍을 요청합니다." },
}}
/>

| `provider` | 단어 타이밍 | 참고 |
| --- | --- | --- |
| [`elevenlabs-stt`](https://nodaro.ai/docs/models/audio/elevenlabs-stt) | 항상 | `diarize`와 `tagAudioEvents`를 지원하는 유일한 엔진입니다. |
| [`incredibly-fast-whisper`](https://nodaro.ai/docs/models/audio/incredibly-fast-whisper) | `wordTimestamps: true`일 때만 | 이 플래그가 없으면 작업은 성공하고 요금이 청구되지만, 구문 세그먼트만 있고 `words` 목록은 비어 있습니다. |
| [`whisper`](https://nodaro.ai/docs/models/audio/whisper) | 없음 | 구문 세그먼트만 반환합니다. `wordTimestamps: true`는 크레딧이 쓰이기 전에 `400 validation_error`로 거부됩니다. |

`provider`를 생략하면 `whisper`가 쓰이므로, `provider` 없이 `wordTimestamps: true`를 보내도 400을 받습니다. 단어 타이밍이 필요하면 `elevenlabs-stt`를 지정하거나, `incredibly-fast-whisper`를 `wordTimestamps: true`와 함께 지정하세요. 키네틱 자막에는 단어 타이밍이 필요합니다.

```ts
const { jobId } = await client.audio.transcribe({ audioUrl: talkUrl, provider: "elevenlabs-stt" })
```

완료된 작업의 `output_data`는 `TranscribeJobOutput`입니다.

| 필드 | 단위 | 내용 |
| --- | --- | --- |
| `text` | | 스크립트 전체를 하나의 문자열로 담습니다. |
| `language` | | 감지되었거나 요청한 언어 코드입니다. |
| `words` | 밀리초 | 단어마다 항목 하나로, `{ text, startMs, endMs, speaker? }` 형태입니다. 엔진에 단어 타이밍을 요청하지 않았으면 비어 있습니다. |
| `json` | 밀리초 | 정규화된 스크립트 `{ version, language, words, segments? }`로, [`client.edit`](https://nodaro.ai/docs/developers/sdk/editing)에서 받는 형식입니다. |
| `segments` | **초** | 원본 구문 범위로, 이전 엔진만 반환합니다. `elevenlabs-stt`는 반환하지 않으므로 `words`를 읽으세요. |

`words`는 [`client.media.addCaptions()`](https://nodaro.ai/docs/developers/sdk/media-and-uploads#mediaaddcaptionsinput)가 받는 `captions`와 형태가 정확히 같으므로, 스크립트를 고친 다음 자막으로 입힐 수 있습니다.

## 자주 묻는 질문

### Nodaro SDK로 동영상 속 보이스를 바꾸려면 어떻게 하나요?

videoUrl과 voiceId를 넣어 client.voices.change를 호출하세요. 서버가 보이스를 바꾸고 원래 동영상에 다시 입힙니다. 작업을 폴링해 output_data.videoUrl을 받으세요.

### 녹음 속 화자마다 다른 보이스를 주려면 어떻게 하나요?

감지된 화자 순서대로 화자당 항목 하나씩 담은 orderedVoices로 client.voices.recast를 호출하세요. 자기 목소리를 유지할 화자에는 null을 쓰세요. Nodaro Cloud에서 실행됩니다.

### SDK로 아직 보이스를 복제할 수 있나요?

아니요. 보이스 복제는 2026년 9월에 종료되었으며, 이제 createClone은 410 voice_cloning_retired로 실패합니다. 기존 복제 보이스는 계속 사용할 수 있습니다. 대신 client.voices.design으로 새 보이스를 디자인하세요.

### 단어 타이밍을 반환하는 음성 텍스트 변환 엔진은 무엇인가요?

elevenlabs-stt는 항상 단어 타이밍을 반환합니다. incredibly-fast-whisper는 wordTimestamps를 true로 설정했을 때만 반환하고, whisper는 반환하지 않습니다.
