설명 영상과 슬라이드
텍스트나 URL을 내레이션이 있는 페이지 묶음(설명, 스토리, 프레젠테이션 슬라이드)으로 바꾸고 영상으로 렌더링하는 하나의 엔드포인트 패밀리입니다.
설명 영상, 스토리 영상, 프레젠테이션 슬라이드는 하나의 엔드포인트 패밀리의 세 가지 모드입니다. 소스와 보이스를 보내면 ListenHub가 내레이션을 작성하고 페이지마다 비주얼을 생성합니다. 생성된 페이지는 그대로 소재로 활용할 수도 있고, 내레이션이 포함된 영상으로 렌더링할 수도 있습니다.
세 엔드포인트는 모든 모드에서 공통입니다:
| 엔드포인트 | 용도 |
|---|---|
POST /v1/storybook/episodes | 에피소드를 생성합니다. mode로 설명, 스토리, 슬라이드를 선택합니다 |
GET /v1/storybook/episodes/{episodeId} | 진행 상태를 폴링한 뒤 페이지와 에셋 URL을 읽습니다 |
POST /v1/storybook/episodes/{episodeId}/video | 완성된 페이지를 영상으로 렌더링합니다 |
이 페이지의 모든 엔드포인트는 OpenAPI Base URL https://api.marswave.ai/openapi를 사용하며,
Authorization: Bearer $LISTENHUB_API_KEY 헤더로 API 키를 인증합니다. 키는
listenhub.ai/settings/api-keys에서 만드세요.
모드 선택
mode는 선택 사항이며 기본값은 info입니다. 세 모드 사이에서 다른 필드는 이것 하나뿐이며 — 이 페이지의
나머지 내용은 세 모드 모두에 적용됩니다.
mode | 생성되는 것 | 1페이지 | 적합한 용도 |
|---|---|---|---|
info (기본값) | 인포그래픽, 일러스트, 데이터 시각화 | 매거진 스타일 표지 | 지식 설명, 제품 소개 |
story | 스토리 장면 일러스트 | 스토리 표지 | 스토리 공유, 사례 연구 |
slides | PPT 레이아웃(그리드, 프로세스 플로우, 큰 숫자 히어로) | 프레젠테이션 타이틀 페이지 | 회의 프레젠테이션, 비즈니스 리포트, 컨퍼런스 발표 |
워크플로
에피소드 생성
소스, 화자, 원하는 mode를 지정해 POST /v1/storybook/episodes를 호출합니다. 반환된 episodeId를
저장하세요.
완료까지 폴링
최초 60초 대기 후, processStatus가 success가 될 때까지 GET /v1/storybook/episodes/{episodeId}를
10초마다 폴링합니다.
원본 소재 활용(선택)
pages[]에는 생성된 이미지와 내레이션 스크립트가 들어 있습니다. 그것만 필요하다면 여기서 끝내도 됩니다.
영상 렌더링
POST /v1/storybook/episodes/{episodeId}/video를 호출해 각 페이지를 내레이션이 포함된 영상으로 합성합니다.
영상 폴링
videoStatus가 success가 될 때까지 폴링한 뒤 videoUrl을 다운로드합니다.
에피소드 생성
POST /v1/storybook/episodes
curl -X POST "https://api.marswave.ai/openapi/v1/storybook/episodes" \
-H "Authorization: Bearer $LISTENHUB_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"sources": [
{"type": "url", "content": "https://example.com/article"}
],
"speakers": [
{"speakerId": "<SPEAKER_ID>"}
],
"language": "en",
"mode": "info"
}'const response = await fetch(
'https://api.marswave.ai/openapi/v1/storybook/episodes',
{
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.LISTENHUB_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
sources: [{ type: 'url', content: 'https://example.com/article' }],
speakers: [{ speakerId: '<SPEAKER_ID>' }],
language: 'en',
mode: 'info',
}),
},
)
const { data } = await response.json()
const episodeId = data.episodeIdimport os
import requests
response = requests.post(
'https://api.marswave.ai/openapi/v1/storybook/episodes',
headers={'Authorization': f'Bearer {os.environ["LISTENHUB_API_KEY"]}'},
json={
'sources': [{'type': 'url', 'content': 'https://example.com/article'}],
'speakers': [{'speakerId': '<SPEAKER_ID>'}],
'language': 'en',
'mode': 'info',
},
)
episode_id = response.json()['data']['episodeId']응답:
{
"code": 0,
"message": "",
"data": { "episodeId": "665f1d4e8b3a3f001234abcd" }
}요청 파라미터
| 파라미터 | 타입 | 필수 | 설명 |
|---|---|---|---|
sources | array(1) | 예 | 콘텐츠 소스. 정확히 1개 |
sources[].type | string | 예 | "text" 또는 "url" |
sources[].content | string | 예 | 텍스트 콘텐츠, 또는 type이 "url"일 때는 URL 자체 |
sources[].uri | string | 아니요 | 허용되지만 무시됩니다 — url 소스에서는 서버가 content에서 uri를 도출합니다 |
sources[].metadata | object | 아니요 | 소스 메타데이터 |
speakers | array(1) | 예¹ | 음성 설정. 최대 1개 |
speakers[].speakerId | string | 예 | 화자 ID(화자 참고) |
skipAudio | boolean | 아니요 | 기본값은 false. true이면 이미지와 텍스트만 생성되며 내레이션 오디오는 만들어지지 않습니다 |
language | string | 아니요 | 언어 코드(예: "en", "zh"). 기본값은 en이며, 소스에서 추론되지 않습니다 |
mode | string | 아니요 | "info"(기본값), "story", "slides" 중 하나 |
style | string | 아니요 | 비주얼 스타일 ID. 생략하면 해당 모드의 기본 스타일이 사용됩니다. 허용되는 ID는 공개 계약에 포함되지 않습니다 |
¹ skipAudio가 true가 아닌 한 speakers는 필수이며, true인 경우에는 생략할 수 있습니다.
language는 콘텐츠에서 감지되지 않습니다. 생략하면 소스 언어와 상관없이 영어 에피소드가 생성되므로,
영어가 아닌 소스에서는 명시적으로 지정하세요.
비주얼과 스크립트만 필요할 때는 skipAudio: true로 설정하세요 — 오디오 렌더링 없이도 페이지
이미지와 내레이션 텍스트는 에피소드에서 그대로 받을 수 있습니다.
에피소드 폴링
GET /v1/storybook/episodes/{episodeId}
반환된 episodeId로 processStatus가 success가 될 때까지 폴링합니다.
curl "https://api.marswave.ai/openapi/v1/storybook/episodes/{episodeId}" \
-H "Authorization: Bearer $LISTENHUB_API_KEY"응답(processStatus가 success일 때):
{
"code": 0,
"message": "",
"data": {
"episodeId": "{episodeId}",
"createdAt": 1700000000,
"mode": "info",
"processStatus": "success",
"credits": 30,
"title": "How AI Is Changing the World",
"cover": "https://assets.listenhub.app/covers/{episodeId}.png",
"audioUrl": "https://assets.listenhub.app/storybook/{episodeId}.mp3",
"audioDuration": 180,
"videoUrl": "",
"videoStatus": "not_generated",
"pages": [
{
"text": "Artificial intelligence has transformed industries worldwide...",
"pageNumber": 1,
"imageUrl": "https://assets.listenhub.app/pages/{episodeId}-1.png",
"audioTimestamp": 0
},
{
"text": "From healthcare to finance, AI applications continue to expand...",
"pageNumber": 2,
"imageUrl": "https://assets.listenhub.app/pages/{episodeId}-2.png",
"audioTimestamp": 25.3
}
]
}
}원본 소재: pages[]의 각 항목에는 imageUrl(생성된 비주얼), text(내레이션 스크립트),
audioTimestamp(해당 페이지가 audioUrl에서 시작하는 지점)가 들어 있습니다. 이를 다운로드하면 영상을
렌더링하지 않고도 자체 덱을 구성하거나 편집할 수 있습니다.
응답 필드
| 필드 | 타입 | 설명 |
|---|---|---|
episodeId | string | 폴링에 사용한 에피소드 식별자 |
mode | string | 이 에피소드가 생성된 모드 |
processStatus | string | 아래 참고 |
videoStatus | string | 아래 참고 |
credits | number | 이 에피소드가 지금까지 소모한 크레딧 |
failCode | number | 실패 코드. processStatus가 fail일 때 존재합니다 |
message | string | 현재 상태에 대한 사람이 읽을 수 있는 상세 설명 |
title / cover | string | 생성된 제목과 표지 이미지 |
audioUrl / audioDuration | string / number | 내레이션 오디오와 그 길이(초) |
videoUrl | string | 렌더링된 영상. videoStatus가 success가 된 뒤 제공됩니다 |
pages[] | array | 페이지별 text, pageNumber, imageUrl, audioTimestamp |
크레딧은 미리 견적으로 제시되지 않고 에피소드 단위로 보고됩니다 — 플랫폼이 생성 작업에 어떻게 과금하는지는 크레딧 및 요금을 참고하세요.
processStatus
| 값 | 의미 |
|---|---|
pending | 처리 중 |
success | 완료 |
fail | 실패 — failCode와 message를 확인하고 에러 처리를 참고하세요 |
videoStatus
| 값 | 의미 |
|---|---|
not_generated | 영상 생성이 아직 트리거되지 않음 |
pending | 영상 생성 중 |
success | 영상 준비 완료(videoUrl 사용 가능) |
fail | 영상 생성 실패 |
생성에는 보통 2–5분이 걸립니다. 권장 폴링 방식: 60초 대기 후 10초마다 폴링합니다.
영상 렌더링
POST /v1/storybook/episodes/{episodeId}/video
완료된 에피소드의 영상 생성을 트리거합니다. 먼저 processStatus가 success여야 합니다.
curl -X POST "https://api.marswave.ai/openapi/v1/storybook/episodes/{episodeId}/video" \
-H "Authorization: Bearer $LISTENHUB_API_KEY"응답:
{
"code": 0,
"message": "",
"data": { "success": true }
}트리거한 후에는 videoStatus가 success가 될 때까지 GET /v1/storybook/episodes/{episodeId}를 폴링한 뒤
videoUrl을 읽습니다.