Skip to content

Commit 20594b7

Browse files
Merge pull request #207 from modelstudioai/fix/speech-qwen-audio-voices
Add qwen-audio TTS voices
2 parents 4dd0d68 + 1bcf84c commit 20594b7

3 files changed

Lines changed: 136 additions & 31 deletions

File tree

packages/commands/src/commands/speech/synthesize.ts

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { emitResult, emitBare } from "bailian-cli-runtime";
2626
import { VOICE_TTS_PAGE } from "bailian-cli-runtime";
2727

2828
const COSYVOICE_CLONE_DESIGN_DOC = `${DOCS_HOSTS.cn}/cosyvoice-clone-design-api`;
29+
const QWEN_AUDIO_TTS_VOICE_DOC = `${DOCS_HOSTS.cn}/qwen-audio-tts-voice-list`;
2930

3031
interface VoiceEntry {
3132
voice: string;
@@ -34,6 +35,32 @@ interface VoiceEntry {
3435
lang: string;
3536
}
3637

38+
// qwen-audio-3.0-tts-plus system voices (official docs list only these two)
39+
const QWEN_AUDIO_30_TTS_PLUS_VOICES: VoiceEntry[] = [
40+
{ voice: "longanlingxin", name: "龙安灵心", desc: "知心温暖音", lang: "中文/英文" },
41+
{ voice: "longanlufeng", name: "龙安鲁风", desc: "明亮开朗音", lang: "中文/英文" },
42+
];
43+
44+
// qwen-audio-3.0-tts-flash system voices
45+
const QWEN_AUDIO_30_TTS_FLASH_VOICES: VoiceEntry[] = [
46+
// Social companion (premium Chinese)
47+
{ voice: "longanfengyue", name: "龙安风悦", desc: "自然亲切音", lang: "中文/英文" },
48+
{ voice: "longanyuanfei", name: "龙安元妃", desc: "高傲妃子音", lang: "中文/英文" },
49+
{ voice: "longanlingxi", name: "龙安灵希", desc: "可爱甜美音", lang: "中文/英文" },
50+
{ voice: "longanxiaoxin", name: "龙安小昕", desc: "亲切活泼音", lang: "中文/英文" },
51+
{ voice: "longanhuan_v3.6", name: "龙安欢", desc: "欢脱元气女", lang: "中文/英文" },
52+
// Kids / toys (premium children)
53+
{ voice: "longjielidou_v3.6", name: "龙杰力豆", desc: "天真男童", lang: "中文/英文" },
54+
{ voice: "longpaopao_v3.6", name: "龙泡泡", desc: "软糯可爱音", lang: "中文/英文" },
55+
// Character / game (premium Chinese)
56+
{ voice: "longhuohuo_v3.6", name: "龙火火", desc: "顽皮少年音", lang: "中文/英文" },
57+
{ voice: "longchuanshu_v3.6", name: "龙川叔", desc: "川普大叔音", lang: "中文/英文" },
58+
// Social companion / assistant (premium English)
59+
{ voice: "loongmary", name: "loongmary", desc: "温暖英音", lang: "英文" },
60+
{ voice: "loongeva_v3.6", name: "loongeva", desc: "高智美音", lang: "英文" },
61+
{ voice: "loongjohn", name: "loongJohn", desc: "沉稳亲切美音", lang: "英文" },
62+
];
63+
3764
// cosyvoice-v3-flash system voices
3865
const COSYVOICE_V3_FLASH_VOICES: VoiceEntry[] = [
3966
// 社交陪伴
@@ -111,18 +138,27 @@ const COSYVOICE_V3_FLASH_VOICES: VoiceEntry[] = [
111138
];
112139

113140
const MODEL_VOICES: Record<string, VoiceEntry[]> = {
141+
"qwen-audio-3.0-tts-plus": QWEN_AUDIO_30_TTS_PLUS_VOICES,
142+
"qwen-audio-3.0-tts-flash": QWEN_AUDIO_30_TTS_FLASH_VOICES,
114143
"cosyvoice-v3-flash": COSYVOICE_V3_FLASH_VOICES,
115144
"cosyvoice-v3-plus": COSYVOICE_V3_FLASH_VOICES,
116145
"cosyvoice-v3.5-flash": [],
117146
"cosyvoice-v3.5-plus": [],
118147
"cosyvoice-v2": [],
119148
};
120149

150+
/** Official voice-list docs URL for the model family. */
151+
function voiceDocsUrl(model: string): string {
152+
if (model.startsWith("qwen-audio-")) return QWEN_AUDIO_TTS_VOICE_DOC;
153+
return VOICE_TTS_PAGE;
154+
}
155+
121156
function printVoiceList(model: string): void {
122157
const voices = MODEL_VOICES[model];
158+
const docsUrl = voiceDocsUrl(model);
123159
if (!voices) {
124160
process.stdout.write(`No built-in voice list available for model: ${model}\n`);
125-
process.stdout.write(`Browse voices in the console: ${VOICE_TTS_PAGE}\n`);
161+
process.stdout.write(`See official voice list: ${docsUrl}\n`);
126162
return;
127163
}
128164
if (voices.length === 0) {
@@ -138,11 +174,13 @@ function printVoiceList(model: string): void {
138174
`${col("VOICE ID", 26)} ${col("NAME", 10)} ${col("DESCRIPTION", 16)} LANGUAGE\n`,
139175
);
140176
process.stdout.write(`${"-".repeat(26)} ${"-".repeat(10)} ${"-".repeat(16)} ${"-".repeat(12)}\n`);
141-
for (const v of voices) {
142-
process.stdout.write(`${col(v.voice, 26)} ${col(v.name, 10)} ${col(v.desc, 16)} ${v.lang}\n`);
177+
for (const entry of voices) {
178+
process.stdout.write(
179+
`${col(entry.voice, 26)} ${col(entry.name, 10)} ${col(entry.desc, 16)} ${entry.lang}\n`,
180+
);
143181
}
144182
process.stdout.write(`\nTotal: ${voices.length} voices\n`);
145-
process.stdout.write(`Preview and browse more voices in the console: \n${VOICE_TTS_PAGE}\n`);
183+
process.stdout.write(`Preview and browse more voices: \n${docsUrl}\n`);
146184
}
147185

148186
const SYNTHESIZE_FLAGS = {
@@ -177,9 +215,9 @@ const SYNTHESIZE_FLAGS = {
177215
valueHint: "<voice>",
178216
description: {
179217
"en-US":
180-
"Voice ID. Use --list-voices to see built-in voices for cosyvoice-v3-flash; for v3.5-flash provide a clone/design voice ID",
218+
"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",
181219
"zh-CN":
182-
"音色 ID。使用 --list-voices 查看 cosyvoice-v3-flash 的内置音色;使用 v3.5-flash 时需提供复刻/设计音色 ID",
220+
"音色 ID。使用 --list-voices 查看所选模型的内置音色(如 qwen-audio-3.0-tts-plus/flash、cosyvoice-v3-flash;使用 v3.5-flash 时需提供复刻/设计音色 ID",
183221
},
184222
},
185223
listVoices: {
@@ -290,10 +328,11 @@ export default defineCommand({
290328
usageArgs: "--text <text> [flags]",
291329
flags: SYNTHESIZE_FLAGS,
292330
exampleArgs: [
331+
"--list-voices --model qwen-audio-3.0-tts-plus",
293332
"--list-voices --model cosyvoice-v3-flash",
294333
{
295-
"en-US": '--text "Hello, I am Qwen" --voice <voice_id>',
296-
"zh-CN": '--text "你好,我是通义千问" --voice <voice_id>',
334+
"en-US": '--text "Hello, I am Qwen" --model qwen-audio-3.0-tts-plus --voice longanlingxin',
335+
"zh-CN": '--text "你好,我是通义千问" --model qwen-audio-3.0-tts-plus --voice longanlingxin',
297336
},
298337
{
299338
"en-US": '--text "Hello world" --voice <voice_id> --language en',

packages/commands/tests/e2e/speech-list-voices.e2e.test.ts

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import { writeFileSync } from "node:fs";
2+
import { join } from "node:path";
13
import { describe, expect, test } from "vite-plus/test";
2-
import { isDashScopeE2EReady, runCommandHelp, runCommandE2e } from "./helpers.ts";
4+
import { isDashScopeE2EReady, makeE2eOutputDir, runCommandHelp, runCommandE2e } from "./helpers.ts";
35
import { SPEECH_ROUTES } from "./topic-routes.ts";
46

57
/**
@@ -26,6 +28,66 @@ describe("e2e: speech list-voices", () => {
2628
expect(exitCode, stderr).toBe(0);
2729
expect(stderr).toMatch(/recognize|--url|audio|model/i);
2830
});
31+
32+
// --list-voices 只读本地目录,但 auth: apiKey 在 run() 前仍要求凭证
33+
test("【qwen-audio-3.0-tts-plus】获取音色列表", async () => {
34+
const { stdout, stderr, exitCode } = await runCommandE2e(SPEECH_ROUTES, [
35+
"speech",
36+
"synthesize",
37+
"--list-voices",
38+
"--model",
39+
"qwen-audio-3.0-tts-plus",
40+
"--api-key",
41+
"sk-e2e-placeholder",
42+
]);
43+
expect(exitCode, stderr).toBe(0);
44+
expect(stdout).toContain("longanlingxin");
45+
expect(stdout).toContain("longanlufeng");
46+
});
47+
48+
test("【qwen-audio-3.0-tts-flash】获取音色列表", async () => {
49+
const { stdout, stderr, exitCode } = await runCommandE2e(SPEECH_ROUTES, [
50+
"speech",
51+
"synthesize",
52+
"--list-voices",
53+
"--model",
54+
"qwen-audio-3.0-tts-flash",
55+
"--api-key",
56+
"sk-e2e-placeholder",
57+
]);
58+
expect(exitCode, stderr).toBe(0);
59+
expect(stdout).toContain("longanfengyue");
60+
expect(stdout).toContain("loongjohn");
61+
});
62+
63+
// 激活 token-plan 后不传 --model,应打出默认 qwen-audio plus 音色
64+
test("Token Plan 未显式传 --model 时 --list-voices 使用默认 qwen-audio TTS", async () => {
65+
const configDir = makeE2eOutputDir("speech-list-voices-token-plan-default");
66+
writeFileSync(
67+
join(configDir, "config.json"),
68+
JSON.stringify({
69+
"token-plan": {
70+
api_key: "sk-sp-e2e-placeholder",
71+
base_url: "https://token-plan.cn-beijing.maas.aliyuncs.com",
72+
default_speech_model: "qwen-audio-3.0-tts-plus",
73+
},
74+
}),
75+
);
76+
77+
const { stdout, stderr, exitCode } = await runCommandE2e(
78+
SPEECH_ROUTES,
79+
["speech", "synthesize", "--config", "token-plan", "--list-voices"],
80+
{
81+
BAILIAN_CONFIG_DIR: configDir,
82+
DASHSCOPE_API_KEY: "",
83+
DASHSCOPE_BASE_URL: "",
84+
},
85+
);
86+
87+
expect(exitCode, stderr).toBe(0);
88+
expect(stdout).toContain("longanlingxin");
89+
expect(stdout).toContain("longanlufeng");
90+
});
2991
});
3092

3193
describe.skipIf(!isDashScopeE2EReady())("e2e: speech list-voices", () => {

skills/bailian-gen/reference/speech.md

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -100,36 +100,40 @@ bl speech recognize --url https://example.com/audio.mp3 --model qwen-audio-3.0-a
100100

101101
#### Flags
102102

103-
| Flag | Type | Required | Description |
104-
| -------------------------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------- |
105-
| `--text <text>` | string | no | Text to synthesize into speech (or use --text-file) |
106-
| `--text-file <path>` | string | no | Read text from a file instead of --text |
107-
| `--model <model>` | string | no | Model ID (default: configured Profile TTS model, otherwise cosyvoice-v3-flash). System voices vary by model |
108-
| `--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 |
109-
| `--list-voices` | switch | no | List built-in system voices for the selected model and exit (console link shown in output) |
110-
| `--format <mp3\|pcm\|wav\|opus>` | string | no | Audio format: mp3, pcm, wav, opus (default: mp3; streaming default: pcm) |
111-
| `--sample-rate <rate>` | string | no | Audio sample rate in Hz (e.g. 24000) |
112-
| `--volume <volume>` | string | no | Volume 0-100 (default: 50) |
113-
| `--rate <rate>` | string | no | Speech rate 0.5-2.0 (default: 1.0) |
114-
| `--pitch <pitch>` | string | no | Pitch multiplier 0.5-2.0 (default: 1.0) |
115-
| `--seed <seed>` | string | no | Random seed 0-65535 for reproducible synthesis |
116-
| `--language <lang>` | string | no | Language hint (e.g. zh, en, ja, ko, fr, de) |
117-
| `--instruction <text>` | string | no | Natural language instruction to control speech style (e.g. "Use a gentle tone") |
118-
| `--enable-ssml` | switch | no | Enable SSML markup parsing in input text |
119-
| `--out <path>` | string | no | Save audio to file (default: auto-generate in temp dir) |
120-
| `--stream` | switch | no | Stream raw PCM audio to stdout (pipe to player) |
121-
| `--concurrent <n>` | number | no | Run N parallel requests (default: 1) |
122-
| `--api-key <key>` | string | no | API key |
123-
| `--base-url <url>` | string | no | API base URL |
103+
| Flag | Type | Required | Description |
104+
| -------------------------------- | ------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
105+
| `--text <text>` | string | no | Text to synthesize into speech (or use --text-file) |
106+
| `--text-file <path>` | string | no | Read text from a file instead of --text |
107+
| `--model <model>` | string | no | Model ID (default: configured Profile TTS model, otherwise cosyvoice-v3-flash). System voices vary by model |
108+
| `--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 |
109+
| `--list-voices` | switch | no | List built-in system voices for the selected model and exit (console link shown in output) |
110+
| `--format <mp3\|pcm\|wav\|opus>` | string | no | Audio format: mp3, pcm, wav, opus (default: mp3; streaming default: pcm) |
111+
| `--sample-rate <rate>` | string | no | Audio sample rate in Hz (e.g. 24000) |
112+
| `--volume <volume>` | string | no | Volume 0-100 (default: 50) |
113+
| `--rate <rate>` | string | no | Speech rate 0.5-2.0 (default: 1.0) |
114+
| `--pitch <pitch>` | string | no | Pitch multiplier 0.5-2.0 (default: 1.0) |
115+
| `--seed <seed>` | string | no | Random seed 0-65535 for reproducible synthesis |
116+
| `--language <lang>` | string | no | Language hint (e.g. zh, en, ja, ko, fr, de) |
117+
| `--instruction <text>` | string | no | Natural language instruction to control speech style (e.g. "Use a gentle tone") |
118+
| `--enable-ssml` | switch | no | Enable SSML markup parsing in input text |
119+
| `--out <path>` | string | no | Save audio to file (default: auto-generate in temp dir) |
120+
| `--stream` | switch | no | Stream raw PCM audio to stdout (pipe to player) |
121+
| `--concurrent <n>` | number | no | Run N parallel requests (default: 1) |
122+
| `--api-key <key>` | string | no | API key |
123+
| `--base-url <url>` | string | no | API base URL |
124124

125125
#### Examples
126126

127+
```bash
128+
bl speech synthesize --list-voices --model qwen-audio-3.0-tts-plus
129+
```
130+
127131
```bash
128132
bl speech synthesize --list-voices --model cosyvoice-v3-flash
129133
```
130134

131135
```bash
132-
bl speech synthesize --text "Hello, I am Qwen" --voice <voice_id>
136+
bl speech synthesize --text "Hello, I am Qwen" --model qwen-audio-3.0-tts-plus --voice longanlingxin
133137
```
134138

135139
```bash

0 commit comments

Comments
 (0)