テキスト読み上げ
低レイテンシの単一話者ストリーミングから長文の非同期ナレーションまで、5 つのエンドポイントでテキストを自然な音声に変換します。
ListenHub はテキスト読み上げ(Text to Speech)のエンドポイントを複数提供しており、それぞれ異なる用途に合わせて調整されています。base URL、認証方式、speaker ID は共通ですが、レイテンシ、レスポンスの種類、対応する話者数が異なります。
すべてのリクエストは https://api.marswave.ai/openapi/v1 に送信し、API キーで認証します:
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、短い複数話者スクリプトを 1 回の呼び出しでホスト済み 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.mp3const 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 playerimport 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)リクエストパラメータ
| フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
input | string | はい | 合成するテキスト。最大 20,000 文字。 |
voice | string | はい | Speaker ID(Speakers の speakerId の値)。 |
response_format | string | いいえ | 要求する音声フォーマット。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 キーと 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.mp3const 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
準備済みの複数話者スクリプトから 1 つの音声ファイルを生成します。各行が自身の speakerId を持つため、対話に合わせて話者を交互に切り替えられます。呼び出しは同期で、ホスト済みの音声 URL と字幕をレスポンスで返すので、ポーリングは不要です。
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'])リクエストパラメータ
| フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
scripts | array | はい | 1 行以上のスクリプト。記載順に合成されます。 |
scripts[].content | string | はい | 行のテキスト。空にはできません。全行を合計した長さの上限は 20,000 文字です。 |
scripts[].speakerId | string | はい | この行の 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
}
}| フィールド | 型 | 説明 |
|---|---|---|
audioUrl | string | 生成された MP3 ファイルの URL。 |
audioDuration | integer | 音声の長さ(ミリ秒)。 |
subtitlesUrl | string | SRT 字幕ファイルの URL。有効期限は 24 時間です。 |
taskId | string | タスク ID。問い合わせの際に伝えると、サポートが該当リクエストを特定できます。 |
credits | integer | このリクエストで消費されたクレジット。 |
長文テキスト読み上げ
POST /v1/flow-speech/episodes
まとまったテキスト、または URL の内容を単一話者のナレーションに変換します。このエンドポイントは非同期で動作し、リクエストは即座に episodeId を返すので、処理完了後にポーリングで音声を取得します。同期呼び出しでは待ち時間が現実的でないような、長めの入力を想定しています。
ソーステキストの扱い方は 2 つのモードで制御します:
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 からコンテンツを読み込む
type を url にし、ページのアドレスを 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'])リクエストパラメータ
| フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
sources | array | はい | コンテンツソース。ちょうど 1 件を指定します。 |
sources[].type | string | はい | text または url。 |
sources[].content | string | text の場合 | 読み上げるテキスト。最小 10 文字、最大 20,000 文字。ごく短いクリップには代わりに /v1/speech を使ってください。 |
sources[].uri | string | url の場合(推奨) | 読み込み元のページ URL。url ソースでは uri か content のいずれかが必要です。 |
speakers | array | はい | 話者リスト。ちょうど 1 件を指定します。 |
speakers[].speakerId | string | はい | Speaker ID。 |
language | string | いいえ | ソースの言語:en、zh、ja。省略時はコンテンツから推定されます。 |
mode | string | いいえ | smart(AI による推敲)または direct(そのまま)。デフォルトは smart。 |
レスポンスにはタスク ID のみが含まれます:
{
"code": 0,
"message": "",
"data": {
"episodeId": "665f1c2a9b3e4d0012a4c8e1"
}
}結果をポーリングする
GET /v1/flow-speech/episodes/{episodeId}
返された episodeId で、processStatus が success になるまでポーリングします。
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'))ジョブが完了すると(processStatus が success):
{
"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..."
}
}| フィールド | 型 | 説明 |
|---|---|---|
episodeId | string | エピソードの識別子。 |
createdAt | integer | 作成時刻のタイムスタンプ(ミリ秒)。 |
processStatus | string | 現在の状態:pending、success、fail。success になるまでポーリングします。fail はジョブが完了しなかったことを示します。 |
failCode | integer | 失敗時に返され、その理由を示します。 |
completedTime | integer | 完了時刻のタイムスタンプ(ミリ秒)。 |
title | string | 生成されたエピソードのタイトル。 |
outline | string | 生成されたナレーションのアウトライン。 |
cover | string | カバー画像の URL。 |
audioUrl | string | MP3 音声ファイルの URL。 |
audioStreamUrl | string | HLS ストリーミング URL(.m3u8)。 |
subtitlesUrl | string | SRT 字幕ファイルの URL。 |
scripts | string | ナレーションの全文スクリプト。 |
長文ジョブは通常 1〜2 分で完了します。実用的なポーリング方針は、作成から 30 秒待ってから 10 秒ごとに問い合わせることです。失敗時は processStatus が fail になり、failCode がその理由を示します。
複数話者 Direct(非同期)
POST /v1/flow-speech/episodes/tts
長く、準備済みの複数話者スクリプトをエピソードに変換します。これは /v1/speech の非同期・複数話者版です。各行が自身の speakerId を保持し、テキストはそのまま(direct モードで)合成され、呼び出しはポーリング用の episodeId を返します。複数話者スクリプトが長すぎて 1 回の同期 /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'])リクエストパラメータ
| フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
scripts | array | はい | 1 行以上のスクリプト。記載順に合成されます。 |
scripts[].content | string | はい | 行のテキスト。空にはできません。全行を合計した長さの上限は 20,000 文字です。 |
scripts[].speakerId | string | はい | この行の Speaker ID。行ごとに異なる話者を指定できます。 |
title | string | いいえ | 任意のエピソードタイトル。省略時は自動生成されます。 |
レスポンスは episodeId を返します。結果のポーリングには、長文ジョブと同じステータスエンドポイント GET /v1/flow-speech/episodes/{episodeId} を使います。
{
"code": 0,
"message": "",
"data": {
"episodeId": "665f1c2a9b3e4d0012a4c8e1"
}
}