npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@marswave/listenhub-sdk

v0.0.21

Published

JavaScript SDK for ListenHub API

Readme

@marswave/listenhub-sdk

npm version license types

ListenHub API 的 JavaScript SDK。

English

安装

npm i @marswave/listenhub-sdk

快速开始

OpenAPI Key(推荐用于服务端)

无需 OAuth 流程,传入 API Key 即可:

import {OpenAPIClient} from '@marswave/listenhub-sdk';

const client = new OpenAPIClient({apiKey: 'lh_sk_...'});
// 或设置 LISTENHUB_API_KEY 环境变量后直接 new OpenAPIClient()

const {items: speakers} = await client.listSpeakers({language: 'en'});
const {episodeId} = await client.createFlowSpeech({
	sources: [{type: 'text', content: 'Hello world'}],
	speakers: [{speakerId: speakers[0].speakerId}],
});

OAuth(用于客户端应用)

克隆仓库并运行 OAuth 登录示例——它会打开浏览器、处理回调并打印你的 token:

git clone https://github.com/marswaveai/listenhub-sdk.git
cd listenhub-sdk
pnpm i
npx tsx examples/oauth-login.ts

客户端选项

SDK 为两种认证模式提供两个客户端:

// OpenAPI Key —— 服务端,无需用户登录
const openapi = new OpenAPIClient({
	apiKey: 'lh_sk_...', // 或 LISTENHUB_API_KEY 环境变量
	baseURL: 'https://api.marswave.ai/openapi', // 或 LISTENHUB_OPENAPI_URL 环境变量
	timeout: 60_000,
	maxRetries: 2,
});

// OAuth access token —— 客户端,需要用户登录
const client = new ListenHubClient({
	accessToken: 'token', // 静态字符串或 () => string | undefined
	baseURL: 'https://api.listenhub.ai/api', // 或 LISTENHUB_API_URL 环境变量
	timeout: 30_000,
	maxRetries: 2,
});

Base URL 配置

每个客户端有各自的默认 Base URL 和各自的覆盖变量:

| 客户端 | 默认 Base URL | 覆盖变量 | | ----------------- | --------------------------------- | ----------------------- | | OpenAPIClient | https://api.marswave.ai/openapi | LISTENHUB_OPENAPI_URL | | ListenHubClient | https://api.listenhub.ai/api | LISTENHUB_API_URL |

每个客户端的取值顺序为 baseURL 选项 → 环境变量 → 内置默认值,显式传入的 baseURL 始终优先。覆盖值是整个 Base URL,含路径前缀——OpenAPIClient 的 URL 以 /openapi 结尾,ListenHubClient 的 URL 以 /api 结尾。你传给 client.api 的路径是相对于它的(v1/...,不带前导斜杠),所以路径前缀必须保留在 Base URL 里。

网络受限环境的覆盖

如果 listenhub.ai / marswave.ai 默认地址在你的网络下不可达(例如整个 listenhub.ai 域当前在中国大陆被封),把 Base URL 设为一个可达的主机。截至 2026-07-24,listenhub.app 主机是一个已验证可用的覆盖地址:

// OpenAPI 客户端
const openapi = new OpenAPIClient({
	apiKey: 'lh_sk_...',
	baseURL: 'https://api.listenhub.app/openapi',
});

// OAuth 客户端
const client = new ListenHubClient({
	accessToken: 'token',
	baseURL: 'https://api.listenhub.app/api',
});

或通过环境变量,不改动代码:

export LISTENHUB_OPENAPI_URL="https://api.listenhub.app/openapi"  # OpenAPIClient
export LISTENHUB_API_URL="https://api.listenhub.app/api"          # ListenHubClient

这是网络受限环境的覆盖方式,不是新默认值——SDK 出厂仍是 .ai / marswave.ai 默认地址,网络正常的调用方不需要设置它们。listenhub.app 只是当前已验证的示例;若它也变得不可达,把 Base URL 指向任何一个提供同样 API 的可达主机即可,保持 /openapi/api 后缀不变。

自动域选择

baseURL 和环境变量都没设时,默认域在连接层不可达,SDK 会自己选域:

  • 第一个连不上的请求触发切换,切到内置备选域(两个 client 都是 api.listenhub.app),并把结果写进 ~/.config/listenhub/domain.json(遵循 XDG_CONFIG_HOME)。
  • 之后所有请求——包括 POST——直接发往选定域,只发一次。不会再打不通的域,也不会重复发送。
  • 选定域之后又连不上,这条记录会被清掉,下次重新探默认域。

依赖它之前要知道两条规则:

  • 非幂等请求绝不会被换域重发。 创建、生成、上传这类 POST 失败后不会被发去另一个域——连接失败并不能证明服务端没收到,重发可能导致双份扣费或重复产出。SDK 只记录下可达的域,并抛出 DomainSwitchedError 提示你重试;你重试的那一次就走新域了。GET 是幂等的,会透明恢复。
  • 显式 baseURL 或环境变量会完全关掉这套机制。 一旦你钉死了 Base URL,SDK 就只往那里发,永不切换。

不想写完整 URL 的话,设 LISTENHUB_DOMAIN=app 即可钉住备选域(default 则强制用出厂默认)。这样连第一次失败的请求都省了,已经确知默认域被墙时很有用。没有文件系统的环境(浏览器、只读容器)会退化成进程内存级缓存——写不进磁盘绝不会让请求失败。

排查 fetch failed

TypeError: fetch failed 表示请求根本没有收到 HTTP 响应——连接在 DNS / TLS / 代理 / Base URL 层就失败了,因此没有状态码,SDK 也无法把它包装成 ListenHubError。按顺序检查:

  1. DNS / TLS / 代理——从当前环境能否解析并连上该主机?
  2. Base URL——客户端指向的是一个可达的主机吗?在受限网络下,使用上面的覆盖地址(https://api.listenhub.app/openapihttps://api.listenhub.app/api)。

服务器一旦可达,连接层错误就会变成结构化错误:HTTP 401,或携带 status 和业务 code(例如 21007)的 ListenHubError。这些是服务已可达后的鉴权 / 请求问题,不是连通性问题——和 fetch failed 属于两类不同的失败。

listenhub.app 是当前已验证可达的主机;若它变化,换成任何可达且保持 /openapi/api 后缀的主机即可。

示例

OpenAPI Key

| 文件 | 说明 | | -------------------------------------------------------- | ---------------------------------------- | | examples/openapi-basic.ts | 创建 flow speech、轮询并查看积分 | | examples/voice-clone.ts | 上传参考音频克隆音色、确认后直接合成语音 |

OAuth(ListenHubClient)

| 文件 | 说明 | | -------------------------------------------------------------------------- | ------------------------------------------- | | examples/oauth-login.ts | 浏览器 OAuth 登录流程 | | examples/basic.ts | 签到、API key、错误处理 | | examples/create-podcast.ts | 创建双人播客并轮询结果 | | examples/create-tts.ts | 从纯文本生成语音 | | examples/create-explainer-video.ts | 从 URL 生成讲解视频 | | examples/create-slides.ts | 幻灯片演示 | | examples/create-ai-image.ts | 从提示词生成 AI 图片 | | examples/music.ts | 音乐生成与音频翻唱 | | examples/video-generation.ts | SeeDance2.0 视频生成 | | examples/listenhub-voice.ts | ListenHub Voice 生成(listenhub-voice-1.0) |

文档

| 文档 | 说明 | | ------------------------------------ | ------------------------------------ | | Architecture | 模块依赖图与职责 | | Client Behavior | 请求/响应流程、hooks、重试与刷新逻辑 | | Testing | 测试分层、运行测试、mock server 搭建 |

API

认证

| 方法 | 说明 | | --------------------------------- | ---------------------------------------------- | | connectInit({callbackPort}) | 启动 OAuth 流程,返回 authUrlsessionId | | connectToken({sessionId, code}) | 用授权码换取 token | | refresh({refreshToken}) | 刷新过期的 access token | | revoke({refreshToken}) | 撤销 refresh token |

签到

| 方法 | 说明 | | ----------------- | ---------------------- | | checkinSubmit() | 提交每日签到 | | checkinStatus() | 查看签到连续天数与状态 |

设置

| 方法 | 说明 | | -------------------- | ------------------------------------------ | | getApiKey() | 获取当前 API key | | regenerateApiKey() | 重新生成 API key(触发 onApiKeyChanged) | | getSettings() | 获取各产品类型的模板设置 |

内容创作

| 方法 | 说明 | | ------------------------------ | --------------------- | | createPodcast(params) | 创建播客(单人/双人) | | createTTS(params) | 创建语音合成音频 | | createExplainerVideo(params) | 创建讲解视频 | | createSlides(params) | 创建幻灯片演示 | | createAIImage(params) | 从提示词生成 AI 图片 |

音乐

由 Mureka 提供方驱动(默认)。生成类接口是异步的——返回 taskId,需轮询 getMusicTask(taskId) 直到状态为 success。分析类接口(recognize / describe / stem)是同步的。 文件入参接受 Blob(浏览器 File,或 Node 18+ 里的 new Blob([buffer]))。

| 方法 | 类型 | 说明 | | --------------------------------- | ---- | --------------------------------------- | | createMusicGenerate(params) | 异步 | 从文本提示词 / 歌词生成音乐 | | createMusicRemix(params) | 异步 | 用已有音频 + 新歌词重制歌曲 | | createMusicInstrumental(params) | 异步 | 生成器乐曲(提示词或参考音频) | | createMusicSoundtrack(params) | 异步 | 从图片或视频生成音乐 | | createMusicTrack(params) | 异步 | 生成单条乐器/人声轨道 | | createMusicExtend(params) | 异步 | 续写已有歌曲(legacy Suno) | | createMusicCover(params) | 异步 | 已废弃 —— 通过 legacy Suno 提供方翻唱 | | recognizeMusic(params) | 同步 | 转写歌词并带时间戳 | | describeMusic(params) | 同步 | 分析音频(描述、标签、流派、乐器) | | stemMusic(params) | 同步 | 分离音轨(返回 ZIP 下载链接) | | getMusicTask(taskId) | — | 获取音乐任务状态与详情 | | listMusicTasks(params?) | — | 列出音乐任务,支持可选过滤 |

// 生成后轮询
const {taskId} = await client.createMusicGenerate({prompt: 'lo-fi chill beats', model: 'auto'});
let task = await client.getMusicTask(taskId);
while (task.status !== 'success' && task.status !== 'failed') {
	await new Promise((r) => setTimeout(r, 5000));
	task = await client.getMusicTask(taskId);
}

// 从本地文件翻唱(Node 18+)
import {readFile} from 'node:fs/promises';
const audio = new Blob([await readFile('./song.mp3')]);
await client.createMusicRemix({
	audio,
	audioFilename: 'song.mp3',
	lyrics: '...',
	prompt: 'jazzy remix',
});

// 同步分析
const {result} = await client.describeMusic({audio, audioFilename: 'song.mp3'});

视频生成(SeeDance2.0 / HappyHorse / PixVerse)

| 方法 | 说明 | | ---------------------------------------- | ---------------------------------- | | createVideoGeneration(params) | 创建视频生成任务 | | uploadVideoReferenceImage(params) | 上传本地图片并返回 Seedance 元数据 | | getVideoGenerationTask(taskId) | 获取视频生成任务状态与详情 | | listVideoGenerationTasks(params?) | 列出视频生成任务,支持可选过滤 | | estimateVideoGenerationCredits(params) | 生成前预估积分消耗 | | createPixVerseVideoGeneration(params) | 创建 PixVerse 视频生成任务 | | estimatePixVerseVideoCredits(params) | 生成前预估 PixVerse 积分消耗 |

支持的模型:doubao-seedance-2-prodoubao-seedance-2-fasthappyhorse;PixVerse:pixversev6v5v4.5

Seedance 的参考图片/视频需要尺寸做服务端校验。把媒体 URL 放进 content,把尺寸放进顶级的 referenceImages / referenceVideos

// 本地图片辅助方法:上传图片并读取宽高用于 Seedance 校验。
import {readFile} from 'node:fs/promises';

const firstFrame = await client.uploadVideoReferenceImage({
	file: new Blob([await readFile('./cat.png')], {type: 'image/png'}),
	fileName: 'cat.png',
	role: 'first_frame',
});

await client.createVideoGeneration({
	model: 'doubao-seedance-2-fast',
	content: [{type: 'text', text: 'A cat running through a garden'}, firstFrame.content],
	referenceImages: [firstFrame.referenceImage],
	resolution: '720p',
	duration: 5,
});

// URL 入参:显式提供元数据。
await client.createVideoGeneration({
	model: 'doubao-seedance-2-fast',
	content: [
		{type: 'text', text: 'A cat running through a garden'},
		{type: 'image_url', image_url: {url: 'https://example.com/cat.jpg'}, role: 'first_frame'},
	],
	referenceImages: [{role: 'first_frame', width: 1080, height: 1920, size: 3_600_000}],
	resolution: '720p',
	duration: 5,
});

HappyHorse 示例:

// 文生视频
await client.createVideoGeneration({
	model: 'happyhorse',
	content: [{type: 'text', text: '一只猫在月球上跳舞'}],
	resolution: '720p',
	ratio: '4:5',
	duration: 5,
});

// 图生视频
await client.createVideoGeneration({
	model: 'happyhorse',
	content: [
		{type: 'text', text: '让画面动起来'},
		{type: 'image_url', image_url: {url: 'https://...'}, role: 'first_frame'},
	],
	resolution: '1080p',
	duration: 5,
});

// 视频编辑
await client.createVideoGeneration({
	model: 'happyhorse',
	content: [
		{type: 'text', text: '将背景替换为星空'},
		{type: 'video_url', video_url: {url: 'https://...'}, role: 'reference_video'},
	],
	resolution: '720p',
	duration: 5,
	inputVideoDuration: 10,
	audioSetting: 'origin',
});

PixVerse 示例:

PixVerse 使用独立的端点(createPixVerseVideoGeneration),请求形态由 capability 驱动。 用共享的 getVideoGenerationTask / listVideoGenerationTasks 轮询结果。

// 预估积分
const {credits} = await client.estimatePixVerseVideoCredits({
	capability: 'text_to_video',
	quality: '720p',
	duration: 5,
});

// 文生视频(默认:model 'pixverse'、language 'en'、quality '720p'、aspectRatio '16:9')
await client.createPixVerseVideoGeneration({
	capability: 'text_to_video',
	prompt: '一只猫在花园里奔跑',
	quality: '720p',
	aspectRatio: '16:9',
	duration: 5,
});

// 图生视频
await client.createPixVerseVideoGeneration({
	capability: 'image_to_video',
	prompt: '让画面动起来',
	images: [{url: 'https://example.com/cat.jpg'}],
	quality: '1080p',
	duration: 5,
});

// 营销 Agent(promo_mix 需要 >= 4 张图片;agent 的 duration 必须是 20/30/60)
await client.createPixVerseVideoGeneration({
	capability: 'agent',
	prompt: '为这款产品制作一支广告',
	images: [
		{url: 'https://example.com/p1.jpg'},
		{url: 'https://example.com/p2.jpg'},
		{url: 'https://example.com/p3.jpg'},
		{url: 'https://example.com/p4.jpg'},
	],
	quality: '1080p',
	duration: 30,
	pixverse: {agentType: 'promo_mix'},
});

ListenHub Voice(listenhub-voice-1.0)

| 方法 | 说明 | | ---------------------------------- | --------------------------------------- | | createListenHubVoice(params) | 创建 ListenHub Voice 生成任务 | | getListenHubVoiceTask(taskId) | 获取 ListenHub Voice 任务状态与音频 URL | | listListenHubVoiceTasks(params?) | 列出 ListenHub Voice 任务,支持可选过滤 |

约束(服务端强制):text <= 1400 字符;voices 1-3 项;voicesimage 互斥;durationHint[1, 110]audioUrl 仅在 status === 'success' 时存在。

// 单音色(id 是 ListenHub 的 speakerInnerId 或官方平台的 voice_type)
const task = await client.createListenHubVoice({
	text: '欢迎收听 ListenHub。',
	voices: [{type: 'speaker', id: 'zh_female_wanwanxiaohe_moon_bigtts'}],
	audioConfig: {format: 'mp3'},
	durationHint: 30,
});

// 多音色对白(每一项应为自定义参考音频)
await client.createListenHubVoice({
	text: '@音频1 你好。@音频2 你也好。',
	voices: [
		{type: 'reference', url: 'https://example.com/voice-a.mp3'},
		{type: 'reference', url: 'https://example.com/voice-b.mp3'},
	],
});

// 图片转音频(与 voices 互斥)
await client.createListenHubVoice({
	text: '为这张图配一段旁白。',
	image: {url: 'https://example.com/scene.jpg'},
});

const detail = await client.getListenHubVoiceTask(task.taskId);
if (detail.status === 'success') {
	console.log(detail.audioUrl, detail.audioDuration);
}

按产品列表

| 方法 | 说明 | | ------------------------------ | -------------------------- | | listPodcasts(params?) | 列出播客 | | listTTS(params?) | 列出语音合成 | | listExplainerVideos(params?) | 列出讲解视频 | | listSlides(params?) | 列出幻灯片 | | listAIImages(params?) | 列出 AI 生成的图片 | | getCreation(episodeId) | 获取完整作品详情 | | deleteCreations({ids}) | 批量删除作品(含 AI 视频) | | deleteAIImages({ids}) | 批量删除 AI 图片 |

用户

| 方法 | 说明 | | ------------------- | ---------------------- | | getCurrentUser() | 获取当前用户资料 | | getSubscription() | 获取订阅与积分使用信息 |

声音

| 方法 | 说明 | | ----------------------- | ------------------ | | listSpeakers(params?) | 按语言列出可用声音 |

语音克隆

上传参考音频建任务,轮询到克隆完成后确认成音色,之后可反复使用。等级配额内确认不扣积分, 超出配额后每次扣 300 积分,且必须显式传 useCredits 授权。本客户端只支持 zh / en

| 方法 | 说明 | | -------------------------------------------- | ---------------------------------- | | createVoiceClone(params) | 用 1-6 个参考音频文件创建克隆任务 | | getVoiceCloneTask(taskId) | 轮询任务;克隆失败时抛错 | | confirmVoiceClone(params) | 把已完成的任务确认成私有音色 | | listVoiceCloneSpeakers() | 列出私有音色,含配额与剩余确认次数 | | getVoiceCloneSpeaker(speakerId) | 获取单个私有音色 | | updateVoiceCloneSpeaker(speakerId, params) | 改音色名称或性别 | | deleteVoiceCloneSpeaker(speakerId) | 删除音色并释放一个名额 |

自定义请求

client.api 暴露底层的 ky 实例,用于 SDK 尚未覆盖的端点:

const user = await client.api.get('v1/users/me').json();

OpenAPIClient API

OpenAPIClient 使用 API Key 认证访问所有 OpenAPI 端点。

声音

| 方法 | 说明 | | ----------------------- | ------------------ | | listSpeakers(params?) | 按语言列出可用声音 |

Flow Speech

| 方法 | 说明 | | ------------------------------------------- | ----------------------------- | | createFlowSpeech(params) | 创建 flow speech | | getFlowSpeech(episodeId) | 获取 flow speech 状态与详情 | | getFlowSpeechTextStream(episodeId, event) | 流式输出脚本或大纲文本(SSE) | | createFlowSpeechTTS(params) | 从脚本创建 flow speech |

播客

| 方法 | 说明 | | ---------------------------------------- | ----------------------------- | | createPodcast(params) | 创建播客 | | getPodcast(episodeId) | 获取播客状态与详情 | | getPodcastTextStream(episodeId, event) | 流式输出脚本或大纲文本(SSE) | | createPodcastTextContent(params) | 仅创建文本内容(不生成音频) | | generatePodcastAudio(episodeId) | 为纯文本作品生成音频 |

TTS

| 方法 | 说明 | | --------------------- | ----------------------------- | | speech(params) | 多音色语音,返回音频 URL | | tts(params) | 单音色 TTS,返回音频流 | | audioSpeech(params) | OpenAI 兼容的 TTS,返回音频流 |

故事书

| 方法 | 说明 | | ----------------------------------- | ---------------- | | createStorybook(params) | 创建故事书 | | getStorybook(episodeId) | 获取故事书详情 | | generateStorybookVideo(episodeId) | 从故事书生成视频 |

图片

| 方法 | 说明 | | --------------------- | ---------------------------- | | createImage(params) | 生成图片(google 或 openai) |

文件

| 方法 | 说明 | | -------------------- | ----------------------------- | | createFileUpload() | 创建预签名上传 URL | | uploadFile() | 创建预签名 URL 并上传文件数据 |

视频生成

| 方法 | 说明 | | ----------------------------------- | ---------------------------------- | | createVideoGeneration(params) | 创建视频生成任务 | | uploadVideoReferenceImage(params) | 上传本地图片并返回 Seedance 元数据 | | getVideoGenerationTask(taskId) | 获取任务状态与视频 URL | | listVideoGenerationTasks(params?) | 列出任务,支持可选过滤 | | estimateVideoCredits(params) | 生成前预估积分消耗 |

ListenHub Voice

| 方法 | 说明 | | ---------------------------------- | ----------------------------- | | createListenHubVoice(params) | 创建 ListenHub Voice 生成任务 | | getListenHubVoiceTask(taskId) | 获取任务状态与音频 URL | | listListenHubVoiceTasks(params?) | 列出任务,支持可选过滤 |

语音克隆

流程与 OAuth 客户端相同,差别是:支持 ja;每次创建都必须声明 consentConfirmed: true (即调用方声明已获得被克隆者授权);可用 autoConfirm 让「发现任务完成的那次轮询」直接确认。 getVoiceCloneTask 有三种终态——克隆失败(errorCode / errorMessage)、克隆完成但未确认 (demoAudioUrl,自动确认失败时另带 confirmError)、确认成功(speakerId,可直接用于 /v1/speech/v1/tts/v1/audio/speech)。

| 方法 | 说明 | | -------------------------------------------- | --------------------------------------- | | createVoiceClone(params) | 创建克隆任务,必须带 consentConfirmed | | getVoiceCloneTask(taskId) | 轮询任务;开启 autoConfirm 时同时确认 | | confirmVoiceClone(params) | 确认已完成的任务,返回 speakerId | | listVoiceCloneSpeakers() | 列出私有音色,含配额与剩余确认次数 | | getVoiceCloneSpeaker(speakerId) | 获取单个私有音色 | | updateVoiceCloneSpeaker(speakerId, params) | 改音色名称或性别 | | deleteVoiceCloneSpeaker(speakerId) | 删除音色并释放一个名额 |

内容提取

| 方法 | 说明 | | ------------------------------ | --------------- | | createContentExtract(params) | 从 URL 提取内容 | | getContentExtract(taskId) | 获取提取结果 |

用户

| 方法 | 说明 | | ------------------- | ---------------------- | | getSubscription() | 获取订阅与积分使用信息 |

限流

遇到 429 Too Many Requests 时,SDK 会读取 Retry-After 头并自动重试,最多重试 maxRetries 次(默认 2 次)。

许可证

MIT