보이스 클로닝
참조 오디오를 업로드해 재사용 가능한 프라이빗 보이스를 만들고, 확인한 뒤 그 speaker ID를 음성 및 TTS 엔드포인트에서 사용합니다.
Voice Cloning API는 짧은 녹음을 재사용 가능한 프라이빗 보이스로 바꿉니다. 참조 오디오를 업로드하고, 클로닝이 끝날 때까지 폴링(polling)한 뒤 결과를 확인(confirm)하면 speakerId를 얻습니다. 이 ID는 다른 보이스와 똑같이 /v1/speech, /v1/tts, /v1/audio/speech에서 동작합니다. 클로닝된 보이스는 API 키에 연결된 계정에 속하며 GET /v1/speakers/list 결과에도 나타납니다.
모든 엔드포인트는 https://api.marswave.ai/openapi/v1/voice-clone 아래에 있으며 Authorization: Bearer $LISTENHUB_API_KEY로 인증합니다.
보이스를 클로닝하려면 클로닝 대상 본인의 동의가 필요합니다. 모든 생성 요청에는 consentConfirmed=true가 포함되어야 하며, 이는 해당 동의를 확보했다는 선언입니다 — 이 값이 없으면 요청이 거부되고, 선언 자체는 작업과 함께 저장됩니다. 동의를 얻고 지키는 책임은 호출자에게 있습니다.
모든 응답은 { "code": 0, "message": "", "data": { ... } }로 감싸집니다. code가 0이 아니면 오류입니다 — 오류 처리를 참고하세요. 아래 예시는 모두 data에서 필드를 읽습니다.
제한과 비용
| 항목 | 값 |
|---|---|
| 참조 오디오 | 1–6개 파일, 파일당 ≤5MB, 총 ≤20MB |
| 언어 | zh, en, ja |
| 레이트 리밋 | 사용자당 분당 생성 요청 5회 |
| 요금제 | 유료 요금제 전용 — 무료 요금제에서 확인하면 NEED_UPGRADE 반환 |
| 확인 횟수 | 요금제 주기별 할당량 내에서는 무료, 초과분은 1회당 300 크레딧 |
| 저장 보이스 수 | 요금제별 상한(maxSpeakers), 보이스를 삭제하면 자리가 하나 확보됨 |
| 미확인 작업 | 7일 후 만료 — 보이스를 남기려면 확인해야 함 |
할당량을 넘어선 뒤에는 useCredits=true를 보낼 때만 확인에 과금됩니다. 보내지 않으면 요청이 NEED_CREDIT을 반환하고 아무것도 차감되지 않습니다.
클로닝하는 두 가지 방법
2단계(기본값) — 업로드하고, 미리듣기를 확인한 뒤 결정합니다:
POST /v1/voice-clone/clone이taskId를 반환합니다.status가completed가 될 때까지GET /v1/voice-clone/clone/{taskId}를 폴링합니다. 응답에는 임시 보이스의 미리듣기인demoAudioUrl이 담깁니다.- 이름과 성별을 담아
POST /v1/voice-clone/confirm을 호출하면 작업이 영구 프라이빗 보이스로 바뀌고speakerId가 반환됩니다.
한 번에 — 생성 요청에 autoConfirm=true(그리고 name, gender)를 설정합니다. 클로닝 완료를 처음 확인한 폴링이 그대로 보이스를 확인하고 같은 응답에서 speakerId를 반환합니다. 두 번째 요청은 필요 없습니다.
autoConfirm=true에서는 크레딧을 차감하는 쪽이 폴링 요청입니다. 반복되거나 동시에 실행되는 폴링이 이중 과금되는 일은 없습니다 — 확인은 원자적 잠금으로 보호되며, 두 번째 시도는 이미 확인됨으로 거부됩니다.
폴링 응답 읽기
GET /v1/voice-clone/clone/{taskId}에는 세 가지 종료 형태가 있습니다. 다음 순서로 확인하세요:
| 결과 | 판별 방법 | 얻는 것 |
|---|---|---|
| 클로닝 실패 | status: "failed" | errorCode와 errorMessage |
| 클로닝됨, 미확인 | status: "completed"이고 speakerId 없음 | demoAudioUrl, 자동 확인이 실패했다면 confirmError |
| 확인됨 | speakerId 존재 | speakerId, 음성 엔드포인트에서 바로 사용 가능 |
가운데 행이 autoConfirm=true에서 놓치기 쉬운 경우입니다. 클로닝은 성공했지만 보이스 저장은 실패한 상태로, 크레딧 부족, 할당량 소진, 보이스 개수 상한 도달이 원인입니다. 어느 쪽인지는 confirmError가 알려줍니다. 클론 결과는 그대로 남아 있으므로 원인을 해결한 뒤 POST /v1/voice-clone/confirm을 직접 호출하면 됩니다.
| 상태 | 의미 |
|---|---|
pending | 작업 생성됨, 처리 대기 중 |
processing | 클로닝 진행 중 |
completed | 클로닝 완료 — 미리듣기 가능, 확인된 상태가 아닐 수 있음 |
failed | 클로닝 실패, errorMessage가 이유를 설명 |
재시도
| 상태 코드 | 발생 시점 | 대응 방법 |
|---|---|---|
429 | 같은 계정의 다른 확인이 진행 중이거나, 분당 생성 5회를 초과함 | Retry-After(기본 2초)만큼 기다렸다가 재시도 |
503 | 확인이 의존하는 구성 요소가 일시적으로 사용 불가 | Retry-After(기본 5초)만큼 기다렸다가 재시도 |
둘 다 안전하게 재시도할 수 있으며, 어느 쪽도 크레딧을 차감하지 않습니다.
클론 작업 생성
POST /v1/voice-clone/clone
JSON이 아니라 multipart/form-data로 전송합니다. 파일마다 audioFiles 필드를 반복해서 넣으세요.
# Two-step
curl -X POST "https://api.marswave.ai/openapi/v1/voice-clone/clone" \
-H "Authorization: Bearer $LISTENHUB_API_KEY" \
-F "audioFiles=@reference.mp3" \
-F "language=en" \
-F "consentConfirmed=true"
# One-shot: clone and confirm in the same flow
curl -X POST "https://api.marswave.ai/openapi/v1/voice-clone/clone" \
-H "Authorization: Bearer $LISTENHUB_API_KEY" \
-F "audioFiles=@part-1.mp3" \
-F "audioFiles=@part-2.mp3" \
-F "language=ja" \
-F "consentConfirmed=true" \
-F "autoConfirm=true" \
-F "name=My API Voice" \
-F "gender=female" \
-F "useCredits=true"import { readFile } from 'node:fs/promises'
const form = new FormData()
form.append('audioFiles', new Blob([await readFile('reference.mp3')]), 'reference.mp3')
form.append('language', 'en')
form.append('consentConfirmed', 'true')
const response = await fetch('https://api.marswave.ai/openapi/v1/voice-clone/clone', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.LISTENHUB_API_KEY}` },
body: form,
})
const { data } = await response.json()
console.log('Task:', data.taskId)import os
import requests
with open('reference.mp3', 'rb') as audio:
response = requests.post(
'https://api.marswave.ai/openapi/v1/voice-clone/clone',
headers={'Authorization': f'Bearer {os.environ["LISTENHUB_API_KEY"]}'},
files=[('audioFiles', ('reference.mp3', audio, 'audio/mpeg'))],
data={'language': 'en', 'consentConfirmed': 'true'},
)
data = response.json()['data']
print('Task:', data['taskId'])요청 파라미터:
| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
audioFiles | file | 예 | 참조 오디오 파일 1–6개. 여러 개일 때는 필드를 반복해서 전송 |
language | string | 예 | zh, en, ja 중 하나 |
consentConfirmed | boolean | 예 | 반드시 true — 클로닝 대상의 동의를 확보했다는 선언 |
mode | string | 아니오 | upload(기본값이자 유일하게 허용되는 값) |
autoConfirm | boolean | 아니오 | 클로닝 완료를 발견한 폴링에서 바로 보이스를 확인. 기본값 false |
name | string | autoConfirm 사용 시 | 보이스 이름, 최대 50자 |
gender | string | autoConfirm 사용 시 | male, female, other 중 하나 |
useCredits | boolean | 아니오 | 할당량 소진 후 300 크레딧 차감을 승인. 기본값 false |
반환:
{
"code": 0,
"message": "",
"data": {
"taskId": "6915bde9cca4d3c8ecb3eaf5",
"status": "pending"
}
}클론 작업 폴링
GET /v1/voice-clone/clone/{taskId}
curl -X GET "https://api.marswave.ai/openapi/v1/voice-clone/clone/{taskId}" \
-H "Authorization: Bearer $LISTENHUB_API_KEY"const response = await fetch(
`https://api.marswave.ai/openapi/v1/voice-clone/clone/${taskId}`,
{ headers: { Authorization: `Bearer ${process.env.LISTENHUB_API_KEY}` } },
)
const { data } = await response.json()
if (data.status === 'failed') throw new Error(data.errorMessage)
if (data.speakerId) console.log('Ready to speak with:', data.speakerId)
else if (data.confirmError) console.warn('Cloned but not saved:', data.confirmError)
else if (data.demoAudioUrl) console.log('Preview:', data.demoAudioUrl)import os
import requests
response = requests.get(
f'https://api.marswave.ai/openapi/v1/voice-clone/clone/{task_id}',
headers={'Authorization': f'Bearer {os.environ["LISTENHUB_API_KEY"]}'},
)
data = response.json()['data']
if data['status'] == 'failed':
raise RuntimeError(data['errorMessage'])
if data.get('speakerId'):
print('Ready to speak with:', data['speakerId'])
elif data.get('confirmError'):
print('Cloned but not saved:', data['confirmError'])
elif data.get('demoAudioUrl'):
print('Preview:', data['demoAudioUrl'])클로닝 완료, 확인 대기 중:
{
"code": 0,
"message": "",
"data": {
"status": "completed",
"demoAudioUrl": "https://assets.listenhub.ai/voice-clone/demo-6915bde9.mp3"
}
}확인됨:
{
"code": 0,
"message": "",
"data": {
"status": "completed",
"demoAudioUrl": "https://assets.listenhub.ai/voice-clone/demo-6915bde9.mp3",
"speakerId": "voice-clone-6915bde9cca4d3c8ecb3eaf5"
}
}클론 작업 확인
POST /v1/voice-clone/confirm
완료된 작업을 영구 프라이빗 보이스로 바꿉니다. 같은 작업에 대해 반복 호출하면 ALREADY_CONFIRMED를 반환하며 아무것도 차감되지 않습니다.
curl -X POST "https://api.marswave.ai/openapi/v1/voice-clone/confirm" \
-H "Authorization: Bearer $LISTENHUB_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"taskId": "6915bde9cca4d3c8ecb3eaf5",
"name": "My API Voice",
"gender": "female",
"useCredits": true
}'const response = await fetch('https://api.marswave.ai/openapi/v1/voice-clone/confirm', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.LISTENHUB_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ taskId, name: 'My API Voice', gender: 'female', useCredits: true }),
})
const { data } = await response.json()
console.log('Speaker:', data.speakerId)| 필드 | 타입 | 필수 | 설명 |
|---|---|---|---|
taskId | string | 예 | 완료된 클론 작업 |
name | string | 예 | 보이스 이름, 최대 50자 |
gender | string | 예 | male, female, other 중 하나 |
useCredits | boolean | 아니오 | 할당량 초과분에 대한 300 크레딧 차감을 승인. 기본값 false |
반환:
{
"code": 0,
"message": "",
"data": { "speakerId": "voice-clone-6915bde9cca4d3c8ecb3eaf5" }
}클로닝한 보이스로 음성 생성
보이스를 지정하는 자리에 speakerId를 그대로 넣으면 됩니다:
curl -X POST "https://api.marswave.ai/openapi/v1/speech" \
-H "Authorization: Bearer $LISTENHUB_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"scripts": [
{
"content": "This sentence is spoken by my own cloned voice.",
"speakerId": "voice-clone-6915bde9cca4d3c8ecb3eaf5"
}
]
}'프라이빗 보이스 목록 조회
GET /v1/voice-clone/speakers
curl -X GET "https://api.marswave.ai/openapi/v1/voice-clone/speakers" \
-H "Authorization: Bearer $LISTENHUB_API_KEY"{
"code": 0,
"message": "",
"data": {
"speakers": [
{
"id": "6915c0a2cca4d3c8ecb3eb01",
"name": "My API Voice",
"speakerInnerId": "voice-clone-6915bde9cca4d3c8ecb3eaf5",
"language": "en",
"gender": "female",
"demoAudioUrl": "https://assets.listenhub.ai/voice-clone/demo-6915bde9.mp3",
"createdAt": "2026-07-30T09:10:11.000Z"
}
],
"quota": 2,
"isLimitReached": false,
"maxSpeakers": 2,
"remainingConfirmations": 1
}
}| 필드 | 설명 |
|---|---|
speakers[].speakerInnerId | 음성 및 TTS 엔드포인트에 전달할 ID |
quota | 구독 주기마다 포함되는 확인 횟수 |
remainingConfirmations | 현재 주기에 남은 확인 횟수 |
maxSpeakers | 요금제가 동시에 보관할 수 있는 프라이빗 보이스 수 |
isLimitReached | 이번 주기의 확인 횟수를 모두 소진하면 true |
보이스 조회, 이름 변경, 삭제
| 메서드 | 경로 | 설명 |
|---|---|---|
GET | /v1/voice-clone/speakers/{speakerId} | 프라이빗 보이스 하나 조회 |
PUT | /v1/voice-clone/speakers/{speakerId} | name 및/또는 gender 수정(최소 하나는 전송) |
DELETE | /v1/voice-clone/speakers/{speakerId} | 보이스 삭제 |
# Rename
curl -X PUT "https://api.marswave.ai/openapi/v1/voice-clone/speakers/{speakerId}" \
-H "Authorization: Bearer $LISTENHUB_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "Narrator (EN)" }'
# Delete — frees one slot against maxSpeakers
curl -X DELETE "https://api.marswave.ai/openapi/v1/voice-clone/speakers/{speakerId}" \
-H "Authorization: Bearer $LISTENHUB_API_KEY"GET과 PUT은 보이스 자체를 반환합니다:
{
"code": 0,
"message": "",
"data": {
"id": "6915c0a2cca4d3c8ecb3eb01",
"speakerInnerId": "voice-clone-6915bde9cca4d3c8ecb3eaf5",
"name": "Narrator (EN)",
"language": "en",
"gender": "female",
"demoAudioUrl": "https://assets.listenhub.ai/voice-clone/demo-6915bde9.mp3",
"createdAt": "2026-07-30T09:10:11.000Z",
"updatedAt": "2026-07-30T10:02:44.000Z"
}
}DELETE는 { "speakerId": "..." }를 반환합니다. 삭제하면 자리가 하나 확보되지만, 이번 주기에 이미 사용한 확인 횟수는 돌려받지 못합니다.
오류
| 오류 | 의미 |
|---|---|
NEED_UPGRADE | 보이스 클로닝에는 유료 요금제가 필요합니다 |
NEED_CREDIT | 할당량이 소진되었고 useCredits가 설정되지 않음 — 차감 없음 |
SPEAKER_LIMIT_REACHED | 프라이빗 보이스 수가 이미 상한입니다. 하나를 먼저 삭제하세요 |
ALREADY_CONFIRMED | 이미 확인된 작업입니다. 이중 과금 없음 |
AUDIO_DURATION_INVALID | 참조 오디오가 너무 짧거나 너무 깁니다 |
NO_VALID_SPEECH | 참조 오디오에서 음성이 감지되지 않았습니다 |
TASK_FAILED | 클로닝 실패. errorMessage에 상세 내용이 담깁니다 |
전체 오류 구조는 오류 처리를 참고하세요.