文本转语音
通过五个端点将文本转换为自然语音,覆盖从低延迟单人流式到长文本异步朗读的各类场景。
ListenHub 提供多个文本转语音端点,每个针对不同的使用场景。它们共用相同的 base URL、鉴权方式和 speaker ID,区别在于延迟、响应类型以及支持的音色数量。
所有请求都发往 https://api.marswave.ai/openapi/v1,并使用 API key 鉴权:
Authorization: Bearer $LISTENHUB_API_KEY在 listenhub.ai/settings/api-keys 创建 key。每个 JSON 响应都包裹为 { "code": 0, "message": "", "data": { ... } };code 非零表示出错。流式端点(/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.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());
// 将 `buffer` 写入文件,或把 `response.body` 接入播放器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)请求参数
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
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
/v1/tts 的精确别名,位于 OpenAI SDK 及 OpenAI 兼容集成默认调用的路径上。请求体、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.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
从已准备好的多人脚本生成单个音频文件。每一行都带有自己的 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 | 是 | 一行或多行脚本,按顺序合成。 |
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,完成后你再轮询获取音频。它专为较长输入设计——这类输入用同步调用等待并不现实。
两种模式控制源文本的处理方式:
smart(默认)——先整理文本:修正标点、语法和格式,使粗糙或粘贴而来的输入也能读得自然。direct——逐字合成文本,不做任何改写。当脚本已是终稿时使用。
约束:sources 必须恰好一项,speaker 必须恰好一个,文本来源至少 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 会在合成前抓取并提取可读内容。(为向后兼容,仍接受用 content 代替 uri。)
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 | 是 | 内容来源。恰好一项。 |
sources[].type | string | 是 | text 或 url。 |
sources[].content | string | text 时必填 | 待朗读的文本。最少 10 个字符,最多 20,000。极短的片段请改用 /v1/speech。 |
sources[].uri | string | url 时推荐 | 待读取的网页 URL。url 来源中 uri 与 content 必须至少有一个。 |
speakers | array | 是 | 音色列表。恰好一项。 |
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 | 完整朗读脚本文本。 |
长文本任务通常在一到两分钟内完成。一个实用的轮询策略:创建后等待 30 秒,然后每 10 秒轮询一次。失败时 processStatus 为 fail,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'])请求参数
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
scripts | array | 是 | 一行或多行脚本,按顺序合成。 |
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"
}
}