diff --git a/packages/commands/src/commands/speech/synthesize.ts b/packages/commands/src/commands/speech/synthesize.ts index b6a5d178..023709ea 100644 --- a/packages/commands/src/commands/speech/synthesize.ts +++ b/packages/commands/src/commands/speech/synthesize.ts @@ -26,6 +26,7 @@ import { emitResult, emitBare } from "bailian-cli-runtime"; import { VOICE_TTS_PAGE } from "bailian-cli-runtime"; const COSYVOICE_CLONE_DESIGN_DOC = `${DOCS_HOSTS.cn}/cosyvoice-clone-design-api`; +const QWEN_AUDIO_TTS_VOICE_DOC = `${DOCS_HOSTS.cn}/qwen-audio-tts-voice-list`; interface VoiceEntry { voice: string; @@ -34,6 +35,32 @@ interface VoiceEntry { lang: string; } +// qwen-audio-3.0-tts-plus system voices (official docs list only these two) +const QWEN_AUDIO_30_TTS_PLUS_VOICES: VoiceEntry[] = [ + { voice: "longanlingxin", name: "龙安灵心", desc: "知心温暖音", lang: "中文/英文" }, + { voice: "longanlufeng", name: "龙安鲁风", desc: "明亮开朗音", lang: "中文/英文" }, +]; + +// qwen-audio-3.0-tts-flash system voices +const QWEN_AUDIO_30_TTS_FLASH_VOICES: VoiceEntry[] = [ + // Social companion (premium Chinese) + { voice: "longanfengyue", name: "龙安风悦", desc: "自然亲切音", lang: "中文/英文" }, + { voice: "longanyuanfei", name: "龙安元妃", desc: "高傲妃子音", lang: "中文/英文" }, + { voice: "longanlingxi", name: "龙安灵希", desc: "可爱甜美音", lang: "中文/英文" }, + { voice: "longanxiaoxin", name: "龙安小昕", desc: "亲切活泼音", lang: "中文/英文" }, + { voice: "longanhuan_v3.6", name: "龙安欢", desc: "欢脱元气女", lang: "中文/英文" }, + // Kids / toys (premium children) + { voice: "longjielidou_v3.6", name: "龙杰力豆", desc: "天真男童", lang: "中文/英文" }, + { voice: "longpaopao_v3.6", name: "龙泡泡", desc: "软糯可爱音", lang: "中文/英文" }, + // Character / game (premium Chinese) + { voice: "longhuohuo_v3.6", name: "龙火火", desc: "顽皮少年音", lang: "中文/英文" }, + { voice: "longchuanshu_v3.6", name: "龙川叔", desc: "川普大叔音", lang: "中文/英文" }, + // Social companion / assistant (premium English) + { voice: "loongmary", name: "loongmary", desc: "温暖英音", lang: "英文" }, + { voice: "loongeva_v3.6", name: "loongeva", desc: "高智美音", lang: "英文" }, + { voice: "loongjohn", name: "loongJohn", desc: "沉稳亲切美音", lang: "英文" }, +]; + // cosyvoice-v3-flash system voices const COSYVOICE_V3_FLASH_VOICES: VoiceEntry[] = [ // 社交陪伴 @@ -111,6 +138,8 @@ const COSYVOICE_V3_FLASH_VOICES: VoiceEntry[] = [ ]; const MODEL_VOICES: Record = { + "qwen-audio-3.0-tts-plus": QWEN_AUDIO_30_TTS_PLUS_VOICES, + "qwen-audio-3.0-tts-flash": QWEN_AUDIO_30_TTS_FLASH_VOICES, "cosyvoice-v3-flash": COSYVOICE_V3_FLASH_VOICES, "cosyvoice-v3-plus": COSYVOICE_V3_FLASH_VOICES, "cosyvoice-v3.5-flash": [], @@ -118,11 +147,18 @@ const MODEL_VOICES: Record = { "cosyvoice-v2": [], }; +/** Official voice-list docs URL for the model family. */ +function voiceDocsUrl(model: string): string { + if (model.startsWith("qwen-audio-")) return QWEN_AUDIO_TTS_VOICE_DOC; + return VOICE_TTS_PAGE; +} + function printVoiceList(model: string): void { const voices = MODEL_VOICES[model]; + const docsUrl = voiceDocsUrl(model); if (!voices) { process.stdout.write(`No built-in voice list available for model: ${model}\n`); - process.stdout.write(`Browse voices in the console: ${VOICE_TTS_PAGE}\n`); + process.stdout.write(`See official voice list: ${docsUrl}\n`); return; } if (voices.length === 0) { @@ -138,11 +174,13 @@ function printVoiceList(model: string): void { `${col("VOICE ID", 26)} ${col("NAME", 10)} ${col("DESCRIPTION", 16)} LANGUAGE\n`, ); process.stdout.write(`${"-".repeat(26)} ${"-".repeat(10)} ${"-".repeat(16)} ${"-".repeat(12)}\n`); - for (const v of voices) { - process.stdout.write(`${col(v.voice, 26)} ${col(v.name, 10)} ${col(v.desc, 16)} ${v.lang}\n`); + for (const entry of voices) { + process.stdout.write( + `${col(entry.voice, 26)} ${col(entry.name, 10)} ${col(entry.desc, 16)} ${entry.lang}\n`, + ); } process.stdout.write(`\nTotal: ${voices.length} voices\n`); - process.stdout.write(`Preview and browse more voices in the console: \n${VOICE_TTS_PAGE}\n`); + process.stdout.write(`Preview and browse more voices: \n${docsUrl}\n`); } const SYNTHESIZE_FLAGS = { @@ -177,9 +215,9 @@ const SYNTHESIZE_FLAGS = { valueHint: "", description: { "en-US": - "Voice ID. Use --list-voices to see built-in voices for cosyvoice-v3-flash; for v3.5-flash provide a clone/design voice ID", + "Voice ID. Use --list-voices for the selected model (e.g. qwen-audio-3.0-tts-plus/flash, cosyvoice-v3-flash); for v3.5-flash provide a clone/design voice ID", "zh-CN": - "音色 ID。使用 --list-voices 查看 cosyvoice-v3-flash 的内置音色;使用 v3.5-flash 时需提供复刻/设计音色 ID", + "音色 ID。使用 --list-voices 查看所选模型的内置音色(如 qwen-audio-3.0-tts-plus/flash、cosyvoice-v3-flash);使用 v3.5-flash 时需提供复刻/设计音色 ID", }, }, listVoices: { @@ -290,10 +328,11 @@ export default defineCommand({ usageArgs: "--text [flags]", flags: SYNTHESIZE_FLAGS, exampleArgs: [ + "--list-voices --model qwen-audio-3.0-tts-plus", "--list-voices --model cosyvoice-v3-flash", { - "en-US": '--text "Hello, I am Qwen" --voice ', - "zh-CN": '--text "你好,我是通义千问" --voice ', + "en-US": '--text "Hello, I am Qwen" --model qwen-audio-3.0-tts-plus --voice longanlingxin', + "zh-CN": '--text "你好,我是通义千问" --model qwen-audio-3.0-tts-plus --voice longanlingxin', }, { "en-US": '--text "Hello world" --voice --language en', diff --git a/packages/commands/tests/e2e/speech-list-voices.e2e.test.ts b/packages/commands/tests/e2e/speech-list-voices.e2e.test.ts index 7d74d071..94d4ab79 100644 --- a/packages/commands/tests/e2e/speech-list-voices.e2e.test.ts +++ b/packages/commands/tests/e2e/speech-list-voices.e2e.test.ts @@ -1,5 +1,7 @@ +import { writeFileSync } from "node:fs"; +import { join } from "node:path"; import { describe, expect, test } from "vite-plus/test"; -import { isDashScopeE2EReady, runCommandHelp, runCommandE2e } from "./helpers.ts"; +import { isDashScopeE2EReady, makeE2eOutputDir, runCommandHelp, runCommandE2e } from "./helpers.ts"; import { SPEECH_ROUTES } from "./topic-routes.ts"; /** @@ -26,6 +28,66 @@ describe("e2e: speech list-voices", () => { expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/recognize|--url|audio|model/i); }); + + // --list-voices 只读本地目录,但 auth: apiKey 在 run() 前仍要求凭证 + test("【qwen-audio-3.0-tts-plus】获取音色列表", async () => { + const { stdout, stderr, exitCode } = await runCommandE2e(SPEECH_ROUTES, [ + "speech", + "synthesize", + "--list-voices", + "--model", + "qwen-audio-3.0-tts-plus", + "--api-key", + "sk-e2e-placeholder", + ]); + expect(exitCode, stderr).toBe(0); + expect(stdout).toContain("longanlingxin"); + expect(stdout).toContain("longanlufeng"); + }); + + test("【qwen-audio-3.0-tts-flash】获取音色列表", async () => { + const { stdout, stderr, exitCode } = await runCommandE2e(SPEECH_ROUTES, [ + "speech", + "synthesize", + "--list-voices", + "--model", + "qwen-audio-3.0-tts-flash", + "--api-key", + "sk-e2e-placeholder", + ]); + expect(exitCode, stderr).toBe(0); + expect(stdout).toContain("longanfengyue"); + expect(stdout).toContain("loongjohn"); + }); + + // 激活 token-plan 后不传 --model,应打出默认 qwen-audio plus 音色 + test("Token Plan 未显式传 --model 时 --list-voices 使用默认 qwen-audio TTS", async () => { + const configDir = makeE2eOutputDir("speech-list-voices-token-plan-default"); + writeFileSync( + join(configDir, "config.json"), + JSON.stringify({ + "token-plan": { + api_key: "sk-sp-e2e-placeholder", + base_url: "https://token-plan.cn-beijing.maas.aliyuncs.com", + default_speech_model: "qwen-audio-3.0-tts-plus", + }, + }), + ); + + const { stdout, stderr, exitCode } = await runCommandE2e( + SPEECH_ROUTES, + ["speech", "synthesize", "--config", "token-plan", "--list-voices"], + { + BAILIAN_CONFIG_DIR: configDir, + DASHSCOPE_API_KEY: "", + DASHSCOPE_BASE_URL: "", + }, + ); + + expect(exitCode, stderr).toBe(0); + expect(stdout).toContain("longanlingxin"); + expect(stdout).toContain("longanlufeng"); + }); }); describe.skipIf(!isDashScopeE2EReady())("e2e: speech list-voices", () => { diff --git a/skills/bailian-gen/reference/speech.md b/skills/bailian-gen/reference/speech.md index 817faae1..324e8021 100644 --- a/skills/bailian-gen/reference/speech.md +++ b/skills/bailian-gen/reference/speech.md @@ -100,36 +100,40 @@ bl speech recognize --url https://example.com/audio.mp3 --model qwen-audio-3.0-a #### Flags -| Flag | Type | Required | Description | -| -------------------------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------- | -| `--text ` | string | no | Text to synthesize into speech (or use --text-file) | -| `--text-file ` | string | no | Read text from a file instead of --text | -| `--model ` | string | no | Model ID (default: configured Profile TTS model, otherwise cosyvoice-v3-flash). System voices vary by model | -| `--voice ` | string | no | Voice ID. Use --list-voices to see built-in voices for cosyvoice-v3-flash; for v3.5-flash provide a clone/design voice ID | -| `--list-voices` | switch | no | List built-in system voices for the selected model and exit (console link shown in output) | -| `--format ` | string | no | Audio format: mp3, pcm, wav, opus (default: mp3; streaming default: pcm) | -| `--sample-rate ` | string | no | Audio sample rate in Hz (e.g. 24000) | -| `--volume ` | string | no | Volume 0-100 (default: 50) | -| `--rate ` | string | no | Speech rate 0.5-2.0 (default: 1.0) | -| `--pitch ` | string | no | Pitch multiplier 0.5-2.0 (default: 1.0) | -| `--seed ` | string | no | Random seed 0-65535 for reproducible synthesis | -| `--language ` | string | no | Language hint (e.g. zh, en, ja, ko, fr, de) | -| `--instruction ` | string | no | Natural language instruction to control speech style (e.g. "Use a gentle tone") | -| `--enable-ssml` | switch | no | Enable SSML markup parsing in input text | -| `--out ` | string | no | Save audio to file (default: auto-generate in temp dir) | -| `--stream` | switch | no | Stream raw PCM audio to stdout (pipe to player) | -| `--concurrent ` | number | no | Run N parallel requests (default: 1) | -| `--api-key ` | string | no | API key | -| `--base-url ` | string | no | API base URL | +| Flag | Type | Required | Description | +| -------------------------------- | ------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `--text ` | string | no | Text to synthesize into speech (or use --text-file) | +| `--text-file ` | string | no | Read text from a file instead of --text | +| `--model ` | string | no | Model ID (default: configured Profile TTS model, otherwise cosyvoice-v3-flash). System voices vary by model | +| `--voice ` | string | no | Voice ID. Use --list-voices for the selected model (e.g. qwen-audio-3.0-tts-plus/flash, cosyvoice-v3-flash); for v3.5-flash provide a clone/design voice ID | +| `--list-voices` | switch | no | List built-in system voices for the selected model and exit (console link shown in output) | +| `--format ` | string | no | Audio format: mp3, pcm, wav, opus (default: mp3; streaming default: pcm) | +| `--sample-rate ` | string | no | Audio sample rate in Hz (e.g. 24000) | +| `--volume ` | string | no | Volume 0-100 (default: 50) | +| `--rate ` | string | no | Speech rate 0.5-2.0 (default: 1.0) | +| `--pitch ` | string | no | Pitch multiplier 0.5-2.0 (default: 1.0) | +| `--seed ` | string | no | Random seed 0-65535 for reproducible synthesis | +| `--language ` | string | no | Language hint (e.g. zh, en, ja, ko, fr, de) | +| `--instruction ` | string | no | Natural language instruction to control speech style (e.g. "Use a gentle tone") | +| `--enable-ssml` | switch | no | Enable SSML markup parsing in input text | +| `--out ` | string | no | Save audio to file (default: auto-generate in temp dir) | +| `--stream` | switch | no | Stream raw PCM audio to stdout (pipe to player) | +| `--concurrent ` | number | no | Run N parallel requests (default: 1) | +| `--api-key ` | string | no | API key | +| `--base-url ` | string | no | API base URL | #### Examples +```bash +bl speech synthesize --list-voices --model qwen-audio-3.0-tts-plus +``` + ```bash bl speech synthesize --list-voices --model cosyvoice-v3-flash ``` ```bash -bl speech synthesize --text "Hello, I am Qwen" --voice +bl speech synthesize --text "Hello, I am Qwen" --model qwen-audio-3.0-tts-plus --voice longanlingxin ``` ```bash