@@ -12,6 +12,11 @@ import {
1212 stripUndefined ,
1313 taskPath ,
1414 speechRecognizePath ,
15+ resolveAsrApi ,
16+ buildAsrFlashRequest ,
17+ extractAsrFlashText ,
18+ type AsrApiRoute ,
19+ type AsrFlashFamily ,
1520 type OutputFormat ,
1621 type FlagsDef ,
1722 type ParsedFlags ,
@@ -27,8 +32,18 @@ const RECOGNIZE_FLAGS = {
2732 description : "Audio file URL or local file path (repeatable, max 100)" ,
2833 required : true ,
2934 } ,
30- model : { type : "string" , valueHint : "<model>" , description : "Model ID (default: fun-asr)" } ,
31- language : { type : "string" , valueHint : "<lang>" , description : "Language hint (e.g. zh, en, ja)" } ,
35+ model : {
36+ type : "string" ,
37+ valueHint : "<model>" ,
38+ description :
39+ "Model ID (default: fun-asr). Async: fun-asr / *-filetrans / paraformer-*; sync: qwen3-asr-flash* / fun-asr-flash* / qwen-audio-*-asr-flash" ,
40+ } ,
41+ language : {
42+ type : "string" ,
43+ valueHint : "<lang>" ,
44+ description :
45+ "Language hint (e.g. zh, en, ja). Async & input-audio sync: language_hints; qwen3 sync: asr_options.language" ,
46+ } ,
3247 diarization : { type : "switch" , description : "Enable automatic speaker diarization" } ,
3348 speakerCount : {
3449 type : "number" ,
@@ -55,8 +70,26 @@ const RECOGNIZE_FLAGS = {
5570} satisfies FlagsDef ;
5671type RecognizeFlags = ParsedFlags < typeof RECOGNIZE_FLAGS > ;
5772
73+ function assertSyncFlashFlagsAllowed ( flags : RecognizeFlags , model : string ) : void {
74+ const unsupported : string [ ] = [ ] ;
75+ if ( flags . diarization === true ) unsupported . push ( "--diarization" ) ;
76+ if ( flags . speakerCount !== undefined ) unsupported . push ( "--speaker-count" ) ;
77+ if ( flags . vocabularyId !== undefined ) unsupported . push ( "--vocabulary-id" ) ;
78+ if ( flags . channelId !== undefined ) unsupported . push ( "--channel-id" ) ;
79+ if ( flags . async === true ) unsupported . push ( "--async" ) ;
80+ if ( flags . pollInterval !== undefined ) unsupported . push ( "--poll-interval" ) ;
81+
82+ if ( unsupported . length > 0 ) {
83+ throw new BailianError (
84+ `Model "${ model } " uses sync Flash ASR and does not support: ${ unsupported . join ( ", " ) } .\n` +
85+ `Hint: Use an async filetrans model (e.g. fun-asr, qwen3-asr-flash-filetrans) for those flags.` ,
86+ ExitCode . USAGE ,
87+ ) ;
88+ }
89+ }
90+
5891export default defineCommand ( {
59- description : "Recognize speech from audio files (FunAudio-ASR)" ,
92+ description : "Recognize speech from audio files (FunAudio-ASR / Qwen-ASR Flash )" ,
6093 auth : "apiKey" ,
6194 usageArgs : "--url <audio-url> [flags]" ,
6295 flags : RECOGNIZE_FLAGS ,
@@ -68,6 +101,7 @@ export default defineCommand({
68101 "--url https://example.com/audio.mp3 --vocabulary-id vocab-abc123" ,
69102 "--url https://example.com/audio.mp3 --out result.json" ,
70103 "--url https://example.com/audio.mp3 --async --quiet" ,
104+ "--url https://example.com/audio.mp3 --model qwen-audio-3.0-asr-flash --language en" ,
71105 ] ,
72106 async run ( ctx ) {
73107 const { settings, flags } = ctx ;
@@ -90,19 +124,64 @@ export default defineCommand({
90124 }
91125
92126 const model = flags . model || "fun-asr" ;
127+ const route = resolveAsrApi ( model ) ;
128+ if ( route . kind === "unsupported" ) {
129+ throw new BailianError (
130+ route . unsupportedReason ?? `Unsupported ASR model: ${ model } ` ,
131+ ExitCode . USAGE ,
132+ ) ;
133+ }
134+
135+ if ( route . kind === "sync-flash" ) {
136+ assertSyncFlashFlagsAllowed ( flags , model ) ;
137+ if ( rawUrls . length !== 1 ) {
138+ throw new BailianError (
139+ `Model "${ model } " is a sync Flash ASR model and accepts exactly one --url (got ${ rawUrls . length } ).\n` +
140+ `Hint: Pass a single audio URL, or use an async filetrans model for batch files.` ,
141+ ExitCode . USAGE ,
142+ ) ;
143+ }
144+ }
145+ if (
146+ route . kind === "async-filetrans" &&
147+ route . asyncInputStyle === "file_url" &&
148+ rawUrls . length !== 1
149+ ) {
150+ throw new BailianError (
151+ `Model "${ model } " accepts exactly one --url (got ${ rawUrls . length } ).\n` +
152+ "Hint: qwen3-asr-flash-filetrans* requires a single file_url." ,
153+ ExitCode . USAGE ,
154+ ) ;
155+ }
156+
93157 const format = detectOutputFormat ( settings . output ) ;
94158
95159 // Auto-upload local files in parallel
96- const resolvedUrls = await Promise . all ( rawUrls . map ( ( u ) => ctx . client . uploadFile ( u , model ) ) ) ;
160+ const resolvedUrls = await Promise . all ( rawUrls . map ( ( url ) => ctx . client . uploadFile ( url , model ) ) ) ;
161+
162+ if ( route . kind === "sync-flash" ) {
163+ await handleSyncFlashMode (
164+ ctx . client ,
165+ settings ,
166+ flags ,
167+ format ,
168+ model ,
169+ route ,
170+ resolvedUrls [ 0 ] ! ,
171+ ) ;
172+ return ;
173+ }
174+
97175 const channelId = flags . channelId ;
98176 const language = flags . language ;
99177 const vocabularyId = flags . vocabularyId ;
100178
101179 const body : DashScopeASRRequest = {
102180 model,
103- input : {
104- file_urls : resolvedUrls ,
105- } ,
181+ input :
182+ route . asyncInputStyle === "file_url"
183+ ? { file_url : resolvedUrls [ 0 ] ! }
184+ : { file_urls : resolvedUrls } ,
106185 parameters : {
107186 channel_id : channelId !== undefined ? [ channelId ] : [ 0 ] ,
108187 language_hints : language ? [ language ] : undefined ,
@@ -116,7 +195,7 @@ export default defineCommand({
116195 stripUndefined ( body . parameters as Record < string , unknown > ) ;
117196
118197 if ( settings . dryRun ) {
119- emitResult ( { request : body , mode : "async" } , format ) ;
198+ emitResult ( { request : body , mode : "async" , path : speechRecognizePath ( ) } , format ) ;
120199 return ;
121200 }
122201
@@ -128,6 +207,53 @@ export default defineCommand({
128207 } ,
129208} ) ;
130209
210+ async function handleSyncFlashMode (
211+ client : Client ,
212+ settings : Settings ,
213+ flags : RecognizeFlags ,
214+ format : OutputFormat ,
215+ model : string ,
216+ route : AsrApiRoute ,
217+ audioUrl : string ,
218+ ) : Promise < void > {
219+ const flashFamily = route . flashFamily as AsrFlashFamily ;
220+ const body = buildAsrFlashRequest ( {
221+ model,
222+ audioUrl,
223+ language : flags . language ,
224+ flashFamily,
225+ } ) ;
226+
227+ if ( settings . dryRun ) {
228+ emitResult ( { request : body , mode : "sync" , path : route . path } , format ) ;
229+ return ;
230+ }
231+
232+ if ( ! settings . quiet ) {
233+ process . stderr . write ( `[Model: ${ model } ] [Mode: sync] [Files: 1]\n` ) ;
234+ }
235+
236+ const response = await client . requestJson < Record < string , unknown > > ( {
237+ path : route . path ,
238+ method : "POST" ,
239+ body,
240+ } ) ;
241+
242+ const text = extractAsrFlashText ( response , flashFamily ) ;
243+ if ( text ) {
244+ process . stdout . write ( text . endsWith ( "\n" ) ? text : `${ text } \n` ) ;
245+ } else {
246+ emitBare ( JSON . stringify ( response ) ) ;
247+ }
248+
249+ if ( flags . out ) {
250+ writeFileSync ( flags . out , JSON . stringify ( response , null , 2 ) + "\n" ) ;
251+ if ( ! settings . quiet ) {
252+ process . stderr . write ( `Full result saved to: ${ flags . out } \n` ) ;
253+ }
254+ }
255+ }
256+
131257async function handleAsyncMode (
132258 client : Client ,
133259 settings : Settings ,
@@ -160,12 +286,12 @@ async function handleAsyncMode(
160286 url : pollUrl ,
161287 intervalSec : pollInterval ,
162288 timeoutSec : settings . timeout ,
163- isComplete : ( d ) => ( d as DashScopeASRTaskResult ) . output . task_status === "SUCCEEDED" ,
164- isFailed : ( d ) => ( d as DashScopeASRTaskResult ) . output . task_status === "FAILED" ,
165- getStatus : ( d ) => ( d as DashScopeASRTaskResult ) . output . task_status ,
166- getErrorMessage : ( d ) => {
167- const o = ( d as DashScopeASRTaskResult ) . output ;
168- return ( o as unknown as Record < string , unknown > ) . message as string | undefined ;
289+ isComplete : ( data ) => ( data as DashScopeASRTaskResult ) . output . task_status === "SUCCEEDED" ,
290+ isFailed : ( data ) => ( data as DashScopeASRTaskResult ) . output . task_status === "FAILED" ,
291+ getStatus : ( data ) => ( data as DashScopeASRTaskResult ) . output . task_status ,
292+ getErrorMessage : ( data ) => {
293+ const output = ( data as DashScopeASRTaskResult ) . output ;
294+ return ( output as unknown as Record < string , unknown > ) . message as string | undefined ;
169295 } ,
170296 } ) ;
171297
@@ -179,12 +305,14 @@ async function handleAsyncMode(
179305 // Collect all transcription data for --out
180306 const allTransData : Record < string , unknown > [ ] = [ ] ;
181307
182- for ( let i = 0 ; i < results . length ; i ++ ) {
183- const subResult = results [ i ] ! ;
308+ for ( let index = 0 ; index < results . length ; index ++ ) {
309+ const subResult = results [ index ] ! ;
184310 const isMulti = fileCount > 1 ;
185311
186312 if ( isMulti ) {
187- process . stdout . write ( `=== [${ i + 1 } /${ results . length } ] ${ subResult . file_url ?? "" } ===\n` ) ;
313+ process . stdout . write (
314+ `=== [${ index + 1 } /${ results . length } ] ${ subResult . file_url ?? "" } ===\n` ,
315+ ) ;
188316 }
189317
190318 if ( subResult . subtask_status === "FAILED" ) {
0 commit comments