Skip to content

Commit c2d1770

Browse files
committed
feat(speech): add instant hotwords, context, and vocabulary CRUD
1 parent 2090293 commit c2d1770

32 files changed

Lines changed: 2215 additions & 85 deletions

README.md

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -111,13 +111,14 @@ irm https://bailian.aliyun.com/cli/install.ps1 | iex
111111

112112
Once installed, just describe your task to your AI Agent — no need to assemble commands by hand.
113113

114-
| Scenario | What to say to your Agent |
115-
| ------------------------ | --------------------------------------------------------------------------------- |
116-
| Managed Agent | "Create a Managed Agent that can generate short-film storyboards and videos." |
117-
| Image & video generation | "Generate an image of a cat in a spacesuit on Mars, then turn it into a video." |
118-
| Usage & quota | "Show my recent model usage, free-tier quota, and rate limits." |
119-
| Model selection | "Recommend a model for image understanding and customer support." |
120-
| About Bailian CLI | "Tell me what Bailian CLI can do for me, and suggest how to use it for my needs." |
114+
| Scenario | What to say to your Agent |
115+
| ------------------------ | --------------------------------------------------------------------------------------------- |
116+
| Managed Agent | "Create a Managed Agent that can generate short-film storyboards and videos." |
117+
| Image & video generation | "Generate an image of a cat in a spacesuit on Mars, then turn it into a video." |
118+
| Speech recognition | "Transcribe this audio; if proper nouns are wrong, add hot words and try again." |
119+
| Usage & quota | "Show my recent model usage, free-tier quota, and rate limits." |
120+
| Model selection | "Recommend a model for image understanding and customer support." |
121+
| About Bailian CLI | "Tell me what Bailian CLI can do for me, and suggest how to use it for my needs." |
121122

122123
> More examples and scenarios: [Aliyun Model Studio CLI Site](https://bailian.console.aliyun.com/cli?source_channel=cli_github&)
123124

README.zh.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ irm https://bailian.aliyun.com/cli/install.ps1 | iex
114114
| ---------------- | ----------------------------------------------------------------------- |
115115
| Managed Agent | “帮我创建一个能够生成短片分镜和视频的 Managed Agent。” |
116116
| 图片和视频生成 | “生成一张穿着太空服的猫站在火星上的图片,再把它制作成一段视频。” |
117+
| 语音识别 | “把这段音频转写成文字,专有名词识别不准的话帮我加上热词再试。” |
117118
| 用量与额度 | “查看最近的模型用量、免费额度和限流情况。” |
118119
| 模型选型 | “推荐一个适合图片理解和智能客服的模型。” |
119120
| 了解 Bailian CLI | “介绍一下 Bailian CLI 能帮我完成哪些任务,并根据我的需求推荐使用方式。” |

packages/cli/README.md

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -111,13 +111,14 @@ irm https://bailian.aliyun.com/cli/install.ps1 | iex
111111

112112
Once installed, just describe your task to your AI Agent — no need to assemble commands by hand.
113113

114-
| Scenario | What to say to your Agent |
115-
| ------------------------ | --------------------------------------------------------------------------------- |
116-
| Managed Agent | "Create a Managed Agent that can generate short-film storyboards and videos." |
117-
| Image & video generation | "Generate an image of a cat in a spacesuit on Mars, then turn it into a video." |
118-
| Usage & quota | "Show my recent model usage, free-tier quota, and rate limits." |
119-
| Model selection | "Recommend a model for image understanding and customer support." |
120-
| About Bailian CLI | "Tell me what Bailian CLI can do for me, and suggest how to use it for my needs." |
114+
| Scenario | What to say to your Agent |
115+
| ------------------------ | --------------------------------------------------------------------------------------------- |
116+
| Managed Agent | "Create a Managed Agent that can generate short-film storyboards and videos." |
117+
| Image & video generation | "Generate an image of a cat in a spacesuit on Mars, then turn it into a video." |
118+
| Speech recognition | "Transcribe this audio; if proper nouns are wrong, add hot words and try again." |
119+
| Usage & quota | "Show my recent model usage, free-tier quota, and rate limits." |
120+
| Model selection | "Recommend a model for image understanding and customer support." |
121+
| About Bailian CLI | "Tell me what Bailian CLI can do for me, and suggest how to use it for my needs." |
121122

122123
> More examples and scenarios: [Aliyun Model Studio CLI Site](https://bailian.console.aliyun.com/cli?source_channel=cli_github&)
123124

packages/cli/README.zh.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ irm https://bailian.aliyun.com/cli/install.ps1 | iex
114114
| ---------------- | ----------------------------------------------------------------------- |
115115
| Managed Agent | “帮我创建一个能够生成短片分镜和视频的 Managed Agent。” |
116116
| 图片和视频生成 | “生成一张穿着太空服的猫站在火星上的图片,再把它制作成一段视频。” |
117+
| 语音识别 | “把这段音频转写成文字,专有名词识别不准的话帮我加上热词再试。” |
117118
| 用量与额度 | “查看最近的模型用量、免费额度和限流情况。” |
118119
| 模型选型 | “推荐一个适合图片理解和智能客服的模型。” |
119120
| 了解 Bailian CLI | “介绍一下 Bailian CLI 能帮我完成哪些任务,并根据我的需求推荐使用方式。” |

packages/cli/src/commands.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,11 @@ import {
7070
searchWeb,
7171
speechSynthesize,
7272
speechRecognize,
73+
speechVocabularyCreate,
74+
speechVocabularyList,
75+
speechVocabularyGet,
76+
speechVocabularyUpdate,
77+
speechVocabularyDelete,
7378
fileUpload,
7479
consoleCall,
7580
usageFree,
@@ -286,6 +291,11 @@ export const commands: Record<string, AnyCommand> = {
286291
"search web": searchWeb,
287292
"speech synthesize": speechSynthesize,
288293
"speech recognize": speechRecognize,
294+
"speech vocabulary create": speechVocabularyCreate,
295+
"speech vocabulary list": speechVocabularyList,
296+
"speech vocabulary get": speechVocabularyGet,
297+
"speech vocabulary update": speechVocabularyUpdate,
298+
"speech vocabulary delete": speechVocabularyDelete,
289299
"file upload": fileUpload,
290300
"console call": consoleCall,
291301
"usage free": usageFree,

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

Lines changed: 73 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@ import {
1414
speechRecognizePath,
1515
resolveAsrApi,
1616
buildAsrFlashRequest,
17+
buildAsrContextMessages,
1718
buildAsyncAsrLanguageFields,
1819
collectAsrTranscriptionItems,
1920
extractAsrFlashText,
21+
parseInstantVocabulary,
2022
type AsrApiRoute,
2123
type AsrFlashFamily,
2224
type OutputFormat,
@@ -73,8 +75,30 @@ const RECOGNIZE_FLAGS = {
7375
type: "string",
7476
valueHint: "<id>",
7577
description: {
76-
"en-US": "Hot-word vocabulary ID for improved accuracy",
77-
"zh-CN": "用于提升识别准确率的热词表 ID",
78+
"en-US":
79+
"Pre-built hot-word vocabulary ID (create it via `speech vocabulary create`). Its target_model must exactly match --model, otherwise it is silently ignored. Wider model support than --vocabulary, including Fun-ASR and Paraformer",
80+
"zh-CN":
81+
"预编译热词列表 ID(可用 `speech vocabulary create` 创建)。其 target_model 必须与 --model 完全一致,否则静默失效且无报错。支持模型比 --vocabulary 更广,含 Fun-ASR 与 Paraformer 系列",
82+
},
83+
},
84+
vocabulary: {
85+
type: "string",
86+
valueHint: "<json>",
87+
description: {
88+
"en-US":
89+
"Instant hot words as JSON object of word→weight, e.g. '{\"Fendouzhe\":4}'. Weight 1-5 (4 recommended; higher values can hurt other words), 50 for super hot word. No pre-built vocabulary needed. Takes effect only on Qwen-Audio-3.0-ASR-Flash models",
90+
"zh-CN":
91+
"即时热词,JSON 对象「热词→权重」,例如 '{\"奋斗者\":4}'。权重 1-5(推荐 4,过高会拖累其他词),50 表示超级热词。无需预先创建热词表。仅 Qwen-Audio-3.0-ASR-Flash 系列模型生效",
92+
},
93+
},
94+
context: {
95+
type: "string",
96+
valueHint: "<text>",
97+
description: {
98+
"en-US":
99+
"Context enhancement word list to improve accuracy on proper nouns; must contain the target words themselves (a topic description alone has little effect); max 400 chars. Takes effect only on Qwen-Audio-3.0-ASR-Flash and Fun-ASR-Flash models",
100+
"zh-CN":
101+
"上下文增强词表,提升专有名词准确率;须包含待识别的原词本身(只写主题描述效果有限),最长 400 字符。仅 Qwen-Audio-3.0-ASR-Flash 系列与 Fun-ASR-Flash 模型生效",
78102
},
79103
},
80104
channelId: {
@@ -110,9 +134,11 @@ function assertSyncFlashFlagsAllowed(
110134
const unsupported: string[] = [];
111135
if (flags.diarization === true) unsupported.push("--diarization");
112136
if (flags.speakerCount !== undefined) unsupported.push("--speaker-count");
113-
// qwen3 sync Flash does not use vocabulary_id; input-audio Flash (fun-asr-flash* / qwen-audio-*-asr-flash) does
114-
if (flashFamily === "qwen3" && flags.vocabularyId !== undefined) {
115-
unsupported.push("--vocabulary-id");
137+
// qwen3 sync Flash has no place for vocabulary_id / vocabulary / context in its body shape
138+
if (flashFamily === "qwen3") {
139+
if (flags.vocabularyId !== undefined) unsupported.push("--vocabulary-id");
140+
if (flags.vocabulary !== undefined) unsupported.push("--vocabulary");
141+
if (flags.context !== undefined) unsupported.push("--context");
116142
}
117143
if (flags.channelId !== undefined) unsupported.push("--channel-id");
118144
if (flags.async === true) unsupported.push("--async");
@@ -121,12 +147,34 @@ function assertSyncFlashFlagsAllowed(
121147
if (unsupported.length > 0) {
122148
throw new BailianError(
123149
`Model "${model}" uses sync Flash ASR and does not support: ${unsupported.join(", ")}.\n` +
124-
`Hint: Use an async filetrans model (e.g. fun-asr, qwen3-asr-flash-filetrans) for those flags.`,
150+
syncFlashUnsupportedHint(unsupported),
125151
ExitCode.USAGE,
126152
);
127153
}
128154
}
129155

156+
/** Pick a hint that matches the rejected flags (vocab/context vs diarization/async/…). */
157+
function syncFlashUnsupportedHint(unsupported: string[]): string {
158+
const vocabularyRelated = new Set(["--vocabulary", "--vocabulary-id", "--context"]);
159+
const hasVocabularyRelated = unsupported.some((flag) => vocabularyRelated.has(flag));
160+
const hasOtherFlags = unsupported.some((flag) => !vocabularyRelated.has(flag));
161+
162+
if (hasVocabularyRelated && !hasOtherFlags) {
163+
return (
164+
"Hint: Use qwen-audio-3.0-asr-flash (or an async filetrans model such as " +
165+
"qwen-audio-3.0-asr-flash-filetrans) for vocabulary/context flags."
166+
);
167+
}
168+
if (hasVocabularyRelated && hasOtherFlags) {
169+
return (
170+
"Hint: For vocabulary/context flags use qwen-audio-3.0-asr-flash or " +
171+
"qwen-audio-3.0-asr-flash-filetrans; for the other flags use an async filetrans model " +
172+
"(e.g. fun-asr)."
173+
);
174+
}
175+
return "Hint: Use an async filetrans model (e.g. fun-asr, qwen3-asr-flash-filetrans) for those flags.";
176+
}
177+
130178
export default defineCommand({
131179
description: {
132180
"en-US": "Recognize speech from audio files (FunAudio-ASR / Qwen-ASR Flash)",
@@ -141,6 +189,8 @@ export default defineCommand({
141189
"--url https://example.com/meeting.wav --diarization --speaker-count 3",
142190
"--url https://example.com/audio.mp3 --language zh",
143191
"--url https://example.com/audio.mp3 --vocabulary-id vocab-abc123",
192+
'--url https://example.com/audio.mp3 --model qwen-audio-3.0-asr-flash-filetrans --vocabulary \'{"奋斗者":4,"鲸落":4}\'',
193+
'--url https://example.com/audio.mp3 --model qwen-audio-3.0-asr-flash-filetrans --context "奋斗者号 鲸落 深海勇士"',
144194
"--url https://example.com/audio.mp3 --out result.json",
145195
"--url https://example.com/audio.mp3 --async --quiet",
146196
"--url https://example.com/audio.mp3 --model qwen-audio-3.0-asr-flash --language en",
@@ -198,6 +248,11 @@ export default defineCommand({
198248

199249
const format = detectOutputFormat(settings.output);
200250

251+
const vocabulary =
252+
flags.vocabulary !== undefined
253+
? parseInstantVocabulary(flags.vocabulary)
254+
: undefined;
255+
201256
// Auto-upload local files in parallel
202257
const resolvedUrls = await Promise.all(rawUrls.map((url) => ctx.client.uploadFile(url, model)));
203258

@@ -210,6 +265,7 @@ export default defineCommand({
210265
model,
211266
route,
212267
resolvedUrls[0]!,
268+
vocabulary,
213269
);
214270
return;
215271
}
@@ -223,16 +279,21 @@ export default defineCommand({
223279

224280
const body: DashScopeASRRequest = {
225281
model,
226-
input:
227-
route.asyncInputStyle === "file_url"
282+
input: {
283+
...(route.asyncInputStyle === "file_url"
228284
? { file_url: resolvedUrls[0]! }
229-
: { file_urls: resolvedUrls },
285+
: { file_urls: resolvedUrls }),
286+
...(flags.context !== undefined
287+
? { context: buildAsrContextMessages(flags.context) }
288+
: {}),
289+
},
230290
parameters: {
231291
channel_id: channelId !== undefined ? [channelId] : [0],
232292
...languageFields,
233293
diarization_enabled: diarization ? true : undefined,
234294
speaker_count: speakerCount,
235295
vocabulary_id: vocabularyId,
296+
vocabulary,
236297
},
237298
};
238299

@@ -260,13 +321,16 @@ async function handleSyncFlashMode(
260321
model: string,
261322
route: AsrApiRoute,
262323
audioUrl: string,
324+
vocabulary: Record<string, number> | undefined,
263325
): Promise<void> {
264326
const flashFamily = route.flashFamily as AsrFlashFamily;
265327
const body = buildAsrFlashRequest({
266328
model,
267329
audioUrl,
268330
language: flags.language,
269331
vocabularyId: flags.vocabularyId,
332+
vocabulary,
333+
context: flags.context,
270334
flashFamily,
271335
});
272336

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import {
2+
defineCommand,
3+
detectOutputFormat,
4+
speechVocabularyPath,
5+
buildVocabularyRequest,
6+
createVocabulary,
7+
type FlagsDef,
8+
type ParsedFlags,
9+
} from "bailian-cli-core";
10+
import { emitResult, emitBare } from "bailian-cli-runtime";
11+
import {
12+
VOCABULARY_BODY_FLAGS,
13+
VOCABULARY_LIMIT_NOTES,
14+
validateVocabularySource,
15+
readVocabularyEntries,
16+
} from "./shared.ts";
17+
18+
const CREATE_FLAGS = {
19+
model: {
20+
type: "string",
21+
valueHint: "<model>",
22+
description: {
23+
"en-US":
24+
"ASR model this vocabulary is built for (required). Must exactly match the --model passed to `speech recognize` later, otherwise the vocabulary is silently ignored",
25+
"zh-CN":
26+
"该热词表服务的 ASR 模型(必填)。必须与后续 `speech recognize` 的 --model 完全一致,否则热词表静默失效",
27+
},
28+
required: true,
29+
},
30+
prefix: {
31+
type: "string",
32+
valueHint: "<prefix>",
33+
description: {
34+
"en-US":
35+
"Custom vocabulary prefix (required). Digits and lowercase letters only, max 10 chars",
36+
"zh-CN": "热词表自定义前缀(必填)。仅允许数字和小写字母,最长 10 个字符",
37+
},
38+
required: true,
39+
},
40+
...VOCABULARY_BODY_FLAGS,
41+
} satisfies FlagsDef;
42+
type CreateFlags = ParsedFlags<typeof CREATE_FLAGS>;
43+
44+
export default defineCommand({
45+
description: {
46+
"en-US": "Create a precompiled hot-word vocabulary for ASR",
47+
"zh-CN": "创建用于语音识别的预编译热词表",
48+
},
49+
auth: "apiKey",
50+
usageArgs: "--model <model> --prefix <prefix> (--words <json> | --words-file <path>) [flags]",
51+
flags: CREATE_FLAGS,
52+
notes: [
53+
{
54+
"en-US":
55+
"The --model must exactly match the --model used later with `speech recognize --vocabulary-id`; a mismatch causes silent failure with no error.",
56+
"zh-CN":
57+
"--model 必须与后续 `speech recognize --vocabulary-id` 使用的 --model 完全一致;不一致时热词表会静默失效且无报错。",
58+
},
59+
...VOCABULARY_LIMIT_NOTES,
60+
],
61+
exampleArgs: [
62+
{
63+
"en-US": '--model fun-asr --prefix demo --words \'{"Fendouzhe":4,"Jingluo":4}\'',
64+
"zh-CN": '--model fun-asr --prefix demo --words \'{"奋斗者":4,"鲸落":4}\'',
65+
},
66+
{
67+
"en-US":
68+
'--model paraformer-v2 --prefix demo --words \'[{"text":"Fendouzhe","weight":4,"lang":"zh"}]\'',
69+
"zh-CN":
70+
'--model paraformer-v2 --prefix demo --words \'[{"text":"奋斗者","weight":4,"lang":"zh"}]\'',
71+
},
72+
{
73+
"en-US": '--model fun-asr --prefix demo --words \'{"Fendouzhe":4}\' --lang zh',
74+
"zh-CN": '--model fun-asr --prefix demo --words \'{"奋斗者":4}\' --lang zh',
75+
},
76+
"--model fun-asr --prefix demo --words-file ./hotwords.json",
77+
{
78+
"en-US": '--model fun-asr --prefix demo --words \'{"Fendouzhe":4}\' --quiet',
79+
"zh-CN": '--model fun-asr --prefix demo --words \'{"奋斗者":4}\' --quiet',
80+
},
81+
],
82+
validate: (flags: CreateFlags) => validateVocabularySource(flags),
83+
async run(ctx) {
84+
const { settings, flags } = ctx;
85+
const vocabulary = readVocabularyEntries(flags);
86+
const format = detectOutputFormat(settings.output);
87+
88+
const request = buildVocabularyRequest("create_vocabulary", {
89+
target_model: flags.model,
90+
prefix: flags.prefix,
91+
vocabulary,
92+
});
93+
94+
if (settings.dryRun) {
95+
emitResult(
96+
{
97+
endpoint: ctx.client.url(speechVocabularyPath()),
98+
request,
99+
},
100+
format,
101+
);
102+
return;
103+
}
104+
105+
const response = await createVocabulary(ctx.client, {
106+
targetModel: flags.model,
107+
prefix: flags.prefix,
108+
vocabulary,
109+
});
110+
111+
if (settings.quiet || format === "text") {
112+
emitBare(response.output?.vocabulary_id ?? "");
113+
} else {
114+
emitResult(response, format);
115+
}
116+
},
117+
});

0 commit comments

Comments
 (0)