示例
八个可直接运行的 OpenAPIClient TypeScript 配方 —— 播客、flow speech、TTS、图片、视频、音乐、内容提取与错误处理。
一套基于 OpenAPIClient(API key,服务端)的完整、可复制粘贴的配方集。每个都是独立的 TypeScript 文件,用 tsx 即可运行。它们共享两条约定,先说明一次:
- 从环境变量构造客户端。
new OpenAPIClient()会读取LISTENHUB_API_KEY。在 listenhub.ai/settings/api-keys 创建 key,并保存在服务端。参见 Authentication。 - 生成是异步的。 create 调用会立即返回
episodeId或taskId;之后你轮询某个get*方法,直到状态离开pending/generating。下面每个配方的轮询循环都用同一个sleep辅助函数。
运行任意配方:LISTENHUB_API_KEY=lh_sk_... npx tsx recipe.ts。SDK 仅支持 ESM,需要 Node.js >= 20。面向已登录用户、代表用户操作的应用,请改用带 OAuth 的 ListenHubClient —— 参见 Authentication。
创建播客并轮询至完成
一档基于 URL 的双主持人播客。先按语言列出 speaker,启动 episode,再轮询 getPodcast,直到 processStatus 离开 pending。
import { OpenAPIClient } from '@marswave/listenhub-sdk';
const client = new OpenAPIClient(); // 读取 LISTENHUB_API_KEY
// 按语言挑两个声音。
const { items: speakers } = await client.listSpeakers({ language: 'en' });
const [host, guest] = speakers;
const { episodeId } = await client.createPodcast({
query: 'Explain how transformers work in large language models',
sources: [
{
type: 'url',
content: 'https://en.wikipedia.org/wiki/Transformer_(deep_learning_architecture)',
},
],
speakers: [{ speakerId: host.speakerId }, { speakerId: guest.speakerId }],
language: 'en',
});
console.log(`Created podcast: ${episodeId}`);
let detail = await client.getPodcast(episodeId);
while (detail.processStatus === 'pending') {
await sleep(5000);
detail = await client.getPodcast(episodeId);
console.log(`Status: ${detail.processStatus}`);
}
if (detail.audioUrl) {
console.log(`Title: ${detail.title}`);
console.log(`Audio: ${detail.audioUrl}`);
} else {
console.error(`Generation failed (failCode ${detail.failCode}): ${detail.message}`);
}
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}多 speaker flow speech
Flow speech 用一个或多个声音来朗读文本或网页。在 speakers 里传入多个条目,它们会在脚本中交替出现。create-and-poll 的形态与播客配方一致 —— 只是方法名不同。
import { OpenAPIClient } from '@marswave/listenhub-sdk';
const client = new OpenAPIClient();
const { items: speakers } = await client.listSpeakers({ language: 'en' });
const [voiceA, voiceB] = speakers;
const { episodeId } = await client.createFlowSpeech({
sources: [{ type: 'url', uri: 'https://en.wikipedia.org/wiki/Mars' }],
speakers: [{ speakerId: voiceA.speakerId }, { speakerId: voiceB.speakerId }],
language: 'en',
mode: 'smart', // 'smart' 会把来源改写成脚本;'direct' 原样朗读
});
console.log(`Created flow speech: ${episodeId}`);
let detail = await client.getFlowSpeech(episodeId);
while (detail.processStatus === 'pending') {
await sleep(3000);
detail = await client.getFlowSpeech(episodeId);
console.log(`Status: ${detail.processStatus}`);
}
if (detail.audioUrl) {
console.log(`Title: ${detail.title}`);
console.log(`Audio: ${detail.audioUrl}`);
} else {
console.error(`Generation failed (failCode ${detail.failCode}): ${detail.message}`);
}
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}文本转语音,保存为文件
tts 是同步的,返回原始音频 Response(无 envelope,无需轮询)。把响应体直接写到磁盘。传入 voice(来自 listSpeakers 的 speakerId)和可选的 response_format。
import { writeFile } from 'node:fs/promises';
import { OpenAPIClient } from '@marswave/listenhub-sdk';
const client = new OpenAPIClient();
const { items: speakers } = await client.listSpeakers({ language: 'en' });
const voice = speakers[0];
// `tts` 返回原始 Response —— 直接读取字节。
const response = await client.tts({
input: 'Hello world, this is ListenHub speaking.',
voice: voice.speakerId,
response_format: 'mp3', // 'mp3' | 'opus' | 'aac' | 'flac' | 'wav' | 'pcm'
});
const audio = Buffer.from(await response.arrayBuffer());
await writeFile('speech.mp3', audio);
console.log(`Wrote ${audio.byteLength} bytes to speech.mp3`);若要用多行脚本得到一个托管 URL 加字幕、而非原始字节,请用 speech({ scripts: [{ content, speakerId }] }) —— 它会解析为 { audioUrl, audioDuration, subtitlesUrl, taskId, credits }。
用参考图生成图片
createImage 是同步的,直接返回结果。通过传入 referenceImages 让生成基于一张已有图片 —— 可以是托管文件(fileData)或内联 base64(inlineData)。响应是无类型对象,因此读取字段时要做防御性处理。
import { OpenAPIClient } from '@marswave/listenhub-sdk';
const client = new OpenAPIClient();
const result = await client.createImage({
provider: 'gemini',
prompt: 'Redraw this scene as a watercolor painting at golden hour',
referenceImages: [
{
fileData: {
fileUri: 'https://storage.googleapis.com/your-bucket/reference.png',
mimeType: 'image/png',
},
},
],
imageConfig: {
imageSize: '2K', // '1K' | '2K' | '4K'
aspectRatio: '16:9', // '16:9' | '4:3' | '1:1' | '3:4' | '9:16' | '21:9'
},
});
// createImage 返回无类型 record —— 先打印看结构,再读字段。
console.log(result);若用内联图片数据,把 fileData 换成 inlineData: { data: '<base64>', mimeType: 'image/png' }。参考图用来引导构图与风格;具体改动仍由 prompt 驱动。
生成视频(SeeDance)并轮询
createVideoGeneration 运行 Doubao SeeDance 模型。先用 estimateVideoCredits 估算成本,启动任务,再轮询 getVideoGenerationTask,直到状态为 success 或 failed。content 由文本加可选参考帧组成。
import { OpenAPIClient } from '@marswave/listenhub-sdk';
const client = new OpenAPIClient();
// 在投入昂贵任务前先估算。
const estimate = await client.estimateVideoCredits({
model: 'doubao-seedance-2-fast',
resolution: '720p',
duration: 5,
});
console.log(`Estimated credits: ${estimate.credits}`);
const task = await client.createVideoGeneration({
model: 'doubao-seedance-2-fast', // 'doubao-seedance-2-pro' | 'doubao-seedance-2-fast' | 'happyhorse'
content: [
{ type: 'text', text: 'A cat sprinting through a sunlit garden' },
{
type: 'image_url',
image_url: { url: 'https://example.com/cat.jpg' },
role: 'first_frame', // 'first_frame' | 'last_frame' | 'reference_image'
},
],
resolution: '720p', // '480p' | '720p' | '1080p'
duration: 5,
});
console.log(`Task created: ${task.taskId} (${task.status})`);
let detail = await client.getVideoGenerationTask(task.taskId);
while (detail.status !== 'success' && detail.status !== 'failed') {
await sleep(10_000);
detail = await client.getVideoGenerationTask(task.taskId);
console.log(`Status: ${detail.status}`);
}
if (detail.status === 'success') {
console.log(`Video: ${detail.videoUrl}`);
console.log(`Seed: ${detail.seed}`);
} else {
console.error('Video generation failed');
}
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}生成音乐并轮询
createMusicGenerate 返回一个任务;轮询 getMusicTask,直到它离开进行中的状态。完成的任务带有一个 tracks 数组 —— 每条 track 都有自己的 title 和 audioUrl。
import { OpenAPIClient } from '@marswave/listenhub-sdk';
const client = new OpenAPIClient();
const job = await client.createMusicGenerate({
prompt: 'Upbeat lo-fi hip hop beat with jazzy piano chords',
style: 'lo-fi',
title: 'Late Night Study',
});
console.log(`Music task: ${job.taskId} (${job.status})`);
let task = await client.getMusicTask(job.taskId);
while (task.status !== 'success' && task.status !== 'failed') {
await sleep(10_000);
task = await client.getMusicTask(job.taskId);
console.log(`Status: ${task.status}`);
}
if (task.status === 'success') {
for (const track of task.tracks) {
console.log(`${track.title} — ${track.audioUrl}`);
}
} else {
console.error(`Failed: ${task.errorMessage}`);
}
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}从 URL 提取内容
createContentExtract 从网页抽取干净文本(及可选的元数据)。它返回一个 taskId;轮询 getContentExtract,直到状态为 completed 或 failed,再读取 data.content。适合作为播客或 flow speech 之前的素材准备步骤。
import { OpenAPIClient } from '@marswave/listenhub-sdk';
const client = new OpenAPIClient();
const { taskId } = await client.createContentExtract({
source: { type: 'url', uri: 'https://en.wikipedia.org/wiki/Mars' },
options: { summarize: true, maxLength: 2000 },
});
console.log(`Extract task: ${taskId}`);
let detail = await client.getContentExtract(taskId);
while (detail.status === 'processing') {
await sleep(3000);
detail = await client.getContentExtract(taskId);
console.log(`Status: ${detail.status}`);
}
if (detail.status === 'completed') {
console.log(detail.data?.content);
} else {
console.error(`Extract failed (failCode ${detail.failCode}): ${detail.message}`);
}
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}用 ListenHubError 处理错误
每个方法在 code 非零或 HTTP 出错时都会抛出 ListenHubError。捕获它,把 API 失败与网络或程序错误区分开,并按 status 分支处理鉴权与限流场景。联系支持时引用的就是 requestId。
import { OpenAPIClient, ListenHubError } from '@marswave/listenhub-sdk';
const client = new OpenAPIClient();
try {
// 一个不存在的 ID 会以结构化 API 错误的形式出现。
const detail = await client.getPodcast('nonexistent-id');
console.log(detail.title);
} catch (err) {
if (err instanceof ListenHubError) {
console.error(`[${err.status}] ${err.code}: ${err.message}`);
if (err.requestId) console.error(`request ${err.requestId}`);
if (err.status === 401 || err.status === 403) {
// 凭据被拒 —— 轮换 API key。
} else if (err.status === 429) {
// 客户端已重试到 maxRetries 上限,仍处于限流中。
}
} else {
// 网络故障、超时或代码 bug —— 不是 API 错误。
throw err;
}
}429 Too Many Requests 会在到达你的 catch 之前,依据 Retry-After 头自动重试(最多 maxRetries 次,默认 2)。参见 Configuration。
生成前先看成本
积分成本随产品、时长和选项变化,因此这些配方不写具体数字。用 getSubscription() 读取实时余额(totalAvailableCredits 字段),并在昂贵任务前调用相应的 estimate 接口 —— 例如 estimateVideoCredits。哪些产品提供估算接口,见 OpenAPI reference。