Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 47 additions & 8 deletions packages/commands/src/commands/speech/synthesize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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[] = [
// 社交陪伴
Expand Down Expand Up @@ -111,18 +138,27 @@ const COSYVOICE_V3_FLASH_VOICES: VoiceEntry[] = [
];

const MODEL_VOICES: Record<string, VoiceEntry[]> = {
"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": [],
"cosyvoice-v3.5-plus": [],
"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) {
Expand All @@ -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 = {
Expand Down Expand Up @@ -177,9 +215,9 @@ const SYNTHESIZE_FLAGS = {
valueHint: "<voice>",
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: {
Expand Down Expand Up @@ -290,10 +328,11 @@ export default defineCommand({
usageArgs: "--text <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 <voice_id>',
"zh-CN": '--text "你好,我是通义千问" --voice <voice_id>',
"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 <voice_id> --language en',
Expand Down
64 changes: 63 additions & 1 deletion packages/commands/tests/e2e/speech-list-voices.e2e.test.ts
Original file line number Diff line number Diff line change
@@ -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";

/**
Expand All @@ -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", () => {
Expand Down
48 changes: 26 additions & 22 deletions skills/bailian-gen/reference/speech.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <text>` | string | no | Text to synthesize into speech (or use --text-file) |
| `--text-file <path>` | string | no | Read text from a file instead of --text |
| `--model <model>` | string | no | Model ID (default: configured Profile TTS model, otherwise cosyvoice-v3-flash). System voices vary by model |
| `--voice <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 <mp3\|pcm\|wav\|opus>` | string | no | Audio format: mp3, pcm, wav, opus (default: mp3; streaming default: pcm) |
| `--sample-rate <rate>` | string | no | Audio sample rate in Hz (e.g. 24000) |
| `--volume <volume>` | string | no | Volume 0-100 (default: 50) |
| `--rate <rate>` | string | no | Speech rate 0.5-2.0 (default: 1.0) |
| `--pitch <pitch>` | string | no | Pitch multiplier 0.5-2.0 (default: 1.0) |
| `--seed <seed>` | string | no | Random seed 0-65535 for reproducible synthesis |
| `--language <lang>` | string | no | Language hint (e.g. zh, en, ja, ko, fr, de) |
| `--instruction <text>` | 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 <path>` | 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 <n>` | number | no | Run N parallel requests (default: 1) |
| `--api-key <key>` | string | no | API key |
| `--base-url <url>` | string | no | API base URL |
| Flag | Type | Required | Description |
| -------------------------------- | ------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--text <text>` | string | no | Text to synthesize into speech (or use --text-file) |
| `--text-file <path>` | string | no | Read text from a file instead of --text |
| `--model <model>` | string | no | Model ID (default: configured Profile TTS model, otherwise cosyvoice-v3-flash). System voices vary by model |
| `--voice <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 <mp3\|pcm\|wav\|opus>` | string | no | Audio format: mp3, pcm, wav, opus (default: mp3; streaming default: pcm) |
| `--sample-rate <rate>` | string | no | Audio sample rate in Hz (e.g. 24000) |
| `--volume <volume>` | string | no | Volume 0-100 (default: 50) |
| `--rate <rate>` | string | no | Speech rate 0.5-2.0 (default: 1.0) |
| `--pitch <pitch>` | string | no | Pitch multiplier 0.5-2.0 (default: 1.0) |
| `--seed <seed>` | string | no | Random seed 0-65535 for reproducible synthesis |
| `--language <lang>` | string | no | Language hint (e.g. zh, en, ja, ko, fr, de) |
| `--instruction <text>` | 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 <path>` | 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 <n>` | number | no | Run N parallel requests (default: 1) |
| `--api-key <key>` | string | no | API key |
| `--base-url <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 <voice_id>
bl speech synthesize --text "Hello, I am Qwen" --model qwen-audio-3.0-tts-plus --voice longanlingxin
```

```bash
Expand Down
Loading