@@ -12,6 +12,13 @@ import {
1212 stripUndefined ,
1313 taskPath ,
1414 speechRecognizePath ,
15+ resolveAsrApi ,
16+ buildAsrFlashRequest ,
17+ buildAsyncAsrLanguageFields ,
18+ collectAsrTranscriptionItems ,
19+ extractAsrFlashText ,
20+ type AsrApiRoute ,
21+ type AsrFlashFamily ,
1522 type OutputFormat ,
1623 type FlagsDef ,
1724 type ParsedFlags ,
@@ -27,8 +34,18 @@ const RECOGNIZE_FLAGS = {
2734 description : "Audio file URL or local file path (repeatable, max 100)" ,
2835 required : true ,
2936 } ,
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)" } ,
37+ model : {
38+ type : "string" ,
39+ valueHint : "<model>" ,
40+ description :
41+ "Model ID (default: fun-asr). Async: fun-asr / *-filetrans / paraformer-*; sync: qwen3-asr-flash* / fun-asr-flash* / qwen-audio-*-asr-flash" ,
42+ } ,
43+ language : {
44+ type : "string" ,
45+ valueHint : "<lang>" ,
46+ description :
47+ "Language hint (e.g. zh, en, ja). Classic async/input-audio: language_hints; qwen3-filetrans: language; qwen3 sync: asr_options.language" ,
48+ } ,
3249 diarization : { type : "switch" , description : "Enable automatic speaker diarization" } ,
3350 speakerCount : {
3451 type : "number" ,
@@ -55,8 +72,33 @@ const RECOGNIZE_FLAGS = {
5572} satisfies FlagsDef ;
5673type RecognizeFlags = ParsedFlags < typeof RECOGNIZE_FLAGS > ;
5774
75+ function assertSyncFlashFlagsAllowed (
76+ flags : RecognizeFlags ,
77+ model : string ,
78+ flashFamily : AsrFlashFamily ,
79+ ) : void {
80+ const unsupported : string [ ] = [ ] ;
81+ if ( flags . diarization === true ) unsupported . push ( "--diarization" ) ;
82+ if ( flags . speakerCount !== undefined ) unsupported . push ( "--speaker-count" ) ;
83+ // qwen3 sync Flash does not use vocabulary_id; input-audio Flash (fun-asr-flash* / qwen-audio-*-asr-flash) does
84+ if ( flashFamily === "qwen3" && flags . vocabularyId !== undefined ) {
85+ unsupported . push ( "--vocabulary-id" ) ;
86+ }
87+ if ( flags . channelId !== undefined ) unsupported . push ( "--channel-id" ) ;
88+ if ( flags . async === true ) unsupported . push ( "--async" ) ;
89+ if ( flags . pollInterval !== undefined ) unsupported . push ( "--poll-interval" ) ;
90+
91+ if ( unsupported . length > 0 ) {
92+ throw new BailianError (
93+ `Model "${ model } " uses sync Flash ASR and does not support: ${ unsupported . join ( ", " ) } .\n` +
94+ `Hint: Use an async filetrans model (e.g. fun-asr, qwen3-asr-flash-filetrans) for those flags.` ,
95+ ExitCode . USAGE ,
96+ ) ;
97+ }
98+ }
99+
58100export default defineCommand ( {
59- description : "Recognize speech from audio files (FunAudio-ASR)" ,
101+ description : "Recognize speech from audio files (FunAudio-ASR / Qwen-ASR Flash )" ,
60102 auth : "apiKey" ,
61103 usageArgs : "--url <audio-url> [flags]" ,
62104 flags : RECOGNIZE_FLAGS ,
@@ -68,6 +110,7 @@ export default defineCommand({
68110 "--url https://example.com/audio.mp3 --vocabulary-id vocab-abc123" ,
69111 "--url https://example.com/audio.mp3 --out result.json" ,
70112 "--url https://example.com/audio.mp3 --async --quiet" ,
113+ "--url https://example.com/audio.mp3 --model qwen-audio-3.0-asr-flash --language en" ,
71114 ] ,
72115 async run ( ctx ) {
73116 const { settings, flags } = ctx ;
@@ -90,22 +133,70 @@ export default defineCommand({
90133 }
91134
92135 const model = flags . model || "fun-asr" ;
136+ const route = resolveAsrApi ( model ) ;
137+ if ( route . kind === "unsupported" ) {
138+ throw new BailianError (
139+ route . unsupportedReason ?? `Unsupported ASR model: ${ model } ` ,
140+ ExitCode . USAGE ,
141+ ) ;
142+ }
143+
144+ if ( route . kind === "sync-flash" ) {
145+ assertSyncFlashFlagsAllowed ( flags , model , route . flashFamily ! ) ;
146+ if ( rawUrls . length !== 1 ) {
147+ throw new BailianError (
148+ `Model "${ model } " is a sync Flash ASR model and accepts exactly one --url (got ${ rawUrls . length } ).\n` +
149+ `Hint: Pass a single audio URL, or use an async filetrans model for batch files.` ,
150+ ExitCode . USAGE ,
151+ ) ;
152+ }
153+ }
154+ if (
155+ route . kind === "async-filetrans" &&
156+ route . asyncInputStyle === "file_url" &&
157+ rawUrls . length !== 1
158+ ) {
159+ throw new BailianError (
160+ `Model "${ model } " accepts exactly one --url (got ${ rawUrls . length } ).\n` +
161+ "Hint: qwen3-asr-flash-filetrans* requires a single file_url." ,
162+ ExitCode . USAGE ,
163+ ) ;
164+ }
165+
93166 const format = detectOutputFormat ( settings . output ) ;
94167
95168 // Auto-upload local files in parallel
96- const resolvedUrls = await Promise . all ( rawUrls . map ( ( u ) => ctx . client . uploadFile ( u , model ) ) ) ;
169+ const resolvedUrls = await Promise . all ( rawUrls . map ( ( url ) => ctx . client . uploadFile ( url , model ) ) ) ;
170+
171+ if ( route . kind === "sync-flash" ) {
172+ await handleSyncFlashMode (
173+ ctx . client ,
174+ settings ,
175+ flags ,
176+ format ,
177+ model ,
178+ route ,
179+ resolvedUrls [ 0 ] ! ,
180+ ) ;
181+ return ;
182+ }
183+
97184 const channelId = flags . channelId ;
98- const language = flags . language ;
99185 const vocabularyId = flags . vocabularyId ;
186+ const languageFields = buildAsyncAsrLanguageFields (
187+ route . asyncLanguageStyle ?? "language_hints" ,
188+ flags . language ,
189+ ) ;
100190
101191 const body : DashScopeASRRequest = {
102192 model,
103- input : {
104- file_urls : resolvedUrls ,
105- } ,
193+ input :
194+ route . asyncInputStyle === "file_url"
195+ ? { file_url : resolvedUrls [ 0 ] ! }
196+ : { file_urls : resolvedUrls } ,
106197 parameters : {
107198 channel_id : channelId !== undefined ? [ channelId ] : [ 0 ] ,
108- language_hints : language ? [ language ] : undefined ,
199+ ... languageFields ,
109200 diarization_enabled : diarization ? true : undefined ,
110201 speaker_count : speakerCount ,
111202 vocabulary_id : vocabularyId ,
@@ -116,7 +207,7 @@ export default defineCommand({
116207 stripUndefined ( body . parameters as Record < string , unknown > ) ;
117208
118209 if ( settings . dryRun ) {
119- emitResult ( { request : body , mode : "async" } , format ) ;
210+ emitResult ( { request : body , mode : "async" , path : speechRecognizePath ( ) } , format ) ;
120211 return ;
121212 }
122213
@@ -128,6 +219,55 @@ export default defineCommand({
128219 } ,
129220} ) ;
130221
222+ async function handleSyncFlashMode (
223+ client : Client ,
224+ settings : Settings ,
225+ flags : RecognizeFlags ,
226+ format : OutputFormat ,
227+ model : string ,
228+ route : AsrApiRoute ,
229+ audioUrl : string ,
230+ ) : Promise < void > {
231+ const flashFamily = route . flashFamily as AsrFlashFamily ;
232+ const body = buildAsrFlashRequest ( {
233+ model,
234+ audioUrl,
235+ language : flags . language ,
236+ vocabularyId : flags . vocabularyId ,
237+ flashFamily,
238+ } ) ;
239+
240+ if ( settings . dryRun ) {
241+ emitResult ( { request : body , mode : "sync" , path : route . path } , format ) ;
242+ return ;
243+ }
244+
245+ if ( ! settings . quiet ) {
246+ process . stderr . write ( `[Model: ${ model } ] [Mode: sync] [Files: 1]\n` ) ;
247+ }
248+
249+ const response = await client . requestJson < Record < string , unknown > > ( {
250+ path : route . path ,
251+ method : "POST" ,
252+ headers : { "X-DashScope-SSE" : "disable" } ,
253+ body,
254+ } ) ;
255+
256+ const text = extractAsrFlashText ( response , flashFamily ) ;
257+ if ( text ) {
258+ process . stdout . write ( text . endsWith ( "\n" ) ? text : `${ text } \n` ) ;
259+ } else {
260+ emitBare ( JSON . stringify ( response ) ) ;
261+ }
262+
263+ if ( flags . out ) {
264+ writeFileSync ( flags . out , JSON . stringify ( response , null , 2 ) + "\n" ) ;
265+ if ( ! settings . quiet ) {
266+ process . stderr . write ( `Full result saved to: ${ flags . out } \n` ) ;
267+ }
268+ }
269+ }
270+
131271async function handleAsyncMode (
132272 client : Client ,
133273 settings : Settings ,
@@ -160,16 +300,16 @@ async function handleAsyncMode(
160300 url : pollUrl ,
161301 intervalSec : pollInterval ,
162302 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 ;
303+ isComplete : ( data ) => ( data as DashScopeASRTaskResult ) . output . task_status === "SUCCEEDED" ,
304+ isFailed : ( data ) => ( data as DashScopeASRTaskResult ) . output . task_status === "FAILED" ,
305+ getStatus : ( data ) => ( data as DashScopeASRTaskResult ) . output . task_status ,
306+ getErrorMessage : ( data ) => {
307+ const output = ( data as DashScopeASRTaskResult ) . output ;
308+ return ( output as unknown as Record < string , unknown > ) . message as string | undefined ;
169309 } ,
170310 } ) ;
171311
172- const results = result . output . results ?? [ ] ;
312+ const results = collectAsrTranscriptionItems ( result . output ) ;
173313
174314 if ( results . length === 0 ) {
175315 emitResult ( { task_id : taskId , status : result . output . task_status } , format ) ;
@@ -179,12 +319,14 @@ async function handleAsyncMode(
179319 // Collect all transcription data for --out
180320 const allTransData : Record < string , unknown > [ ] = [ ] ;
181321
182- for ( let i = 0 ; i < results . length ; i ++ ) {
183- const subResult = results [ i ] ! ;
322+ for ( let index = 0 ; index < results . length ; index ++ ) {
323+ const subResult = results [ index ] ! ;
184324 const isMulti = fileCount > 1 ;
185325
186326 if ( isMulti ) {
187- process . stdout . write ( `=== [${ i + 1 } /${ results . length } ] ${ subResult . file_url ?? "" } ===\n` ) ;
327+ process . stdout . write (
328+ `=== [${ index + 1 } /${ results . length } ] ${ subResult . file_url ?? "" } ===\n` ,
329+ ) ;
188330 }
189331
190332 if ( subResult . subtask_status === "FAILED" ) {
0 commit comments