1212# See the License for the specific language governing permissions and
1313# limitations under the License.
1414
15+ import asyncio
1516import logging
1617import os
1718import tempfile
19+ from collections import defaultdict
1820from typing import TYPE_CHECKING , List , Optional , Tuple
1921
22+ from xoscar import extensible
23+
2024from ...device_utils import (
2125 get_available_device ,
2226 get_device_preferred_dtype ,
2327 is_device_available ,
2428)
29+ from ...utils import make_hashable
30+ from ..batch import BatchMixin
2531
2632if TYPE_CHECKING :
2733 from .core import AudioModelFamilyV2
2834
2935logger = logging .getLogger (__name__ )
3036
37+ QWEN3_ASR_DEFAULT_BATCH_INTERVAL = 0.1
38+
3139
32- class Qwen3ASRModel :
40+ class Qwen3ASRModel ( BatchMixin ) :
3341 def __init__ (
3442 self ,
3543 model_uid : str ,
@@ -44,8 +52,22 @@ def __init__(
4452 self ._model_spec = model_spec
4553 self ._device = device
4654 self ._model = None
55+ batch_size = kwargs .pop ("batch_size" , None )
56+ batch_interval = kwargs .pop ("batch_interval" , QWEN3_ASR_DEFAULT_BATCH_INTERVAL )
4757 self ._kwargs = kwargs
4858
59+ batching_kwargs = {}
60+ max_inference_batch_size = kwargs .get ("max_inference_batch_size" )
61+ if batch_size is not None :
62+ batching_kwargs ["batch_size" ] = batch_size
63+ elif max_inference_batch_size is not None and int (max_inference_batch_size ) > 0 :
64+ # qwen_asr consumes this as a native from_pretrained argument;
65+ # reuse it as Xinference's request-batch limit when no override is set.
66+ batching_kwargs ["batch_size" ] = max_inference_batch_size
67+ if batch_interval is not None :
68+ batching_kwargs ["batch_interval" ] = batch_interval
69+ BatchMixin .__init__ (self , self ._batch_transcribe , ** batching_kwargs )
70+
4971 @property
5072 def model_ability (self ):
5173 return self ._model_spec .model_ability
@@ -139,7 +161,108 @@ def _extract_text_and_language(self, result) -> Tuple[str, Optional[str]]:
139161
140162 return str (result ), None
141163
142- def transcriptions (
164+ def _format_transcription_result (self , result , response_format : str ):
165+ text , detected_language = self ._extract_text_and_language (result )
166+ if response_format == "json" :
167+ return {"text" : text }
168+ if response_format == "verbose_json" :
169+ return {
170+ "task" : "transcribe" ,
171+ "language" : detected_language ,
172+ "text" : text ,
173+ }
174+ raise ValueError (f"Unsupported response format: { response_format } " )
175+
176+ def _run_transcription_batch (
177+ self ,
178+ audios : List [bytes ],
179+ languages : List [Optional [str ]],
180+ response_formats : List [str ],
181+ transcription_kwargs : dict ,
182+ ) -> List [dict ]:
183+ assert self ._model is not None
184+ with tempfile .TemporaryDirectory () as temp_dir :
185+ audio_paths = []
186+ for i , audio in enumerate (audios ):
187+ audio_path = os .path .join (temp_dir , f"audio-{ i } " )
188+ with open (audio_path , "wb" ) as f :
189+ f .write (audio )
190+ audio_paths .append (audio_path )
191+
192+ results = self ._model .transcribe (
193+ audio = audio_paths ,
194+ language = languages ,
195+ ** transcription_kwargs ,
196+ )
197+
198+ if not isinstance (results , list ):
199+ raise RuntimeError (
200+ f"Qwen3-ASR returned an invalid batch result: { type (results )} "
201+ )
202+ if len (results ) != len (audios ):
203+ raise RuntimeError (
204+ "Qwen3-ASR returned a different number of results than inputs: "
205+ f"{ len (results )} != { len (audios )} "
206+ )
207+ return [
208+ self ._format_transcription_result (result , response_format )
209+ for result , response_format in zip (results , response_formats )
210+ ]
211+
212+ @extensible
213+ def _batch_transcribe (
214+ self ,
215+ audio : bytes ,
216+ language : Optional [str ],
217+ response_format : str ,
218+ transcription_kwargs : dict ,
219+ ) -> dict :
220+ return self ._run_transcription_batch (
221+ [audio ], [language ], [response_format ], transcription_kwargs
222+ )[0 ]
223+
224+ @_batch_transcribe .batch # type: ignore
225+ async def _batch_transcribe (self , args_list , kwargs_list ):
226+ grouped = defaultdict (
227+ lambda : {
228+ "audios" : [],
229+ "languages" : [],
230+ "response_formats" : [],
231+ "transcription_kwargs" : None ,
232+ "indices" : [],
233+ }
234+ )
235+
236+ for index , (args , kwargs ) in enumerate (zip (args_list , kwargs_list )):
237+ if kwargs :
238+ raise TypeError ("Qwen3-ASR internal batch call expects positional args" )
239+ audio , language , response_format , transcription_kwargs = args
240+ key = make_hashable (transcription_kwargs )
241+ group = grouped [key ]
242+ group ["audios" ].append (audio )
243+ group ["languages" ].append (language )
244+ group ["response_formats" ].append (response_format )
245+ group ["transcription_kwargs" ] = transcription_kwargs
246+ group ["indices" ].append (index )
247+
248+ results_with_indices = []
249+ for group in grouped .values ():
250+ results = await asyncio .to_thread (
251+ self ._run_transcription_batch ,
252+ group ["audios" ],
253+ group ["languages" ],
254+ group ["response_formats" ],
255+ group ["transcription_kwargs" ],
256+ )
257+ results_with_indices .extend (zip (group ["indices" ], results ))
258+
259+ results_with_indices .sort (key = lambda item : item [0 ])
260+ return [result for _ , result in results_with_indices ]
261+
262+ def _get_batch_size (self , * args , ** kwargs ) -> int :
263+ return 1
264+
265+ async def transcriptions (
143266 self ,
144267 audio : bytes ,
145268 language : Optional [str ] = None ,
@@ -163,25 +286,12 @@ def transcriptions(
163286 logger .warning (
164287 "Prompt for Qwen3-ASR transcriptions will be ignored: %s" , prompt
165288 )
289+ if response_format not in ("json" , "verbose_json" ):
290+ raise ValueError (f"Unsupported response format: { response_format } " )
166291
167292 kw = dict (getattr (self ._model_spec , "default_transcription_config" , None ) or {})
168293 kw .update (kwargs )
169-
170- with tempfile .NamedTemporaryFile (buffering = 0 ) as f :
171- f .write (audio )
172- assert self ._model is not None
173- result = self ._model .transcribe (audio = f .name , language = language , ** kw )
174- text , detected_language = self ._extract_text_and_language (result )
175-
176- if response_format == "json" :
177- return {"text" : text }
178- if response_format == "verbose_json" :
179- return {
180- "task" : "transcribe" ,
181- "language" : detected_language ,
182- "text" : text ,
183- }
184- raise ValueError (f"Unsupported response format: { response_format } " )
294+ return await self ._batch_transcribe (audio , language , response_format , kw )
185295
186296 def translations (
187297 self ,
0 commit comments