예제
OpenAPIClient로 바로 실행할 수 있는 여덟 가지 TypeScript 레시피 — 팟캐스트, flow speech, TTS, 이미지, 비디오, 음악, 콘텐츠 추출, 에러 처리.
OpenAPIClient(API 키, 서버 사이드) 위에 만든, 복사해서 그대로 쓸 수 있는 완결형 레시피 모음입니다. 각 레시피는 tsx로 실행할 수 있는 독립된 TypeScript 파일입니다. 모두 한 번 짚고 갈 만한 두 가지 공통 규칙을 따릅니다:
- 환경 변수로 클라이언트를 생성하세요.
new OpenAPIClient()는LISTENHUB_API_KEY를 읽습니다. listenhub.ai/settings/api-keys에서 키를 발급받고, 서버 측에만 두세요. 인증을 참고하세요. - 생성은 비동기입니다. create 호출은 즉시
episodeId또는taskId를 반환하며, 상태가pending/generating에서 벗어날 때까지get*메서드를 폴링(polling)합니다. 아래 모든 레시피는 폴링 루프에 동일한sleep헬퍼를 사용합니다.
레시피는 LISTENHUB_API_KEY=lh_sk_... npx tsx recipe.ts로 실행하세요. SDK는 ESM 전용이며 Node.js >= 20이 필요합니다. 로그인한 사용자를 대신해 동작하는 사용자 대상 앱에서는 대신 OAuth를 쓰는 ListenHubClient를 사용하세요 — 인증 참고.
팟캐스트 생성하고 완료될 때까지 폴링하기
URL을 근거로 삼는 2인 진행 팟캐스트입니다. 해당 언어의 스피커(speaker)를 조회하고, 에피소드를 시작한 뒤, processStatus가 pending에서 벗어날 때까지 getPodcast를 폴링합니다.
import { OpenAPIClient } from '@marswave/listenhub-sdk';
const client = new OpenAPIClient(); // reads LISTENHUB_API_KEY
// Pick two voices for the language.
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));
}멀티 스피커 flow speech
Flow speech는 텍스트나 웹 페이지를 하나 이상의 보이스로 내레이션합니다. speakers에 여러 항목을 넘기면 스크립트 전반에 걸쳐 번갈아 사용됩니다. 생성 후 폴링하는 구조는 팟캐스트 레시피와 동일하며, 메서드 이름만 달라집니다.
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' rewrites the source into a script; 'direct' reads it as-is
});
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)을 파일로 저장하기
tts는 동기 방식이며 원본 오디오 Response를 그대로 반환합니다(래퍼도 폴링도 없습니다). 본문을 바로 디스크에 기록하세요. 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` returns the raw Response — read its bytes directly.
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 }로 resolve됩니다.
레퍼런스 이미지로 이미지 생성하기
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 returns an untyped record — log it to see the shape, then read fields.
console.log(result);인라인 이미지 데이터를 쓰려면 fileData 대신 inlineData: { data: '<base64>', mimeType: 'image/png' }를 넣으세요. 레퍼런스 이미지는 구도와 스타일을 잡아 주며, 원하는 변화를 이끄는 것은 여전히 prompt입니다.
비디오 생성(SeeDance)하고 폴링하기
createVideoGeneration은 Doubao SeeDance 모델을 실행합니다. 먼저 estimateVideoCredits로 비용을 추정하고, 작업을 시작한 뒤, 상태가 success 또는 failed가 될 때까지 getVideoGenerationTask를 폴링하세요. content는 텍스트와 선택적인 레퍼런스 프레임으로 구성합니다.
import { OpenAPIClient } from '@marswave/listenhub-sdk';
const client = new OpenAPIClient();
// Estimate before committing to an expensive job.
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는 작업(task)을 반환합니다. 진행 중 상태에서 벗어날 때까지 getMusicTask를 폴링하세요. 완료된 작업에는 tracks 배열이 담겨 있으며, 각 트랙은 저마다 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를 반환하므로, 상태가 completed 또는 failed가 될 때까지 getContentExtract를 폴링한 다음 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가 0이 아니거나 HTTP 에러가 발생하면 ListenHubError를 던집니다. 이를 catch하면 API 실패를 네트워크 오류나 코드 버그와 구분할 수 있고, status로 분기해 인증 실패와 레이트 리밋 상황에 대응할 수 있습니다. 지원팀에 문의할 때 함께 알려야 할 값이 requestId입니다.
import { OpenAPIClient, ListenHubError } from '@marswave/listenhub-sdk';
const client = new OpenAPIClient();
try {
// A bad ID surfaces as a structured API error.
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) {
// Credential rejected — rotate the API key.
} else if (err.status === 429) {
// The client already retried up to maxRetries; you are still limited.
}
} else {
// Network failure, timeout, or a bug — not an API error.
throw err;
}
}429 Too Many Requests는 catch에 도달하기 전에 Retry-After 헤더를 이용해 자동으로 재시도됩니다(최대 maxRetries회, 기본값 2). 설정을 참고하세요.
생성 전에 비용 확인하기
크레딧 비용은 제품, 길이, 옵션에 따라 달라지므로 이 레시피들에서는 구체적인 수치를 제시하지 않습니다. getSubscription()의 totalAvailableCredits 필드로 실시간 잔액을 확인하고, 비용이 큰 작업을 실행하기 전에는 해당하는 추정 엔드포인트(예: estimateVideoCredits)를 호출하세요. 어떤 제품이 추정 엔드포인트를 제공하는지는 OpenAPI 레퍼런스를 참고하세요.