ListenHubSDKs & CLI
JavaScript SDK

示例

八个可直接运行的 OpenAPIClient TypeScript 配方 —— 播客、flow speech、TTS、图片、视频、音乐、内容提取与错误处理。

一套基于 OpenAPIClient(API key,服务端)的完整、可复制粘贴的配方集。每个都是独立的 TypeScript 文件,用 tsx 即可运行。它们共享两条约定,先说明一次:

  • 从环境变量构造客户端。 new OpenAPIClient() 会读取 LISTENHUB_API_KEY。在 listenhub.ai/settings/api-keys 创建 key,并保存在服务端。参见 Authentication
  • 生成是异步的。 create 调用会立即返回 episodeIdtaskId;之后你轮询某个 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(来自 listSpeakersspeakerId)和可选的 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,直到状态为 successfailedcontent 由文本加可选参考帧组成。

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 都有自己的 titleaudioUrl

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,直到状态为 completedfailed,再读取 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

下一步

On this page