ListenHubOpenAPI
API 레퍼런스

보이스 클로닝

참조 오디오를 업로드해 재사용 가능한 프라이빗 보이스를 만들고, 확인한 뒤 그 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단계(기본값) — 업로드하고, 미리듣기를 확인한 뒤 결정합니다:

  1. POST /v1/voice-clone/clonetaskId를 반환합니다.
  2. statuscompleted가 될 때까지 GET /v1/voice-clone/clone/{taskId}를 폴링합니다. 응답에는 임시 보이스의 미리듣기인 demoAudioUrl이 담깁니다.
  3. 이름과 성별을 담아 POST /v1/voice-clone/confirm을 호출하면 작업이 영구 프라이빗 보이스로 바뀌고 speakerId가 반환됩니다.

한 번에 — 생성 요청에 autoConfirm=true(그리고 name, gender)를 설정합니다. 클로닝 완료를 처음 확인한 폴링이 그대로 보이스를 확인하고 같은 응답에서 speakerId를 반환합니다. 두 번째 요청은 필요 없습니다.

autoConfirm=true에서는 크레딧을 차감하는 쪽이 폴링 요청입니다. 반복되거나 동시에 실행되는 폴링이 이중 과금되는 일은 없습니다 — 확인은 원자적 잠금으로 보호되며, 두 번째 시도는 이미 확인됨으로 거부됩니다.

폴링 응답 읽기

GET /v1/voice-clone/clone/{taskId}에는 세 가지 종료 형태가 있습니다. 다음 순서로 확인하세요:

결과판별 방법얻는 것
클로닝 실패status: "failed"errorCodeerrorMessage
클로닝됨, 미확인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'])

요청 파라미터:

필드타입필수설명
audioFilesfile참조 오디오 파일 1–6개. 여러 개일 때는 필드를 반복해서 전송
languagestringzh, en, ja 중 하나
consentConfirmedboolean반드시 true — 클로닝 대상의 동의를 확보했다는 선언
modestring아니오upload(기본값이자 유일하게 허용되는 값)
autoConfirmboolean아니오클로닝 완료를 발견한 폴링에서 바로 보이스를 확인. 기본값 false
namestringautoConfirm 사용 시보이스 이름, 최대 50자
genderstringautoConfirm 사용 시male, female, other 중 하나
useCreditsboolean아니오할당량 소진 후 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)
필드타입필수설명
taskIdstring완료된 클론 작업
namestring보이스 이름, 최대 50자
genderstringmale, female, other 중 하나
useCreditsboolean아니오할당량 초과분에 대한 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"

GETPUT은 보이스 자체를 반환합니다:

{
  "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에 상세 내용이 담깁니다

전체 오류 구조는 오류 처리를 참고하세요.

이 페이지의 내용