mirror of
https://github.com/marswaveai/skills.git
synced 2026-09-19 08:45:03 +08:00
docs: CLI migration design spec (#19)
This commit is contained in:
@@ -1,13 +1,8 @@
|
||||
<h1 align="center">MarsWave Skills</h1>
|
||||
|
||||
<p align="center">
|
||||
<strong>Explain Anything. In Videos, Podcasts, and More.</strong>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://listenhub.ai"><img alt="ListenHub" src="https://img.shields.io/badge/Made%20by%20ListenHub-000?logo=listenhub&logoColor=fff" /></a>
|
||||
<a href="https://github.com/marswaveai"><img alt="MarsWave" src="https://img.shields.io/badge/Made%20by%20MarsWave-000?logoColor=fff" /></a>
|
||||
<a href="https://discord.gg/ZbwA7g2guU"><img alt="Discord" src="https://img.shields.io/discord/1365293903405645886?label=Discord&logo=discord&color=eee&labelColor=5865f2&logoColor=fff" /></a>
|
||||
<a href="https://x.com/ListenHub"><img alt="Twitter" src="https://img.shields.io/twitter/follow/ListenHub?logo=x" /></a>
|
||||
<a href="https://github.com/marswaveai/skills/blob/main/LICENSE"><img alt="License" src="https://img.shields.io/github/license/marswaveai/skills?color=blue" /></a>
|
||||
<br />
|
||||
English | <a href="./README.zh.md">简体中文</a>
|
||||
@@ -15,7 +10,7 @@ English | <a href="./README.zh.md">简体中文</a>
|
||||
|
||||
---
|
||||
|
||||
You have ideas worth sharing. [ListenHub](https://listenhub.ai) turns them into content people actually want to watch and listen to — no editing skills required.
|
||||
AI-powered skills for your coding agent — by [MarsWave](https://github.com/marswaveai).
|
||||
|
||||
## Install
|
||||
|
||||
@@ -25,13 +20,13 @@ npx skills add marswaveai/skills
|
||||
|
||||
## Update
|
||||
|
||||
**Via npx skills** (recommended for most users):
|
||||
**Via npx skills** (recommended):
|
||||
|
||||
```bash
|
||||
npx skills update -g
|
||||
```
|
||||
|
||||
**Via Git** (for contributors or local development):
|
||||
**Via Git** (for contributors):
|
||||
|
||||
```bash
|
||||
cd path/to/marswaveai/skills
|
||||
@@ -42,45 +37,59 @@ Restart your agent (Claude Code, Cursor, etc.) after updating.
|
||||
|
||||
## Skills
|
||||
|
||||
### ListenHub — Content Creation
|
||||
|
||||
Turn ideas into videos, podcasts, and more. Powered by [ListenHub](https://listenhub.ai).
|
||||
|
||||
| Skill | Trigger | What it does |
|
||||
|-------|---------|-------------|
|
||||
| `/podcast` | "make a podcast", "播客" | Generate podcast episodes (solo, dialogue, debate) |
|
||||
| `/explainer` | "explainer video", "解说视频" | Narrated explainer videos with AI visuals |
|
||||
| `/slides` | "slides", "幻灯片" | Create slide decks with AI visuals |
|
||||
| `/tts` | "read aloud", "TTS", "朗读" | Text-to-speech and voice narration |
|
||||
| `/music` | "music", "音乐" | AI music generation and covers |
|
||||
| `/image-gen` | "generate image", "画一张" | AI image generation from text prompts |
|
||||
| `/content-parser` | "parse this URL", "解析链接" | Extract content from URLs |
|
||||
| `/asr` | "transcribe", "语音转文字", "ASR" | Transcribe audio files to text |
|
||||
| `/creator` | "创作", "写公众号", "小红书", "口播" | Creator workflow — platform-ready content packages |
|
||||
| `/cola-avatar-pack` | "生成形象", "avatar", "表情包", "梗图" | Generate Cola avatar, profile card, emoji GIFs & meme stickers |
|
||||
|
||||
## Supported Inputs
|
||||
**Setup:**
|
||||
|
||||
- Any topic you can describe
|
||||
- YouTube videos
|
||||
- Article URLs
|
||||
- Plain text
|
||||
- Image prompts
|
||||
- Audio files
|
||||
```bash
|
||||
npm install -g @marswave/listenhub-cli
|
||||
listenhub auth login
|
||||
```
|
||||
|
||||
## Setup
|
||||
> `/content-parser` and `/creator` still require a [ListenHub API Key](https://listenhub.ai/settings/api-keys).
|
||||
|
||||
**ListenHub API Key** — [Get yours](https://listenhub.ai/settings/api-keys) (Pro plan required)
|
||||
### COLA
|
||||
|
||||
Keys auto-configure on first use.
|
||||
| Skill | Trigger | What it does |
|
||||
|-------|---------|-------------|
|
||||
| `/cola-avatar-pack` | "生成形象", "avatar", "表情包", "梗图" | Generate pixel-art avatar, profile card, emoji GIFs & meme stickers |
|
||||
|
||||
**Setup:** Requires Python 3.10+ and Pillow. See [cola-avatar-pack/SKILL.md](cola-avatar-pack/SKILL.md).
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
├── shared/ # API reference, auth, common patterns
|
||||
├── shared/ # Shared infrastructure (auth, CLI patterns)
|
||||
│
|
||||
│ # ListenHub
|
||||
├── podcast/ # Podcast generation
|
||||
├── explainer/ # Explainer videos
|
||||
├── tts/ # TTS and voice narration
|
||||
├── slides/ # Slide decks
|
||||
├── tts/ # Text-to-speech
|
||||
├── music/ # AI music generation
|
||||
├── image-gen/ # AI image generation
|
||||
├── content-parser/ # URL content extraction
|
||||
├── asr/ # Audio transcription
|
||||
├── creator/ # Creator workflow (WeChat, Xiaohongshu, narration)
|
||||
├── cola-avatar-pack/ # Cola avatar, profile card, emoji GIFs & meme stickers
|
||||
└── listenhub/ # Deprecated (see DEPRECATED.md)
|
||||
├── creator/ # Creator workflow
|
||||
├── listenhub-cli/ # CLI auth and setup
|
||||
├── listenhub/ # Router skill
|
||||
│
|
||||
│ # COLA
|
||||
└── cola-avatar-pack/ # Avatar pack generation
|
||||
```
|
||||
|
||||
## Supported Clients
|
||||
|
||||
+36
-27
@@ -1,13 +1,8 @@
|
||||
<h1 align="center">MarsWave Skills</h1>
|
||||
|
||||
<p align="center">
|
||||
<strong>解说万物,一键生成视频、播客、语音</strong>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://listenhub.ai"><img alt="ListenHub" src="https://img.shields.io/badge/Made%20by%20ListenHub-000?logo=listenhub&logoColor=fff" /></a>
|
||||
<a href="https://github.com/marswaveai"><img alt="MarsWave" src="https://img.shields.io/badge/Made%20by%20MarsWave-000?logoColor=fff" /></a>
|
||||
<a href="https://discord.gg/ZbwA7g2guU"><img alt="Discord" src="https://img.shields.io/discord/1365293903405645886?label=Discord&logo=discord&color=eee&labelColor=5865f2&logoColor=fff" /></a>
|
||||
<a href="https://x.com/ListenHub"><img alt="Twitter" src="https://img.shields.io/twitter/follow/ListenHub?logo=x" /></a>
|
||||
<a href="https://github.com/marswaveai/skills/blob/main/LICENSE"><img alt="License" src="https://img.shields.io/github/license/marswaveai/skills?color=blue" /></a>
|
||||
<br />
|
||||
<a href="./README.md">English</a> | 简体中文
|
||||
@@ -15,7 +10,7 @@
|
||||
|
||||
---
|
||||
|
||||
你有值得分享的想法。[ListenHub](https://listenhub.ai) 把它们变成人们真正想看、想听的内容——无需剪辑技能。
|
||||
为编程 agent 打造的 AI 技能集——来自 [MarsWave](https://github.com/marswaveai)。
|
||||
|
||||
## 安装
|
||||
|
||||
@@ -25,69 +20,83 @@ npx skills add marswaveai/skills
|
||||
|
||||
## 更新
|
||||
|
||||
**通过 npx skills**(推荐大多数用户使用):
|
||||
**npx skills**(推荐):
|
||||
|
||||
```bash
|
||||
npx skills update -g
|
||||
```
|
||||
|
||||
**通过 Git**(适合贡献者或本地开发):
|
||||
**Git**(适合贡献者):
|
||||
|
||||
```bash
|
||||
cd path/to/marswaveai/skills
|
||||
git pull origin main
|
||||
```
|
||||
|
||||
更新后需重启你的 agent(Claude Code、Cursor 等)。
|
||||
更新后需重启 agent(Claude Code、Cursor 等)。
|
||||
|
||||
## 技能列表
|
||||
|
||||
### ListenHub — 内容创作
|
||||
|
||||
把想法变成视频、播客等多种内容形式。由 [ListenHub](https://listenhub.ai) 驱动。
|
||||
|
||||
| 技能 | 触发词 | 功能 |
|
||||
|------|--------|------|
|
||||
| `/podcast` | "做播客"、"podcast" | 生成播客单集(独白、对话、辩论) |
|
||||
| `/explainer` | "解说视频"、"explainer video" | 带 AI 配图的解说视频 |
|
||||
| `/slides` | "幻灯片"、"slides" | AI 配图的演示文稿 |
|
||||
| `/tts` | "朗读"、"TTS"、"语音合成" | 文字转语音、配音 |
|
||||
| `/music` | "音乐"、"music" | AI 音乐生成、翻唱 |
|
||||
| `/image-gen` | "生成图片"、"画一张" | AI 图片生成 |
|
||||
| `/content-parser` | "解析链接"、"提取内容" | URL 内容提取 |
|
||||
| `/asr` | "转录"、"语音转文字"、"ASR" | 音频文件转文字 |
|
||||
| `/creator` | "创作"、"写公众号"、"小红书"、"口播" | 创作者工作流 — 一键生成平台内容包 |
|
||||
| `/cola-avatar-pack` | "生成形象"、"avatar"、"表情包"、"梗图" | 生成 Cola 专属形象、资料卡、表情 GIF 和梗图贴纸 |
|
||||
| `/asr` | "转录"、"语音转文字"、"ASR" | 音频转文字 |
|
||||
| `/creator` | "创作"、"写公众号"、"小红书"、"口播" | 创作者工作流——一键生成平台内容包 |
|
||||
|
||||
## 支持的输入
|
||||
**配置:**
|
||||
|
||||
- 任何你能描述的主题
|
||||
- YouTube 视频
|
||||
- 文章链接
|
||||
- 纯文本
|
||||
- 图片描述
|
||||
- 音频文件
|
||||
```bash
|
||||
npm install -g @marswave/listenhub-cli
|
||||
listenhub auth login
|
||||
```
|
||||
|
||||
## 配置
|
||||
> `/content-parser` 和 `/creator` 仍需要 [ListenHub API Key](https://listenhub.ai/zh/settings/api-keys)。
|
||||
|
||||
**ListenHub API Key** — [获取](https://listenhub.ai/zh/settings/api-keys)(Pro 订阅)
|
||||
### COLA
|
||||
|
||||
首次使用时自动配置。
|
||||
| 技能 | 触发词 | 功能 |
|
||||
|------|--------|------|
|
||||
| `/cola-avatar-pack` | "生成形象"、"avatar"、"表情包"、"梗图" | 生成像素风专属形象、资料卡、表情 GIF 和梗图贴纸 |
|
||||
|
||||
**配置:** 需要 Python 3.10+ 和 Pillow。详见 [cola-avatar-pack/SKILL.md](cola-avatar-pack/SKILL.md)。
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
├── shared/ # API 参考、认证、通用模式
|
||||
├── shared/ # 公共基础设施(认证、CLI 模式)
|
||||
│
|
||||
│ # ListenHub
|
||||
├── podcast/ # 播客生成
|
||||
├── explainer/ # 解说视频
|
||||
├── slides/ # 演示文稿
|
||||
├── tts/ # 文字转语音
|
||||
├── music/ # AI 音乐生成
|
||||
├── image-gen/ # AI 图片生成
|
||||
├── content-parser/ # URL 内容提取
|
||||
├── asr/ # 音频转文字
|
||||
├── creator/ # 创作者工作流
|
||||
├── cola-avatar-pack/ # Cola 专属形象、资料卡、表情 GIF、梗图贴纸
|
||||
└── listenhub/ # 已弃用(见 DEPRECATED.md)
|
||||
├── listenhub-cli/ # CLI 认证与配置
|
||||
├── listenhub/ # 路由 skill
|
||||
│
|
||||
│ # COLA
|
||||
└── cola-avatar-pack/ # 形象包生成
|
||||
```
|
||||
|
||||
## 支持的客户端
|
||||
|
||||
Claude Code · Cursor · Windsurf · OpenCode · Codex · Trae 等
|
||||
|
||||
如有任何问题,欢迎联系我们:support@marswave.ai
|
||||
如有问题,欢迎联系:support@marswave.ai
|
||||
|
||||
## 许可证
|
||||
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ Run `coli asr --help` for current CLI options and supported flags.
|
||||
|
||||
- No shell scripts. Use direct commands only.
|
||||
- Always read config following `shared/config-pattern.md` before any interaction
|
||||
- Follow `shared/common-patterns.md` for interaction patterns
|
||||
- Follow `shared/cli-patterns.md` for interaction patterns
|
||||
- Never ask more than one question at a time
|
||||
|
||||
<HARD-GATE>
|
||||
|
||||
+225
-9
@@ -31,9 +31,9 @@ Extract and normalize content from URLs across supported platforms. Returns stru
|
||||
|
||||
## Hard Constraints
|
||||
|
||||
- No shell scripts. Construct curl commands from the API reference files listed in Resources
|
||||
- Always read `shared/authentication.md` for API key and headers
|
||||
- Follow `shared/common-patterns.md` for polling, errors, and interaction patterns
|
||||
- No shell scripts. Construct curl commands from the API Reference (Inlined) section below
|
||||
- See § API Reference (Inlined) below for API key and headers
|
||||
- See § API Reference (Inlined) below for polling, errors, and interaction patterns
|
||||
- URL must be a valid HTTP(S) URL
|
||||
- Always read config following `shared/config-pattern.md` before any interaction
|
||||
- Never save files to `~/Downloads/` or `.listenhub/` — save to the current working directory
|
||||
@@ -200,13 +200,229 @@ Wait for explicit confirmation before calling the API.
|
||||
|
||||
**Estimated time**: 10-30 seconds depending on content size and platform.
|
||||
|
||||
## API Reference
|
||||
## API Reference (Inlined)
|
||||
|
||||
- Content extract: `shared/api-content-extract.md`
|
||||
- Supported platforms: `references/supported-platforms.md`
|
||||
- Polling: `shared/common-patterns.md` § Async Polling
|
||||
- Error handling: `shared/common-patterns.md` § Error Handling
|
||||
- Config pattern: `shared/config-pattern.md`
|
||||
### Authentication
|
||||
|
||||
**Environment variable**: `LISTENHUB_API_KEY` (format: `lh_sk_...`)
|
||||
|
||||
Store in `~/.zshrc` (macOS) or `~/.bashrc` (Linux):
|
||||
|
||||
```bash
|
||||
export LISTENHUB_API_KEY="lh_sk_..."
|
||||
```
|
||||
|
||||
**How to obtain**: Visit https://listenhub.ai/settings/api-keys (Pro plan required).
|
||||
|
||||
**Base URL**: `https://api.marswave.ai/openapi/v1`
|
||||
|
||||
**Required headers** (every request):
|
||||
|
||||
```
|
||||
Authorization: Bearer $LISTENHUB_API_KEY
|
||||
Content-Type: application/json
|
||||
X-Source: skills
|
||||
```
|
||||
|
||||
The `X-Source: skills` header identifies requests as coming from Claude Code skills (CLI tool).
|
||||
|
||||
**curl template:**
|
||||
|
||||
```bash
|
||||
curl -sS -X POST "https://api.marswave.ai/openapi/v1/{endpoint}" \
|
||||
-H "Authorization: Bearer $LISTENHUB_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Source: skills" \
|
||||
-d '{ ... }'
|
||||
```
|
||||
|
||||
For GET requests, omit `-d` and change `-X POST` to `-X GET`.
|
||||
|
||||
**Security notes:**
|
||||
- Never log or display full API keys in output
|
||||
- API keys are transmitted via HTTPS only
|
||||
- Do not pass sensitive or confidential information as content input — it is sent to external APIs for processing
|
||||
|
||||
---
|
||||
|
||||
### POST /v1/content/extract
|
||||
|
||||
Create a content extraction task for a URL. Returns a `taskId` for polling.
|
||||
|
||||
**Request body:**
|
||||
|
||||
| Field | Required | Type | Description |
|
||||
|-------|----------|------|-------------|
|
||||
| source | **Yes** | object | Source to extract from |
|
||||
| source.type | **Yes** | string | Must be `"url"` |
|
||||
| source.uri | **Yes** | string | Valid HTTP(S) URL to extract content from |
|
||||
| options | No | object | Extraction options |
|
||||
| options.summarize | No | boolean | Whether to generate a summary |
|
||||
| options.maxLength | No | integer | Maximum content length |
|
||||
| options.twitter | No | object | Twitter/X specific options |
|
||||
| options.twitter.count | No | integer | Number of tweets to fetch (1-100, default 20) |
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"taskId": "69a7dac700cf95938f86d9bb"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Error codes:**
|
||||
|
||||
| Code | Meaning |
|
||||
|------|---------|
|
||||
| 29003 | Validation error (`"source.uri" is required`, `"source.uri" must be a valid uri`) |
|
||||
| 21007 | Invalid API key |
|
||||
|
||||
---
|
||||
|
||||
### GET /v1/content/extract/{taskId}
|
||||
|
||||
Get extraction task status and results.
|
||||
|
||||
**Path params:**
|
||||
|
||||
| Param | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| taskId | string | 24-char hex task ID |
|
||||
|
||||
**Response states:**
|
||||
|
||||
- **processing** — Task is still running
|
||||
- **completed** — Extraction finished, data available
|
||||
- **failed** — Extraction failed, check `failCode` and `message`
|
||||
|
||||
**Response (processing):**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"taskId": "69a7dac700cf95938f86d9bb",
|
||||
"status": "processing",
|
||||
"createdAt": "2025-04-09T12:00:00Z",
|
||||
"data": null,
|
||||
"credits": 0,
|
||||
"failCode": null,
|
||||
"message": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response (completed):**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"taskId": "69a7dac700cf95938f86d9bb",
|
||||
"status": "completed",
|
||||
"createdAt": "2025-04-09T12:00:00Z",
|
||||
"data": {
|
||||
"content": "Extracted text content...",
|
||||
"metadata": {
|
||||
"title": "Article Title",
|
||||
"author": "Author Name",
|
||||
"publishedAt": "2025-04-01T08:00:00Z"
|
||||
},
|
||||
"references": [
|
||||
"https://example.com/related-article"
|
||||
]
|
||||
},
|
||||
"credits": 5,
|
||||
"failCode": null,
|
||||
"message": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response (failed):**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"taskId": "69a7dac700cf95938f86d9bb",
|
||||
"status": "failed",
|
||||
"createdAt": "2025-04-09T12:00:00Z",
|
||||
"data": null,
|
||||
"credits": 0,
|
||||
"failCode": "EXTRACT_FAILED",
|
||||
"message": "Unable to extract content from the provided URL"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key fields:**
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| status | string | `processing`, `completed`, or `failed` |
|
||||
| data.data.content | string | Extracted text content |
|
||||
| data.data.metadata | object | Page metadata (title, author, publishedAt) |
|
||||
| data.data.references | array | Referenced URLs (array of strings) |
|
||||
| credits | integer | Credits consumed |
|
||||
| failCode | string | Error code (null on success) |
|
||||
| message | string | Error message (null on success) |
|
||||
|
||||
**Error codes:**
|
||||
|
||||
| Code | Meaning |
|
||||
|------|---------|
|
||||
| 29003 | Invalid taskId format |
|
||||
| 25002 | Task not found |
|
||||
|
||||
---
|
||||
|
||||
### Polling Pattern
|
||||
|
||||
5-second interval, 60 polls max. Run with `run_in_background: true` and `timeout: 300000`.
|
||||
|
||||
**Two-step pattern:**
|
||||
|
||||
1. **Submit (foreground)**: POST the creation request, extract `taskId` from the response.
|
||||
2. **Poll (background)**: Run the polling loop with `run_in_background: true`. You will be notified automatically when it completes.
|
||||
|
||||
The exact polling bash command is already specified in the Workflow section (Step 5).
|
||||
|
||||
---
|
||||
|
||||
### Error Handling
|
||||
|
||||
**HTTP status codes:**
|
||||
|
||||
| Code | Meaning | Action |
|
||||
|------|---------|--------|
|
||||
| 200 | Success | Parse response body |
|
||||
| 400 | Bad request | Check parameters |
|
||||
| 401 | Invalid API key | Re-check `LISTENHUB_API_KEY` |
|
||||
| 402 | Insufficient credits | Inform user to recharge |
|
||||
| 403 | Forbidden | No permission for this resource |
|
||||
| 429 | Rate limited | Exponential backoff, retry after delay |
|
||||
| 500/502/503/504 | Server error | Retry up to 3 times |
|
||||
|
||||
**Retry strategy:**
|
||||
|
||||
- **429 rate limit**: Wait 15 seconds, then retry (exponential backoff)
|
||||
- **5xx server errors**: Retry up to 3 times with 5-second intervals
|
||||
- **Network errors**: Retry up to 3 times
|
||||
|
||||
**Application error codes:**
|
||||
|
||||
| Code | Meaning |
|
||||
|------|---------|
|
||||
| 21007 | Invalid user API key |
|
||||
| 25429 | Rate limited (application-level) |
|
||||
|
||||
## Example
|
||||
|
||||
|
||||
+16
-36
@@ -31,9 +31,9 @@ Generate platform-specific content packages by orchestrating existing skills. In
|
||||
|
||||
## Hard Constraints
|
||||
|
||||
- No shell scripts. Construct curl commands from the API reference files in `shared/`
|
||||
- Use `listenhub` CLI commands for image-gen and TTS. Use curl for content-parser (see `content-parser/SKILL.md` § API Reference).
|
||||
- Always read config following `shared/config-pattern.md` before any interaction
|
||||
- Follow `shared/common-patterns.md` for polling, errors, and interaction patterns
|
||||
- Follow `shared/cli-patterns.md` for polling, errors, and interaction patterns
|
||||
- Never save files to `~/Downloads/` or `.listenhub/` — save content packages to the current working directory
|
||||
- JSON parsing: use `jq` only (no python3, awk)
|
||||
|
||||
@@ -46,7 +46,7 @@ Use AskUserQuestion for every multiple-choice step. One question at a time. Wait
|
||||
</HARD-GATE>
|
||||
|
||||
<HARD-GATE>
|
||||
API Key Check at Confirmation Gate: If the pipeline includes any remote API call (image-gen, content-parser, tts), check `LISTENHUB_API_KEY` before proceeding. If missing, run interactive setup from `shared/authentication.md`. Pure text-only pipelines (e.g., topic → narration script without TTS) can proceed without an API key.
|
||||
API Key Check at Confirmation Gate: If the pipeline includes any remote API call (image-gen, content-parser, tts), check authentication before proceeding. For CLI-based calls (image-gen, TTS), run `listenhub auth login` if not authenticated. For content-parser calls, configure `LISTENHUB_API_KEY` (see `content-parser/SKILL.md` § Authentication). Pure text-only pipelines (e.g., topic → narration script without TTS) can proceed without authentication.
|
||||
</HARD-GATE>
|
||||
|
||||
## Step -1: API Key Check
|
||||
@@ -208,7 +208,7 @@ Otherwise:
|
||||
- Narration without TTS → no API key needed
|
||||
- Web/article URL input → needs content-parser → requires API key (audio/video URLs use local `coli asr`, no API key needed)
|
||||
|
||||
If API key required and missing: run `shared/authentication.md` interactive setup.
|
||||
If API key required and missing: for CLI-based calls, run `listenhub auth login`. For content-parser calls, configure `LISTENHUB_API_KEY` (see `content-parser/SKILL.md` § Authentication).
|
||||
|
||||
**Show confirmation summary:**
|
||||
|
||||
@@ -250,7 +250,7 @@ RESPONSE=$(curl -sS -X POST "https://api.marswave.ai/openapi/v1/content/extract"
|
||||
TASK_ID=$(echo "$RESPONSE" | jq -r '.data.taskId')
|
||||
```
|
||||
|
||||
Then poll in background. Run this as a **separate Bash call** with `run_in_background: true` and `timeout: 600000` (per `shared/common-patterns.md`). The polling loop itself runs up to 300s (60 polls × 5s); `timeout: 600000` is set higher at the tool level to give the Bash process headroom beyond the poll budget:
|
||||
Then poll in background. Run this as a **separate Bash call** with `run_in_background: true` and `timeout: 600000` (per `shared/cli-patterns.md`). The polling loop itself runs up to 300s (60 polls × 5s); `timeout: 600000` is set higher at the tool level to give the Bash process headroom beyond the poll budget:
|
||||
|
||||
```bash
|
||||
# Run with: run_in_background: true, timeout: 600000
|
||||
@@ -283,17 +283,10 @@ If extraction fails: tell user "URL 解析失败,你可以直接粘贴文字
|
||||
**For image generation** (called by wechat and xiaohongshu templates):
|
||||
|
||||
```bash
|
||||
RESPONSE=$(curl -sS -X POST "https://api.marswave.ai/openapi/v1/images/generation" \
|
||||
-H "Authorization: Bearer $LISTENHUB_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Source: skills" \
|
||||
--max-time 600 \
|
||||
-d '{
|
||||
"provider": "google",
|
||||
"model": "gemini-3-pro-image-preview",
|
||||
"prompt": "<generated prompt>",
|
||||
"imageConfig": {"imageSize": "2K", "aspectRatio": "<ratio>"}
|
||||
}')
|
||||
RESPONSE=$(listenhub image create \
|
||||
--prompt "<generated prompt>" \
|
||||
--aspect-ratio "<ratio>" \
|
||||
--json)
|
||||
|
||||
BASE64_DATA=$(echo "$RESPONSE" | jq -r '.candidates[0].content.parts[0].inlineData.data // .data')
|
||||
# macOS uses -D, Linux uses -d (detect platform)
|
||||
@@ -310,22 +303,9 @@ Generate images **sequentially** (not parallel) to respect rate limits.
|
||||
|
||||
**For TTS** (called by narration template when user wants audio):
|
||||
|
||||
Use `@file` pattern per `shared/common-patterns.md` to handle special chars in script text:
|
||||
|
||||
```bash
|
||||
# Write TTS request to temp file (handles quotes, newlines safely)
|
||||
cat > /tmp/creator-tts-request.json << ENDJSON
|
||||
{"input": $(echo "$SCRIPT_TEXT" | jq -Rs .), "voice": "$SPEAKER_ID"}
|
||||
ENDJSON
|
||||
|
||||
curl -sS -X POST "https://api.marswave.ai/openapi/v1/tts" \
|
||||
-H "Authorization: Bearer $LISTENHUB_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Source: skills" \
|
||||
-d @/tmp/creator-tts-request.json \
|
||||
--output "{slug}-narration/audio.mp3"
|
||||
|
||||
rm /tmp/creator-tts-request.json
|
||||
listenhub tts create --text "$(cat /tmp/lh-content.txt)" --speaker "$SPEAKER_ID" --json \
|
||||
| jq -r '.data' | base64 -D > "{slug}-narration/audio.mp3"
|
||||
```
|
||||
|
||||
### Step 6: Assemble Output
|
||||
@@ -401,13 +381,13 @@ If the user says "重置风格偏好" or "reset style":
|
||||
|
||||
## API Reference
|
||||
|
||||
- Authentication & headers: `shared/authentication.md`
|
||||
- Image generation: `shared/api-image.md`
|
||||
- Content extraction: `shared/api-content-extract.md`
|
||||
- TTS (text-to-speech): `shared/api-tts.md`
|
||||
- Authentication: `shared/cli-authentication.md`
|
||||
- Image generation: CLI: `listenhub image create` (see `shared/cli-patterns.md`)
|
||||
- Content extraction: `content-parser/SKILL.md` § API Reference (Inlined)
|
||||
- TTS (text-to-speech): CLI: `listenhub tts create` (see `shared/cli-patterns.md`)
|
||||
- Speaker selection: `shared/speaker-selection.md`
|
||||
- Config pattern: `shared/config-pattern.md`
|
||||
- Common patterns (polling, errors): `shared/common-patterns.md`
|
||||
- Common patterns (polling, errors): `shared/cli-patterns.md`
|
||||
- Output mode: `shared/output-mode.md`
|
||||
|
||||
## Composability
|
||||
|
||||
@@ -32,20 +32,10 @@ If generating audio:
|
||||
- English: "Mars" (`cozy-man-english`)
|
||||
- On first TTS use, ask the user via AskUserQuestion if they want to choose a different speaker. Save their choice to `preferences.narration.defaultSpeaker` for future runs.
|
||||
|
||||
2. Call TTS API (use `@file` pattern for safe text handling per `shared/common-patterns.md`):
|
||||
2. Call TTS API:
|
||||
```bash
|
||||
cat > /tmp/creator-tts-request.json << ENDJSON
|
||||
{"input": $(echo "$SCRIPT_TEXT" | jq -Rs .), "voice": "$SPEAKER_ID"}
|
||||
ENDJSON
|
||||
|
||||
curl -sS -X POST "https://api.marswave.ai/openapi/v1/tts" \
|
||||
-H "Authorization: Bearer $LISTENHUB_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Source: skills" \
|
||||
-d @/tmp/creator-tts-request.json \
|
||||
--output "{output}/audio.mp3"
|
||||
|
||||
rm /tmp/creator-tts-request.json
|
||||
listenhub tts create --text "$(cat /tmp/lh-content.txt)" --speaker "$SPEAKER_ID" --json \
|
||||
| jq -r '.data' | base64 -D > "{output}/audio.mp3"
|
||||
```
|
||||
|
||||
Note: TTS max input is ~10,000 characters. For longer scripts, this is still well within limits for narration (typically 300-2000 chars).
|
||||
|
||||
@@ -51,7 +51,7 @@ For each planned illustration, call the image generation API:
|
||||
- **Model**: `gemini-3-pro-image-preview`
|
||||
- **Cover**: aspect ratio `3:2`, size `2K`
|
||||
- **Body images**: aspect ratio `3:2` or `16:9`, size `2K`
|
||||
- **Timeout**: `--max-time 600` on curl (per `shared/api-image.md`)
|
||||
- **Timeout**: `--timeout 600` (use `listenhub image create --json`)
|
||||
|
||||
Save images to `{output}/images/cover.jpg`, `{output}/images/section-1.jpg`, etc.
|
||||
|
||||
|
||||
@@ -114,7 +114,7 @@ For each prompt in `prompts.json`:
|
||||
- **Model**: `gemini-3-pro-image-preview`
|
||||
- **Aspect ratio**: `3:4` (portrait, standard Xiaohongshu card)
|
||||
- **Size**: `2K`
|
||||
- **Timeout**: `--max-time 600` on curl (per `shared/api-image.md`)
|
||||
- **Timeout**: `--timeout 600` (use `listenhub image create --json`)
|
||||
|
||||
Save to `{output}/cards/01-cover.jpg`, `{output}/cards/02-page.jpg`, etc.
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,370 +0,0 @@
|
||||
# Creator Workflow Skill — Design Spec
|
||||
|
||||
## Overview
|
||||
|
||||
A template-based creator workflow skill that orchestrates existing skills (content-parser, image-gen, tts, asr) to produce platform-specific content. Users provide a topic, URL, text, or audio/video file, select a platform template, and get a fully assembled content package — automatically, with no mid-process interruptions.
|
||||
|
||||
## Target Platforms
|
||||
|
||||
| Platform | Output | Status |
|
||||
|----------|--------|--------|
|
||||
| WeChat (公众号) | Long-form article with AI-generated illustrations | V1 |
|
||||
| Xiaohongshu (小红书) | Image cards (图文) + long text, both modes | V1 |
|
||||
| Narration (口播) | Spoken script + optional TTS audio | V1 |
|
||||
| PPT | Slide deck | Future |
|
||||
|
||||
## Hard Constraints
|
||||
|
||||
- **No shell scripts**: Construct all API calls with `curl` referencing `shared/api-*.md`
|
||||
- **Always read config first**: Follow `shared/config-pattern.md` Step -1 and Step 0 before any interaction
|
||||
- **Language adaptation** `<HARD-GATE>`: All UI text (questions, confirmations, errors, output summaries) follows the user's input language. Chinese input → Chinese output.
|
||||
- **AskUserQuestion for choices** `<HARD-GATE>`: Use AskUserQuestion for all multiple-choice parameters. One question at a time, wait for answer.
|
||||
- **Confirmation before API calls** `<HARD-GATE>`: After input is understood and template is selected, show a brief summary of what will be generated and wait for user confirmation before executing the pipeline.
|
||||
- **Artifact output**: Save to current working directory, never `~/Downloads/` or `.listenhub/`
|
||||
- **Dedup filenames**: Use `-2`, `-3` pattern if file already exists
|
||||
- **Slug naming**: Follow `shared/config-pattern.md` slug generation rules
|
||||
- **JSON parsing**: Use `jq` only (no python3, awk)
|
||||
- **Polling**: Follow `shared/common-patterns.md` async polling pattern with `run_in_background: true`
|
||||
|
||||
## When NOT to Use
|
||||
|
||||
- User wants a single image without a content workflow → use image-gen directly
|
||||
- User wants a single TTS audio → use tts directly
|
||||
- User wants to transcribe audio → use asr directly
|
||||
- User wants a podcast episode → use podcast directly
|
||||
- User wants to extract content from a URL without further processing → use content-parser directly
|
||||
|
||||
Creator is for **multi-step content production** that combines writing + media generation into a platform-ready package.
|
||||
|
||||
## Architecture: Template Registry (方案 B)
|
||||
|
||||
The skill consists of a lightweight dispatcher (SKILL.md) and self-contained template folders. Adding a new platform = adding a new folder. The dispatcher does not need modification.
|
||||
|
||||
### Directory Structure
|
||||
|
||||
```
|
||||
creator/
|
||||
├── SKILL.md # Dispatcher: input recognition, template selection,
|
||||
│ # pipeline execution, preference evolution
|
||||
├── shared -> ../shared # Reuse existing infrastructure
|
||||
│
|
||||
└── templates/
|
||||
├── wechat/
|
||||
│ ├── template.md # Workflow steps, output format, skill calls
|
||||
│ └── style.md # WeChat writing style guide
|
||||
│
|
||||
├── xiaohongshu/
|
||||
│ ├── template.md # Two sub-modes (cards / long text)
|
||||
│ └── style.md # Xiaohongshu writing style guide
|
||||
│
|
||||
├── narration/
|
||||
│ ├── template.md # Script generation workflow
|
||||
│ └── style.md # Spoken-word style guide
|
||||
│
|
||||
└── ppt/ # Future placeholder (not created in V1)
|
||||
├── template.md
|
||||
└── style.md
|
||||
```
|
||||
|
||||
## Dispatcher Logic (SKILL.md)
|
||||
|
||||
The dispatcher handles four responsibilities: **Input Understanding → Template Matching → Pipeline Execution → Preference Update**.
|
||||
|
||||
### 1. Input Understanding
|
||||
|
||||
| Input Type | Detection | Auto Action |
|
||||
|-----------|-----------|-------------|
|
||||
| URL (web/article) | `http(s)://` prefix | Call content-parser to extract content |
|
||||
| URL (audio/video) | File extension `.mp3/.mp4/.wav`, or known video platforms (youtube.com, bilibili.com, douyin.com) | Call asr to transcribe → text material |
|
||||
| Local file | File path exists on disk | Read file / call asr for audio |
|
||||
| Raw text | Not a URL, not a file path | Use directly as material |
|
||||
| Topic/keywords | Short text, no explicit material | AI writes from scratch |
|
||||
|
||||
### 2. Template Matching
|
||||
|
||||
- If user specifies platform in their prompt (e.g., "写篇公众号"), match directly
|
||||
- Otherwise, present template selection via AskUserQuestion:
|
||||
```
|
||||
你想用哪个创作模板?
|
||||
(1) 公众号长文
|
||||
(2) 小红书图文 + 长文
|
||||
(3) 口播稿
|
||||
```
|
||||
|
||||
### 3. Pipeline Execution
|
||||
|
||||
The dispatcher reads `templates/{name}/template.md` and executes steps sequentially. Each step is one of:
|
||||
|
||||
- **Internal operation**: AI writing (applying style.md + user preferences)
|
||||
- **Skill call**: Call image-gen, tts, content-parser, asr APIs (referencing shared/ docs)
|
||||
- **Output operation**: Write files, assemble content package
|
||||
|
||||
**Image generation calls**: When a template needs multiple images (e.g., WeChat article with 3-5 illustrations), generate them sequentially. If some fail, deliver what succeeded and annotate failures in the output summary.
|
||||
|
||||
After all parameters are collected and input is processed, execution is automatic — no mid-step interruptions. (The confirmation gate happens before execution starts, per Hard Constraints.)
|
||||
|
||||
### 4. Preference Update
|
||||
|
||||
After execution, record output path in history. On next run, compare previous output with user's edits to learn style adjustments. See "Style Evolution System" section below.
|
||||
|
||||
## Template Designs
|
||||
|
||||
### WeChat (公众号)
|
||||
|
||||
**Workflow**:
|
||||
1. Acquire material (parse input → text content)
|
||||
2. Generate article outline (title + section headings)
|
||||
3. Write body section by section (apply style.md)
|
||||
4. Identify illustration positions (one every 300-500 chars, cover image required)
|
||||
5. Call image-gen for each illustration (unified style, respecting user preferences)
|
||||
6. Assemble output
|
||||
|
||||
**Output structure**:
|
||||
```
|
||||
{slug}-wechat/
|
||||
├── article.md # Body text, images referenced via relative paths
|
||||
├── images/
|
||||
│ ├── cover.jpg # Cover image
|
||||
│ ├── section-1.jpg # Section illustrations
|
||||
│ └── section-2.jpg
|
||||
├── .original/
|
||||
│ └── article.md # Snapshot for style learning diff
|
||||
└── meta.json # Title, summary, tags
|
||||
```
|
||||
|
||||
**Style baseline** (style.md): Authoritative tone, structured with clear headings, in-depth analysis, professional yet accessible. Paragraphs 3-4 lines max.
|
||||
|
||||
### Xiaohongshu (小红书)
|
||||
|
||||
**Two sub-modes (default: both; user can choose one via preferences)**:
|
||||
|
||||
**Card mode (图文)**:
|
||||
1. Acquire material
|
||||
2. Distill content into 5-8 key points/pages
|
||||
3. Design each page: core quote + brief explanation + visual theme description
|
||||
4. Call image-gen to generate integrated text-on-image cards for each page
|
||||
- **Note**: AI image generation models may render Chinese text imperfectly. Generated cards may need manual text adjustment. Prompts should keep on-card text short (under 10 characters per line) for best results.
|
||||
5. Generate cover card (attention-grabbing title)
|
||||
|
||||
**Long text mode**:
|
||||
1. Rewrite same material in Xiaohongshu long-text style
|
||||
2. Add title, tags, emoji accents
|
||||
|
||||
**Output structure**:
|
||||
```
|
||||
{slug}-xiaohongshu/
|
||||
├── cards/
|
||||
│ ├── 01-cover.jpg # Cover card
|
||||
│ ├── 02-page.jpg # Content card 1
|
||||
│ ├── 03-page.jpg # ...
|
||||
│ └── prompts.json # Prompt record per card (for regeneration)
|
||||
├── long-text.md # Long text version
|
||||
├── .original/
|
||||
│ └── long-text.md # Snapshot for style learning diff
|
||||
└── meta.json # Title, tags, topics
|
||||
```
|
||||
|
||||
**Style baseline** (style.md): Light, punchy, trendy. Short sentences, strategic emoji, numbers in titles, hook-first structure. Cards: bold typography, clean layout, one core message per page.
|
||||
|
||||
### Narration (口播)
|
||||
|
||||
**Workflow**:
|
||||
1. Acquire material
|
||||
2. Generate script outline
|
||||
3. Write spoken script (apply style.md: conversational, rhythmic, with pause markers)
|
||||
4. (Optional) Call tts to generate voiceover audio
|
||||
|
||||
**Output structure**:
|
||||
```
|
||||
{slug}-narration/
|
||||
├── script.md # Spoken script
|
||||
├── audio.mp3 # Voiceover (if generated)
|
||||
├── .original/
|
||||
│ └── script.md # Snapshot for style learning diff
|
||||
└── meta.json
|
||||
```
|
||||
|
||||
**Style baseline** (style.md): Conversational, rhythmic flow, natural pauses (marked with `...` or `——`), emotional words, direct address ("你知道吗"), avoid written-language structures.
|
||||
|
||||
## Skill Orchestration
|
||||
|
||||
### Dependency Detection & API Key Strategy
|
||||
|
||||
**API Key requirement depends on whether the pipeline calls remote APIs**:
|
||||
|
||||
| Scenario | Needs `LISTENHUB_API_KEY`? | Reason |
|
||||
|----------|--------------------------|--------|
|
||||
| Topic → narration script (text only) | No | Pure AI writing, no API calls |
|
||||
| Topic → WeChat article with images | **Yes** | image-gen requires API |
|
||||
| URL → any template | **Yes** | content-parser requires API |
|
||||
| Audio/video → any template | No (ASR is local) | But if template then needs images/TTS, yes |
|
||||
|
||||
**Rule**: Check at the confirmation gate (before pipeline execution) whether any step in the selected template's pipeline requires `LISTENHUB_API_KEY`. If yes and the key is missing, run the interactive setup from `shared/authentication.md` — do NOT skip and do NOT proceed without it. Image generation and content extraction are core to the output quality and should not be silently skipped.
|
||||
|
||||
**ASR is the exception**: It uses the local `coli` CLI tool (see `asr/SKILL.md`), not a remote API. If `coli` is unavailable, suggest installation and skip the transcription step.
|
||||
|
||||
### Audio/Video Input Handling
|
||||
|
||||
When the input is an audio/video URL (not a local file):
|
||||
|
||||
1. Download the file to `/tmp/creator-{slug}.{ext}` using `curl -L -o`
|
||||
2. Call `coli` to transcribe the downloaded file
|
||||
3. Delete the downloaded file after transcription: `rm /tmp/creator-{slug}.{ext}`
|
||||
4. Use the transcript as text material for the selected template
|
||||
|
||||
For local audio/video files, call `coli` directly on the file path (no download/cleanup needed).
|
||||
|
||||
### Call Method
|
||||
|
||||
Creator does NOT nest-trigger other skills. It directly calls their APIs using `curl`, referencing `shared/api-*.md` documentation. The template.md files specify which API endpoints to call and how to use the results.
|
||||
|
||||
### Input → Skill Routing
|
||||
|
||||
| Input Feature | Skill Called | Purpose |
|
||||
|--------------|-------------|---------|
|
||||
| `http(s)://` non-audio/video | content-parser | Extract web content |
|
||||
| `.mp3/.mp4/.wav` URL or local file | asr | Transcribe to text |
|
||||
| Template needs illustrations | image-gen | Generate AI images |
|
||||
| Narration template with voiceover | tts | Text to speech |
|
||||
| Xiaohongshu card mode | image-gen | Generate text-on-image cards |
|
||||
|
||||
## Style Evolution System
|
||||
|
||||
### Storage
|
||||
|
||||
Preferences are stored alongside config in `.listenhub/creator/config.json` under a `preferences` key, following the single-config-file convention from `shared/config-pattern.md`.
|
||||
|
||||
## Config Schema
|
||||
|
||||
### Step -1: API Key Check
|
||||
|
||||
Deferred until the confirmation gate. If the selected template's pipeline includes any remote API call (image-gen, content-parser, tts), check `LISTENHUB_API_KEY` at that point. If missing, run interactive setup from `shared/authentication.md`. Pure text-only pipelines (e.g., topic → narration script without TTS) can proceed without an API key.
|
||||
|
||||
### Step 0: Config Setup (Zero-Question Boot)
|
||||
|
||||
On first run, silently create `.listenhub/creator/config.json` with defaults:
|
||||
|
||||
```json
|
||||
{
|
||||
"outputMode": "download",
|
||||
"language": null,
|
||||
"preferences": {
|
||||
"wechat": { "styleNotes": [], "history": [] },
|
||||
"xiaohongshu": { "styleNotes": [], "mode": "both", "history": [] },
|
||||
"narration": { "styleNotes": [], "defaultSpeaker": null, "history": [] }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Follows `shared/config-pattern.md` conventions.
|
||||
|
||||
### Full Config Schema
|
||||
|
||||
```json
|
||||
{
|
||||
"outputMode": "download",
|
||||
"language": null,
|
||||
"preferences": {
|
||||
"wechat": {
|
||||
"styleNotes": [
|
||||
"段落保持在3-4行以内",
|
||||
"配图偏好:科技感、扁平插画"
|
||||
],
|
||||
"history": [
|
||||
{
|
||||
"date": "2026-03-22",
|
||||
"output": "ai-future-wechat/",
|
||||
"topic": "AI的未来"
|
||||
}
|
||||
]
|
||||
},
|
||||
"xiaohongshu": {
|
||||
"styleNotes": ["标题必须带数字", "少用emoji"],
|
||||
"mode": "both",
|
||||
"cardStyle": "简约白底",
|
||||
"history": []
|
||||
},
|
||||
"narration": {
|
||||
"styleNotes": [],
|
||||
"defaultSpeaker": null,
|
||||
"history": []
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `mode` (xiaohongshu only): `"both"` | `"cards"` | `"long-text"` — which sub-mode to generate
|
||||
- `styleNotes`: Max 10 entries per platform, newest replaces oldest
|
||||
- `defaultSpeaker` (narration only): Speaker ID for TTS (e.g., `CN-Man-Beijing-V2`), `null` uses built-in defaults
|
||||
- `history`: Last 5 generation records for automatic learning
|
||||
|
||||
### Evolution Mechanisms
|
||||
|
||||
**Automatic learning**:
|
||||
- Each generation saves a snapshot of the main output file alongside the output. For example, `{slug}-wechat/.original/article.md` is an exact copy of the generated `article.md` before any user edits.
|
||||
- Each generation records output folder path in `config.json` → `preferences.{platform}.history` (relative to the CWD where creator was invoked)
|
||||
- **CWD dependency**: Style learning only works when the user runs creator from the same working directory as the previous generation. If the previous output folder is not found at the recorded relative path, style learning is silently skipped.
|
||||
- On next run, if `.original/article.md` exists and differs from the current `article.md`, compute the diff
|
||||
- Present the diff to the AI with the prompt: "The user edited the generated content. What style preferences can you infer? Express each as a short directive (e.g., '减少 emoji 使用', '段落更短')."
|
||||
- Append inferred notes to `styleNotes`; array keeps latest 10 entries, newer replaces older
|
||||
- If the file was not modified (or `.original/` doesn't exist), skip learning
|
||||
|
||||
**Manual tuning**:
|
||||
- User can say "记住:我的公众号不要用问句结尾" → append to platform's styleNotes
|
||||
- User can say "重置风格偏好" → clear styleNotes for that platform
|
||||
|
||||
**Style application order**:
|
||||
1. `style.md` — platform baseline style (shared across all users)
|
||||
2. `config.json` → `preferences.{platform}.styleNotes` — user's personal adjustments (additive)
|
||||
|
||||
## Trigger & Interaction
|
||||
|
||||
### SKILL.md Frontmatter
|
||||
|
||||
```yaml
|
||||
name: creator
|
||||
description: 创作者工作流 - 一键生成公众号/小红书/口播等平台内容
|
||||
metadata:
|
||||
openclaw:
|
||||
emoji: "✍️"
|
||||
# No hard env requirement — API key is checked at confirmation gate
|
||||
# only when the pipeline includes remote API calls (image-gen, content-parser, tts)
|
||||
```
|
||||
|
||||
**Trigger keywords**: `"创作"`, `"写公众号"`, `"小红书"`, `"口播"`, `"creator"`, `"content workflow"`, `"帮我写一篇"`, `"生成内容"`
|
||||
|
||||
### Interaction Flow Example
|
||||
|
||||
```
|
||||
User: 帮我根据这个链接写篇公众号 https://example.com/article
|
||||
|
||||
Creator:
|
||||
1. Identify: URL input + WeChat template specified ✓
|
||||
2. [Silent] Read config + preferences
|
||||
3. [Silent] Call content-parser to parse URL → text material
|
||||
4. [Silent] Read wechat/template.md + wechat/style.md + user preferences
|
||||
5. [Silent] Generate outline → write body
|
||||
6. [Silent] Identify illustration positions → call image-gen for 3-5 images
|
||||
7. Output:
|
||||
✅ 文章已生成!保存在 ai-developments-wechat/
|
||||
|
||||
📄 article.md — 正文(约 2000 字)
|
||||
🖼️ images/ — 4 张配图(含封面)
|
||||
📋 meta.json — 标题、摘要、标签
|
||||
|
||||
你可以编辑 article.md,下次我会学习你的修改偏好。
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
- content-parser fails → "URL 解析失败,你可以直接粘贴文字内容给我"
|
||||
- image-gen 429 → Exponential backoff (15s → 30s → 60s), retry up to 3 times; if still fails, deliver article without that image, annotate which images were not generated
|
||||
- API key missing at confirmation gate → Run interactive setup, do not proceed without it
|
||||
- `coli` unavailable → Skip transcription step, ask user to paste text instead or install coli
|
||||
- tts fails → Deliver script without audio, note that TTS generation failed
|
||||
|
||||
## Scope Boundaries
|
||||
|
||||
**V1 includes**: WeChat, Xiaohongshu (cards + long text), Narration templates. Style evolution. Skill orchestration with content-parser, image-gen, tts, asr.
|
||||
|
||||
**V1 excludes**: PPT template (future). Custom user-defined templates. Publishing/posting to platforms. Analytics or A/B testing.
|
||||
+84
-90
@@ -8,8 +8,8 @@ metadata:
|
||||
openclaw:
|
||||
emoji: "🎬"
|
||||
requires:
|
||||
env: ["LISTENHUB_API_KEY"]
|
||||
primaryEnv: "LISTENHUB_API_KEY"
|
||||
bin: ["listenhub"]
|
||||
primaryBin: "listenhub"
|
||||
---
|
||||
|
||||
## When to Use
|
||||
@@ -32,23 +32,22 @@ Generate explainer videos that combine a single narrator's voiceover with AI-gen
|
||||
|
||||
## Hard Constraints
|
||||
|
||||
- No shell scripts. Construct curl commands from the API reference files listed in Resources
|
||||
- Always read `shared/authentication.md` for API key and headers
|
||||
- Follow `shared/common-patterns.md` for polling, errors, and interaction patterns
|
||||
- Always read config following `shared/config-pattern.md` before any interaction
|
||||
- Never hardcode speaker IDs — always fetch from the speakers API
|
||||
- Follow `shared/cli-patterns.md` for execution modes, error handling, and interaction patterns
|
||||
- Always follow `shared/cli-authentication.md` for auth checks
|
||||
- Never hardcode speaker IDs — always fetch from the speakers CLI when the user wants to change voice
|
||||
- Never save files to `~/Downloads/` or `.listenhub/` — save artifacts to the current working directory with friendly topic-based names (see `shared/config-pattern.md` § Artifact Naming)
|
||||
- Explainer uses exactly 1 speaker
|
||||
- Mode must be `info` (for Info style) or `story` (for Story style) — never `slides` (use `/slides` skill instead)
|
||||
|
||||
<HARD-GATE>
|
||||
Use the AskUserQuestion tool for every multiple-choice step — do NOT print options as plain text. Ask one question at a time. Wait for the user's answer before proceeding to the next step. After all parameters are collected, summarize the choices and ask the user to confirm. Do NOT call any generation API until the user has explicitly confirmed.
|
||||
Use the AskUserQuestion tool for every multiple-choice step — do NOT print options as plain text. Ask one question at a time. Wait for the user's answer before proceeding to the next step. After all parameters are collected, summarize the choices and ask the user to confirm. Do NOT call any CLI command until the user has explicitly confirmed.
|
||||
|
||||
</HARD-GATE>
|
||||
|
||||
## Step -1: API Key Check
|
||||
## Step -1: CLI Auth Check
|
||||
|
||||
Follow `shared/config-pattern.md` § API Key Check. If the key is missing, stop immediately.
|
||||
Follow `shared/config-pattern.md` § CLI Auth Check. If the CLI is not installed or the user is not logged in, auto-install and auto-login per `shared/cli-authentication.md` — never ask the user to run commands manually.
|
||||
|
||||
## Step 0: Config Setup
|
||||
|
||||
@@ -122,6 +121,7 @@ Question: "What language?"
|
||||
Options:
|
||||
- "Chinese (zh)" — Content in Mandarin Chinese
|
||||
- "English (en)" — Content in English
|
||||
- "Japanese (ja)" — Content in Japanese
|
||||
```
|
||||
|
||||
### Step 3: Style
|
||||
@@ -144,6 +144,8 @@ Follow `shared/speaker-selection.md`:
|
||||
- Show the speaker in the confirmation summary (Step 6) — user can change from there if desired
|
||||
- Only show the full speaker list if the user explicitly asks to change voice
|
||||
|
||||
Speaker query: see `shared/cli-speakers.md` for listing and filtering speakers.
|
||||
|
||||
Only 1 speaker is supported for explainer videos.
|
||||
|
||||
### Step 5: Output Type
|
||||
@@ -171,34 +173,46 @@ Ready to generate explainer:
|
||||
Proceed?
|
||||
```
|
||||
|
||||
Wait for explicit confirmation before calling any API.
|
||||
Wait for explicit confirmation before running any CLI command.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Submit (foreground)**: `POST /storybook/episodes` with content, speaker, language, mode → extract `episodeId`
|
||||
2. Tell the user the task is submitted
|
||||
3. **Poll (background)**: Run the following **exact** bash command with `run_in_background: true` and `timeout: 600000`. Do NOT use python3, awk, or any other JSON parser — use `jq` as shown:
|
||||
Run the CLI command with `run_in_background: true` and `timeout: 660000`. The CLI blocks until generation completes and returns the final result as JSON:
|
||||
|
||||
```bash
|
||||
listenhub explainer create \
|
||||
--query "{topic}" \
|
||||
--mode {info|story} \
|
||||
--lang {en|zh|ja} \
|
||||
--speaker "{name}" \
|
||||
--speaker-id "{id}" \
|
||||
--timeout 600 \
|
||||
--json
|
||||
```
|
||||
|
||||
If the command fails (non-zero exit), check stderr for error details. See `shared/cli-patterns.md` § Error Handling for exit codes and common errors.
|
||||
|
||||
**Optional flags** (add when applicable):
|
||||
- `--source-url "{url}"` — if the user provided a reference URL
|
||||
- `--skip-audio` — if text-only output (no video)
|
||||
- `--image-size {2K|4K}` — image resolution (default: 2K)
|
||||
- `--aspect-ratio {16:9|9:16|1:1}` — video aspect ratio (default: 16:9)
|
||||
- `--style "{style}"` — visual style for AI-generated images
|
||||
|
||||
Tell the user the task is submitted. When notified of completion, **parse and present result**:
|
||||
|
||||
Parse the CLI JSON output for key fields:
|
||||
```bash
|
||||
EPISODE_ID="<id-from-step-1>"
|
||||
for i in $(seq 1 30); do
|
||||
RESULT=$(curl -sS "https://api.marswave.ai/openapi/v1/storybook/episodes/$EPISODE_ID" \
|
||||
-H "Authorization: Bearer $LISTENHUB_API_KEY" \
|
||||
-H "X-Source: skills" 2>/dev/null)
|
||||
STATUS=$(echo "$RESULT" | tr -d '\000-\037\177' | jq -r '.data.processStatus // "pending"')
|
||||
case "$STATUS" in
|
||||
success|completed) echo "$RESULT"; exit 0 ;;
|
||||
failed|error) echo "FAILED: $RESULT" >&2; exit 1 ;;
|
||||
*) sleep 10 ;;
|
||||
esac
|
||||
done
|
||||
echo "TIMEOUT" >&2; exit 2
|
||||
EPISODE_ID=$(echo "$RESULT" | jq -r '.episodeId')
|
||||
AUDIO_URL=$(echo "$RESULT" | jq -r '.audioUrl // empty')
|
||||
VIDEO_URL=$(echo "$RESULT" | jq -r '.videoUrl // empty')
|
||||
CREDITS=$(echo "$RESULT" | jq -r '.credits // empty')
|
||||
```
|
||||
|
||||
4. When notified, **download and present script**:
|
||||
|
||||
Read `OUTPUT_MODE` from config. Follow `shared/output-mode.md` for behavior.
|
||||
|
||||
**If text-only output**:
|
||||
|
||||
**`inline` or `both`**: Present the script inline.
|
||||
|
||||
Present:
|
||||
@@ -211,56 +225,36 @@ Wait for explicit confirmation before calling any API.
|
||||
```
|
||||
|
||||
**`download` or `both`**: Also save the script file. Generate a topic slug following `shared/config-pattern.md` § Artifact Naming.
|
||||
- If text-only output: save as `{slug}-explainer.md` in cwd (dedup if exists)
|
||||
- If text+video output: create `{slug}-explainer/` folder (dedup if exists), write `script.md` inside
|
||||
- Save as `{slug}-explainer.md` in cwd (dedup if exists)
|
||||
- Present the save path in addition to the above summary.
|
||||
|
||||
5. **If video requested**: `POST /storybook/episodes/{episodeId}/video` (foreground) → **poll again (background)** using the **exact** bash command below with `run_in_background: true` and `timeout: 600000`. Poll for `videoStatus`, not `processStatus`:
|
||||
**If text + video output**:
|
||||
|
||||
```bash
|
||||
EPISODE_ID="<id-from-step-1>"
|
||||
for i in $(seq 1 30); do
|
||||
RESULT=$(curl -sS "https://api.marswave.ai/openapi/v1/storybook/episodes/$EPISODE_ID" \
|
||||
-H "Authorization: Bearer $LISTENHUB_API_KEY" \
|
||||
-H "X-Source: skills" 2>/dev/null)
|
||||
STATUS=$(echo "$RESULT" | tr -d '\000-\037\177' | jq -r '.data.videoStatus // "pending"')
|
||||
case "$STATUS" in
|
||||
success|completed) echo "$RESULT"; exit 0 ;;
|
||||
failed|error) echo "FAILED: $RESULT" >&2; exit 1 ;;
|
||||
*) sleep 10 ;;
|
||||
esac
|
||||
done
|
||||
echo "TIMEOUT" >&2; exit 2
|
||||
**`inline` or `both`**: Display video URL and audio URL as clickable links.
|
||||
|
||||
Present:
|
||||
```
|
||||
6. When notified, **download and present result**:
|
||||
解说视频已生成!
|
||||
|
||||
**Present result**
|
||||
视频链接:{videoUrl}
|
||||
音频链接:{audioUrl}
|
||||
消耗积分:{credits}
|
||||
```
|
||||
|
||||
Read `OUTPUT_MODE` from config. Follow `shared/output-mode.md` for behavior.
|
||||
|
||||
**`inline` or `both`**: Display video URL and audio URL as clickable links.
|
||||
|
||||
Present:
|
||||
```
|
||||
解说视频已生成!
|
||||
|
||||
视频链接:{videoUrl}
|
||||
音频链接:{audioUrl}
|
||||
时长:{duration}s
|
||||
消耗积分:{credits}
|
||||
```
|
||||
|
||||
**`download` or `both`**: Also download the audio file into the `{slug}-explainer/` folder.
|
||||
```bash
|
||||
curl -sS -o "{slug}-explainer/audio.mp3" "{audioUrl}"
|
||||
```
|
||||
Present:
|
||||
```
|
||||
已保存到当前目录:
|
||||
{slug}-explainer/
|
||||
script.md
|
||||
audio.mp3
|
||||
```
|
||||
**`download` or `both`**: Also save files. Generate a topic slug following `shared/config-pattern.md` § Artifact Naming.
|
||||
- Create `{slug}-explainer/` folder (dedup if exists)
|
||||
- Write `script.md` inside
|
||||
- Download audio:
|
||||
```bash
|
||||
listenhub download "{audioUrl}" -o "{slug}-explainer/audio.mp3"
|
||||
```
|
||||
- Present:
|
||||
```
|
||||
已保存到当前目录:
|
||||
{slug}-explainer/
|
||||
script.md
|
||||
audio.mp3
|
||||
```
|
||||
|
||||
### After Successful Generation
|
||||
|
||||
@@ -277,19 +271,20 @@ echo "$NEW_CONFIG" > "$CONFIG_PATH"
|
||||
|
||||
**Estimated times**:
|
||||
- Text script only: 2-3 minutes
|
||||
- Text + Video: 3-5 minutes
|
||||
- Text + Video: 5-10 minutes
|
||||
|
||||
## API Reference
|
||||
## Resources
|
||||
|
||||
- Speaker list: `shared/api-speakers.md`
|
||||
- CLI authentication: `shared/cli-authentication.md`
|
||||
- CLI patterns: `shared/cli-patterns.md`
|
||||
- Speaker query: `shared/cli-speakers.md`
|
||||
- Speaker selection guide: `shared/speaker-selection.md`
|
||||
- Episode creation: `shared/api-storybook.md`
|
||||
- Polling: `shared/common-patterns.md` § Async Polling
|
||||
- Config pattern: `shared/config-pattern.md`
|
||||
- Output mode: `shared/output-mode.md`
|
||||
|
||||
## Composability
|
||||
|
||||
- **Invokes**: speakers API (for speaker selection); may invoke `/speech` for voiceover
|
||||
- **Invokes**: speakers CLI (for speaker selection); may invoke `/speech` for voiceover
|
||||
- **Invoked by**: content-planner (Phase 3)
|
||||
|
||||
## Example
|
||||
@@ -300,20 +295,19 @@ echo "$NEW_CONFIG" > "$CONFIG_PATH"
|
||||
1. Topic: "Claude Code introduction"
|
||||
2. Ask language → "English"
|
||||
3. Ask style → "Info"
|
||||
4. Fetch speakers, user picks "cozy-man-english"
|
||||
4. Use default speaker "Mars" (cozy-man-english)
|
||||
5. Ask output → "Text + Video"
|
||||
|
||||
```bash
|
||||
curl -sS -X POST "https://api.marswave.ai/openapi/v1/storybook/episodes" \
|
||||
-H "Authorization: Bearer $LISTENHUB_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Source: skills" \
|
||||
-d '{
|
||||
"sources": [{"type": "text", "content": "Introduce Claude Code: what it is, key features, and how to get started"}],
|
||||
"speakers": [{"speakerId": "cozy-man-english"}],
|
||||
"language": "en",
|
||||
"mode": "info"
|
||||
}'
|
||||
# Run with run_in_background: true, timeout: 660000
|
||||
listenhub explainer create \
|
||||
--query "Introduce Claude Code: what it is, key features, and how to get started" \
|
||||
--mode info \
|
||||
--lang en \
|
||||
--speaker "Mars" \
|
||||
--speaker-id "cozy-man-english" \
|
||||
--timeout 600 \
|
||||
--json
|
||||
```
|
||||
|
||||
Poll until text is ready, then generate video if requested.
|
||||
Parse result for `episodeId`, `audioUrl`, `videoUrl`, `credits`, and present to user.
|
||||
|
||||
+99
-118
@@ -8,8 +8,8 @@ metadata:
|
||||
openclaw:
|
||||
emoji: "🖼️"
|
||||
requires:
|
||||
env: ["LISTENHUB_API_KEY"]
|
||||
primaryEnv: "LISTENHUB_API_KEY"
|
||||
bin: ["listenhub"]
|
||||
primaryBin: "listenhub"
|
||||
---
|
||||
|
||||
## When to Use
|
||||
@@ -28,24 +28,22 @@ metadata:
|
||||
|
||||
## Purpose
|
||||
|
||||
Generate AI images using the Labnana API. Supports text prompts with optional reference images, multiple resolutions, and aspect ratios. Images are saved as local files.
|
||||
Generate AI images using the ListenHub CLI. Supports text prompts with optional reference images (local files or URLs), multiple resolutions, and aspect ratios. Images are saved as local files.
|
||||
|
||||
## Hard Constraints
|
||||
|
||||
- No shell scripts. Construct curl commands from the API reference files listed in Resources
|
||||
- Always read `shared/authentication.md` for API key and headers
|
||||
- Follow `shared/common-patterns.md` for error handling
|
||||
- Image generation uses a **different base URL**: `https://api.marswave.ai/openapi/v1`
|
||||
- Always check CLI auth following `shared/cli-authentication.md`
|
||||
- Follow `shared/cli-patterns.md` for command execution and error handling
|
||||
- Always read config following `shared/config-pattern.md` before any interaction
|
||||
- Output saved to `.listenhub/image-gen/YYYY-MM-DD-{jobId}/` — never `~/Downloads/`
|
||||
|
||||
<HARD-GATE>
|
||||
Use the AskUserQuestion tool for every multiple-choice step — do NOT print options as plain text. Ask one question at a time. Wait for the user's answer before proceeding to the next step. After all parameters are collected, summarize the choices and ask the user to confirm. Do NOT call the image generation API until the user has explicitly confirmed.
|
||||
Use the AskUserQuestion tool for every multiple-choice step — do NOT print options as plain text. Ask one question at a time. Wait for the user's answer before proceeding to the next step. After all parameters are collected, summarize the choices and ask the user to confirm. Do NOT call the image generation command until the user has explicitly confirmed.
|
||||
</HARD-GATE>
|
||||
|
||||
## Step -1: API Key Check
|
||||
## Step -1: CLI Auth Check
|
||||
|
||||
Follow `shared/config-pattern.md` § API Key Check. If the key is missing, stop immediately.
|
||||
Follow `shared/cli-authentication.md` § Auth Check. If CLI is not installed or not logged in, auto-install and auto-login — never ask the user to run commands manually.
|
||||
|
||||
## Step 0: Config Setup
|
||||
|
||||
@@ -135,29 +133,17 @@ If flash model was selected, also offer: `1:4` (narrow portrait), `4:1` (wide la
|
||||
```
|
||||
Question: "Any reference images for style guidance?"
|
||||
Options:
|
||||
- "Yes, I have URL(s)" — Provide reference image URLs
|
||||
- "Yes, I have local file(s)" — Provide local file paths (base64 mode)
|
||||
- "Yes" — Provide file paths or URLs
|
||||
- "No references" — Generate from prompt only
|
||||
```
|
||||
|
||||
**If URL mode**: Collect URLs (comma-separated, max 14). For each URL, infer mimeType from suffix and build:
|
||||
```json
|
||||
{ "fileData": { "fileUri": "<url>", "mimeType": "<inferred>" } }
|
||||
```
|
||||
Suffix mapping: `.jpg`/`.jpeg` → `image/jpeg`, `.png` → `image/png`, `.webp` → `image/webp`, `.gif` → `image/gif`
|
||||
**If yes**: Collect reference image paths or URLs (comma-separated). The CLI handles both local files and URLs natively — no need to distinguish between them.
|
||||
|
||||
**If local file (base64) mode**: Collect file paths (comma-separated, max 14). For each file, encode to base64 and infer mimeType from suffix:
|
||||
```bash
|
||||
# macOS
|
||||
BASE64_REF=$(base64 -i /path/to/image.png)
|
||||
# Linux
|
||||
BASE64_REF=$(base64 -w 0 /path/to/image.png)
|
||||
```
|
||||
Build:
|
||||
```json
|
||||
{ "inlineData": { "data": "<base64-encoded>", "mimeType": "<inferred>" } }
|
||||
```
|
||||
Suffix mapping: `.jpg`/`.jpeg` → `image/jpeg`, `.png` → `image/png`, `.webp` → `image/webp`, `.heic` → `image/heic`, `.heif` → `image/heif`
|
||||
- Max 5 references
|
||||
- Supported formats: jpg, png, webp, gif
|
||||
- Max 10MB per file
|
||||
|
||||
Each reference will be passed as a `--reference` flag to the CLI.
|
||||
|
||||
### Step 5: Confirm & Generate
|
||||
|
||||
@@ -170,67 +156,83 @@ Ready to generate image:
|
||||
Model: {pro / flash}
|
||||
Resolution: {1K / 2K / 4K}
|
||||
Aspect ratio: {ratio}
|
||||
References: {yes — N URL(s) / yes — N local file(s) / no}
|
||||
References: {yes — N image(s) / no}
|
||||
|
||||
Proceed?
|
||||
```
|
||||
|
||||
Wait for explicit confirmation before calling the API.
|
||||
Wait for explicit confirmation before running the CLI command.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Build request**: Construct JSON with provider, model, prompt, imageConfig, and optional referenceImages (URL-based via `fileData` or base64 via `inlineData`)
|
||||
2. **Encode local files** (if base64 mode): For each local file path, encode to base64 and build `inlineData` objects
|
||||
3. **Submit**: `POST https://api.marswave.ai/openapi/v1/images/generation` with timeout of 600s
|
||||
4. **Extract image**: Parse base64 data from response
|
||||
5. **Decode and present result**
|
||||
1. **Build CLI command**: Construct the `listenhub image create` command with all collected parameters.
|
||||
|
||||
Read `OUTPUT_MODE` from config. Follow `shared/output-mode.md` for behavior.
|
||||
2. **Execute**: Run the command with `run_in_background: true` and `timeout: 180000`:
|
||||
|
||||
**`inline` or `both`**: Decode base64 to a temp file, then use the Read tool.
|
||||
```bash
|
||||
listenhub image create \
|
||||
--prompt "{description}" \
|
||||
--model "{model}" \
|
||||
--lang "{lang}" \
|
||||
--aspect-ratio {16:9|9:16|1:1} \
|
||||
--size {1K|2K|4K} \
|
||||
--json
|
||||
```
|
||||
|
||||
```bash
|
||||
JOB_ID=$(date +%s)
|
||||
echo "$BASE64_DATA" | base64 -D > /tmp/image-gen-${JOB_ID}.jpg
|
||||
```
|
||||
Then use the Read tool on `/tmp/image-gen-{jobId}.jpg`. The image displays inline in the conversation.
|
||||
If reference images were provided, add `--reference` for each:
|
||||
```bash
|
||||
listenhub image create \
|
||||
--prompt "{description}" \
|
||||
--model "{model}" \
|
||||
--lang "{lang}" \
|
||||
--aspect-ratio 16:9 \
|
||||
--size 2K \
|
||||
--reference ./sketch.png \
|
||||
--reference ./photo.jpg \
|
||||
--json
|
||||
```
|
||||
|
||||
Present:
|
||||
```
|
||||
图片已生成!
|
||||
```
|
||||
The `--lang` flag provides a language hint for the prompt. Detect from the user's prompt language (e.g., Chinese prompt → `zh`, English prompt → `en`).
|
||||
|
||||
**`download` or `both`**: Save to the artifact directory.
|
||||
3. **Parse result and present**
|
||||
|
||||
```bash
|
||||
JOB_ID=$(date +%s)
|
||||
DATE=$(date +%Y-%m-%d)
|
||||
JOB_DIR=".listenhub/image-gen/${DATE}-${JOB_ID}"
|
||||
mkdir -p "$JOB_DIR"
|
||||
echo "$BASE64_DATA" | base64 -D > "${JOB_DIR}/${JOB_ID}.jpg"
|
||||
```
|
||||
Read `OUTPUT_MODE` from config. Follow `shared/output-mode.md` for behavior.
|
||||
|
||||
Present:
|
||||
```
|
||||
图片已生成!
|
||||
Parse the CLI JSON output to extract the image URL:
|
||||
```bash
|
||||
IMAGE_URL=$(echo "$RESULT" | jq -r '.imageUrl')
|
||||
```
|
||||
|
||||
已保存到 .listenhub/image-gen/{YYYY-MM-DD}-{jobId}/:
|
||||
{jobId}.jpg
|
||||
```
|
||||
**`inline` or `both`**: Download to a temp file, then use the Read tool.
|
||||
|
||||
**Base64 decoding** (cross-platform):
|
||||
```bash
|
||||
JOB_ID=$(date +%s)
|
||||
listenhub download "$IMAGE_URL" -o /tmp/image-gen-${JOB_ID}.jpg
|
||||
```
|
||||
Then use the Read tool on `/tmp/image-gen-{jobId}.jpg`. The image displays inline in the conversation.
|
||||
|
||||
```bash
|
||||
# Linux
|
||||
echo "$BASE64_DATA" | base64 -d > output.jpg
|
||||
Present:
|
||||
```
|
||||
图片已生成!
|
||||
```
|
||||
|
||||
# macOS
|
||||
echo "$BASE64_DATA" | base64 -D > output.jpg
|
||||
# or
|
||||
echo "$BASE64_DATA" | base64 --decode > output.jpg
|
||||
```
|
||||
**`download` or `both`**: Save to the artifact directory.
|
||||
|
||||
**Retry logic**: On 429 (rate limit), wait 15 seconds and retry. Max 3 retries.
|
||||
```bash
|
||||
JOB_ID=$(date +%s)
|
||||
DATE=$(date +%Y-%m-%d)
|
||||
JOB_DIR=".listenhub/image-gen/${DATE}-${JOB_ID}"
|
||||
mkdir -p "$JOB_DIR"
|
||||
listenhub download "$IMAGE_URL" -o "${JOB_DIR}/${JOB_ID}.jpg"
|
||||
```
|
||||
|
||||
Present:
|
||||
```
|
||||
图片已生成!
|
||||
|
||||
已保存到 .listenhub/image-gen/{YYYY-MM-DD}-{jobId}/:
|
||||
{jobId}.jpg
|
||||
```
|
||||
|
||||
## Prompt Handling
|
||||
|
||||
@@ -253,12 +255,14 @@ echo "$BASE64_DATA" | base64 --decode > output.jpg
|
||||
|
||||
## API Reference
|
||||
|
||||
- Image generation: `shared/api-image.md`
|
||||
- Error handling: `shared/common-patterns.md` § Error Handling
|
||||
- CLI authentication: `shared/cli-authentication.md`
|
||||
- CLI execution patterns: `shared/cli-patterns.md`
|
||||
- Config pattern: `shared/config-pattern.md`
|
||||
- Output mode: `shared/output-mode.md`
|
||||
|
||||
## Composability
|
||||
|
||||
- **Invokes**: nothing (direct API call)
|
||||
- **Invokes**: nothing (direct CLI call)
|
||||
- **Invoked by**: platform skills for cover images (Phase 2)
|
||||
|
||||
## Example
|
||||
@@ -273,61 +277,38 @@ echo "$BASE64_DATA" | base64 --decode > output.jpg
|
||||
5. No references
|
||||
|
||||
```bash
|
||||
RESPONSE=$(curl -sS -X POST "https://api.marswave.ai/openapi/v1/images/generation" \
|
||||
-H "Authorization: Bearer $LISTENHUB_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Source: skills" \
|
||||
--max-time 600 \
|
||||
-d '{
|
||||
"provider": "google",
|
||||
"model": "gemini-3-pro-image-preview",
|
||||
"prompt": "cyberpunk city at night",
|
||||
"imageConfig": {"imageSize": "2K", "aspectRatio": "16:9"}
|
||||
}')
|
||||
|
||||
BASE64_DATA=$(echo "$RESPONSE" | jq -r '.candidates[0].content.parts[0].inlineData.data // .data')
|
||||
JOB_ID=$(date +%s)
|
||||
DATE=$(date +%Y-%m-%d)
|
||||
JOB_DIR=".listenhub/image-gen/${DATE}-${JOB_ID}"
|
||||
mkdir -p "$JOB_DIR"
|
||||
echo "$BASE64_DATA" | base64 -D > "${JOB_DIR}/${JOB_ID}.jpg"
|
||||
listenhub image create \
|
||||
--prompt "cyberpunk city at night" \
|
||||
--model "gemini-3-pro-image-preview" \
|
||||
--lang en \
|
||||
--aspect-ratio 16:9 \
|
||||
--size 2K \
|
||||
--json
|
||||
```
|
||||
|
||||
Decode the base64 data per `outputMode` (see `shared/output-mode.md`).
|
||||
Parse CLI JSON output per `outputMode` (see `shared/output-mode.md`).
|
||||
|
||||
### Example 2 — With Local Reference Image (base64)
|
||||
### Example 2 — With Reference Images
|
||||
|
||||
**User**: "Generate an image in this style" (provides a local file path)
|
||||
**User**: "Generate an image in this style" (provides local files and a URL)
|
||||
|
||||
**Agent workflow**:
|
||||
1. Ask prompt → "a serene mountain lake at dawn"
|
||||
2. Ask model → "pro"
|
||||
3. Ask resolution → "2K"
|
||||
4. Ask ratio → "16:9"
|
||||
5. References → local file → `/path/to/style-reference.png`
|
||||
5. References → `/path/to/style-reference.png`, `https://example.com/photo.jpg`
|
||||
|
||||
```bash
|
||||
# Encode local reference image
|
||||
BASE64_REF=$(base64 -i /path/to/style-reference.png)
|
||||
|
||||
RESPONSE=$(curl -sS -X POST "https://api.marswave.ai/openapi/v1/images/generation" \
|
||||
-H "Authorization: Bearer $LISTENHUB_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
--max-time 600 \
|
||||
-d "{
|
||||
\"provider\": \"google\",
|
||||
\"model\": \"gemini-3-pro-image-preview\",
|
||||
\"prompt\": \"a serene mountain lake at dawn\",
|
||||
\"imageConfig\": {\"imageSize\": \"2K\", \"aspectRatio\": \"16:9\"},
|
||||
\"referenceImages\": [{\"inlineData\": {\"data\": \"$BASE64_REF\", \"mimeType\": \"image/png\"}}]
|
||||
}")
|
||||
|
||||
BASE64_DATA=$(echo "$RESPONSE" | jq -r '.candidates[0].content.parts[0].inlineData.data // .data')
|
||||
JOB_ID=$(date +%s)
|
||||
DATE=$(date +%Y-%m-%d)
|
||||
JOB_DIR=".listenhub/image-gen/${DATE}-${JOB_ID}"
|
||||
mkdir -p "$JOB_DIR"
|
||||
echo "$BASE64_DATA" | base64 -D > "${JOB_DIR}/${JOB_ID}.jpg"
|
||||
listenhub image create \
|
||||
--prompt "a serene mountain lake at dawn" \
|
||||
--model "gemini-3-pro-image-preview" \
|
||||
--lang en \
|
||||
--aspect-ratio 16:9 \
|
||||
--size 2K \
|
||||
--reference /path/to/style-reference.png \
|
||||
--reference https://example.com/photo.jpg \
|
||||
--json
|
||||
```
|
||||
|
||||
Decode the base64 data per `outputMode` (see `shared/output-mode.md`).
|
||||
Parse CLI JSON output per `outputMode` (see `shared/output-mode.md`).
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
---
|
||||
name: listenhub-cli
|
||||
description: |
|
||||
ListenHub CLI skills router. Routes to the correct skill based on user intent.
|
||||
Triggers on: "make a podcast", "explainer video", "read aloud", "TTS",
|
||||
"generate image", "做播客", "解说视频", "朗读", "生成图片", "幻灯片",
|
||||
"slides", "音乐", "music", "generate music", "翻唱", "cover song",
|
||||
"parse URL", "解析链接", "提取内容".
|
||||
metadata:
|
||||
openclaw:
|
||||
emoji: "🎧"
|
||||
requires:
|
||||
bin: ["listenhub"]
|
||||
primaryBin: "listenhub"
|
||||
---
|
||||
|
||||
## Purpose
|
||||
|
||||
This is a router skill. When users trigger a general ListenHub action, this skill identifies the intent and delegates to the appropriate specialized skill.
|
||||
|
||||
## Routing Table
|
||||
|
||||
| User intent | Keywords | Route to |
|
||||
|-------------|----------|----------|
|
||||
| Podcast | "podcast", "播客", "debate", "dialogue" | `/podcast` |
|
||||
| Explainer video | "explainer", "解说视频", "tutorial video" | `/explainer` |
|
||||
| Slides / PPT | "slides", "幻灯片", "PPT", "presentation" | `/slides` |
|
||||
| TTS / Read aloud | "TTS", "read aloud", "朗读", "配音", "语音合成" | `/tts` |
|
||||
| Image generation | "generate image", "画一张", "生成图片", "AI图" | `/image-gen` |
|
||||
| Music | "music", "音乐", "生成音乐", "翻唱", "cover" | `/music` |
|
||||
| Content extraction | "parse URL", "extract content", "解析链接" | `/content-parser` |
|
||||
| Audio transcription | "transcribe", "ASR", "语音转文字" | `/asr` |
|
||||
| Creator workflow | "创作", "写公众号", "小红书", "口播" | `/creator` |
|
||||
|
||||
## How to Route
|
||||
|
||||
1. Read the user's message and identify which category it falls into
|
||||
2. Tell the user which skill you're routing to
|
||||
3. Follow that skill's SKILL.md completely
|
||||
|
||||
If the intent is ambiguous, ask the user to clarify:
|
||||
|
||||
```
|
||||
Question: "What would you like to create?"
|
||||
Options:
|
||||
- "Podcast" — Audio discussion on a topic
|
||||
- "Explainer Video" — Narrated video with AI visuals
|
||||
- "Slides" — Slide deck / presentation
|
||||
- "Music" — AI-generated music or cover
|
||||
```
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Most skills require the ListenHub CLI. Check:
|
||||
|
||||
```bash
|
||||
listenhub auth status --json
|
||||
```
|
||||
|
||||
If not installed or not logged in, handle automatically — never ask the user to run install commands:
|
||||
|
||||
1. Auto-install: run `npm install -g @marswave/listenhub-cli` silently
|
||||
2. Auto-login: run `listenhub auth login` (opens browser)
|
||||
|
||||
Exception: `/asr` runs locally and needs no CLI or API key.
|
||||
@@ -1,14 +0,0 @@
|
||||
# ListenHub Skill — DEPRECATED
|
||||
|
||||
This monolithic skill has been decomposed into individual skills:
|
||||
|
||||
- `/podcast` — Podcast generation (solo, dialogue, debate)
|
||||
- `/explainer` — Explainer videos with narration and AI visuals
|
||||
- `/tts` — Text-to-speech and voice narration
|
||||
- `/image-gen` — AI image generation
|
||||
- `/content-parser` — URL content extraction
|
||||
|
||||
Shared infrastructure (API reference, authentication, common patterns) is in `shared/`.
|
||||
|
||||
**Migration date**: 2026-03-04
|
||||
**Issue**: [MARS-3517](https://linear.app/marswave/issue/MARS-3517)
|
||||
+54
-22
@@ -1,33 +1,65 @@
|
||||
---
|
||||
name: listenhub
|
||||
description: |
|
||||
DEPRECATED — replaced by individual skills. Use when the user triggers any
|
||||
ListenHub action: "make a podcast", "explainer video", "read aloud", "TTS",
|
||||
"generate image", "解说视频", "播客", "朗读", "生成图片".
|
||||
ListenHub CLI skills router. Routes to the correct skill based on user intent.
|
||||
Triggers on: "make a podcast", "explainer video", "read aloud", "TTS",
|
||||
"generate image", "做播客", "解说视频", "朗读", "生成图片", "幻灯片",
|
||||
"slides", "音乐", "music", "generate music", "翻唱", "cover song",
|
||||
"parse URL", "解析链接", "提取内容".
|
||||
metadata:
|
||||
openclaw:
|
||||
emoji: "🎧"
|
||||
requires:
|
||||
bin: ["listenhub"]
|
||||
primaryBin: "listenhub"
|
||||
---
|
||||
|
||||
## ⚠️ This skill has been replaced
|
||||
## Purpose
|
||||
|
||||
The `listenhub` skill has been split into focused individual skills.
|
||||
This is a router skill. When users trigger a general ListenHub action, this skill identifies the intent and delegates to the appropriate specialized skill.
|
||||
|
||||
**Tell the user:**
|
||||
## Routing Table
|
||||
|
||||
> This skill is outdated. Please upgrade with:
|
||||
>
|
||||
> ```bash
|
||||
> npx skills add marswaveai/skills
|
||||
> ```
|
||||
>
|
||||
> Then restart your agent.
|
||||
| User intent | Keywords | Route to |
|
||||
|-------------|----------|----------|
|
||||
| Podcast | "podcast", "播客", "debate", "dialogue" | `/podcast` |
|
||||
| Explainer video | "explainer", "解说视频", "tutorial video" | `/explainer` |
|
||||
| Slides / PPT | "slides", "幻灯片", "PPT", "presentation" | `/slides` |
|
||||
| TTS / Read aloud | "TTS", "read aloud", "朗读", "配音", "语音合成" | `/tts` |
|
||||
| Image generation | "generate image", "画一张", "生成图片", "AI图" | `/image-gen` |
|
||||
| Music | "music", "音乐", "生成音乐", "翻唱", "cover" | `/music` |
|
||||
| Content extraction | "parse URL", "extract content", "解析链接" | `/content-parser` |
|
||||
| Audio transcription | "transcribe", "ASR", "语音转文字" | `/asr` |
|
||||
| Creator workflow | "创作", "写公众号", "小红书", "口播" | `/creator` |
|
||||
|
||||
After upgrading, the new skills work the same way — just say what you want:
|
||||
## How to Route
|
||||
|
||||
| What you wanted | New skill |
|
||||
|----------------|-----------|
|
||||
| Make a podcast | `/podcast` |
|
||||
| Explainer video | `/explainer` |
|
||||
| Read aloud / TTS | `/tts` |
|
||||
| Generate image | `/image-gen` |
|
||||
| Extract URL content | `/content-parser` |
|
||||
1. Read the user's message and identify which category it falls into
|
||||
2. Tell the user which skill you're routing to
|
||||
3. Follow that skill's SKILL.md completely
|
||||
|
||||
**Do not attempt the original task until the user has upgraded.**
|
||||
If the intent is ambiguous, ask the user to clarify:
|
||||
|
||||
```
|
||||
Question: "What would you like to create?"
|
||||
Options:
|
||||
- "Podcast" — Audio discussion on a topic
|
||||
- "Explainer Video" — Narrated video with AI visuals
|
||||
- "Slides" — Slide deck / presentation
|
||||
- "Music" — AI-generated music or cover
|
||||
```
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Most skills require the ListenHub CLI. Check:
|
||||
|
||||
```bash
|
||||
listenhub auth status --json
|
||||
```
|
||||
|
||||
If not installed or not logged in, handle automatically — never ask the user to run install commands:
|
||||
|
||||
1. Auto-install: run `npm install -g @marswave/listenhub-cli` silently
|
||||
2. Auto-login: run `listenhub auth login` (opens browser)
|
||||
|
||||
Exception: `/asr` runs locally and needs no CLI or API key.
|
||||
|
||||
+364
@@ -0,0 +1,364 @@
|
||||
---
|
||||
name: music
|
||||
description: |
|
||||
Generate AI music or create covers from reference audio. Triggers on: "音乐",
|
||||
"music", "生成音乐", "generate music", "翻唱", "cover", "作曲", "compose",
|
||||
"create a song", "做一首歌".
|
||||
metadata:
|
||||
openclaw:
|
||||
emoji: "🎵"
|
||||
requires:
|
||||
bin: ["listenhub"]
|
||||
primaryBin: "listenhub"
|
||||
---
|
||||
|
||||
## When to Use
|
||||
|
||||
- User wants to generate original AI music from a prompt
|
||||
- User wants to create a cover from reference audio
|
||||
- User says "音乐", "music", "生成音乐", "generate music", "翻唱", "cover", "作曲", "compose", "create a song", or "做一首歌"
|
||||
|
||||
## When NOT to Use
|
||||
|
||||
- User wants text-to-speech reading (use `/speech`)
|
||||
- User wants a podcast discussion (use `/podcast`)
|
||||
- User wants an explainer video with narration (use `/explainer`)
|
||||
- User wants to transcribe audio to text (use `/asr`)
|
||||
|
||||
## Purpose
|
||||
|
||||
Generate original AI music from text prompts, or create cover versions from reference audio. Two modes:
|
||||
|
||||
1. **Generate** (original): Create a new song from a text prompt, with optional style, title, and instrumental-only options.
|
||||
2. **Cover**: Transform a reference audio file into a new version, with optional style modifications.
|
||||
|
||||
## Hard Constraints
|
||||
|
||||
- Always read config following `shared/config-pattern.md` before any interaction
|
||||
- Follow `shared/cli-patterns.md` for execution modes, error handling, and interaction patterns
|
||||
- Always follow `shared/cli-authentication.md` for auth checks
|
||||
- Never save files to `~/Downloads/` or `.listenhub/` — save artifacts to the current working directory with friendly topic-based names (see `shared/config-pattern.md` § Artifact Naming)
|
||||
- No speakers involved — music generation does not use speaker selection
|
||||
- Audio file constraints for cover mode: mp3, wav, flac, m4a, ogg, aac; max 20MB
|
||||
- Long timeout: 600s default. Use `run_in_background: true` with `timeout: 660000`
|
||||
|
||||
<HARD-GATE>
|
||||
Use the AskUserQuestion tool for every multiple-choice step — do NOT print options as plain text. Ask one question at a time. Wait for the user's answer before proceeding to the next step. After all parameters are collected, summarize the choices and ask the user to confirm. Do NOT call any CLI command until the user has explicitly confirmed.
|
||||
|
||||
</HARD-GATE>
|
||||
|
||||
## Step -1: CLI Auth Check
|
||||
|
||||
Follow `shared/cli-authentication.md`. If the CLI is not installed or the user is not logged in, auto-install and auto-login — never ask the user to run commands manually.
|
||||
|
||||
## Step 0: Config Setup
|
||||
|
||||
Follow `shared/config-pattern.md` Step 0 (Zero-Question Boot).
|
||||
|
||||
**If file doesn't exist** — silently create with defaults and proceed:
|
||||
```bash
|
||||
mkdir -p ".listenhub/music"
|
||||
echo '{"outputMode":"download","language":null}' > ".listenhub/music/config.json"
|
||||
CONFIG_PATH=".listenhub/music/config.json"
|
||||
CONFIG=$(cat "$CONFIG_PATH")
|
||||
```
|
||||
**Do NOT ask any setup questions.** Proceed directly to the Interaction Flow.
|
||||
|
||||
**If file exists** — read config silently and proceed:
|
||||
```bash
|
||||
CONFIG_PATH=".listenhub/music/config.json"
|
||||
[ ! -f "$CONFIG_PATH" ] && CONFIG_PATH="$HOME/.listenhub/music/config.json"
|
||||
CONFIG=$(cat "$CONFIG_PATH")
|
||||
```
|
||||
|
||||
### Setup Flow (user-initiated reconfigure only)
|
||||
|
||||
Only run when the user explicitly asks to reconfigure. Display current settings:
|
||||
```
|
||||
当前配置 (music):
|
||||
输出方式:{inline / download / both}
|
||||
语言偏好:{zh / en / 未设置}
|
||||
```
|
||||
|
||||
Then ask:
|
||||
|
||||
1. **outputMode**: Follow `shared/output-mode.md` § Setup Flow Question.
|
||||
|
||||
2. **Language** (optional): "默认语言?"
|
||||
- "中文 (zh)"
|
||||
- "English (en)"
|
||||
- "每次手动选择" → keep `null`
|
||||
|
||||
After collecting answers, save immediately:
|
||||
```bash
|
||||
NEW_CONFIG=$(echo "$CONFIG" | jq --arg m "$OUTPUT_MODE" '. + {"outputMode": $m}')
|
||||
if [ "$LANGUAGE" != "null" ]; then
|
||||
NEW_CONFIG=$(echo "$NEW_CONFIG" | jq --arg lang "$LANGUAGE" '. + {"language": $lang}')
|
||||
fi
|
||||
echo "$NEW_CONFIG" > "$CONFIG_PATH"
|
||||
CONFIG=$(cat "$CONFIG_PATH")
|
||||
```
|
||||
|
||||
## Interaction Flow
|
||||
|
||||
### Step 1: Mode
|
||||
|
||||
Ask the user which mode they want, unless the intent is already clear from their message (e.g., "翻唱" or "cover" implies cover mode; "作曲" or "compose" implies generate mode).
|
||||
|
||||
```
|
||||
Question: "选择音乐生成模式:"
|
||||
Options:
|
||||
- "原创 (Generate)" — 从文字描述生成全新歌曲
|
||||
- "翻唱 (Cover)" — 基于参考音频生成新版本
|
||||
```
|
||||
|
||||
### Step 2a: Prompt (generate mode)
|
||||
|
||||
If the user chose **Generate**, ask for the song description:
|
||||
|
||||
> "请描述你想要的歌曲(主题、情绪、歌词片段等):"
|
||||
|
||||
Accept free text. This maps to `--prompt`.
|
||||
|
||||
### Step 2b: Reference Audio (cover mode)
|
||||
|
||||
If the user chose **Cover**, ask for the reference audio:
|
||||
|
||||
> "请提供参考音频文件路径或 URL:"
|
||||
|
||||
Accept a local file path or URL. This maps to `--audio`.
|
||||
|
||||
**Validate the input:**
|
||||
|
||||
- If a local path: verify the file exists and check the extension is one of: `mp3`, `wav`, `flac`, `m4a`, `ogg`, `aac`
|
||||
- If a URL: accept as-is (the CLI will validate)
|
||||
- Check file size does not exceed 20 MB for local files:
|
||||
```bash
|
||||
FILE_SIZE=$(stat -f%z "{path}" 2>/dev/null || stat -c%s "{path}" 2>/dev/null)
|
||||
if [ "$FILE_SIZE" -gt 20971520 ]; then
|
||||
echo "File exceeds 20 MB limit"
|
||||
fi
|
||||
```
|
||||
|
||||
If validation fails, inform the user and re-ask.
|
||||
|
||||
Optionally, the user may also provide a prompt to guide the cover style.
|
||||
|
||||
### Step 3: Style (optional)
|
||||
|
||||
Ask for an optional style descriptor:
|
||||
|
||||
> "指定音乐风格?(如 pop、rock、jazz、电子、古风等,留空则由 AI 自动选择)"
|
||||
|
||||
Accept free text or empty. This maps to `--style`.
|
||||
|
||||
### Step 4: Title (optional)
|
||||
|
||||
Ask for an optional title:
|
||||
|
||||
> "歌曲标题?(留空则自动生成)"
|
||||
|
||||
Accept free text or empty. This maps to `--title`.
|
||||
|
||||
### Step 5: Instrumental
|
||||
|
||||
```
|
||||
Question: "是否纯音乐(无人声)?"
|
||||
Options:
|
||||
- "否,带人声(默认)"
|
||||
- "是,纯音乐"
|
||||
```
|
||||
|
||||
Default is "no" (with vocals). If the user selects "是", add `--instrumental` flag.
|
||||
|
||||
### Step 6: Confirm & Generate
|
||||
|
||||
Summarize all choices:
|
||||
|
||||
**Generate mode:**
|
||||
```
|
||||
准备生成音乐:
|
||||
|
||||
模式:原创 (Generate)
|
||||
描述:{prompt}
|
||||
风格:{style / 自动}
|
||||
标题:{title / 自动}
|
||||
人声:{带人声 / 纯音乐}
|
||||
|
||||
确认?
|
||||
```
|
||||
|
||||
**Cover mode:**
|
||||
```
|
||||
准备生成音乐:
|
||||
|
||||
模式:翻唱 (Cover)
|
||||
参考音频:{path-or-url}
|
||||
描述:{prompt / 无}
|
||||
风格:{style / 自动}
|
||||
标题:{title / 自动}
|
||||
人声:{带人声 / 纯音乐}
|
||||
|
||||
确认?
|
||||
```
|
||||
|
||||
Wait for explicit confirmation before running any CLI command.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Submit (background)**: Run the CLI command with `run_in_background: true` and `timeout: 660000`:
|
||||
|
||||
**Generate mode:**
|
||||
```bash
|
||||
listenhub music generate \
|
||||
--prompt "{prompt}" \
|
||||
--style "{style}" \
|
||||
--title "{title}" \
|
||||
--instrumental \
|
||||
--json
|
||||
```
|
||||
|
||||
**Cover mode:**
|
||||
```bash
|
||||
listenhub music cover \
|
||||
--audio "{path-or-url}" \
|
||||
--prompt "{prompt}" \
|
||||
--style "{style}" \
|
||||
--title "{title}" \
|
||||
--instrumental \
|
||||
--json
|
||||
```
|
||||
|
||||
Flag notes:
|
||||
- `--prompt` — text description of the music (required for generate, optional for cover)
|
||||
- `--audio` — reference audio file path or URL (cover mode only, required)
|
||||
- `--style` — optional style/genre hint; omit if not provided
|
||||
- `--title` — optional track title; omit if not provided
|
||||
- `--instrumental` — add this flag for instrumental-only (no vocals); omit if not selected
|
||||
- Omit `--prompt` in cover mode if not provided
|
||||
|
||||
The CLI handles polling internally. Music generation takes up to 10 minutes.
|
||||
|
||||
2. Tell the user the task is submitted and that they will be notified when it finishes.
|
||||
|
||||
3. When notified of completion, **present the result**:
|
||||
|
||||
Parse the CLI JSON output for key fields:
|
||||
```bash
|
||||
AUDIO_URL=$(echo "$RESULT" | jq -r '.audioUrl')
|
||||
TITLE=$(echo "$RESULT" | jq -r '.title // "Untitled"')
|
||||
DURATION=$(echo "$RESULT" | jq -r '.duration // empty')
|
||||
CREDITS=$(echo "$RESULT" | jq -r '.credits // empty')
|
||||
```
|
||||
|
||||
Read `OUTPUT_MODE` from config. Follow `shared/output-mode.md` for behavior.
|
||||
|
||||
**`inline` or `both`**: Display audio URL as a clickable link.
|
||||
|
||||
```
|
||||
音乐已生成!
|
||||
|
||||
标题:{title}
|
||||
在线收听:{audioUrl}
|
||||
时长:{duration}s
|
||||
消耗积分:{credits}
|
||||
```
|
||||
|
||||
**`download` or `both`**: Also download the file. Generate a slug from the title following `shared/config-pattern.md` § Artifact Naming.
|
||||
```bash
|
||||
SLUG="{slug}" # e.g. "summer-breeze"
|
||||
NAME="${SLUG}.mp3"
|
||||
# Dedup: if file exists, append -2, -3, etc.
|
||||
BASE="${NAME%.*}"; EXT="${NAME##*.}"; i=2
|
||||
while [ -e "$NAME" ]; do NAME="${BASE}-${i}.${EXT}"; i=$((i+1)); done
|
||||
curl -sS -o "$NAME" "{audioUrl}"
|
||||
```
|
||||
Present:
|
||||
```
|
||||
已保存到当前目录:
|
||||
{NAME}
|
||||
```
|
||||
|
||||
### After Successful Generation
|
||||
|
||||
Update config with the language used this session if the user explicitly specified one:
|
||||
|
||||
```bash
|
||||
if [ -n "$LANGUAGE" ]; then
|
||||
NEW_CONFIG=$(echo "$CONFIG" | jq --arg lang "$LANGUAGE" '. + {"language": $lang}')
|
||||
echo "$NEW_CONFIG" > "$CONFIG_PATH"
|
||||
fi
|
||||
```
|
||||
|
||||
**Estimated times**:
|
||||
- Music generation: 5-10 minutes
|
||||
|
||||
## Resources
|
||||
|
||||
- CLI authentication: `shared/cli-authentication.md`
|
||||
- CLI patterns: `shared/cli-patterns.md`
|
||||
- Config pattern: `shared/config-pattern.md`
|
||||
- Output mode: `shared/output-mode.md`
|
||||
|
||||
## Composability
|
||||
|
||||
- **Invokes**: nothing
|
||||
- **Invoked by**: content-planner (Phase 3)
|
||||
|
||||
## Examples
|
||||
|
||||
**Generate original:**
|
||||
|
||||
> "帮我做一首关于夏天海边的歌"
|
||||
|
||||
1. Detect: generate mode ("做一首歌")
|
||||
2. Read config (first run: create defaults with `outputMode: "download"`)
|
||||
3. Infer: mode = generate, prompt = "夏天海边的歌"
|
||||
4. Ask: style? title? instrumental?
|
||||
5. Confirm summary → user confirms
|
||||
|
||||
```bash
|
||||
listenhub music generate \
|
||||
--prompt "关于夏天海边的歌" \
|
||||
--json
|
||||
```
|
||||
|
||||
Wait for CLI to return result, then download `{slug}.mp3` to cwd.
|
||||
|
||||
**Cover from file:**
|
||||
|
||||
> "用这个音频翻唱一下 demo.mp3,jazz 风格"
|
||||
|
||||
1. Detect: cover mode ("翻唱")
|
||||
2. Validate: `demo.mp3` exists, is a supported format, under 20 MB
|
||||
3. Infer: style = "jazz" from user input
|
||||
4. Ask: title? instrumental?
|
||||
5. Confirm summary → user confirms
|
||||
|
||||
```bash
|
||||
listenhub music cover \
|
||||
--audio "demo.mp3" \
|
||||
--style "jazz" \
|
||||
--json
|
||||
```
|
||||
|
||||
Wait for CLI to return result, then download `{slug}.mp3` to cwd.
|
||||
|
||||
**Generate instrumental:**
|
||||
|
||||
> "Create an instrumental electronic track for a game intro"
|
||||
|
||||
1. Detect: generate mode ("Create ... track")
|
||||
2. Infer: style = "electronic", instrumental = yes
|
||||
3. Ask: title?
|
||||
4. Confirm summary → user confirms
|
||||
|
||||
```bash
|
||||
listenhub music generate \
|
||||
--prompt "instrumental electronic track for a game intro" \
|
||||
--style "electronic" \
|
||||
--instrumental \
|
||||
--json
|
||||
```
|
||||
|
||||
Wait for CLI to return result, then download `{slug}.mp3` to cwd.
|
||||
+53
-83
@@ -8,8 +8,8 @@ metadata:
|
||||
openclaw:
|
||||
emoji: "🎙️"
|
||||
requires:
|
||||
env: ["LISTENHUB_API_KEY"]
|
||||
primaryEnv: "LISTENHUB_API_KEY"
|
||||
bin: ["listenhub"]
|
||||
primaryBin: "listenhub"
|
||||
---
|
||||
|
||||
## When to Use
|
||||
@@ -32,11 +32,10 @@ Generate podcast episodes with 1-2 AI speakers discussing a topic. Supports quic
|
||||
|
||||
## Hard Constraints
|
||||
|
||||
- No shell scripts. Construct curl commands from the API reference files listed in Resources
|
||||
- Always read `shared/authentication.md` for API key and headers
|
||||
- Follow `shared/common-patterns.md` for polling, errors, and interaction patterns
|
||||
- Always check CLI auth following `shared/cli-authentication.md`
|
||||
- Follow `shared/cli-patterns.md` for command execution and error handling
|
||||
- Never hardcode speaker IDs in API calls — use built-in defaults from `shared/speaker-selection.md` as fallback only; fetch from the speakers API when the user wants to change voice
|
||||
- Never fabricate API endpoints or parameters
|
||||
- Never fabricate CLI commands or parameters
|
||||
- Always read config following `shared/config-pattern.md` before any interaction
|
||||
- Always follow `shared/speaker-selection.md` for speaker selection (text table + free-text input)
|
||||
- Never save files to `~/Downloads/` or `.listenhub/` — save artifacts to the current working directory with friendly topic-based names (see `shared/config-pattern.md` § Artifact Naming)
|
||||
@@ -46,9 +45,9 @@ Use the AskUserQuestion tool for every multiple-choice step — do NOT print opt
|
||||
|
||||
</HARD-GATE>
|
||||
|
||||
## Step -1: API Key Check
|
||||
## Step -1: CLI Auth Check
|
||||
|
||||
Follow `shared/config-pattern.md` § API Key Check. If the key is missing, stop immediately.
|
||||
Follow `shared/cli-authentication.md` § Auth Check. If the CLI is not installed or the user is not logged in, auto-install and auto-login — never ask the user to run commands manually.
|
||||
|
||||
## Step 0: Config Setup
|
||||
|
||||
@@ -57,7 +56,7 @@ Follow `shared/config-pattern.md` Step 0 (Zero-Question Boot).
|
||||
**If file doesn't exist** — silently create with defaults and proceed:
|
||||
```bash
|
||||
mkdir -p ".listenhub/podcast"
|
||||
echo '{"outputMode":"inline","language":null,"defaultMode":"quick","defaultMethod":"one-step","defaultSpeakers":{}}' > ".listenhub/podcast/config.json"
|
||||
echo '{"outputMode":"inline","language":null,"defaultMode":"quick","defaultSpeakers":{}}' > ".listenhub/podcast/config.json"
|
||||
CONFIG_PATH=".listenhub/podcast/config.json"
|
||||
CONFIG=$(cat "$CONFIG_PATH")
|
||||
```
|
||||
@@ -78,7 +77,6 @@ Only run when the user explicitly asks to reconfigure. Display current settings:
|
||||
输出方式:{inline / download / both}
|
||||
语言偏好:{zh / en / 未设置}
|
||||
默认模式:{quick / deep / debate / 未设置}
|
||||
默认生成方式:{one-step / two-step}
|
||||
默认主播:{speakerName(s) / 使用内置默认}
|
||||
```
|
||||
|
||||
@@ -97,11 +95,6 @@ Then ask these questions in order and save:
|
||||
- "Debate — 辩论对话"
|
||||
- "每次手动选择" → keep `null`
|
||||
|
||||
4. **Method** (optional): "默认生成方式?"
|
||||
- "一步生成(推荐)" → `defaultMethod: "one-step"`
|
||||
- "两步生成(先预览文本)" → `defaultMethod: "two-step"`
|
||||
- "每次手动选择" → keep `null`
|
||||
|
||||
After collecting answers, save immediately:
|
||||
```bash
|
||||
NEW_CONFIG=$(echo "$CONFIG" | jq --arg m "$OUTPUT_MODE" '. + {"outputMode": $m}')
|
||||
@@ -113,10 +106,6 @@ fi
|
||||
if [ "$MODE" != "null" ]; then
|
||||
NEW_CONFIG=$(echo "$NEW_CONFIG" | jq --arg mode "$MODE" '. + {"defaultMode": $mode}')
|
||||
fi
|
||||
# Save method if user chose one
|
||||
if [ "$METHOD" != "null" ]; then
|
||||
NEW_CONFIG=$(echo "$NEW_CONFIG" | jq --arg method "$METHOD" '. + {"defaultMethod": $method}')
|
||||
fi
|
||||
echo "$NEW_CONFIG" > "$CONFIG_PATH"
|
||||
CONFIG=$(cat "$CONFIG_PATH")
|
||||
```
|
||||
@@ -171,13 +160,7 @@ Follow `shared/speaker-selection.md`:
|
||||
|
||||
For 2-speaker mode (dialogue/debate): use Primary + Secondary defaults for the language.
|
||||
|
||||
### Step 6: Generation Method
|
||||
|
||||
**Default: "one-step"** — skip this question unless:
|
||||
- `config.defaultMethod` is set → use that value silently
|
||||
- User explicitly asks to review text first → use "two-step"
|
||||
|
||||
### Step 7: Confirm & Generate
|
||||
### Step 6: Confirm & Generate
|
||||
|
||||
Summarize all choices:
|
||||
|
||||
@@ -189,37 +172,47 @@ Ready to generate podcast:
|
||||
Language: {language}
|
||||
Speakers: {speaker name(s)}
|
||||
References: {yes/no + brief description}
|
||||
Method: {one-step/two-step}
|
||||
|
||||
Proceed?
|
||||
```
|
||||
|
||||
Wait for explicit confirmation before calling any API. The user can adjust any parameter here before confirming.
|
||||
Wait for explicit confirmation before calling any CLI command. The user can adjust any parameter here before confirming.
|
||||
|
||||
## Workflow
|
||||
|
||||
### One-Step Generation
|
||||
### Generation
|
||||
|
||||
1. **Submit (foreground)**: `POST /podcast/episodes` with collected parameters → extract `episodeId`
|
||||
2. Tell the user the task is submitted
|
||||
3. **Poll (background)**: Run the following **exact** bash command with `run_in_background: true` and `timeout: 600000`. Do NOT use python3, awk, or any other JSON parser — use `jq` as shown:
|
||||
1. **Submit (background)**: Run the CLI command with `run_in_background: true` and `timeout: 360000`:
|
||||
|
||||
```bash
|
||||
EPISODE_ID="<id-from-step-1>"
|
||||
for i in $(seq 1 30); do
|
||||
RESULT=$(curl -sS "https://api.marswave.ai/openapi/v1/podcast/episodes/$EPISODE_ID" \
|
||||
-H "Authorization: Bearer $LISTENHUB_API_KEY" \
|
||||
-H "X-Source: skills" 2>/dev/null)
|
||||
STATUS=$(echo "$RESULT" | tr -d '\000-\037\177' | jq -r '.data.processStatus // "pending"')
|
||||
case "$STATUS" in
|
||||
success|completed) echo "$RESULT"; exit 0 ;;
|
||||
failed|error) echo "FAILED: $RESULT" >&2; exit 1 ;;
|
||||
*) sleep 10 ;;
|
||||
esac
|
||||
done
|
||||
echo "TIMEOUT" >&2; exit 2
|
||||
listenhub podcast create \
|
||||
--query "{topic}" \
|
||||
--source-url "{url}" \
|
||||
--source-text "{text}" \
|
||||
--mode {quick|deep|debate} \
|
||||
--lang {en|zh|ja} \
|
||||
--speaker "{name}" \
|
||||
--speaker "{name2}" \
|
||||
--json
|
||||
```
|
||||
4. When notified of completion, **Step 6: Present result**
|
||||
|
||||
Flag notes:
|
||||
- `--query` — the topic or question to discuss
|
||||
- `--source-url` — repeatable, one per URL reference
|
||||
- `--source-text` — repeatable, one per text block reference
|
||||
- `--mode` — one of `quick`, `deep`, `debate`
|
||||
- `--lang` — language code
|
||||
- `--speaker` — repeatable (max 2); use speaker display names
|
||||
- `--speaker-id` — alternative to `--speaker`; use speaker IDs instead of names
|
||||
- Omit `--source-url` / `--source-text` if the user provided no references
|
||||
|
||||
The CLI handles polling internally and returns the final result when generation completes.
|
||||
|
||||
2. Tell the user the task is submitted and that they will be notified when it finishes.
|
||||
|
||||
3. When notified of completion, **Present result**:
|
||||
|
||||
Parse the CLI JSON output to extract fields: `audioUrl`, `subtitlesUrl`, `audioDuration`, `credits`.
|
||||
|
||||
Read `OUTPUT_MODE` from config. Follow `shared/output-mode.md` for behavior.
|
||||
|
||||
@@ -249,26 +242,7 @@ Wait for explicit confirmation before calling any API. The user can adjust any p
|
||||
已保存到当前目录:
|
||||
{NAME}
|
||||
```
|
||||
5. Offer to show transcript or provide download URL on request
|
||||
|
||||
### Two-Step Generation
|
||||
|
||||
1. **Step 1 — Submit text (foreground)**: `POST /podcast/episodes/text-content` → extract `episodeId`
|
||||
2. **Poll text (background)**: Use the exact `jq`-based polling loop above (substitute endpoint `podcast/episodes/text-content/{episodeId}` if needed), with `run_in_background: true` and `timeout: 600000`
|
||||
3. When notified, **save draft to a topic-based folder in cwd**:
|
||||
- Generate a topic slug following `shared/config-pattern.md` § Artifact Naming
|
||||
- Create `{slug}-podcast/` folder (dedup if exists)
|
||||
- Write `draft.md` (human-readable: `**{speakerName}**: {content}` per line)
|
||||
- Write `draft.json` (raw `scripts` array)
|
||||
- Present the draft location and content preview
|
||||
4. **STOP**: Present the draft and wait for explicit user approval
|
||||
5. **Step 2 — Submit audio (foreground, after approval)**:
|
||||
- No changes: `POST /podcast/episodes/{episodeId}/audio` with `{}`
|
||||
- With edits: `POST /podcast/episodes/{episodeId}/audio` with modified `{scripts: [...]}`
|
||||
6. **Poll audio (background)**: Same exact `jq`-based loop, `run_in_background: true`, `timeout: 600000`
|
||||
7. When notified, **download audio to the same folder**:
|
||||
- `curl -sS -o {slug}-podcast/podcast.mp3 {audioUrl}`
|
||||
- Present final result (same format as one-step, folder now has draft + final files)
|
||||
4. Offer to show transcript or provide download URL on request
|
||||
|
||||
### After Successful Generation
|
||||
|
||||
@@ -278,18 +252,17 @@ Update config with the choices made this session:
|
||||
NEW_CONFIG=$(echo "$CONFIG" | jq \
|
||||
--arg lang "{language}" \
|
||||
--arg mode "{mode}" \
|
||||
--arg method "{one-step/two-step}" \
|
||||
--argjson speakers '{"{language}": ["{speakerId}"]}' \
|
||||
'. + {"language": $lang, "defaultMode": $mode, "defaultMethod": $method, "defaultSpeakers": (.defaultSpeakers + $speakers)}')
|
||||
'. + {"language": $lang, "defaultMode": $mode, "defaultSpeakers": (.defaultSpeakers + $speakers)}')
|
||||
echo "$NEW_CONFIG" > "$CONFIG_PATH"
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- Speaker list: `shared/api-speakers.md`
|
||||
- Speaker list: `shared/cli-speakers.md`
|
||||
- Speaker selection guide: `shared/speaker-selection.md`
|
||||
- Episode creation: `shared/api-podcast.md`
|
||||
- Polling: `shared/common-patterns.md` § Async Polling
|
||||
- CLI patterns: `shared/cli-patterns.md`
|
||||
- CLI authentication: `shared/cli-authentication.md`
|
||||
- Config pattern: `shared/config-pattern.md`
|
||||
|
||||
## Composability
|
||||
@@ -303,20 +276,17 @@ echo "$NEW_CONFIG" > "$CONFIG_PATH"
|
||||
|
||||
**Agent workflow**:
|
||||
1. Detect: podcast request, topic = "latest AI developments", no references
|
||||
2. Infer: mode = "quick" (default), language = "en" (user wrote in English), 2 speakers (default), one-step (default)
|
||||
2. Infer: mode = "quick" (default), language = "en" (user wrote in English), 2 speakers (default)
|
||||
3. Show confirmation summary → user confirms
|
||||
|
||||
```bash
|
||||
curl -sS -X POST "https://api.marswave.ai/openapi/v1/podcast/episodes" \
|
||||
-H "Authorization: Bearer $LISTENHUB_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Source: skills" \
|
||||
-d '{
|
||||
"sources": [{"type": "text", "content": "The latest AI developments"}],
|
||||
"speakers": [{"speakerId": "cozy-man-english"}],
|
||||
"language": "en",
|
||||
"mode": "deep"
|
||||
}'
|
||||
listenhub podcast create \
|
||||
--query "The latest AI developments" \
|
||||
--mode deep \
|
||||
--lang en \
|
||||
--speaker "Mars" \
|
||||
--speaker "Mia" \
|
||||
--json
|
||||
```
|
||||
|
||||
Poll until complete, then present the result with title and listen link.
|
||||
Wait for CLI to return result, then present with title and listen link.
|
||||
|
||||
@@ -1,191 +0,0 @@
|
||||
# ListenHub API — Content Extract
|
||||
|
||||
**Authentication**: See [authentication.md](./authentication.md)
|
||||
|
||||
### POST /v1/content/extract
|
||||
|
||||
Create a content extraction task for a URL. Returns a `taskId` for polling.
|
||||
|
||||
**Request body:**
|
||||
|
||||
| Field | Required | Type | Description |
|
||||
|-------|----------|------|-------------|
|
||||
| source | **Yes** | object | Source to extract from |
|
||||
| source.type | **Yes** | string | Must be `"url"` |
|
||||
| source.uri | **Yes** | string | Valid HTTP(S) URL to extract content from |
|
||||
| options | No | object | Extraction options |
|
||||
| options.summarize | No | boolean | Whether to generate a summary |
|
||||
| options.maxLength | No | integer | Maximum content length |
|
||||
| options.twitter | No | object | Twitter/X specific options |
|
||||
| options.twitter.count | No | integer | Number of tweets to fetch (1-100, default 20) |
|
||||
|
||||
**curl (basic):**
|
||||
|
||||
```bash
|
||||
curl -sS -X POST "https://api.marswave.ai/openapi/v1/content/extract" \
|
||||
-H "Authorization: Bearer $LISTENHUB_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Source: skills" \
|
||||
-d '{
|
||||
"source": {
|
||||
"type": "url",
|
||||
"uri": "https://en.wikipedia.org/wiki/Topology"
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
**curl (with options):**
|
||||
|
||||
```bash
|
||||
curl -sS -X POST "https://api.marswave.ai/openapi/v1/content/extract" \
|
||||
-H "Authorization: Bearer $LISTENHUB_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Source: skills" \
|
||||
-d '{
|
||||
"source": {
|
||||
"type": "url",
|
||||
"uri": "https://x.com/elonmusk"
|
||||
},
|
||||
"options": {
|
||||
"summarize": true,
|
||||
"maxLength": 5000,
|
||||
"twitter": {
|
||||
"count": 50
|
||||
}
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"taskId": "69a7dac700cf95938f86d9bb"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Error codes:**
|
||||
|
||||
| Code | Meaning |
|
||||
|------|---------|
|
||||
| 29003 | Validation error (`"source.uri" is required`, `"source.uri" must be a valid uri`) |
|
||||
| 21007 | Invalid API key |
|
||||
|
||||
### GET /v1/content/extract/{taskId}
|
||||
|
||||
Get extraction task status and results.
|
||||
|
||||
**Path params:**
|
||||
|
||||
| Param | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| taskId | string | 24-char hex task ID |
|
||||
|
||||
**curl:**
|
||||
|
||||
```bash
|
||||
curl -sS "https://api.marswave.ai/openapi/v1/content/extract/69a7dac700cf95938f86d9bb" \
|
||||
-H "Authorization: Bearer $LISTENHUB_API_KEY" \
|
||||
-H "X-Source: skills"
|
||||
```
|
||||
|
||||
**Response (processing):**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"taskId": "69a7dac700cf95938f86d9bb",
|
||||
"status": "processing",
|
||||
"createdAt": "2025-04-09T12:00:00Z",
|
||||
"data": null,
|
||||
"credits": 0,
|
||||
"failCode": null,
|
||||
"message": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response (completed):**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"taskId": "69a7dac700cf95938f86d9bb",
|
||||
"status": "completed",
|
||||
"createdAt": "2025-04-09T12:00:00Z",
|
||||
"data": {
|
||||
"content": "Extracted text content...",
|
||||
"metadata": {
|
||||
"title": "Article Title",
|
||||
"author": "Author Name",
|
||||
"publishedAt": "2025-04-01T08:00:00Z"
|
||||
},
|
||||
"references": [
|
||||
"https://example.com/related-article"
|
||||
]
|
||||
},
|
||||
"credits": 5,
|
||||
"failCode": null,
|
||||
"message": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response (failed):**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "success",
|
||||
"data": {
|
||||
"taskId": "69a7dac700cf95938f86d9bb",
|
||||
"status": "failed",
|
||||
"createdAt": "2025-04-09T12:00:00Z",
|
||||
"data": null,
|
||||
"credits": 0,
|
||||
"failCode": "EXTRACT_FAILED",
|
||||
"message": "Unable to extract content from the provided URL"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key fields:**
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| status | string | `processing`, `completed`, or `failed` |
|
||||
| data.data.content | string | Extracted text content |
|
||||
| data.data.metadata | object | Page metadata (title, author, publishedAt) |
|
||||
| data.data.references | array | Referenced URLs (array of strings) |
|
||||
| credits | integer | Credits consumed |
|
||||
| failCode | string | Error code (null on success) |
|
||||
| message | string | Error message (null on success) |
|
||||
|
||||
**Error codes:**
|
||||
|
||||
| Code | Meaning |
|
||||
|------|---------|
|
||||
| 29003 | Invalid taskId format |
|
||||
| 25002 | Task not found |
|
||||
|
||||
**Supported URL types:**
|
||||
|
||||
| Category | Platforms |
|
||||
|----------|----------|
|
||||
| Video | YouTube, Bilibili |
|
||||
| Social | Twitter/X (profiles and single tweets), WeChat articles |
|
||||
| Documents | PDF, DOCX (direct URLs) |
|
||||
| Images | JPEG, PNG, etc. (direct URLs) |
|
||||
| Web | Any general web page (Wikipedia, arXiv, GitHub, etc.) |
|
||||
|
||||
**Twitter/X notes:**
|
||||
- For profile URLs (e.g. `https://x.com/username`), use `options.twitter.count` to control tweet count (1-100, default 20)
|
||||
- This option is ignored for non-Twitter URLs
|
||||
@@ -1,154 +0,0 @@
|
||||
# ListenHub API — Image Generation
|
||||
|
||||
**Base URL**: `https://api.marswave.ai/openapi/v1`
|
||||
**Authentication**: Bearer `$LISTENHUB_API_KEY` (same key, different host)
|
||||
|
||||
## POST /images/generation
|
||||
|
||||
Generate an AI image from a text prompt. Synchronous — returns base64-encoded image data directly (no polling needed).
|
||||
|
||||
**Request body:**
|
||||
|
||||
| Field | Required | Type | Description |
|
||||
|-------|----------|------|-------------|
|
||||
| provider | Yes | string | Model provider. Use `"google"` |
|
||||
| prompt | Yes | string | Image description (English recommended) |
|
||||
| model | No | string | `"gemini-3-pro-image-preview"` (default) or `"gemini-3.1-flash-image-preview"` |
|
||||
| imageConfig | No | object | Size and aspect ratio config |
|
||||
| imageConfig.imageSize | No | string | `"1K"`, `"2K"` (default), or `"4K"` |
|
||||
| imageConfig.aspectRatio | No | string | `"1:1"` (default). See aspect ratio table below. |
|
||||
| referenceImages | No | array | Up to 14 reference images for style guidance (see format below) |
|
||||
|
||||
**Aspect ratios:**
|
||||
|
||||
| Ratio | Description | Models |
|
||||
|-------|-------------|--------|
|
||||
| 1:1 | Square | All |
|
||||
| 2:3 | Portrait photo | All |
|
||||
| 3:2 | Landscape photo | All |
|
||||
| 3:4 | Poster portrait | All |
|
||||
| 4:3 | Traditional landscape | All |
|
||||
| 9:16 | Portrait / phone | All |
|
||||
| 16:9 | Landscape / widescreen | All |
|
||||
| 21:9 | Ultrawide | All |
|
||||
| 1:4 | Narrow portrait | gemini-3.1-flash-image-preview only |
|
||||
| 4:1 | Wide landscape | gemini-3.1-flash-image-preview only |
|
||||
| 1:8 | Extreme narrow portrait | gemini-3.1-flash-image-preview only |
|
||||
| 8:1 | Panoramic | gemini-3.1-flash-image-preview only |
|
||||
|
||||
**referenceImages format:**
|
||||
|
||||
Each item must have either `fileData` (URL) or `inlineData` (base64), not both. You can mix URL and base64 items in the same array.
|
||||
|
||||
*URL-based reference:*
|
||||
|
||||
```json
|
||||
{
|
||||
"fileData": {
|
||||
"fileUri": "https://example.com/photo.png",
|
||||
"mimeType": "image/png"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Infer `mimeType` from URL suffix: `.jpg`/`.jpeg` → `image/jpeg`, `.png` → `image/png`, `.webp` → `image/webp`, `.gif` → `image/gif`
|
||||
|
||||
*Base64 reference (inline):*
|
||||
|
||||
```json
|
||||
{
|
||||
"inlineData": {
|
||||
"data": "<base64-encoded-image>",
|
||||
"mimeType": "image/png"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Supported mimeTypes: `image/png`, `image/jpeg`, `image/webp`, `image/heic`, `image/heif`
|
||||
|
||||
To encode a local file as base64:
|
||||
|
||||
```bash
|
||||
# macOS
|
||||
BASE64_REF=$(base64 -i /path/to/image.png)
|
||||
|
||||
# Linux
|
||||
BASE64_REF=$(base64 -w 0 /path/to/image.png)
|
||||
```
|
||||
|
||||
**Constraints:**
|
||||
- Use `--max-time 600` (generation can take up to 10 minutes)
|
||||
- On 429 (rate limit): wait 15s and retry. Max 3 retries.
|
||||
|
||||
**curl (text-only):**
|
||||
|
||||
```bash
|
||||
RESPONSE=$(curl -sS -X POST "https://api.marswave.ai/openapi/v1/images/generation" \
|
||||
-H "Authorization: Bearer $LISTENHUB_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Source: skills" \
|
||||
--max-time 600 \
|
||||
-d '{
|
||||
"provider": "google",
|
||||
"model": "gemini-3-pro-image-preview",
|
||||
"prompt": "cyberpunk city at night, neon lights, highly detailed",
|
||||
"imageConfig": {"imageSize": "2K", "aspectRatio": "16:9"}
|
||||
}')
|
||||
```
|
||||
|
||||
**curl (with base64 reference image):**
|
||||
|
||||
```bash
|
||||
BASE64_REF=$(base64 -i /path/to/reference.png)
|
||||
|
||||
RESPONSE=$(curl -sS -X POST "https://api.marswave.ai/openapi/v1/images/generation" \
|
||||
-H "Authorization: Bearer $LISTENHUB_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
--max-time 600 \
|
||||
-d "{
|
||||
\"provider\": \"google\",
|
||||
\"model\": \"gemini-3-pro-image-preview\",
|
||||
\"prompt\": \"cyberpunk city at night\",
|
||||
\"imageConfig\": {\"imageSize\": \"2K\", \"aspectRatio\": \"16:9\"},
|
||||
\"referenceImages\": [{\"inlineData\": {\"data\": \"$BASE64_REF\", \"mimeType\": \"image/png\"}}]
|
||||
}")
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"candidates": [
|
||||
{
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"inlineData": {
|
||||
"data": "<base64-encoded-jpeg>",
|
||||
"mimeType": "image/jpeg"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Extract base64 data:**
|
||||
|
||||
```bash
|
||||
BASE64_DATA=$(echo "$RESPONSE" | jq -r '.candidates[0].content.parts[0].inlineData.data // .data')
|
||||
```
|
||||
|
||||
**Save to file (macOS):**
|
||||
|
||||
```bash
|
||||
echo "$BASE64_DATA" | base64 -D > ~/Downloads/listenhub-$(date +%Y%m%d-%H%M%S)-0001.jpg
|
||||
```
|
||||
|
||||
**Save to file (Linux):**
|
||||
|
||||
```bash
|
||||
echo "$BASE64_DATA" | base64 -d > ~/Downloads/listenhub-$(date +%Y%m%d-%H%M%S)-0001.jpg
|
||||
```
|
||||
@@ -1,134 +0,0 @@
|
||||
# ListenHub API — Podcast
|
||||
|
||||
**Base URL**: `https://api.marswave.ai/openapi/v1`
|
||||
**Authentication**: See [authentication.md](./authentication.md)
|
||||
|
||||
## Podcast
|
||||
|
||||
### POST /podcast/episodes
|
||||
|
||||
Create a podcast episode.
|
||||
|
||||
**Request body:**
|
||||
|
||||
| Field | Required | Type | Description |
|
||||
|-------|----------|------|-------------|
|
||||
| speakers | **Yes** | array | 1-2 speaker objects `[{speakerId: "..."}]` |
|
||||
| query | No | string | Topic or prompt text |
|
||||
| sources | No | array | Content sources (see Sources format below) |
|
||||
| language | No | string | `en` or `zh` |
|
||||
| mode | No | string | `deep` or `quick` |
|
||||
|
||||
**Sources format:**
|
||||
|
||||
```json
|
||||
[
|
||||
{"type": "url", "content": "https://example.com/article"},
|
||||
{"type": "text", "content": "Topic description or reference text..."}
|
||||
]
|
||||
```
|
||||
|
||||
**Constraints:**
|
||||
- Max 2 speakers
|
||||
|
||||
**curl:**
|
||||
|
||||
```bash
|
||||
curl -sS -X POST "https://api.marswave.ai/openapi/v1/podcast/episodes" \
|
||||
-H "Authorization: Bearer $LISTENHUB_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Source: skills" \
|
||||
-d '{
|
||||
"query": "The future of AI development",
|
||||
"sources": [{"type": "text", "content": "Reference material about AI trends"}],
|
||||
"speakers": [{"speakerId": "cozy-man-english"}],
|
||||
"language": "en",
|
||||
"mode": "deep"
|
||||
}'
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "",
|
||||
"data": {
|
||||
"episodeId": "688c9a27348f001e707ba331"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### GET /podcast/episodes/{episodeId}
|
||||
|
||||
Get podcast episode details and status.
|
||||
|
||||
**Path params:**
|
||||
|
||||
| Param | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| episodeId | string | 24-char hex episode ID |
|
||||
|
||||
**curl:**
|
||||
|
||||
```bash
|
||||
curl -sS "https://api.marswave.ai/openapi/v1/podcast/episodes/688c9a27348f001e707ba331" \
|
||||
-H "Authorization: Bearer $LISTENHUB_API_KEY" \
|
||||
-H "X-Source: skills"
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "",
|
||||
"data": {
|
||||
"episodeId": "688c9a27348f001e707ba331",
|
||||
"createdAt": 1718230400,
|
||||
"credits": 10,
|
||||
"message": "success",
|
||||
"failCode": 0,
|
||||
"processStatus": "success",
|
||||
"completedTime": 1718230400,
|
||||
"sourceProcessResult": {
|
||||
"content": "User-provided source text",
|
||||
"references": [
|
||||
{
|
||||
"type": "url",
|
||||
"urlCitation": {
|
||||
"title": "Reference Title",
|
||||
"url": "https://example.com/reference",
|
||||
"favicon": "https://example.com/favicon.ico"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"title": "My Podcast Title",
|
||||
"outline": "This is the podcast outline.",
|
||||
"cover": "https://example.com/cover.jpg",
|
||||
"audioUrl": "https://gcs.example.com/audio.mp3",
|
||||
"audioStreamUrl": "https://gcs.example.com/audio_stream.m3u8",
|
||||
"scripts": [
|
||||
{
|
||||
"speakerId": "speaker-1",
|
||||
"speakerName": "Host A",
|
||||
"content": "This is the first segment"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key fields:**
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| processStatus | string | `pending`, `success`, or `failed` |
|
||||
| audioUrl | string | Direct audio download URL |
|
||||
| audioStreamUrl | string | M3U8 streaming URL |
|
||||
| scripts | array | Script segments with speaker info and text |
|
||||
| title | string | Generated episode title |
|
||||
| outline | string | Generated outline |
|
||||
| cover | string | Cover image URL |
|
||||
| credits | integer | Credits consumed |
|
||||
@@ -1,53 +0,0 @@
|
||||
# ListenHub API — Speakers
|
||||
|
||||
**Base URL**: `https://api.marswave.ai/openapi/v1`
|
||||
**Authentication**: See [authentication.md](./authentication.md)
|
||||
|
||||
## GET /speakers/list
|
||||
|
||||
Get available voice speakers, optionally filtered by language.
|
||||
|
||||
**Parameters (query string):**
|
||||
|
||||
| Param | Required | Type | Description |
|
||||
|-------|----------|------|-------------|
|
||||
| language | No | string | Filter by language: `zh` or `en` |
|
||||
| status | No | integer | Speaker status: `1` (active, default) or `2` |
|
||||
|
||||
**curl:**
|
||||
|
||||
```bash
|
||||
curl -sS "https://api.marswave.ai/openapi/v1/speakers/list?language=en" \
|
||||
-H "Authorization: Bearer $LISTENHUB_API_KEY" \
|
||||
-H "X-Source: skills"
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "",
|
||||
"data": {
|
||||
"items": [
|
||||
{
|
||||
"name": "Yuanye",
|
||||
"speakerId": "cozy-man-english",
|
||||
"demoAudioUrl": "https://example.com/demo.mp3",
|
||||
"gender": "male",
|
||||
"language": "en"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Fields:**
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| name | string | Display name |
|
||||
| speakerId | string | ID to pass to creation endpoints |
|
||||
| demoAudioUrl | string | Preview audio URL |
|
||||
| gender | string | `male` or `female` |
|
||||
| language | string | `zh` or `en` |
|
||||
@@ -1,163 +0,0 @@
|
||||
# ListenHub API — Storybook
|
||||
|
||||
**Base URL**: `https://api.marswave.ai/openapi/v1`
|
||||
**Authentication**: See [authentication.md](./authentication.md)
|
||||
|
||||
Used by:
|
||||
- `/explainer` skill — mode=`info` (factual/informational) or mode=`story` (narrative)
|
||||
- `/slides` skill — mode=`slides` (PPT-style presentation)
|
||||
|
||||
---
|
||||
|
||||
## POST /v1/storybook/episodes
|
||||
|
||||
Create a storybook episode. Returns an `episodeId` immediately; generation runs asynchronously.
|
||||
|
||||
**Request body:**
|
||||
|
||||
| Field | Required | Type | Description |
|
||||
|-------|----------|------|-------------|
|
||||
| sources | **Yes** | array | Exactly 1 source object |
|
||||
| sources[].type | **Yes** | string | `"text"` or `"url"` |
|
||||
| sources[].content | **Yes** | string | Topic text or URL |
|
||||
| speakers | **Yes** | array | Exactly 1 speaker: `[{"speakerId": "..."}]` |
|
||||
| language | No | string | `"en"` or `"zh"` |
|
||||
| mode | No | string | `"info"` (explainer), `"story"`, or `"slides"` (default: `"info"`) |
|
||||
| style | No | string | Visual style hint (optional, free text) |
|
||||
|
||||
**Constraints:**
|
||||
- Exactly 1 source
|
||||
- Max 1 speaker
|
||||
|
||||
**curl:**
|
||||
|
||||
```bash
|
||||
curl -sS -X POST "https://api.marswave.ai/openapi/v1/storybook/episodes" \
|
||||
-H "Authorization: Bearer $LISTENHUB_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Source: skills" \
|
||||
-d '{
|
||||
"sources": [{"type": "text", "content": "The history of the Roman Empire"}],
|
||||
"speakers": [{"speakerId": "cozy-man-english"}],
|
||||
"language": "en",
|
||||
"mode": "slides"
|
||||
}'
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "",
|
||||
"data": {
|
||||
"episodeId": "688c9a27348f001e707ba331"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## GET /v1/storybook/episodes/{episodeId}
|
||||
|
||||
Get storybook episode status and result.
|
||||
|
||||
**Path params:**
|
||||
|
||||
| Param | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| episodeId | string | 24-char hex episode ID |
|
||||
|
||||
**curl:**
|
||||
|
||||
```bash
|
||||
curl -sS "https://api.marswave.ai/openapi/v1/storybook/episodes/688c9a27348f001e707ba331" \
|
||||
-H "Authorization: Bearer $LISTENHUB_API_KEY" \
|
||||
-H "X-Source: skills"
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "",
|
||||
"data": {
|
||||
"episodeId": "688c9a27348f001e707ba331",
|
||||
"createdAt": 1718230400,
|
||||
"mode": "slides",
|
||||
"processStatus": "success",
|
||||
"completedTime": 1718230450,
|
||||
"credits": 10,
|
||||
"message": "success",
|
||||
"failCode": 0,
|
||||
"title": "The Roman Empire",
|
||||
"cover": "https://example.com/cover.jpg",
|
||||
"audioUrl": "https://gcs.example.com/audio.mp3",
|
||||
"audioDuration": 120,
|
||||
"videoUrl": null,
|
||||
"videoStatus": "not_generated",
|
||||
"pages": [
|
||||
{
|
||||
"text": "The Roman Empire began in 27 BC...",
|
||||
"pageNumber": 1,
|
||||
"imageUrl": "https://example.com/page1.jpg",
|
||||
"audioTimestamp": 0
|
||||
}
|
||||
],
|
||||
"sourceProcessResult": {
|
||||
"query": "The history of the Roman Empire",
|
||||
"content": "Processed source text...",
|
||||
"imageSources": []
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key fields:**
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| processStatus | string | `"pending"`, `"success"`, or `"failed"` |
|
||||
| mode | string | `"info"`, `"story"`, or `"slides"` |
|
||||
| pages | array | Slide pages — each has `text`, `pageNumber`, `imageUrl`, `audioTimestamp` |
|
||||
| audioUrl | string | Narration audio URL |
|
||||
| audioDuration | number | Audio length in seconds |
|
||||
| videoUrl | string | Video URL (null until generated via video endpoint) |
|
||||
| videoStatus | string | `"not_generated"`, `"pending"`, `"success"`, `"failed"` |
|
||||
| credits | integer | Credits consumed |
|
||||
| failCode | number | Non-zero on failure |
|
||||
|
||||
---
|
||||
|
||||
## POST /v1/storybook/episodes/{episodeId}/video
|
||||
|
||||
Trigger video generation for a completed storybook episode. Video combines the page images with narration audio.
|
||||
|
||||
**Path params:**
|
||||
|
||||
| Param | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| episodeId | string | 24-char hex episode ID (must be `processStatus=success`) |
|
||||
|
||||
**curl:**
|
||||
|
||||
```bash
|
||||
curl -sS -X POST "https://api.marswave.ai/openapi/v1/storybook/episodes/688c9a27348f001e707ba331/video" \
|
||||
-H "Authorization: Bearer $LISTENHUB_API_KEY" \
|
||||
-H "X-Source: skills"
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "",
|
||||
"data": {
|
||||
"success": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
After calling this endpoint, poll `GET /v1/storybook/episodes/{episodeId}` and wait for `videoStatus=success`. Then `videoUrl` will contain the video URL.
|
||||
@@ -1,95 +0,0 @@
|
||||
# ListenHub API — TTS
|
||||
|
||||
**Base URL**: `https://api.marswave.ai/openapi/v1`
|
||||
**Authentication**: See [authentication.md](./authentication.md)
|
||||
|
||||
---
|
||||
|
||||
## POST /v1/tts
|
||||
|
||||
Low-latency single-voice TTS. Returns a **streaming binary MP3** — not JSON.
|
||||
|
||||
**Request body:**
|
||||
|
||||
| Field | Required | Type | Description |
|
||||
|-------|----------|------|-------------|
|
||||
| input | Yes | string | Text to convert |
|
||||
| voice | Yes | string | Speaker ID (`speakerId` from speakers API) |
|
||||
| model | No | string | Model name, defaults to `flowtts` |
|
||||
|
||||
**curl:**
|
||||
|
||||
```bash
|
||||
curl -sS -X POST "https://api.marswave.ai/openapi/v1/tts" \
|
||||
-H "Authorization: Bearer $LISTENHUB_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Source: skills" \
|
||||
-d '{
|
||||
"input": "Hello, welcome to ListenHub.",
|
||||
"voice": "EN-Man-General-01"
|
||||
}' \
|
||||
--output /tmp/tts-output.mp3
|
||||
```
|
||||
|
||||
**Response:** Binary MP3 audio stream. On error, falls back to a JSON error object (check HTTP status code first).
|
||||
|
||||
**Key constraints:**
|
||||
- Max ~10,000 characters for `input`
|
||||
- `voice` must be a valid `speakerId` from `GET /speakers/list`
|
||||
|
||||
---
|
||||
|
||||
## POST /v1/speech
|
||||
|
||||
Multi-speaker script-to-audio. Each script segment uses a different voice. Returns audio URL **synchronously**.
|
||||
|
||||
**Request body:**
|
||||
|
||||
| Field | Required | Type | Description |
|
||||
|-------|----------|------|-------------|
|
||||
| scripts | Yes | array | Ordered array of script segments |
|
||||
| scripts[].content | Yes | string | Text for this segment |
|
||||
| scripts[].speakerId | Yes | string | Speaker ID for this segment |
|
||||
| title | No | string | Custom title (auto-generated if omitted) |
|
||||
|
||||
**curl:**
|
||||
|
||||
```bash
|
||||
curl -sS -X POST "https://api.marswave.ai/openapi/v1/speech" \
|
||||
-H "Authorization: Bearer $LISTENHUB_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Source: skills" \
|
||||
-d '{
|
||||
"scripts": [
|
||||
{"content": "Welcome everyone.", "speakerId": "EN-Man-General-01"},
|
||||
{"content": "Today we discuss an interesting topic.", "speakerId": "EN-Woman-General-01"},
|
||||
{"content": "Let us begin.", "speakerId": "EN-Man-General-01"}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "",
|
||||
"data": {
|
||||
"audioUrl": "https://assets.listenhub.ai/listenhub-public-prod/podcast/example.mp3",
|
||||
"audioDuration": 12500,
|
||||
"subtitlesUrl": "https://assets.listenhub.ai/listenhub-public-prod/podcast/example.srt",
|
||||
"taskId": "1eed39d387a046c0a1213e6b8f139d77",
|
||||
"credits": 12
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response fields:**
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| audioUrl | string | MP3 audio file URL |
|
||||
| audioDuration | integer | Duration in milliseconds |
|
||||
| subtitlesUrl | string | SRT subtitle file URL |
|
||||
| taskId | string | Task identifier |
|
||||
| credits | integer | Credits consumed |
|
||||
@@ -1,59 +0,0 @@
|
||||
# Authentication
|
||||
|
||||
## API Key
|
||||
|
||||
All ListenHub API calls require a valid API key.
|
||||
|
||||
**Environment variable**: `LISTENHUB_API_KEY`
|
||||
|
||||
Store in `~/.zshrc` (macOS) or `~/.bashrc` (Linux):
|
||||
|
||||
```bash
|
||||
export LISTENHUB_API_KEY="lh_sk_..."
|
||||
```
|
||||
|
||||
Reload after adding:
|
||||
|
||||
```bash
|
||||
source ~/.zshrc
|
||||
```
|
||||
|
||||
**How to obtain**: Visit https://listenhub.ai/settings/api-keys (Pro plan required).
|
||||
|
||||
## Base URLs
|
||||
|
||||
| Service | Base URL |
|
||||
|---------|----------|
|
||||
| ListenHub API | `https://api.marswave.ai/openapi/v1` |
|
||||
| Image Generation | `https://api.marswave.ai/openapi/v1` |
|
||||
| Staging (ListenHub) | `https://staging-api.marswave.ai/openapi/v1` |
|
||||
|
||||
## Required Headers
|
||||
|
||||
Every request must include:
|
||||
|
||||
```
|
||||
Authorization: Bearer $LISTENHUB_API_KEY
|
||||
Content-Type: application/json
|
||||
X-Source: skills
|
||||
```
|
||||
|
||||
The `X-Source: skills` header identifies requests as coming from Claude Code skills (CLI tool), distinguishing them from `openapi` (web) or other sources on the server side.
|
||||
|
||||
## curl Template
|
||||
|
||||
```bash
|
||||
curl -sS -X POST "https://api.marswave.ai/openapi/v1/{endpoint}" \
|
||||
-H "Authorization: Bearer $LISTENHUB_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Source: skills" \
|
||||
-d '{ ... }'
|
||||
```
|
||||
|
||||
For GET requests, omit `-d` and change `-X POST` to `-X GET`.
|
||||
|
||||
## Security Notes
|
||||
|
||||
- Never log or display full API keys in output
|
||||
- API keys are transmitted via HTTPS only
|
||||
- Do not pass sensitive or confidential information as content input — it is sent to external APIs for processing
|
||||
@@ -0,0 +1,65 @@
|
||||
# CLI Authentication
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Node.js >= 20**
|
||||
- **ListenHub CLI** (auto-installed if missing)
|
||||
|
||||
## Auth Check
|
||||
|
||||
Run this before any CLI operation. The check handles both installation and login automatically — never ask the user to run install commands manually.
|
||||
|
||||
```bash
|
||||
# 1. Auto-install if missing
|
||||
if ! command -v listenhub &>/dev/null; then
|
||||
npm install -g @marswave/listenhub-cli
|
||||
fi
|
||||
|
||||
# 2. Verify install succeeded
|
||||
if ! command -v listenhub &>/dev/null; then
|
||||
echo "INSTALL_FAILED"
|
||||
# Stop here — tell user their Node.js/npm setup needs attention
|
||||
fi
|
||||
|
||||
# 3. Check for updates (skip if just installed)
|
||||
CURRENT_V=$(listenhub --version 2>/dev/null)
|
||||
LATEST_V=$(npm view @marswave/listenhub-cli version 2>/dev/null)
|
||||
if [ -n "$CURRENT_V" ] && [ -n "$LATEST_V" ] && [ "$CURRENT_V" != "$LATEST_V" ]; then
|
||||
npm install -g @marswave/listenhub-cli
|
||||
fi
|
||||
|
||||
# 4. Check auth
|
||||
AUTH=$(listenhub auth status --json 2>/dev/null)
|
||||
AUTHED=$(echo "$AUTH" | jq -r '.authenticated // false')
|
||||
```
|
||||
|
||||
### If install fails
|
||||
|
||||
If `npm install -g` fails (e.g., permission issues, Node.js not available), tell the user:
|
||||
|
||||
> ListenHub CLI auto-install failed. Please check your Node.js (>= 20) and npm setup, then retry.
|
||||
|
||||
Do **not** ask them to run `npm install -g @marswave/listenhub-cli` manually — diagnose the issue first (permissions, PATH, Node version).
|
||||
|
||||
### If not logged in
|
||||
|
||||
If `.authenticated` is `false`, run `listenhub auth login` directly — this opens the browser for OAuth. Wait for completion, then re-check auth status.
|
||||
|
||||
```bash
|
||||
if [ "$AUTHED" != "true" ]; then
|
||||
listenhub auth login
|
||||
# Re-verify after login
|
||||
AUTH=$(listenhub auth status --json 2>/dev/null)
|
||||
AUTHED=$(echo "$AUTH" | jq -r '.authenticated // false')
|
||||
fi
|
||||
```
|
||||
|
||||
### If update check is slow
|
||||
|
||||
The `npm view` call is typically fast (< 2s). If it fails (network issues, npm registry down), silently skip the update and proceed with the installed version. Never block the user on a version check failure.
|
||||
|
||||
## Security
|
||||
|
||||
- Credentials are stored at `~/.config/listenhub/credentials.json` (file mode `0600`)
|
||||
- Tokens refresh automatically — no manual rotation needed
|
||||
- Never log or display tokens in output
|
||||
@@ -0,0 +1,187 @@
|
||||
# CLI Patterns
|
||||
|
||||
Reusable patterns for all skills that use the ListenHub CLI.
|
||||
|
||||
<HARD-GATE>
|
||||
**Language Adaptation**: Always respond in the user's language. Chinese input -> Chinese output. English input -> English output. Mixed -> follow dominant language. This applies to all UI text, questions, confirmations, and error messages.
|
||||
</HARD-GATE>
|
||||
|
||||
## Command Pattern
|
||||
|
||||
```bash
|
||||
listenhub <command> create [options] --json
|
||||
```
|
||||
|
||||
All creation commands follow this shape. The `--json` flag ensures machine-readable output for parsing with jq.
|
||||
|
||||
## Execution Modes
|
||||
|
||||
### Synchronous (default)
|
||||
|
||||
The CLI blocks until the task completes and returns the final result:
|
||||
|
||||
```bash
|
||||
RESULT=$(listenhub podcast create --topic "AI trends" --lang zh --json)
|
||||
echo "$RESULT" | jq -r '.audioUrl'
|
||||
```
|
||||
|
||||
This is the simplest approach. Use it when the expected duration is short or when you want to wait for the result.
|
||||
|
||||
### Async with `--no-wait`
|
||||
|
||||
Returns a creation ID immediately without waiting for completion:
|
||||
|
||||
```bash
|
||||
RESULT=$(listenhub podcast create --topic "AI trends" --lang zh --no-wait --json)
|
||||
ID=$(echo "$RESULT" | jq -r '.id')
|
||||
echo "Submitted: $ID"
|
||||
```
|
||||
|
||||
Check status later:
|
||||
|
||||
```bash
|
||||
listenhub creation get "$ID" --json
|
||||
```
|
||||
|
||||
The `.status` field will be one of: `processing`, `completed`, `failed`.
|
||||
|
||||
### Timeout Reference
|
||||
|
||||
| Content type | Default timeout |
|
||||
|-------------|----------------|
|
||||
| podcast | 300s |
|
||||
| tts | 300s |
|
||||
| explainer | 600s |
|
||||
| slides | 600s |
|
||||
| image | 120s |
|
||||
| music | 600s |
|
||||
|
||||
### Background Execution
|
||||
|
||||
For long-running commands, use the Bash tool's `run_in_background: true` parameter. This keeps the terminal responsive while the CLI waits for completion.
|
||||
|
||||
**Two-step pattern:**
|
||||
|
||||
1. **Submit (foreground)** with `--no-wait` to get the ID. Tell the user the task is submitted.
|
||||
2. **Poll (background)** with `run_in_background: true`:
|
||||
|
||||
```bash
|
||||
# Run with run_in_background: true
|
||||
ID="<id-from-step-1>"
|
||||
for i in $(seq 1 60); do
|
||||
RESULT=$(listenhub creation get "$ID" --json 2>/dev/null)
|
||||
STATUS=$(echo "$RESULT" | jq -r '.status // "processing"')
|
||||
|
||||
case "$STATUS" in
|
||||
completed) echo "$RESULT"; exit 0 ;;
|
||||
failed) echo "FAILED: $RESULT" >&2; exit 1 ;;
|
||||
*) sleep 10 ;;
|
||||
esac
|
||||
done
|
||||
echo "TIMEOUT" >&2; exit 2
|
||||
```
|
||||
|
||||
When the background task finishes, you will be notified with the output. Parse the result and present it to the user. If the task failed or timed out, report the error.
|
||||
|
||||
## JSON Output Parsing
|
||||
|
||||
All CLI commands with `--json` produce structured JSON. Parse with jq:
|
||||
|
||||
```bash
|
||||
RESULT=$(listenhub tts create --text "Hello" --lang en --json)
|
||||
AUDIO_URL=$(echo "$RESULT" | jq -r '.audioUrl')
|
||||
STATUS=$(echo "$RESULT" | jq -r '.status')
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Exit Codes
|
||||
|
||||
| Code | Meaning | Action |
|
||||
|------|---------|--------|
|
||||
| 0 | Success | Parse JSON output |
|
||||
| 1 | General error | Check stderr for details |
|
||||
| 2 | Auth error | Run `listenhub auth login` |
|
||||
| 3 | Timeout | Retry or use `--no-wait` |
|
||||
|
||||
### Common Errors
|
||||
|
||||
| Error | Cause | Fix |
|
||||
|-------|-------|-----|
|
||||
| `command not found: listenhub` | CLI not installed | Auto-install: run `npm install -g @marswave/listenhub-cli` silently, then retry |
|
||||
| `Not authenticated` | Not logged in | Auto-login: run `listenhub auth login` directly |
|
||||
| `Insufficient credits` | Account has no credits | Tell user to recharge at listenhub.ai |
|
||||
| `Rate limited` | Too many requests | Wait and retry |
|
||||
| `Invalid speaker` | Speaker ID not found | Re-query speakers list |
|
||||
| `Request timeout` | Generation took too long | Retry or use `--no-wait` for async |
|
||||
|
||||
### Error Checking Pattern
|
||||
|
||||
```bash
|
||||
RESULT=$(listenhub podcast create --topic "AI" --lang zh --json 2>/tmp/lh-err)
|
||||
EXIT_CODE=$?
|
||||
|
||||
if [ $EXIT_CODE -ne 0 ]; then
|
||||
ERROR=$(cat /tmp/lh-err)
|
||||
case $EXIT_CODE in
|
||||
2) echo "Auth error: run 'listenhub auth login'" ;;
|
||||
3) echo "Timeout: try --no-wait" ;;
|
||||
*) echo "Error: $ERROR" ;;
|
||||
esac
|
||||
rm -f /tmp/lh-err
|
||||
# Handle error appropriately
|
||||
fi
|
||||
rm -f /tmp/lh-err
|
||||
```
|
||||
|
||||
## Interactive Parameter Collection
|
||||
|
||||
Skills must use the **AskUserQuestion tool** for all enumerable parameters, following a **conversational, step-by-step** approach. This renders an interactive picker in the terminal that users can navigate with arrow keys.
|
||||
|
||||
### Conversation Behavior (mandatory)
|
||||
|
||||
1. **One question at a time.** Ask a single question, then STOP and wait for the user's answer before proceeding to the next step. Do not batch multiple steps into one message unless the parameters are explicitly independent (e.g., resolution + aspect ratio).
|
||||
2. **Wait for the answer.** Never assume a default and skip ahead. If the user hasn't answered, do not proceed.
|
||||
3. **Confirm before executing.** After all parameters are collected, summarize the choices and ask the user to confirm before running any CLI command. This is the final gate.
|
||||
4. **Be ready to go back.** If the user changes their mind or says something doesn't look right, revise and re-ask instead of pushing forward.
|
||||
|
||||
### How to Ask
|
||||
|
||||
**Always use the AskUserQuestion tool** -- do NOT print questions as plain text. Each step's `Question` and `Options` map directly to AskUserQuestion parameters:
|
||||
|
||||
```
|
||||
Step definition in SKILL.md: -> AskUserQuestion tool call:
|
||||
|
||||
Question: "What language?" -> question: "What language?"
|
||||
- "Chinese (zh)" -- Mandarin -> options: [{label: "Chinese (zh)", description: "Mandarin"}
|
||||
- "English (en)" -- English -> {label: "English (en)", description: "English"}]
|
||||
```
|
||||
|
||||
For **free text** steps (topic, URL, prompt), just ask the question in a normal text message and wait for the user to type their answer.
|
||||
|
||||
### Parameter Types
|
||||
|
||||
- **Multiple-choice -> AskUserQuestion**: language, mode, speaker count, generation style, resolution, aspect ratio
|
||||
- **Free text -> normal message**: topic, content body, URL, image prompt
|
||||
- **Sequential when dependent**: e.g., speaker list depends on language choice -- ask language first, then fetch speakers and present list
|
||||
- **Batch when independent**: e.g., resolution + aspect ratio can be asked together in one AskUserQuestion call (multiple questions)
|
||||
- **Options include descriptions**: not just labels -- explain what each choice means
|
||||
|
||||
## Long Text Input
|
||||
|
||||
When text content is long (e.g., a full article for TTS), passing it inline may hit shell argument length limits. Write to a temp file and use shell substitution:
|
||||
|
||||
```bash
|
||||
# Write content to temp file
|
||||
cat > /tmp/lh-content.txt << 'ENDCONTENT'
|
||||
Very long text content goes here...
|
||||
ENDCONTENT
|
||||
|
||||
# Use shell substitution to pass the file content
|
||||
listenhub tts create --text "$(cat /tmp/lh-content.txt)" --lang zh --json
|
||||
|
||||
# Clean up
|
||||
rm -f /tmp/lh-content.txt
|
||||
```
|
||||
|
||||
**When to use temp files**: Always use this approach when text content exceeds a few KB.
|
||||
@@ -0,0 +1,78 @@
|
||||
# CLI Speakers
|
||||
|
||||
Query and use voice speakers via the ListenHub CLI.
|
||||
|
||||
## Listing Speakers
|
||||
|
||||
```bash
|
||||
# All speakers
|
||||
listenhub speakers list --json
|
||||
|
||||
# Chinese speakers only
|
||||
listenhub speakers list --lang zh --json
|
||||
|
||||
# English speakers only
|
||||
listenhub speakers list --lang en --json
|
||||
```
|
||||
|
||||
### Parsing the Response
|
||||
|
||||
```bash
|
||||
SPEAKERS=$(listenhub speakers list --lang en --json)
|
||||
|
||||
# List all speaker names
|
||||
echo "$SPEAKERS" | jq -r '.[].name'
|
||||
|
||||
# Get a specific speaker's ID
|
||||
echo "$SPEAKERS" | jq -r '.[] | select(.name == "Mars") | .speakerId'
|
||||
|
||||
# Count available speakers
|
||||
echo "$SPEAKERS" | jq 'length'
|
||||
```
|
||||
|
||||
### Speaker Fields
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| name | string | Display name (e.g., "Mars") |
|
||||
| speakerId | string | ID to use in create commands |
|
||||
| gender | string | `male` or `female` |
|
||||
| language | string | `zh` or `en` |
|
||||
| demoAudioUrl | string | Preview audio URL |
|
||||
|
||||
## Using Speakers in Create Commands
|
||||
|
||||
### By name
|
||||
|
||||
```bash
|
||||
listenhub podcast create --topic "AI" --speaker "Mars" --lang en --json
|
||||
```
|
||||
|
||||
### By ID
|
||||
|
||||
```bash
|
||||
listenhub podcast create --topic "AI" --speaker-id "cozy-man-english" --lang en --json
|
||||
```
|
||||
|
||||
### Multi-speaker (podcast, up to 2)
|
||||
|
||||
Repeat the `--speaker` flag:
|
||||
|
||||
```bash
|
||||
listenhub podcast create --topic "AI" --speaker "Mars" --speaker "Mia" --lang en --json
|
||||
```
|
||||
|
||||
Or with IDs:
|
||||
|
||||
```bash
|
||||
listenhub podcast create --topic "AI" --speaker-id "cozy-man-english" --speaker-id "travel-girl-english" --lang en --json
|
||||
```
|
||||
|
||||
## Integration with Speaker Selection
|
||||
|
||||
The interactive speaker selection flow in [speaker-selection.md](./speaker-selection.md) remains unchanged. The only difference is the underlying query mechanism:
|
||||
|
||||
- **Before**: `GET /speakers/list?language={language}` via curl
|
||||
- **Now**: `listenhub speakers list --lang {language} --json`
|
||||
|
||||
The selection UI, default speakers, input matching, and config persistence all work the same way.
|
||||
@@ -1,181 +0,0 @@
|
||||
# Common Patterns
|
||||
|
||||
Reusable patterns for all skills that call ListenHub APIs.
|
||||
|
||||
<HARD-GATE>
|
||||
**Language Adaptation**: Always respond in the user's language. Chinese input → Chinese output. English input → English output. Mixed → follow dominant language. This applies to all UI text, questions, confirmations, and error messages.
|
||||
</HARD-GATE>
|
||||
|
||||
## Async Polling
|
||||
|
||||
Most generation endpoints are asynchronous: submit a task, get an ID, then poll until completion.
|
||||
|
||||
### Execution Model
|
||||
|
||||
All polling MUST run in the background using Bash `run_in_background: true`. This keeps the terminal responsive while the task processes.
|
||||
|
||||
**Two-step pattern:**
|
||||
|
||||
1. **Submit (foreground)**: POST the creation request, extract the task/episode ID from the response. This is fast and runs in the foreground.
|
||||
2. **Poll (background)**: Run the polling loop with `run_in_background: true`. You will be notified automatically when it completes — do NOT sleep or poll manually.
|
||||
|
||||
### Step 1: Submit (foreground)
|
||||
|
||||
```bash
|
||||
RESPONSE=$(curl -sS -X POST "https://api.marswave.ai/openapi/v1/podcast/episodes" \
|
||||
-H "Authorization: Bearer $LISTENHUB_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Source: skills" \
|
||||
-d '{ ... }')
|
||||
|
||||
EPISODE_ID=$(echo "$RESPONSE" | jq -r '.data.episodeId')
|
||||
echo "Submitted: $EPISODE_ID"
|
||||
```
|
||||
|
||||
After this returns, tell the user the task is submitted and polling will run in the background.
|
||||
|
||||
### Step 2: Poll (background)
|
||||
|
||||
Run this as a **separate Bash call** with `run_in_background: true`:
|
||||
|
||||
```bash
|
||||
# Poll until complete — runs in background
|
||||
EPISODE_ID="<id-from-step-1>"
|
||||
for i in $(seq 1 30); do
|
||||
RESULT=$(curl -sS "https://api.marswave.ai/openapi/v1/podcast/episodes/$EPISODE_ID" \
|
||||
-H "Authorization: Bearer $LISTENHUB_API_KEY" \
|
||||
-H "X-Source: skills" 2>/dev/null)
|
||||
|
||||
STATUS=$(echo "$RESULT" | tr -d '\000-\037\177' | jq -r '.data.processStatus // "pending"')
|
||||
|
||||
case "$STATUS" in
|
||||
success|completed) echo "$RESULT"; exit 0 ;;
|
||||
failed|error) echo "FAILED: $RESULT" >&2; exit 1 ;;
|
||||
*) sleep 10 ;;
|
||||
esac
|
||||
done
|
||||
echo "TIMEOUT" >&2; exit 2
|
||||
```
|
||||
|
||||
### Polling Parameters
|
||||
|
||||
| Parameter | Default | Notes |
|
||||
|-----------|---------|-------|
|
||||
| Interval | 10s | Use 5s for content-parser only |
|
||||
| Max polls | 30 | = 300s timeout at 10s interval |
|
||||
| Timeout (Bash) | 600000 | Set `timeout: 600000` on the Bash tool call |
|
||||
|
||||
### After Completion
|
||||
|
||||
When the background task finishes, you will be notified with the output. Parse the result and present it to the user. If the task failed or timed out, report the error.
|
||||
|
||||
## Standard Response Structure
|
||||
|
||||
All API responses follow this format:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "",
|
||||
"data": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
- `code: 0` = success
|
||||
- Non-zero `code` = error (see Error Handling below)
|
||||
|
||||
## Error Handling
|
||||
|
||||
### HTTP Status Codes
|
||||
|
||||
| Code | Meaning | Action |
|
||||
|------|---------|--------|
|
||||
| 200 | Success | Parse response body |
|
||||
| 400 | Bad request | Check parameters |
|
||||
| 401 | Invalid API key | Re-check `LISTENHUB_API_KEY` |
|
||||
| 402 | Insufficient credits | Inform user to recharge |
|
||||
| 403 | Forbidden | No permission for this resource |
|
||||
| 429 | Rate limited | Exponential backoff, retry after delay |
|
||||
| 500/502/503/504 | Server error | Retry up to 3 times |
|
||||
|
||||
### Retry Strategy
|
||||
|
||||
- **429 rate limit**: Wait 15 seconds, then retry (exponential backoff)
|
||||
- **5xx server errors**: Retry up to 3 times with 5-second intervals
|
||||
- **Network errors**: Retry up to 3 times
|
||||
|
||||
### Application Error Codes
|
||||
|
||||
| Code | Meaning |
|
||||
|------|---------|
|
||||
| 21007 | Invalid user API key |
|
||||
| 25429 | Rate limited (application-level) |
|
||||
|
||||
## Input Validation
|
||||
|
||||
| Constraint | Rule |
|
||||
|-----------|------|
|
||||
| URL format | Must be valid HTTP(S) URL |
|
||||
| Text content length | Max 10,000 characters for TTS |
|
||||
| Supported languages | `zh` (Chinese), `en` (English) |
|
||||
| ID format | Alphanumeric + hyphen + underscore only |
|
||||
| Episode ID format | 24-character hex string (MongoDB ObjectId) |
|
||||
|
||||
## Long Text Input
|
||||
|
||||
When `sources` content is long (e.g., a full article), passing it inline in `-d '{...}'` may hit shell argument length limits. Use `@file` to read the request body from a file:
|
||||
|
||||
```bash
|
||||
# Write request JSON to a temp file
|
||||
cat > /tmp/lh-request.json << 'ENDJSON'
|
||||
{
|
||||
"sources": [{"type": "text", "content": "Very long text content goes here..."}],
|
||||
"speakers": [{"speakerId": "cozy-man-english"}],
|
||||
"language": "en"
|
||||
}
|
||||
ENDJSON
|
||||
|
||||
# Reference the file with @
|
||||
curl -sS -X POST "https://api.marswave.ai/openapi/v1/podcast/episodes" \
|
||||
-H "Authorization: Bearer $LISTENHUB_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Source: skills" \
|
||||
-d @/tmp/lh-request.json
|
||||
```
|
||||
|
||||
**When to use `@file`**: Always use this approach when text content exceeds a few KB. The `@` prefix tells curl to read the body from the file, bypassing shell argument limits entirely.
|
||||
|
||||
**Cleanup**: Remove the temp file after use: `rm /tmp/lh-request.json`
|
||||
|
||||
## Interactive Parameter Collection
|
||||
|
||||
Skills must use the **AskUserQuestion tool** for all enumerable parameters, following a **conversational, step-by-step** approach. This renders an interactive picker in the terminal that users can navigate with arrow keys.
|
||||
|
||||
### Conversation Behavior (mandatory)
|
||||
|
||||
1. **One question at a time.** Ask a single question, then STOP and wait for the user's answer before proceeding to the next step. Do not batch multiple steps into one message unless the parameters are explicitly independent (e.g., resolution + aspect ratio).
|
||||
2. **Wait for the answer.** Never assume a default and skip ahead. If the user hasn't answered, do not proceed.
|
||||
3. **Confirm before executing.** After all parameters are collected, summarize the choices and ask the user to confirm before calling any API. This is the final gate.
|
||||
4. **Be ready to go back.** If the user changes their mind or says something doesn't look right, revise and re-ask instead of pushing forward.
|
||||
|
||||
### How to Ask
|
||||
|
||||
**Always use the AskUserQuestion tool** — do NOT print questions as plain text. Each step's `Question` and `Options` map directly to AskUserQuestion parameters:
|
||||
|
||||
```
|
||||
Step definition in SKILL.md: → AskUserQuestion tool call:
|
||||
|
||||
Question: "What language?" → question: "What language?"
|
||||
- "Chinese (zh)" — Mandarin → options: [{label: "Chinese (zh)", description: "Mandarin"}
|
||||
- "English (en)" — English → {label: "English (en)", description: "English"}]
|
||||
```
|
||||
|
||||
For **free text** steps (topic, URL, prompt), just ask the question in a normal text message and wait for the user to type their answer.
|
||||
|
||||
### Parameter Types
|
||||
|
||||
- **Multiple-choice → AskUserQuestion**: language, mode, speaker count, generation style, resolution, aspect ratio
|
||||
- **Free text → normal message**: topic, content body, URL, image prompt
|
||||
- **Sequential when dependent**: e.g., speaker list depends on language choice — ask language first, then fetch speakers and present list
|
||||
- **Batch when independent**: e.g., resolution + aspect ratio can be asked together in one AskUserQuestion call (multiple questions)
|
||||
- **Options include descriptions**: not just labels — explain what each choice means
|
||||
@@ -2,40 +2,13 @@
|
||||
|
||||
Reusable pattern for per-skill config lookup, creation, and update.
|
||||
|
||||
## API Key Check
|
||||
## CLI Auth Check
|
||||
|
||||
Run this **before Step 0** in every skill that requires `LISTENHUB_API_KEY`.
|
||||
Run this **before Step 0** in every skill that uses the ListenHub CLI.
|
||||
|
||||
```bash
|
||||
[ -z "$LISTENHUB_API_KEY" ] && echo "MISSING" || echo "OK"
|
||||
```
|
||||
Follow `shared/cli-authentication.md` § Auth Check.
|
||||
|
||||
**If `OK`**: proceed to Step 0 silently. Do NOT display or confirm the key.
|
||||
|
||||
**If `MISSING`**: run the interactive setup below. Do NOT stop — guide the user through configuration and then continue.
|
||||
|
||||
### Interactive Key Setup
|
||||
|
||||
1. Tell the user:
|
||||
> `LISTENHUB_API_KEY` 未配置。请前往 https://listenhub.ai/settings/api-keys 获取 API Key(需要 Pro 订阅)。
|
||||
|
||||
2. Use `AskUserQuestion` to collect the key:
|
||||
> 请粘贴你的 API Key(以 `lh_sk_` 开头):
|
||||
|
||||
3. Validate format — must start with `lh_sk_`. If not, re-prompt.
|
||||
|
||||
4. Write to shell profile and source:
|
||||
```bash
|
||||
echo '' >> ~/.zshrc
|
||||
echo 'export LISTENHUB_API_KEY="lh_sk_..."' >> ~/.zshrc
|
||||
source ~/.zshrc
|
||||
```
|
||||
On Linux, use `~/.bashrc` instead.
|
||||
|
||||
5. Confirm to the user:
|
||||
> API Key 已保存到 `~/.zshrc`,后续会话无需重复配置。
|
||||
|
||||
6. **Continue** — proceed to Step 0 and the skill's Interaction Flow. Do NOT ask the user to re-run.
|
||||
If CLI is not installed or not logged in, auto-install and auto-login as described in `shared/cli-authentication.md` — never ask the user to run commands manually.
|
||||
|
||||
## Config Location
|
||||
|
||||
|
||||
@@ -22,13 +22,15 @@ When no user preference is saved, use these built-in defaults. This eliminates t
|
||||
|
||||
## Fetching Speakers
|
||||
|
||||
Always call the speakers API before presenting options (when user requests to change voice):
|
||||
Always query the speaker list before presenting options (when user requests to change voice):
|
||||
|
||||
```
|
||||
GET /speakers/list?language={language}
|
||||
```bash
|
||||
listenhub speakers list --lang {language} --json
|
||||
```
|
||||
|
||||
Never hardcode speaker IDs in API calls — use the defaults above only as fallback when no user preference exists.
|
||||
See `shared/cli-speakers.md` for full query patterns.
|
||||
|
||||
Never hardcode speaker IDs — use the defaults above only as fallback when no user preference exists.
|
||||
|
||||
## Speaker Properties
|
||||
|
||||
|
||||
+343
@@ -0,0 +1,343 @@
|
||||
---
|
||||
name: slides
|
||||
description: |
|
||||
Create slide decks from topics, URLs, or text. Triggers on: "幻灯片", "PPT",
|
||||
"slides", "slide deck", "做幻灯片", "create slides", "presentation".
|
||||
metadata:
|
||||
openclaw:
|
||||
emoji: "📊"
|
||||
requires:
|
||||
bin: ["listenhub"]
|
||||
primaryBin: "listenhub"
|
||||
---
|
||||
|
||||
## When to Use
|
||||
|
||||
- User wants to create a slide deck or presentation
|
||||
- User asks for "slides", "幻灯片", "PPT", or "presentation"
|
||||
- User wants visual content organized into slides from a topic or URL
|
||||
|
||||
## When NOT to Use
|
||||
|
||||
- User wants a narrated video without slides (use `/explainer`)
|
||||
- User wants audio-only content (use `/speech` or `/podcast`)
|
||||
- User wants a podcast-style discussion (use `/podcast`)
|
||||
- User wants to generate a standalone image (use `/image-gen`)
|
||||
|
||||
## Purpose
|
||||
|
||||
Generate slide decks with AI-generated visuals from topics, URLs, or text. By default, slides are generated without audio narration. Narration can be optionally enabled. Ideal for presentations, summaries, and visual storytelling.
|
||||
|
||||
## Hard Constraints
|
||||
|
||||
- Always read config following `shared/config-pattern.md` before any interaction
|
||||
- Follow `shared/cli-patterns.md` for execution modes, error handling, and interaction patterns
|
||||
- Always follow `shared/cli-authentication.md` for auth checks
|
||||
- Follow `shared/speaker-selection.md` when narration is enabled
|
||||
- Never hardcode speaker IDs — always fetch from the speakers CLI when the user wants to change voice
|
||||
- Never save files to `~/Downloads/` or `.listenhub/` — save artifacts to the current working directory with friendly topic-based names (see `shared/config-pattern.md` § Artifact Naming)
|
||||
- Mode is always `slides` — never `info` or `story` (those are for `/explainer`)
|
||||
- Only 1 speaker supported (when narration is enabled)
|
||||
- Default behavior: skip audio (no narration). Only add narration when the user explicitly requests it via `--no-skip-audio`
|
||||
|
||||
<HARD-GATE>
|
||||
Use the AskUserQuestion tool for every multiple-choice step — do NOT print options as plain text. Ask one question at a time. Wait for the user's answer before proceeding to the next step. After all parameters are collected, summarize the choices and ask the user to confirm. Do NOT call any CLI command until the user has explicitly confirmed.
|
||||
|
||||
</HARD-GATE>
|
||||
|
||||
## Step -1: CLI Auth Check
|
||||
|
||||
Follow `shared/cli-authentication.md`. If the CLI is not installed or the user is not logged in, auto-install and auto-login — never ask the user to run commands manually.
|
||||
|
||||
## Step 0: Config Setup
|
||||
|
||||
Follow `shared/config-pattern.md` Step 0 (Zero-Question Boot).
|
||||
|
||||
**If file doesn't exist** — silently create with defaults and proceed:
|
||||
```bash
|
||||
mkdir -p ".listenhub/slides"
|
||||
echo '{"outputMode":"inline","language":null,"defaultSpeakers":{}}' > ".listenhub/slides/config.json"
|
||||
CONFIG_PATH=".listenhub/slides/config.json"
|
||||
CONFIG=$(cat "$CONFIG_PATH")
|
||||
```
|
||||
**Do NOT ask any setup questions.** Proceed directly to the Interaction Flow.
|
||||
|
||||
**If file exists** — read config silently and proceed:
|
||||
```bash
|
||||
CONFIG_PATH=".listenhub/slides/config.json"
|
||||
[ ! -f "$CONFIG_PATH" ] && CONFIG_PATH="$HOME/.listenhub/slides/config.json"
|
||||
CONFIG=$(cat "$CONFIG_PATH")
|
||||
```
|
||||
|
||||
### Setup Flow (user-initiated reconfigure only)
|
||||
|
||||
Only run when the user explicitly asks to reconfigure. Display current settings:
|
||||
```
|
||||
当前配置 (slides):
|
||||
输出方式:{inline / download / both}
|
||||
语言偏好:{zh / en / 未设置}
|
||||
默认主播:{speakerName / 使用内置默认}
|
||||
```
|
||||
|
||||
Then ask:
|
||||
|
||||
1. **outputMode**: Follow `shared/output-mode.md` § Setup Flow Question.
|
||||
|
||||
2. **Language** (optional): "默认语言?"
|
||||
- "中文 (zh)"
|
||||
- "English (en)"
|
||||
- "每次手动选择" → keep `null`
|
||||
|
||||
After collecting answers, save immediately:
|
||||
```bash
|
||||
NEW_CONFIG=$(echo "$CONFIG" | jq --arg m "$OUTPUT_MODE" '. + {"outputMode": $m}')
|
||||
echo "$NEW_CONFIG" > "$CONFIG_PATH"
|
||||
CONFIG=$(cat "$CONFIG_PATH")
|
||||
```
|
||||
|
||||
## Interaction Flow
|
||||
|
||||
### Step 1: Topic / Content
|
||||
|
||||
Free text input. Ask the user:
|
||||
|
||||
> What would you like to create slides about?
|
||||
|
||||
Accept: topic description, text content, URL(s), or any combination.
|
||||
|
||||
### Step 2: Language
|
||||
|
||||
If `config.language` is set, pre-fill and show in summary — skip this question.
|
||||
Otherwise ask:
|
||||
|
||||
```
|
||||
Question: "What language?"
|
||||
Options:
|
||||
- "Chinese (zh)" — Content in Mandarin Chinese
|
||||
- "English (en)" — Content in English
|
||||
- "Japanese (ja)" — Content in Japanese
|
||||
```
|
||||
|
||||
### Step 3: Narration
|
||||
|
||||
Ask:
|
||||
|
||||
```
|
||||
Question: "需要语音旁白吗?(默认否)"
|
||||
Options:
|
||||
- "不需要" — Slides only, no narration
|
||||
- "需要" — Add voice narration to slides
|
||||
```
|
||||
|
||||
Default is no narration. If the user says yes, proceed to Step 4. Otherwise skip to Step 5.
|
||||
|
||||
### Step 4: Speaker Selection (only if narration enabled)
|
||||
|
||||
**Skip this step entirely if narration is not enabled.**
|
||||
|
||||
Follow `shared/speaker-selection.md`:
|
||||
- If `config.defaultSpeakers.{language}` is set → use saved speaker silently
|
||||
- If not set → use **built-in default** from `shared/speaker-selection.md` for the language
|
||||
- Show the speaker in the confirmation summary (Step 5) — user can change from there if desired
|
||||
- Only show the full speaker list if the user explicitly asks to change voice
|
||||
|
||||
Only 1 speaker is supported for slides narration.
|
||||
|
||||
### Step 5: Confirm & Generate
|
||||
|
||||
Summarize all choices:
|
||||
|
||||
**Without narration:**
|
||||
```
|
||||
Ready to generate slides:
|
||||
|
||||
Topic: {topic}
|
||||
Language: {language}
|
||||
Narration: No
|
||||
|
||||
Proceed?
|
||||
```
|
||||
|
||||
**With narration:**
|
||||
```
|
||||
Ready to generate slides:
|
||||
|
||||
Topic: {topic}
|
||||
Language: {language}
|
||||
Narration: Yes
|
||||
Speaker: {speaker name}
|
||||
|
||||
Proceed?
|
||||
```
|
||||
|
||||
Wait for explicit confirmation before running any CLI command.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Submit (background)**: Run the CLI command with `run_in_background: true` and `timeout: 660000`:
|
||||
|
||||
**Without narration (default):**
|
||||
```bash
|
||||
listenhub slides create \
|
||||
--query "{topic}" \
|
||||
--lang {en|zh|ja} \
|
||||
--image-size 2K \
|
||||
--aspect-ratio 16:9 \
|
||||
--timeout 600 \
|
||||
--json
|
||||
```
|
||||
|
||||
**With narration:**
|
||||
```bash
|
||||
listenhub slides create \
|
||||
--query "{topic}" \
|
||||
--lang {en|zh|ja} \
|
||||
--image-size 2K \
|
||||
--aspect-ratio 16:9 \
|
||||
--no-skip-audio \
|
||||
--speaker "{name}" \
|
||||
--timeout 600 \
|
||||
--json
|
||||
```
|
||||
|
||||
If the user provided a source URL, add `--source-url "{url}"`.
|
||||
|
||||
The CLI handles polling internally and returns the final result when generation completes.
|
||||
|
||||
2. Tell the user the task is submitted and that they will be notified when it finishes.
|
||||
|
||||
3. When notified of completion, **parse and present the result**:
|
||||
|
||||
Parse the CLI JSON output for key fields:
|
||||
```bash
|
||||
EPISODE_ID=$(echo "$RESULT" | jq -r '.episodeId')
|
||||
AUDIO_URL=$(echo "$RESULT" | jq -r '.audioUrl // empty')
|
||||
CREDITS=$(echo "$RESULT" | jq -r '.credits // empty')
|
||||
```
|
||||
|
||||
Read `OUTPUT_MODE` from config. Follow `shared/output-mode.md` for behavior.
|
||||
|
||||
**Without narration:**
|
||||
|
||||
**`inline` or `both`**: Present the online link.
|
||||
|
||||
```
|
||||
幻灯片已生成!
|
||||
|
||||
在线查看:https://listenhub.ai/app/slides/{episodeId}
|
||||
消耗积分:{credits}
|
||||
```
|
||||
|
||||
**`download` or `both`**: Also save the script file. Generate a topic slug following `shared/config-pattern.md` § Artifact Naming.
|
||||
- Save as `{slug}-slides.md` in cwd (dedup if exists)
|
||||
- Present the save path in addition to the above summary.
|
||||
|
||||
**With narration:**
|
||||
|
||||
**`inline` or `both`**: Display audio URL as a clickable link.
|
||||
|
||||
```
|
||||
幻灯片已生成!
|
||||
|
||||
在线查看:https://listenhub.ai/app/slides/{episodeId}
|
||||
音频链接:{audioUrl}
|
||||
消耗积分:{credits}
|
||||
```
|
||||
|
||||
**`download` or `both`**: Also save files. Generate a topic slug following `shared/config-pattern.md` § Artifact Naming.
|
||||
- Create `{slug}-slides/` folder (dedup if exists)
|
||||
- Write `script.md` inside
|
||||
- Download audio:
|
||||
```bash
|
||||
curl -sS -o "{slug}-slides/audio.mp3" "{audioUrl}"
|
||||
```
|
||||
- Present:
|
||||
```
|
||||
已保存到当前目录:
|
||||
{slug}-slides/
|
||||
script.md
|
||||
audio.mp3
|
||||
```
|
||||
|
||||
### After Successful Generation
|
||||
|
||||
Update config with the choices made this session:
|
||||
|
||||
```bash
|
||||
NEW_CONFIG=$(echo "$CONFIG" | jq \
|
||||
--arg lang "{language}" \
|
||||
'. + {"language": $lang}')
|
||||
echo "$NEW_CONFIG" > "$CONFIG_PATH"
|
||||
```
|
||||
|
||||
If narration was used, also save the speaker:
|
||||
```bash
|
||||
NEW_CONFIG=$(echo "$CONFIG" | jq \
|
||||
--arg lang "{language}" \
|
||||
--arg speakerId "{speakerId}" \
|
||||
'. + {"language": $lang, "defaultSpeakers": (.defaultSpeakers + {($lang): [$speakerId]})}')
|
||||
echo "$NEW_CONFIG" > "$CONFIG_PATH"
|
||||
```
|
||||
|
||||
**Estimated times**:
|
||||
- Slides without narration: 2-4 minutes
|
||||
- Slides with narration: 4-8 minutes
|
||||
|
||||
## Resources
|
||||
|
||||
- CLI authentication: `shared/cli-authentication.md`
|
||||
- CLI patterns: `shared/cli-patterns.md`
|
||||
- Speaker query: `shared/cli-speakers.md`
|
||||
- Speaker selection guide: `shared/speaker-selection.md`
|
||||
- Config pattern: `shared/config-pattern.md`
|
||||
- Output mode: `shared/output-mode.md`
|
||||
|
||||
## Composability
|
||||
|
||||
- **Invokes**: speakers CLI (for speaker selection when narration enabled)
|
||||
- **Invoked by**: content-planner (Phase 3)
|
||||
|
||||
## Example
|
||||
|
||||
**User**: "帮我做一个关于量子计算的幻灯片"
|
||||
|
||||
**Agent workflow**:
|
||||
1. Topic: "量子计算"
|
||||
2. Language: pre-filled from config or ask → "zh"
|
||||
3. Narration: ask → "不需要"
|
||||
4. Confirm and generate
|
||||
|
||||
```bash
|
||||
listenhub slides create \
|
||||
--query "量子计算" \
|
||||
--lang zh \
|
||||
--image-size 2K \
|
||||
--aspect-ratio 16:9 \
|
||||
--timeout 600 \
|
||||
--json
|
||||
```
|
||||
|
||||
Wait for CLI to return result, then present the online link.
|
||||
|
||||
**User**: "Create slides about React hooks with narration"
|
||||
|
||||
**Agent workflow**:
|
||||
1. Topic: "React hooks"
|
||||
2. Language: ask → "en"
|
||||
3. Narration: ask → "需要"
|
||||
4. Speaker: use built-in default for English
|
||||
5. Confirm and generate
|
||||
|
||||
```bash
|
||||
listenhub slides create \
|
||||
--query "React hooks" \
|
||||
--lang en \
|
||||
--image-size 2K \
|
||||
--aspect-ratio 16:9 \
|
||||
--no-skip-audio \
|
||||
--speaker "Mars" \
|
||||
--timeout 600 \
|
||||
--json
|
||||
```
|
||||
|
||||
Wait for CLI to return result, then present the online link and audio link.
|
||||
+115
-74
@@ -4,8 +4,8 @@ metadata:
|
||||
openclaw:
|
||||
emoji: "🔊"
|
||||
requires:
|
||||
env: ["LISTENHUB_API_KEY"]
|
||||
primaryEnv: "LISTENHUB_API_KEY"
|
||||
bin: ["listenhub"]
|
||||
primaryBin: "listenhub"
|
||||
description: |
|
||||
Text-to-speech and voice narration. Triggers on: "朗读这段", "配音", "TTS",
|
||||
"语音合成", "text to speech", "read this aloud", "convert to speech",
|
||||
@@ -29,21 +29,20 @@ description: |
|
||||
|
||||
Convert text into natural-sounding speech audio. Two paths:
|
||||
|
||||
1. **Quick mode** (`/v1/tts`): Single voice, low-latency, sync MP3 stream. For casual chat, reading snippets, instant audio.
|
||||
2. **Script mode** (`/v1/speech`): Multi-speaker, per-segment voice assignment. For dialogue, audiobooks, scripted content.
|
||||
1. **Quick mode** (`--mode direct`): Single voice, low-latency, sync. For casual chat, reading snippets, instant audio.
|
||||
2. **Script mode** (`--mode smart`): Multi-speaker, per-segment voice assignment. For dialogue, audiobooks, scripted content.
|
||||
|
||||
## Hard Constraints
|
||||
|
||||
- No shell scripts. Construct curl commands from the API reference files listed in Resources
|
||||
- Always read `shared/authentication.md` for API key and headers
|
||||
- Follow `shared/common-patterns.md` for errors and interaction patterns
|
||||
- Never hardcode speaker IDs in API calls — use built-in defaults from `shared/speaker-selection.md` as fallback only; fetch from the speakers API when the user wants to change voice
|
||||
- Always check CLI auth following `shared/cli-authentication.md`
|
||||
- Follow `shared/cli-patterns.md` for CLI execution, errors, and interaction patterns
|
||||
- Never hardcode speaker IDs in CLI calls — use built-in defaults from `shared/speaker-selection.md` as fallback only; fetch from the speakers CLI when the user wants to change voice
|
||||
- Always read config following `shared/config-pattern.md` before any interaction
|
||||
- Always follow `shared/speaker-selection.md` for speaker selection (text table + free-text input)
|
||||
- Never save files to `~/Downloads/` or `/tmp/` as primary output — save artifacts to the current working directory with friendly topic-based names (see `shared/config-pattern.md` § Artifact Naming)
|
||||
|
||||
<HARD-GATE>
|
||||
Use the AskUserQuestion tool for every multiple-choice step — do NOT print options as plain text. Ask one question at a time. Wait for the user's answer before proceeding to the next step. After all parameters are collected, summarize the choices and ask the user to confirm. Do NOT call any generation API until the user has explicitly confirmed.
|
||||
Use the AskUserQuestion tool for every multiple-choice step — do NOT print options as plain text. Ask one question at a time. Wait for the user's answer before proceeding to the next step. After all parameters are collected, summarize the choices and ask the user to confirm. Do NOT call any generation CLI command until the user has explicitly confirmed.
|
||||
|
||||
</HARD-GATE>
|
||||
|
||||
@@ -62,9 +61,9 @@ Determine the mode from the user's input **automatically** before asking any que
|
||||
|
||||
## Interaction Flow
|
||||
|
||||
### Step -1: API Key Check
|
||||
### Step -1: CLI Auth Check
|
||||
|
||||
Follow `shared/config-pattern.md` § API Key Check. If the key is missing, stop immediately.
|
||||
Follow `shared/cli-authentication.md`. If the CLI is not installed or the user is not logged in, auto-install and auto-login — never ask the user to run commands manually.
|
||||
|
||||
### Step 0: Config Setup
|
||||
|
||||
@@ -116,7 +115,7 @@ echo "$NEW_CONFIG" > "$CONFIG_PATH"
|
||||
CONFIG=$(cat "$CONFIG_PATH")
|
||||
```
|
||||
|
||||
### Quick Mode — `POST /v1/tts`
|
||||
### Quick Mode — `listenhub tts create --mode direct`
|
||||
|
||||
**Step 1: Extract text**
|
||||
|
||||
@@ -154,51 +153,58 @@ Proceed?
|
||||
|
||||
**Step 5: Generate**
|
||||
|
||||
For short text, pass inline:
|
||||
```bash
|
||||
curl -sS -X POST "https://api.marswave.ai/openapi/v1/tts" \
|
||||
-H "Authorization: Bearer $LISTENHUB_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Source: skills" \
|
||||
-d '{"input": "...", "voice": "..."}' \
|
||||
--output /tmp/tts-output.mp3
|
||||
RESULT=$(listenhub tts create --text "{text}" --mode direct --speaker "{name}" --lang {lang} --json 2>/tmp/lh-err)
|
||||
EXIT_CODE=$?
|
||||
|
||||
if [ $EXIT_CODE -ne 0 ]; then
|
||||
ERROR=$(cat /tmp/lh-err)
|
||||
case $EXIT_CODE in
|
||||
2) echo "Auth error: run 'listenhub auth login'" ;;
|
||||
3) echo "Timeout: try --no-wait" ;;
|
||||
*) echo "Error: $ERROR" ;;
|
||||
esac
|
||||
rm -f /tmp/lh-err
|
||||
fi
|
||||
rm -f /tmp/lh-err
|
||||
|
||||
AUDIO_URL=$(echo "$RESULT" | jq -r '.audioUrl')
|
||||
```
|
||||
|
||||
For long text, write to a temp file first (see `shared/cli-patterns.md` § Long Text Input):
|
||||
```bash
|
||||
cat > /tmp/lh-content.txt << 'ENDCONTENT'
|
||||
Long text content goes here...
|
||||
ENDCONTENT
|
||||
|
||||
RESULT=$(listenhub tts create --text "$(cat /tmp/lh-content.txt)" --mode direct --speaker "{name}" --lang {lang} --json)
|
||||
AUDIO_URL=$(echo "$RESULT" | jq -r '.audioUrl')
|
||||
|
||||
rm -f /tmp/lh-content.txt
|
||||
```
|
||||
|
||||
**Step 6: Present result**
|
||||
|
||||
Read `OUTPUT_MODE` from config. Follow `shared/output-mode.md` for behavior.
|
||||
|
||||
Use a timestamped jobId: `$(date +%s)`
|
||||
|
||||
**`inline` or `both`** (TTS quick returns a sync audio stream — no `audioUrl`):
|
||||
```bash
|
||||
JOB_ID=$(date +%s)
|
||||
curl -sS -X POST "https://api.marswave.ai/openapi/v1/tts" \
|
||||
-H "Authorization: Bearer $LISTENHUB_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Source: skills" \
|
||||
-d '{"input": "...", "voice": "..."}' \
|
||||
--output /tmp/tts-${JOB_ID}.mp3
|
||||
```
|
||||
Then use the Read tool on `/tmp/tts-{jobId}.mp3`.
|
||||
**`inline` or `both`**: Display the `audioUrl` as a clickable link.
|
||||
|
||||
Present:
|
||||
```
|
||||
Audio generated!
|
||||
|
||||
在线收听:{audioUrl}
|
||||
```
|
||||
|
||||
**`download` or `both`**: Generate a topic slug from the text content following `shared/config-pattern.md` § Artifact Naming.
|
||||
**`download` or `both`**: Also download the file. Generate a topic slug from the text content following `shared/config-pattern.md` § Artifact Naming.
|
||||
```bash
|
||||
SLUG="{topic-slug}" # e.g. "server-maintenance-notice"
|
||||
NAME="${SLUG}.mp3"
|
||||
# Dedup: if file exists, append -2, -3, etc.
|
||||
BASE="${NAME%.*}"; EXT="${NAME##*.}"; i=2
|
||||
while [ -e "$NAME" ]; do NAME="${BASE}-${i}.${EXT}"; i=$((i+1)); done
|
||||
curl -sS -X POST "https://api.marswave.ai/openapi/v1/tts" \
|
||||
-H "Authorization: Bearer $LISTENHUB_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Source: skills" \
|
||||
-d '{"input": "...", "voice": "..."}' \
|
||||
--output "$NAME"
|
||||
curl -sS -o "$NAME" "$AUDIO_URL"
|
||||
```
|
||||
Present:
|
||||
```
|
||||
@@ -210,7 +216,7 @@ Audio generated!
|
||||
|
||||
---
|
||||
|
||||
### Script Mode — `POST /v1/speech`
|
||||
### Script Mode — `listenhub tts create --mode smart`
|
||||
|
||||
**Step 1: Get scripts**
|
||||
|
||||
@@ -221,7 +227,7 @@ Determine whether the user already has a scripts array:
|
||||
|
||||
> "Please provide the script with speaker assignments. Format: each line as `SpeakerName: text content`. I'll convert it."
|
||||
|
||||
Once the user provides the script, parse it into the `scripts` JSON format.
|
||||
Once the user provides the script, parse it into speaker-annotated text.
|
||||
|
||||
**Step 2: Assign voices per character**
|
||||
|
||||
@@ -258,31 +264,55 @@ Proceed?
|
||||
|
||||
**Step 5: Generate**
|
||||
|
||||
Write the request body to a temp file, then submit:
|
||||
Format the script text with speaker markers and submit. For multi-speaker scripts, include speaker names inline in the text. Run with `run_in_background: true` since script mode may take longer.
|
||||
|
||||
**Submit (foreground)** with `--no-wait`:
|
||||
```bash
|
||||
# Write request to temp file
|
||||
cat > /tmp/lh-speech-request.json << 'ENDJSON'
|
||||
{
|
||||
"scripts": [
|
||||
{"content": "...", "speakerId": "..."},
|
||||
{"content": "...", "speakerId": "..."}
|
||||
]
|
||||
}
|
||||
ENDJSON
|
||||
RESULT=$(listenhub tts create --text "{formatted script with speaker markers}" --mode smart --speaker "{name1}" --speaker "{name2}" --lang {lang} --no-wait --json)
|
||||
ID=$(echo "$RESULT" | jq -r '.id')
|
||||
echo "Submitted: $ID"
|
||||
```
|
||||
|
||||
# Submit
|
||||
curl -sS -X POST "https://api.marswave.ai/openapi/v1/speech" \
|
||||
-H "Authorization: Bearer $LISTENHUB_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Source: skills" \
|
||||
-d @/tmp/lh-speech-request.json
|
||||
For long scripts, write to a temp file first:
|
||||
```bash
|
||||
cat > /tmp/lh-content.txt << 'ENDCONTENT'
|
||||
SpeakerA: First line of dialogue
|
||||
SpeakerB: Second line of dialogue
|
||||
...
|
||||
ENDCONTENT
|
||||
|
||||
rm /tmp/lh-speech-request.json
|
||||
RESULT=$(listenhub tts create --text "$(cat /tmp/lh-content.txt)" --mode smart --speaker "{name1}" --speaker "{name2}" --lang {lang} --no-wait --json)
|
||||
ID=$(echo "$RESULT" | jq -r '.id')
|
||||
|
||||
rm -f /tmp/lh-content.txt
|
||||
```
|
||||
|
||||
**Poll (background)** with `run_in_background: true` and `timeout: 600000`:
|
||||
```bash
|
||||
ID="<id-from-above>"
|
||||
for i in $(seq 1 60); do
|
||||
RESULT=$(listenhub creation get "$ID" --json 2>/dev/null)
|
||||
STATUS=$(echo "$RESULT" | jq -r '.status // "processing"')
|
||||
|
||||
case "$STATUS" in
|
||||
completed) echo "$RESULT"; exit 0 ;;
|
||||
failed) echo "FAILED: $RESULT" >&2; exit 1 ;;
|
||||
*) sleep 10 ;;
|
||||
esac
|
||||
done
|
||||
echo "TIMEOUT" >&2; exit 2
|
||||
```
|
||||
|
||||
**Step 6: Present result**
|
||||
|
||||
When the background task completes, parse the result:
|
||||
```bash
|
||||
AUDIO_URL=$(echo "$RESULT" | jq -r '.audioUrl')
|
||||
SUBTITLES_URL=$(echo "$RESULT" | jq -r '.subtitlesUrl // empty')
|
||||
DURATION=$(echo "$RESULT" | jq -r '.audioDuration // empty')
|
||||
CREDITS=$(echo "$RESULT" | jq -r '.credits // empty')
|
||||
```
|
||||
|
||||
Read `OUTPUT_MODE` from config. Follow `shared/output-mode.md` for behavior.
|
||||
|
||||
**`inline` or `both`**: Display the `audioUrl` and `subtitlesUrl` as clickable links.
|
||||
@@ -304,7 +334,7 @@ NAME="${SLUG}.mp3"
|
||||
# Dedup: if file exists, append -2, -3, etc.
|
||||
BASE="${NAME%.*}"; EXT="${NAME##*.}"; i=2
|
||||
while [ -e "$NAME" ]; do NAME="${BASE}-${i}.${EXT}"; i=$((i+1)); done
|
||||
curl -sS -o "$NAME" "{audioUrl}"
|
||||
curl -sS -o "$NAME" "$AUDIO_URL"
|
||||
```
|
||||
Present:
|
||||
```
|
||||
@@ -324,15 +354,16 @@ When saving preferences, merge into `.listenhub/tts/config.json` — do not over
|
||||
|
||||
## API Reference
|
||||
|
||||
- TTS & Speech endpoints: `shared/api-tts.md`
|
||||
- Speaker list: `shared/api-speakers.md`
|
||||
- CLI execution patterns: `shared/cli-patterns.md`
|
||||
- CLI authentication: `shared/cli-authentication.md`
|
||||
- Speaker list: `shared/cli-speakers.md`
|
||||
- Speaker selection guide: `shared/speaker-selection.md`
|
||||
- Error handling: `shared/common-patterns.md` § Error Handling
|
||||
- Long text input: `shared/common-patterns.md` § Long Text Input
|
||||
- Config pattern: `shared/config-pattern.md`
|
||||
- Output mode: `shared/output-mode.md`
|
||||
|
||||
## Composability
|
||||
|
||||
- **Invokes**: speakers API (for speaker selection)
|
||||
- **Invokes**: speakers CLI (for speaker selection)
|
||||
- **Invoked by**: explainer (for voiceover)
|
||||
|
||||
## Examples
|
||||
@@ -342,20 +373,30 @@ When saving preferences, merge into `.listenhub/tts/config.json` — do not over
|
||||
> "TTS this: The server will be down for maintenance at midnight."
|
||||
|
||||
1. Detect: Quick mode (plain text, "TTS this")
|
||||
2. Read config: `quickVoice` is `null`
|
||||
3. Fetch speakers, user picks "Yuanye"
|
||||
4. Ask to save → yes → update config
|
||||
5. `POST /v1/tts` with `input` + `voice`
|
||||
6. Present: `/tmp/tts-output.mp3`
|
||||
2. Read config: `defaultSpeakers.en` is empty
|
||||
3. Use built-in default: Mars (`cozy-man-english`)
|
||||
4. Confirm → user approves
|
||||
5. Generate:
|
||||
```bash
|
||||
RESULT=$(listenhub tts create --text "The server will be down for maintenance at midnight." --mode direct --speaker "Mars" --lang en --json)
|
||||
AUDIO_URL=$(echo "$RESULT" | jq -r '.audioUrl')
|
||||
```
|
||||
6. Present: display `audioUrl` as link (inline mode)
|
||||
|
||||
**Script mode:**
|
||||
|
||||
> "帮我做一段双人对话配音,A说:欢迎大家,B说:谢谢邀请"
|
||||
|
||||
1. Detect: Script mode ("双人对话")
|
||||
2. Parse segments: A → "欢迎大家", B → "谢谢邀请"
|
||||
3. Read config: `scriptVoices` empty
|
||||
4. Fetch `zh` speakers, assign A and B voices
|
||||
5. Ask to save → yes → update config
|
||||
6. `POST /v1/speech` with scripts array
|
||||
7. Present: `audioUrl`, `subtitlesUrl`, duration
|
||||
2. Parse segments: A -> "欢迎大家", B -> "谢谢邀请"
|
||||
3. Read config: `defaultSpeakers.zh` empty
|
||||
4. Use built-in defaults: 原野 (Primary) + 高晴 (Secondary)
|
||||
5. Confirm → user approves
|
||||
6. Generate:
|
||||
```bash
|
||||
RESULT=$(listenhub tts create --text "A: 欢迎大家
|
||||
B: 谢谢邀请" --mode smart --speaker "原野" --speaker "高晴" --lang zh --no-wait --json)
|
||||
ID=$(echo "$RESULT" | jq -r '.id')
|
||||
```
|
||||
7. Poll in background until complete
|
||||
8. Present: `audioUrl`, `subtitlesUrl`, duration
|
||||
|
||||
Reference in New Issue
Block a user