Skip to content

Commit 9379da7

Browse files
committed
fix(speech): align flash vocabulary_id and qwen3-filetrans language params
1 parent ddcd564 commit 9379da7

8 files changed

Lines changed: 104 additions & 21 deletions

File tree

packages/commands/src/commands/speech/recognize.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
speechRecognizePath,
1515
resolveAsrApi,
1616
buildAsrFlashRequest,
17+
buildAsyncAsrLanguageFields,
1718
extractAsrFlashText,
1819
type AsrApiRoute,
1920
type AsrFlashFamily,
@@ -42,7 +43,7 @@ const RECOGNIZE_FLAGS = {
4243
type: "string",
4344
valueHint: "<lang>",
4445
description:
45-
"Language hint (e.g. zh, en, ja). Async & input-audio sync: language_hints; qwen3 sync: asr_options.language",
46+
"Language hint (e.g. zh, en, ja). Classic async/input-audio: language_hints; qwen3-filetrans: language; qwen3 sync: asr_options.language",
4647
},
4748
diarization: { type: "switch", description: "Enable automatic speaker diarization" },
4849
speakerCount: {
@@ -70,11 +71,18 @@ const RECOGNIZE_FLAGS = {
7071
} satisfies FlagsDef;
7172
type RecognizeFlags = ParsedFlags<typeof RECOGNIZE_FLAGS>;
7273

73-
function assertSyncFlashFlagsAllowed(flags: RecognizeFlags, model: string): void {
74+
function assertSyncFlashFlagsAllowed(
75+
flags: RecognizeFlags,
76+
model: string,
77+
flashFamily: AsrFlashFamily,
78+
): void {
7479
const unsupported: string[] = [];
7580
if (flags.diarization === true) unsupported.push("--diarization");
7681
if (flags.speakerCount !== undefined) unsupported.push("--speaker-count");
77-
if (flags.vocabularyId !== undefined) unsupported.push("--vocabulary-id");
82+
// qwen3 sync Flash 不走 vocabulary_id;input-audio Flash(fun-asr-flash* / qwen-audio-*-asr-flash)官方支持
83+
if (flashFamily === "qwen3" && flags.vocabularyId !== undefined) {
84+
unsupported.push("--vocabulary-id");
85+
}
7886
if (flags.channelId !== undefined) unsupported.push("--channel-id");
7987
if (flags.async === true) unsupported.push("--async");
8088
if (flags.pollInterval !== undefined) unsupported.push("--poll-interval");
@@ -133,7 +141,7 @@ export default defineCommand({
133141
}
134142

135143
if (route.kind === "sync-flash") {
136-
assertSyncFlashFlagsAllowed(flags, model);
144+
assertSyncFlashFlagsAllowed(flags, model, route.flashFamily!);
137145
if (rawUrls.length !== 1) {
138146
throw new BailianError(
139147
`Model "${model}" is a sync Flash ASR model and accepts exactly one --url (got ${rawUrls.length}).\n` +
@@ -173,8 +181,11 @@ export default defineCommand({
173181
}
174182

175183
const channelId = flags.channelId;
176-
const language = flags.language;
177184
const vocabularyId = flags.vocabularyId;
185+
const languageFields = buildAsyncAsrLanguageFields(
186+
route.asyncLanguageStyle ?? "language_hints",
187+
flags.language,
188+
);
178189

179190
const body: DashScopeASRRequest = {
180191
model,
@@ -184,7 +195,7 @@ export default defineCommand({
184195
: { file_urls: resolvedUrls },
185196
parameters: {
186197
channel_id: channelId !== undefined ? [channelId] : [0],
187-
language_hints: language ? [language] : undefined,
198+
...languageFields,
188199
diarization_enabled: diarization ? true : undefined,
189200
speaker_count: speakerCount,
190201
vocabulary_id: vocabularyId,
@@ -221,6 +232,7 @@ async function handleSyncFlashMode(
221232
model,
222233
audioUrl,
223234
language: flags.language,
235+
vocabularyId: flags.vocabularyId,
224236
flashFamily,
225237
});
226238

packages/commands/tests/e2e/speech-recognize.e2e.test.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,12 @@ describe("e2e: speech recognize", () => {
3232
path?: string;
3333
request?: {
3434
model?: string;
35-
parameters?: { format?: string; language_hints?: string[] };
35+
parameters?: {
36+
format?: string;
37+
language_hints?: string[];
38+
language?: string;
39+
vocabulary_id?: string;
40+
};
3641
input?: {
3742
file_url?: string;
3843
file_urls?: string[];
@@ -60,26 +65,33 @@ describe("e2e: speech recognize", () => {
6065
"https://dashscope.oss-cn-beijing.aliyuncs.com/samples/audio/paraformer/hello_world_female2.wav",
6166
"--language",
6267
"en",
68+
"--vocabulary-id",
69+
"vocab-e2e",
6370
]);
6471
expect(body.mode).toBe("sync");
6572
expect(body.path).toBe("/api/v1/services/aigc/multimodal-generation/generation");
6673
expect(body.request?.model).toBe("qwen-audio-3.0-asr-flash");
6774
expect(body.request?.parameters?.format).toBe("wav");
6875
expect(body.request?.parameters?.language_hints).toEqual(["en"]);
76+
expect(body.request?.parameters?.vocabulary_id).toBe("vocab-e2e");
6977
expect(body.request?.input?.messages?.[0]?.content?.[0]?.type).toBe("input_audio");
7078
});
7179

72-
test("speech recognize qwen3 filetrans dry-run 使用 file_url 单数字段", async () => {
80+
test("speech recognize qwen3 filetrans dry-run 使用 file_url 与 language", async () => {
7381
const body = await runRecognizeDryRun([
7482
"--model",
7583
"qwen3-asr-flash-filetrans",
7684
"--url",
7785
"https://dashscope.oss-cn-beijing.aliyuncs.com/samples/audio/paraformer/hello_world_female2.wav",
86+
"--language",
87+
"zh",
7888
]);
7989
expect(body.mode).toBe("async");
8090
expect(body.path).toBe("/api/v1/services/audio/asr/transcription");
8191
expect(body.request?.input?.file_url?.startsWith("https://")).toBe(true);
8292
expect(body.request?.input?.file_urls).toBeUndefined();
93+
expect(body.request?.parameters?.language).toBe("zh");
94+
expect(body.request?.parameters?.language_hints).toBeUndefined();
8395
});
8496

8597
test("speech recognize realtime 模型报用法错误", async () => {

packages/core/src/client/asr-routes.ts

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ export interface AsrApiRoute {
3131
* - `file_url`: qwen3-asr-flash-filetrans family
3232
*/
3333
asyncInputStyle?: "file_urls" | "file_url";
34+
/**
35+
* Async transcription language field style.
36+
* - `language_hints`: fun-asr / paraformer / qwen-audio filetrans...
37+
* - `language`: qwen3-asr-flash-filetrans*
38+
*/
39+
asyncLanguageStyle?: "language_hints" | "language";
3440
flashFamily?: AsrFlashFamily;
3541
/** Human-readable reason when kind is unsupported. */
3642
unsupportedReason?: string;
@@ -90,11 +96,13 @@ export function resolveAsrApi(model: string): AsrApiRoute {
9096
}
9197

9298
if (isFiletransModel(model)) {
99+
const isQwen3Filetrans = isQwen3FiletransModel(model);
93100
return {
94101
kind: "async-filetrans",
95102
path: speechRecognizePath(),
96103
useSync: false,
97-
asyncInputStyle: isQwen3FiletransModel(model) ? "file_url" : "file_urls",
104+
asyncInputStyle: isQwen3Filetrans ? "file_url" : "file_urls",
105+
asyncLanguageStyle: isQwen3Filetrans ? "language" : "language_hints",
98106
};
99107
}
100108

@@ -122,6 +130,7 @@ export function resolveAsrApi(model: string): AsrApiRoute {
122130
path: speechRecognizePath(),
123131
useSync: false,
124132
asyncInputStyle: "file_urls",
133+
asyncLanguageStyle: "language_hints",
125134
};
126135
}
127136

@@ -139,22 +148,42 @@ export interface BuildAsrFlashRequestOpts {
139148
model: string;
140149
audioUrl: string;
141150
language?: string;
151+
/** 预编译热词 ID;仅 input-audio Flash(fun-asr-flash* / qwen-audio-*-asr-flash)官方支持 */
152+
vocabularyId?: string;
142153
flashFamily: AsrFlashFamily;
143154
}
144155

156+
/**
157+
* 按异步路由的 language 字段风格构造语种参数。
158+
* qwen3-asr-flash-filetrans* → `language`;其余异步模型 → `language_hints`。
159+
*/
160+
export function buildAsyncAsrLanguageFields(
161+
languageStyle: "language_hints" | "language",
162+
language?: string,
163+
): { language_hints?: string[]; language?: string } {
164+
if (!language) return {};
165+
if (languageStyle === "language") {
166+
return { language };
167+
}
168+
return { language_hints: [language] };
169+
}
170+
145171
/** Build a sync multimodal ASR request body for Flash models. */
146172
export function buildAsrFlashRequest(opts: BuildAsrFlashRequestOpts): Record<string, unknown> {
147-
const { model, audioUrl, language, flashFamily } = opts;
173+
const { model, audioUrl, language, vocabularyId, flashFamily } = opts;
148174

149175
if (flashFamily === "input-audio") {
150-
// 与官方 Qwen-Audio / Fun-ASR-Flash 文档一致:语种走 language_hints
176+
// 与官方 Qwen-Audio / Fun-ASR-Flash 文档一致:语种走 language_hints,热词走 vocabulary_id
151177
const parameters: Record<string, unknown> = {
152178
format: inferAudioFormatHint(audioUrl),
153179
sample_rate: "16000",
154180
};
155181
if (language) {
156182
parameters.language_hints = [language];
157183
}
184+
if (vocabularyId) {
185+
parameters.vocabulary_id = vocabularyId;
186+
}
158187
return {
159188
model,
160189
input: {

packages/core/src/client/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export {
3636
} from "./image-routes.ts";
3737
export {
3838
buildAsrFlashRequest,
39+
buildAsyncAsrLanguageFields,
3940
extractAsrFlashText,
4041
inferAudioFormatHint,
4142
resolveAsrApi,

packages/core/src/types/api.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -538,7 +538,10 @@ export interface DashScopeASRRequest {
538538
};
539539
parameters?: {
540540
channel_id?: number[];
541+
/** fun-asr / paraformer / qwen-audio filetrans 等经典异步模型 */
541542
language_hints?: string[];
543+
/** qwen3-asr-flash-filetrans* 使用单数字段 language */
544+
language?: string;
542545
diarization_enabled?: boolean;
543546
speaker_count?: number;
544547
vocabulary_id?: string;

packages/core/tests/asr-routes.test.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { expect, test } from "vite-plus/test";
22
import {
33
buildAsrFlashRequest,
4+
buildAsyncAsrLanguageFields,
45
extractAsrFlashText,
56
inferAudioFormatHint,
67
resolveAsrApi,
@@ -24,6 +25,7 @@ test("resolveAsrApi routes model families correctly", () => {
2425
useSync: false,
2526
path: "/api/v1/services/audio/asr/transcription",
2627
asyncInputStyle: "file_url",
28+
asyncLanguageStyle: "language",
2729
},
2830
},
2931
{
@@ -32,6 +34,7 @@ test("resolveAsrApi routes model families correctly", () => {
3234
kind: "async-filetrans",
3335
useSync: false,
3436
asyncInputStyle: "file_urls",
37+
asyncLanguageStyle: "language_hints",
3538
},
3639
},
3740
{
@@ -112,6 +115,7 @@ test("buildAsrFlashRequest shapes qwen3 and input-audio bodies", () => {
112115
model: "qwen-audio-3.0-asr-flash",
113116
audioUrl: "https://example.com/a.wav",
114117
language: "en",
118+
vocabularyId: "vocab-abc",
115119
flashFamily: "input-audio",
116120
}),
117121
).toEqual({
@@ -124,8 +128,21 @@ test("buildAsrFlashRequest shapes qwen3 and input-audio bodies", () => {
124128
},
125129
],
126130
},
127-
parameters: { format: "wav", sample_rate: "16000", language_hints: ["en"] },
131+
parameters: {
132+
format: "wav",
133+
sample_rate: "16000",
134+
language_hints: ["en"],
135+
vocabulary_id: "vocab-abc",
136+
},
137+
});
138+
});
139+
140+
test("buildAsyncAsrLanguageFields maps language by async style", () => {
141+
expect(buildAsyncAsrLanguageFields("language_hints", "zh")).toEqual({
142+
language_hints: ["zh"],
128143
});
144+
expect(buildAsyncAsrLanguageFields("language", "zh")).toEqual({ language: "zh" });
145+
expect(buildAsyncAsrLanguageFields("language", undefined)).toEqual({});
129146
});
130147

131148
test("extractAsrFlashText reads qwen3 choices and input-audio text fields", () => {

packages/runtime/src/pipeline/steps/bl-api.ts

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
speechRecognizePath,
1313
resolveAsrApi,
1414
buildAsrFlashRequest,
15+
buildAsyncAsrLanguageFields,
1516
extractAsrFlashText,
1617
stripUndefined,
1718
resolveBooleanFlag,
@@ -596,15 +597,18 @@ export async function speechRecognize(
596597
{ step: "speech/recognize" },
597598
);
598599
}
599-
if (
600-
input.diarization ||
601-
input["speaker-count"] !== undefined ||
602-
input["vocabulary-id"] !== undefined ||
603-
input["channel-id"] !== undefined
604-
) {
600+
const unsupportedFlags: string[] = [];
601+
if (input.diarization) unsupportedFlags.push("diarization");
602+
if (input["speaker-count"] !== undefined) unsupportedFlags.push("speaker-count");
603+
// input-audio Flash 官方支持 vocabulary_id;qwen3 sync Flash 不支持
604+
if (route.flashFamily === "qwen3" && input["vocabulary-id"] !== undefined) {
605+
unsupportedFlags.push("vocabulary-id");
606+
}
607+
if (input["channel-id"] !== undefined) unsupportedFlags.push("channel-id");
608+
if (unsupportedFlags.length > 0) {
605609
throw new PipelineError(
606610
"invalid_input",
607-
`Model "${model}" uses sync Flash ASR and does not support diarization / speaker-count / vocabulary-id / channel-id`,
611+
`Model "${model}" uses sync Flash ASR and does not support: ${unsupportedFlags.join(", ")}`,
608612
{ step: "speech/recognize" },
609613
);
610614
}
@@ -641,6 +645,7 @@ export async function speechRecognize(
641645
model,
642646
audioUrl: fileUrls[0]!,
643647
language: input.language,
648+
vocabularyId: input["vocabulary-id"],
644649
flashFamily,
645650
});
646651
const response = await env.client.requestJson<Record<string, unknown>>({
@@ -657,13 +662,17 @@ export async function speechRecognize(
657662
};
658663
}
659664

665+
const languageFields = buildAsyncAsrLanguageFields(
666+
route.asyncLanguageStyle ?? "language_hints",
667+
input.language,
668+
);
660669
const body: DashScopeASRRequest = {
661670
model,
662671
input:
663672
route.asyncInputStyle === "file_url" ? { file_url: fileUrls[0]! } : { file_urls: fileUrls },
664673
parameters: {
665674
channel_id: input["channel-id"] !== undefined ? [input["channel-id"]] : undefined,
666-
language_hints: input.language ? [input.language] : undefined,
675+
...languageFields,
667676
diarization_enabled: input.diarization,
668677
speaker_count: input["speaker-count"],
669678
vocabulary_id: input["vocabulary-id"],

skills/bailian-gen/reference/speech.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ Index: [index.md](index.md)
2828
| --------------------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
2929
| `--url <url>` | array | yes | Audio file URL or local file path (repeatable, max 100) |
3030
| `--model <model>` | string | no | Model ID (default: fun-asr). Async: fun-asr / _-filetrans / paraformer-_; sync: qwen3-asr-flash* / fun-asr-flash* / qwen-audio-\*-asr-flash |
31-
| `--language <lang>` | string | no | Language hint (e.g. zh, en, ja). Async & input-audio sync: language_hints; qwen3 sync: asr_options.language |
31+
| `--language <lang>` | string | no | Language hint (e.g. zh, en, ja). Classic async/input-audio: language_hints; qwen3-filetrans: language; qwen3 sync: asr_options.language |
3232
| `--diarization` | switch | no | Enable automatic speaker diarization |
3333
| `--speaker-count <n>` | number | no | Expected number of speakers (requires --diarization) |
3434
| `--vocabulary-id <id>` | string | no | Hot-word vocabulary ID for improved accuracy |

0 commit comments

Comments
 (0)