ListenHubOpenAPI
API 레퍼런스

텍스트 음성 변환

저지연 단일 화자 스트리밍부터 장문 비동기 내레이션까지, 5개 엔드포인트로 텍스트를 자연스러운 음성으로 변환합니다.

ListenHub는 여러 개의 텍스트 음성 변환(Text to Speech) 엔드포인트를 제공하며, 각각 서로 다른 형태의 작업에 맞춰 조정되어 있습니다. base URL, 인증 방식, speaker ID는 공통이지만 지연 시간, 응답 형식, 지원하는 화자 수가 다릅니다.

모든 요청은 https://api.marswave.ai/openapi/v1로 보내고 API key로 인증합니다:

Authorization: Bearer $LISTENHUB_API_KEY

키는 listenhub.ai/settings/api-keys에서 만듭니다. 모든 JSON 응답은 { "code": 0, "message": "", "data": { ... } } 형태로 감싸여 있으며, code가 0이 아니면 오류입니다. 스트리밍 엔드포인트(/v1/tts, /v1/audio/speech)는 이 래퍼 대신 원시 바이너리 오디오를 반환합니다.

엔드포인트 선택하기

엔드포인트화자동기 / 비동기응답적합한 용도
POST /v1/tts단일동기바이너리 오디오 스트림실시간 재생, 앱 내 음성, 낮은 지연 시간
POST /v1/audio/speech단일동기바이너리 오디오 스트림OpenAI TTS 엔드포인트의 대체
POST /v1/speech복수동기audioUrl을 포함한 JSON대화, 오디오북, 미리 준비된 다중 화자 스크립트
POST /v1/flow-speech/episodes단일비동기episodeId로 폴링기사·뉴스레터 내레이션, URL을 음성으로
POST /v1/flow-speech/episodes/tts복수비동기episodeId로 폴링그대로 변환하는 긴 다중 화자 스크립트

선택 기준: 하나의 화자로 오디오 바이트를 즉시 돌려받아야 하면 /v1/tts, 짧은 다중 화자 스크립트를 한 번의 호출로 호스팅된 URL까지 받고 싶으면 /v1/speech, 백그라운드에서 돌려야 할 만큼 긴 작업이면 /v1/flow-speech/episodes 계열을 사용하세요.

크레딧 비용은 오디오 길이에 비례하며, 작업이 완료되면 해당 응답의 credits로 반환됩니다. 생성 전에 비용을 추정하려면 API 레퍼런스의 크레딧 견적 엔드포인트를 참고하세요.


스트리밍 TTS

POST /v1/tts

낮은 지연 시간의 단일 화자 합성입니다. 응답 본문은 생성되는 대로 스트리밍되는 원시 바이너리 오디오이므로, 클립 전체가 완성되기 전에 첫 바이트가 도착합니다. 실시간 재생과 대화형 음성 기능에 사용하세요.

이 엔드포인트는 OpenAI 텍스트 음성 변환의 요청 형식(input / voice / response_format)을 그대로 받으므로 기존 클라이언트를 쉽게 이전할 수 있습니다.

curl -X POST "https://api.marswave.ai/openapi/v1/tts" \
  -H "Authorization: Bearer $LISTENHUB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "input": "Hello, welcome to ListenHub text-to-speech.",
    "voice": "EN-Man-General-01",
    "response_format": "mp3"
  }' \
  --output output.mp3
const response = await fetch('https://api.marswave.ai/openapi/v1/tts', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.LISTENHUB_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    input: 'Hello, welcome to ListenHub text-to-speech.',
    voice: 'EN-Man-General-01',
    response_format: 'mp3',
  }),
});
const buffer = Buffer.from(await response.arrayBuffer());
// Write `buffer` to a file, or pipe `response.body` to a player
import os
import requests

response = requests.post(
    'https://api.marswave.ai/openapi/v1/tts',
    headers={'Authorization': f'Bearer {os.environ["LISTENHUB_API_KEY"]}'},
    json={
        'input': 'Hello, welcome to ListenHub text-to-speech.',
        'voice': 'EN-Man-General-01',
        'response_format': 'mp3',
    },
    stream=True,
)

with open('output.mp3', 'wb') as f:
    for chunk in response.iter_content(chunk_size=8192):
        f.write(chunk)

요청 파라미터

필드타입필수설명
inputstring합성할 텍스트. 최대 20,000자.
voicestringSpeaker ID(SpeakersspeakerId 값).
response_formatstring아니요요청할 오디오 형식. mp3, opus, aac, flac, wav, pcm 중 하나. 기본값은 mp3.

응답 본문은 JSON 래퍼가 아니라 바이너리 오디오입니다. 스트림이나 blob으로 읽으세요. 실제로 전달되는 컨테이너는 opus를 제외한 모든 형식에서 MP3(Content-Type: audio/mpeg)이며, opus만 OGG/Opus(Content-Type: audio/ogg)로 전달됩니다. 오디오가 시작되기 전에 요청이 실패하면 응답은 JSON 오류 객체로 대체되므로, 본문을 오디오로 다루기 전에 Content-Type을 확인하세요.


OpenAI 호환 TTS

POST /v1/audio/speech

OpenAI SDK와 OpenAI 호환 연동이 기본적으로 호출하는 경로에 놓인 /v1/tts의 완전한 별칭입니다. 요청 본문, response_format 옵션, 스트리밍 바이너리 응답이 모두 동일합니다. 기존 OpenAI TTS 클라이언트를 이 URL로 돌리고 ListenHub API key와 ListenHub voice ID를 지정하면 코드 변경 없이 전환할 수 있습니다.

curl -X POST "https://api.marswave.ai/openapi/v1/audio/speech" \
  -H "Authorization: Bearer $LISTENHUB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "input": "This endpoint mirrors the OpenAI speech API.",
    "voice": "EN-Woman-General-01",
    "response_format": "mp3"
  }' \
  --output output.mp3
const response = await fetch('https://api.marswave.ai/openapi/v1/audio/speech', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.LISTENHUB_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    input: 'This endpoint mirrors the OpenAI speech API.',
    voice: 'EN-Woman-General-01',
    response_format: 'mp3',
  }),
});
const buffer = Buffer.from(await response.arrayBuffer());
import os
import requests

response = requests.post(
    'https://api.marswave.ai/openapi/v1/audio/speech',
    headers={'Authorization': f'Bearer {os.environ["LISTENHUB_API_KEY"]}'},
    json={
        'input': 'This endpoint mirrors the OpenAI speech API.',
        'voice': 'EN-Woman-General-01',
        'response_format': 'mp3',
    },
    stream=True,
)

with open('output.mp3', 'wb') as f:
    for chunk in response.iter_content(chunk_size=8192):
        f.write(chunk)

요청 파라미터와 응답 동작은 스트리밍 TTS와 동일합니다.


다중 화자 스크립트를 오디오로

POST /v1/speech

미리 준비된 다중 화자 스크립트로 하나의 오디오 파일을 생성합니다. 각 줄이 자체 speakerId를 가지므로 대화에서 화자를 번갈아 쓸 수 있습니다. 호출은 동기 방식이며, 응답에 호스팅된 오디오 URL과 자막이 함께 담겨 반환됩니다. 폴링(polling)은 필요 없습니다.

curl -X POST "https://api.marswave.ai/openapi/v1/speech" \
  -H "Authorization: Bearer $LISTENHUB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "scripts": [
      { "content": "Welcome everyone to this episode.", "speakerId": "EN-Man-General-01" },
      { "content": "Today we are discussing an interesting topic.", "speakerId": "EN-Woman-General-01" },
      { "content": "Great, let us begin.", "speakerId": "EN-Man-General-01" }
    ]
  }'
const response = await fetch('https://api.marswave.ai/openapi/v1/speech', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.LISTENHUB_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    scripts: [
      { content: 'Welcome everyone to this episode.', speakerId: 'EN-Man-General-01' },
      { content: 'Today we are discussing an interesting topic.', speakerId: 'EN-Woman-General-01' },
      { content: 'Great, let us begin.', speakerId: 'EN-Man-General-01' },
    ],
  }),
});
const { data } = await response.json();
console.log(data.audioUrl);
import os
import requests

response = requests.post(
    'https://api.marswave.ai/openapi/v1/speech',
    headers={'Authorization': f'Bearer {os.environ["LISTENHUB_API_KEY"]}'},
    json={
        'scripts': [
            {'content': 'Welcome everyone to this episode.', 'speakerId': 'EN-Man-General-01'},
            {'content': 'Today we are discussing an interesting topic.', 'speakerId': 'EN-Woman-General-01'},
            {'content': 'Great, let us begin.', 'speakerId': 'EN-Man-General-01'},
        ]
    },
)
data = response.json()['data']
print(data['audioUrl'])

요청 파라미터

필드타입필수설명
scriptsarray하나 이상의 스크립트 줄. 순서대로 합성됩니다.
scripts[].contentstring줄의 텍스트. 비어 있으면 안 되며, 모든 줄을 합친 길이는 20,000자로 제한됩니다.
scripts[].speakerIdstring이 줄에 사용할 Speaker ID. 줄마다 다른 화자를 쓸 수 있습니다.

응답

{
  "code": 0,
  "message": "",
  "data": {
    "audioUrl": "https://assets.listenhub.ai/listenhub-public-prod/podcast/example.mp3",
    "audioDuration": 12500,
    "subtitlesUrl": "https://assets.listenhub.ai/listenhub-public-prod/podcast/example.srt",
    "taskId": "1eed39d387a046c0a1213e6b8f139d77",
    "credits": 12
  }
}
필드타입설명
audioUrlstring생성된 MP3 파일의 URL.
audioDurationinteger오디오 길이(밀리초).
subtitlesUrlstringSRT 자막 파일 URL. 24시간 동안 유효합니다.
taskIdstring작업 ID. 문제를 신고할 때 함께 알려주면 지원팀이 해당 요청을 찾을 수 있습니다.
creditsinteger이 요청에서 소모한 크레딧.

장문 텍스트 음성 변환

POST /v1/flow-speech/episodes

텍스트 한 덩어리 또는 URL의 콘텐츠를 단일 화자 내레이션으로 변환합니다. 이 엔드포인트는 비동기로 동작합니다. 요청은 곧바로 episodeId를 반환하고, 작업이 끝나면 폴링해서 오디오를 가져옵니다. 동기 호출로 기다리기에는 현실적이지 않은 긴 입력을 위해 만들어졌습니다.

원본 텍스트를 어떻게 다룰지는 두 가지 모드로 결정합니다:

  • smart(기본값) — 텍스트를 먼저 정리합니다. 문장 부호, 문법, 서식을 교정해 거칠거나 붙여넣은 입력도 자연스럽게 읽히도록 합니다.
  • direct — 다시 쓰지 않고 텍스트를 그대로 합성합니다. 스크립트가 이미 완성된 경우에 사용하세요.

제약: sources 항목은 정확히 1개, 화자도 정확히 1명, 텍스트 소스는 최소 10자(최대 20,000자)여야 합니다.

Smart 모드(AI 다듬기)

curl -X POST "https://api.marswave.ai/openapi/v1/flow-speech/episodes" \
  -H "Authorization: Bearer $LISTENHUB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "sources": [
      {
        "type": "text",
        "content": "welcome to listenhub this text is intentionally rough and punctuation will be improved automatically"
      }
    ],
    "speakers": [
      { "speakerId": "EN-Woman-General-01" }
    ],
    "language": "en",
    "mode": "smart"
  }'
const response = await fetch('https://api.marswave.ai/openapi/v1/flow-speech/episodes', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.LISTENHUB_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    sources: [
      {
        type: 'text',
        content: 'welcome to listenhub this text is intentionally rough and punctuation will be improved automatically',
      },
    ],
    speakers: [{ speakerId: 'EN-Woman-General-01' }],
    language: 'en',
    mode: 'smart',
  }),
});
const { data } = await response.json();
console.log('Episode ID:', data.episodeId);
import os
import requests

response = requests.post(
    'https://api.marswave.ai/openapi/v1/flow-speech/episodes',
    headers={'Authorization': f'Bearer {os.environ["LISTENHUB_API_KEY"]}'},
    json={
        'sources': [
            {
                'type': 'text',
                'content': 'welcome to listenhub this text is intentionally rough and punctuation will be improved automatically',
            }
        ],
        'speakers': [{'speakerId': 'EN-Woman-General-01'}],
        'language': 'en',
        'mode': 'smart',
    },
)
print('Episode ID:', response.json()['data']['episodeId'])

Direct 모드

curl -X POST "https://api.marswave.ai/openapi/v1/flow-speech/episodes" \
  -H "Authorization: Bearer $LISTENHUB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "sources": [
      {
        "type": "text",
        "content": "Welcome to ListenHub. This script is already finalized and should be converted as-is."
      }
    ],
    "speakers": [
      { "speakerId": "EN-Man-General-01" }
    ],
    "language": "en",
    "mode": "direct"
  }'
const response = await fetch('https://api.marswave.ai/openapi/v1/flow-speech/episodes', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.LISTENHUB_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    sources: [
      {
        type: 'text',
        content: 'Welcome to ListenHub. This script is already finalized and should be converted as-is.',
      },
    ],
    speakers: [{ speakerId: 'EN-Man-General-01' }],
    language: 'en',
    mode: 'direct',
  }),
});
const { data } = await response.json();
console.log('Episode ID:', data.episodeId);
import os
import requests

response = requests.post(
    'https://api.marswave.ai/openapi/v1/flow-speech/episodes',
    headers={'Authorization': f'Bearer {os.environ["LISTENHUB_API_KEY"]}'},
    json={
        'sources': [
            {
                'type': 'text',
                'content': 'Welcome to ListenHub. This script is already finalized and should be converted as-is.',
            }
        ],
        'speakers': [{'speakerId': 'EN-Man-General-01'}],
        'language': 'en',
        'mode': 'direct',
    },
)
print('Episode ID:', response.json()['data']['episodeId'])

URL에서 콘텐츠 읽기

typeurl로 설정하고 페이지 주소를 uri에 넣습니다. ListenHub가 합성 전에 페이지를 가져와 읽을 수 있는 본문을 추출합니다. (하위 호환을 위해 uri 대신 content도 여전히 허용됩니다.)

curl -X POST "https://api.marswave.ai/openapi/v1/flow-speech/episodes" \
  -H "Authorization: Bearer $LISTENHUB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "sources": [
      {
        "type": "url",
        "uri": "https://example.com/article.html"
      }
    ],
    "speakers": [
      { "speakerId": "EN-Woman-General-01" }
    ],
    "language": "en",
    "mode": "smart"
  }'
const response = await fetch('https://api.marswave.ai/openapi/v1/flow-speech/episodes', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.LISTENHUB_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    sources: [{ type: 'url', uri: 'https://example.com/article.html' }],
    speakers: [{ speakerId: 'EN-Woman-General-01' }],
    language: 'en',
    mode: 'smart',
  }),
});
const { data } = await response.json();
console.log('Episode ID:', data.episodeId);
import os
import requests

response = requests.post(
    'https://api.marswave.ai/openapi/v1/flow-speech/episodes',
    headers={'Authorization': f'Bearer {os.environ["LISTENHUB_API_KEY"]}'},
    json={
        'sources': [{'type': 'url', 'uri': 'https://example.com/article.html'}],
        'speakers': [{'speakerId': 'EN-Woman-General-01'}],
        'language': 'en',
        'mode': 'smart',
    },
)
print('Episode ID:', response.json()['data']['episodeId'])

요청 파라미터

필드타입필수설명
sourcesarray콘텐츠 소스. 정확히 1개 항목.
sources[].typestringtext 또는 url.
sources[].contentstringtext인 경우내레이션할 텍스트. 최소 10자, 최대 20,000자. 아주 짧은 클립에는 /v1/speech를 대신 사용하세요.
sources[].uristringurl인 경우(권장)읽어올 페이지 URL. url 소스에는 uri 또는 content 중 하나가 반드시 있어야 합니다.
speakersarray화자 목록. 정확히 1개 항목.
speakers[].speakerIdstringSpeaker ID.
languagestring아니요원본 언어: en, zh, ja. 생략하면 콘텐츠에서 추론합니다.
modestring아니요smart(AI 다듬기) 또는 direct(그대로 변환). 기본값은 smart.

응답에는 작업 ID만 담깁니다:

{
  "code": 0,
  "message": "",
  "data": {
    "episodeId": "665f1c2a9b3e4d0012a4c8e1"
  }
}

결과 폴링

GET /v1/flow-speech/episodes/{episodeId}

반환된 episodeIdprocessStatussuccess가 될 때까지 폴링합니다.

curl -X GET "https://api.marswave.ai/openapi/v1/flow-speech/episodes/{episodeId}" \
  -H "Authorization: Bearer $LISTENHUB_API_KEY"
const response = await fetch(
  `https://api.marswave.ai/openapi/v1/flow-speech/episodes/${episodeId}`,
  { headers: { Authorization: `Bearer ${process.env.LISTENHUB_API_KEY}` } },
);
const { data } = await response.json();
console.log('Status:', data.processStatus);
console.log('Audio URL:', data.audioUrl);
import os
import requests

response = requests.get(
    f'https://api.marswave.ai/openapi/v1/flow-speech/episodes/{episode_id}',
    headers={'Authorization': f'Bearer {os.environ["LISTENHUB_API_KEY"]}'},
)
data = response.json()['data']
print('Status:', data['processStatus'])
print('Audio URL:', data.get('audioUrl'))

작업이 완료되면(processStatussuccess):

{
  "code": 0,
  "message": "",
  "data": {
    "episodeId": "665f1c2a9b3e4d0012a4c8e1",
    "createdAt": 1717430000000,
    "processStatus": "success",
    "completedTime": 1717430090000,
    "title": "Article Title",
    "outline": "...",
    "cover": "https://assets.listenhub.ai/.../cover.png",
    "audioUrl": "https://assets.listenhub.ai/listenhub-public-prod/podcast/665f1c2a9b3e4d0012a4c8e1.mp3",
    "audioStreamUrl": "https://assets.listenhub.ai/listenhub-public-prod/podcast/665f1c2a9b3e4d0012a4c8e1.m3u8",
    "subtitlesUrl": "https://assets.listenhub.ai/.../665f1c2a9b3e4d0012a4c8e1.srt",
    "scripts": "Full narration script text..."
  }
}
필드타입설명
episodeIdstring에피소드 식별자.
createdAtinteger생성 시각(밀리초 타임스탬프).
processStatusstring현재 상태: pending, success, fail. success가 될 때까지 폴링하세요. fail은 작업이 완료되지 못했음을 뜻합니다.
failCodeinteger실패 시에만 존재하며, 원인을 나타냅니다.
completedTimeinteger완료 시각(밀리초 타임스탬프).
titlestring생성된 에피소드 제목.
outlinestring생성된 내레이션 개요.
coverstring커버 이미지 URL.
audioUrlstringMP3 오디오 파일 URL.
audioStreamUrlstringHLS 스트리밍 URL(.m3u8).
subtitlesUrlstringSRT 자막 파일 URL.
scriptsstring내레이션 전체 스크립트 텍스트.

장문 작업은 보통 1~2분 안에 끝납니다. 실용적인 폴링 전략은 생성 후 30초를 기다린 다음 10초마다 폴링하는 것입니다. 실패하면 processStatusfail이 되고 failCode에 원인이 담깁니다.


다중 화자 Direct(비동기)

POST /v1/flow-speech/episodes/tts

길고 이미 준비된 다중 화자 스크립트를 에피소드로 변환합니다. /v1/speech의 비동기·다중 화자 버전으로, 각 줄이 자체 speakerId를 유지하고, 텍스트는 그대로(direct 모드) 합성되며, 호출은 폴링용 episodeId를 반환합니다. 다중 화자 스크립트가 너무 길어 한 번의 동기 /v1/speech 요청으로는 처리하기 어려울 때 사용하세요.

curl -X POST "https://api.marswave.ai/openapi/v1/flow-speech/episodes/tts" \
  -H "Authorization: Bearer $LISTENHUB_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Roundtable Discussion",
    "scripts": [
      { "content": "Thanks for joining the roundtable today.", "speakerId": "EN-Man-General-01" },
      { "content": "Happy to be here. Let us dig into the agenda.", "speakerId": "EN-Woman-General-01" }
    ]
  }'
const response = await fetch('https://api.marswave.ai/openapi/v1/flow-speech/episodes/tts', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.LISTENHUB_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    title: 'Roundtable Discussion',
    scripts: [
      { content: 'Thanks for joining the roundtable today.', speakerId: 'EN-Man-General-01' },
      { content: 'Happy to be here. Let us dig into the agenda.', speakerId: 'EN-Woman-General-01' },
    ],
  }),
});
const { data } = await response.json();
console.log('Episode ID:', data.episodeId);
import os
import requests

response = requests.post(
    'https://api.marswave.ai/openapi/v1/flow-speech/episodes/tts',
    headers={'Authorization': f'Bearer {os.environ["LISTENHUB_API_KEY"]}'},
    json={
        'title': 'Roundtable Discussion',
        'scripts': [
            {'content': 'Thanks for joining the roundtable today.', 'speakerId': 'EN-Man-General-01'},
            {'content': 'Happy to be here. Let us dig into the agenda.', 'speakerId': 'EN-Woman-General-01'},
        ],
    },
)
print('Episode ID:', response.json()['data']['episodeId'])

요청 파라미터

필드타입필수설명
scriptsarray하나 이상의 스크립트 줄. 순서대로 합성됩니다.
scripts[].contentstring줄의 텍스트. 비어 있으면 안 되며, 모든 줄을 합친 길이는 20,000자로 제한됩니다.
scripts[].speakerIdstring이 줄에 사용할 Speaker ID. 줄마다 다른 화자를 쓸 수 있습니다.
titlestring아니요사용자 지정 에피소드 제목. 생략하면 자동 생성됩니다.

응답은 episodeId를 반환합니다. 결과는 장문 작업과 동일한 상태 엔드포인트 GET /v1/flow-speech/episodes/{episodeId}로 폴링합니다.

{
  "code": 0,
  "message": "",
  "data": {
    "episodeId": "665f1c2a9b3e4d0012a4c8e1"
  }
}

관련 문서

이 페이지의 내용