Thoroughly explore the qwen3-asr-1.7b-phase2-sft repo at /home/ubuntu/training/qwen3-asr-1.7b-phase2-sft. I need to understand:
1. How inference/transcription works - what model class, processor, how audio is loaded and processed
2. What the model architecture is (Qwen3 based ASR with audio encoder?)
3. How to load a checkpoint and run inference on audio files
4. What dependencies are needed
5. Any existing eval/benchmark scripts

Read all Python files, configs, and scripts. Give me a comprehensive summary of how to do inference with this model.


I'll conduct a thorough exploration of this repository to understand its structure, model architecture, and inference pipeline. Let me start by examining the directory structure and key files.


Bash

find /home/ubuntu/training/qwen3-asr-1.7b-phase2-sft -type f -name "*.py" | head -20


total 208
drwxrwxr-x 10 ubuntu ubuntu  4096 Mar 25 21:14 .
drwxrwxr-x  9 ubuntu ubuntu  4096 Mar 25 21:19 ..
drwxrwxr-x  2 ubuntu ubuntu  4096 Mar 25 02:16 assets
drwxrwxr-x  2 ubuntu ubuntu  4096 Mar 25 02:16 docker
-rw-rw-r--  1 ubuntu ubuntu   655 Mar 25 02:16 .env
drwxrwxr-x  2 ubuntu ubuntu  4096 Mar 25 02:16 examples
drwxrwxr-x  2 ubuntu ubuntu  4096 Mar 25 21:14 finetuning
drwxrwxr-x  8 ubuntu ubuntu  4096 Mar 25 21:14 .git
drwxrwxr-x  4 ubuntu ubuntu  4096 Mar 25 02:16 .github
-rw-rw-r--  1 ubuntu ubuntu   387 Mar 25 02:16 .gitignore
-rwxrwxr-x  1 ubuntu ubuntu  1401 Mar 25 02:16 gpu_idle_watchdog.sh
-rwxrwxr-x  1 ubuntu ubuntu  1881 Mar 25 21:14 launch_production.sh
-rwxrwxr-x  1 ubuntu ubuntu  4893 Mar 25 21:14 launch_resilient.sh
-rw-rw-r--  1 ubuntu ubuntu 11343 Mar 25 02:16 LICENSE
-rw-rw-r--  1 ubuntu ubuntu   228 Mar 25 02:16 MANIFEST.in
-rwxrwxr-x  1 ubuntu ubuntu  4230 Mar 25 02:16 monitor_training.sh
-rw-rw-r--  1 ubuntu ubuntu 18555 Mar 25 02:16 OVERNIGHT_TODO.md
-rwxrwxr-x  1 ubuntu ubuntu  1902 Mar 25 02:16 push_checkpoint_r2.sh
-rw-rw-r--  1 ubuntu ubuntu  1304 Mar 25 02:16 pyproject.toml
drwxrwxr-x  5 ubuntu ubuntu  4096 Mar 25 02:16 qwen_asr
-rw-rw-r--  1 ubuntu ubuntu 60115 Mar 25 02:16 README.md
-rwxrwxr-x  1 ubuntu ubuntu  2885 Mar 25 02:16 run_bench_final.sh
-rwxrwxr-x  1 ubuntu ubuntu  4093 Mar 25 02:16 run_benchmarks.sh
-rwxrwxr-x  1 ubuntu ubuntu  3805 Mar 25 02:16 run_benchmarks_v2.sh
-rwxrwxr-x  1 ubuntu ubuntu  2835 Mar 25 02:16 run_bucketed_smoke.sh
-rwxrwxr-x  1 ubuntu ubuntu  4469 Mar 25 02:16 run_canary_1k.sh
-rwxrwxr-x  1 ubuntu ubuntu  2590 Mar 25 02:16 run_quick_smoke.sh
-rwxrwxr-x  1 ubuntu ubuntu  2116 Mar 25 02:16 smoke_test_profile.sh
drwxrwxr-x  2 ubuntu ubuntu  4096 Mar 25 02:16 tools


Now let me examine the key Python files related to inference and model architecture:


Error: File content (17813 tokens) exceeds maximum allowed tokens (10000). Use offset and limit parameters to read specific portions of the file, or search for specific content instead of reading the whole file.


     1→# coding=utf-8
     2→# Copyright 2026 The Qwen team, Alibaba Group and the HuggingFace Inc. team. All rights reserved.
     3→#
     4→# Licensed under the Apache License, Version 2.0 (the "License");
     5→# you may not use this file except in compliance with the License.
     6→# You may obtain a copy of the License at
     7→#
     8→#     http://www.apache.org/licenses/LICENSE-2.0
     9→#
    10→# Unless required by applicable law or agreed to in writing, software
    11→# distributed under the License is distributed on an "AS IS" BASIS,
    12→# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13→# See the License for the specific language governing permissions and
    14→# limitations under the License.
    15→import math
    16→from dataclasses import dataclass
    17→from typing import Callable, Optional, Union
    18→
    19→import numpy as np
    20→import torch
    21→from torch import nn
    22→from torch.nn import functional as F
    23→
    24→from transformers.activations import ACT2FN
    25→from transformers.cache_utils import Cache, DynamicCache
    26→from transformers.generation import GenerationMixin
    27→from transformers.integrations import use_kernel_forward_from_hub
    28→from transformers.masking_utils import create_causal_mask
    29→from transformers.modeling_flash_attention_utils import FlashAttentionKwargs
    30→from transformers.modeling_layers import GradientCheckpointingLayer
    31→from transformers.modeling_outputs import (
    32→    BaseModelOutput,
    33→    BaseModelOutputWithPast,
    34→    MoeCausalLMOutputWithPast,
    35→)
    36→from transformers.modeling_rope_utils import ROPE_INIT_FUNCTIONS, dynamic_rope_update
    37→from transformers.modeling_utils import ALL_ATTENTION_FUNCTIONS, PreTrainedModel
    38→from transformers.processing_utils import Unpack
    39→from transformers.utils import auto_docstring, can_return_tuple
    40→from transformers.utils.deprecation import deprecate_kwarg
    41→from transformers.utils.generic import TransformersKwargs, check_model_inputs
    42→
    43→from .configuration_qwen3_asr import (
    44→    Qwen3ASRAudioEncoderConfig,
    45→    Qwen3ASRConfig,
    46→    Qwen3ASRThinkerConfig,
    47→)
    48→
    49→
    50→@use_kernel_forward_from_hub("RMSNorm")
    51→class Qwen3ASRTextRMSNorm(nn.Module):
    52→    def __init__(self, hidden_size, eps: float = 1e-6) -> None:
    53→        """
    54→        Qwen3ASRTextRMSNorm is equivalent to T5LayerNorm
    55→        """
    56→        super().__init__()
    57→        self.weight = nn.Parameter(torch.ones(hidden_size))
    58→        self.variance_epsilon = eps
    59→
    60→    def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
    61→        input_dtype = hidden_states.dtype
    62→        hidden_states = hidden_states.to(torch.float32)
    63→        variance = hidden_states.pow(2).mean(-1, keepdim=True)
    64→        hidden_states = hidden_states * torch.rsqrt(variance + self.variance_epsilon)
    65→        return self.weight * hidden_states.to(input_dtype)
    66→
    67→    def extra_repr(self):
    68→        return f"{tuple(self.weight.shape)}, eps={self.variance_epsilon}"
    69→
    70→
    71→def rotate_half(x):
    72→    """Rotates half the hidden dims of the input."""
    73→    x1 = x[..., : x.shape[-1] // 2]
    74→    x2 = x[..., x.shape[-1] // 2 :]
    75→    return torch.cat((-x2, x1), dim=-1)
    76→
    77→
    78→def repeat_kv(hidden_states: torch.Tensor, n_rep: int) -> torch.Tensor:
    79→    """
    80→    This is the equivalent of torch.repeat_interleave(x, dim=1, repeats=n_rep). The hidden states go from (batch,
    81→    num_key_value_heads, seqlen, head_dim) to (batch, num_attention_heads, seqlen, head_dim)
    82→    """
    83→    batch, num_key_value_heads, slen, head_dim = hidden_states.shape
    84→    if n_rep == 1:
    85→        return hidden_states
    86→    hidden_states = hidden_states[:, :, None, :, :].expand(batch, num_key_value_heads, n_rep, slen, head_dim)
    87→    return hidden_states.reshape(batch, num_key_value_heads * n_rep, slen, head_dim)
    88→
    89→
    90→def eager_attention_forward(
    91→    module: nn.Module,
    92→    query: torch.Tensor,
    93→    key: torch.Tensor,
    94→    value: torch.Tensor,
    95→    attention_mask: Optional[torch.Tensor],
    96→    scaling: float,
    97→    dropout: float = 0.0,
    98→    **kwargs: Unpack[TransformersKwargs],
    99→):
   100→    key_states = repeat_kv(key, module.num_key_value_groups)


48-
49-
50-@use_kernel_forward_from_hub("RMSNorm")
51:class Qwen3ASRTextRMSNorm(nn.Module):
52-    def __init__(self, hidden_size, eps: float = 1e-6) -> None:
53-        """
54-        Qwen3ASRTextRMSNorm is equivalent to T5LayerNorm
--
140-    return q_embed, k_embed
141-
142-
143:class Qwen3ASRTextAttention(nn.Module):
144-    """Multi-headed attention from 'Attention Is All You Need' paper"""
145-
146-    def __init__(self, config: Qwen3ASRConfig, layer_idx: int):
--
217-        return attn_output, attn_weights
218-
219-
220:class Qwen3ASRTextMLP(nn.Module):
221-    def __init__(self, config):
222-        super().__init__()
223-        self.config = config
--
233-        return down_proj
234-
235-
236:class Qwen3ASRThinkerTextDecoderLayer(GradientCheckpointingLayer):
237-    def __init__(self, config: Qwen3ASRConfig, layer_idx: int):
238-        super().__init__()
239-        self.hidden_size = config.hidden_size
--
280-
281-
282-@auto_docstring
283:class Qwen3ASRPreTrainedModel(PreTrainedModel):
284-    config: Qwen3ASRConfig
285-    base_model_prefix = "model"
286-    supports_gradient_checkpointing = True
--
296-
297-
298-@dataclass
299:class Qwen3ASRThinkerCausalLMOutputWithPast(MoeCausalLMOutputWithPast):
300-    r"""
301-    Args:
302-        rope_deltas (`torch.LongTensor` of shape `(batch_size, )`, *optional*):
--
317-    return output_lengths
318-
319-
320:class Qwen3ASRPreTrainedModelForConditionalGeneration(Qwen3ASRPreTrainedModel):
321-    def _prepare_4d_causal_attention_mask_with_cache_position(
322-        self,
323-        attention_mask: torch.Tensor,
--
448-        return position_ids, mrope_position_deltas
449-
450-
451:class Qwen3ASRAudioAttention(nn.Module):
452-    """Multi-headed attention from 'Attention Is All You Need' paper"""
453-
454-    def __init__(self, config):
--
520-        return attn_output
521-
522-
523:class Qwen3ASRAudioEncoderLayer(GradientCheckpointingLayer):
524-    def __init__(self, config: Qwen3ASRAudioEncoderConfig):
525-        super().__init__()
526-        self.embed_dim = config.d_model
--
600-    [`Qwen3ASRAudioEncoderLayer`].
601-    """
602-)
603:class Qwen3ASRAudioEncoder(Qwen3ASRPreTrainedModel):
604-    config: Qwen3ASRAudioEncoderConfig
605-    main_input_name = "input_features"
606-    _no_split_modules = ["Qwen3ASRAudioEncoderLayer"]
--
778-        )
779-
780-
781:class Qwen3ASRThinkerTextRotaryEmbedding(nn.Module):
782-    inv_freq: torch.Tensor  # fix linting for `register_buffer`
783-
784-    def __init__(self, config: Qwen3ASRConfig, device=None):
--
837-        return cos.to(dtype=x.dtype), sin.to(dtype=x.dtype)
838-
839-
840:class Qwen3ASRThinkerTextMLP(nn.Module):
841-    def __init__(self, config, intermediate_size=None):
842-        super().__init__()
843-        self.config = config
--
854-
855-
856-@use_kernel_forward_from_hub("RMSNorm")
857:class Qwen3ASRThinkerTextRMSNorm(nn.Module):
858-    def __init__(self, hidden_size, eps=1e-6):
859-        """
860-        Qwen3ASRThinkerTextRMSNorm is equivalent to T5LayerNorm
--
874-        return f"{tuple(self.weight.shape)}, eps={self.variance_epsilon}"
875-
876-
877:class Qwen3ASRThinkerTextAttention(nn.Module):
878-    """Multi-headed attention from 'Attention Is All You Need' paper"""
879-
880-    def __init__(self, config, layer_idx):
--
958-        "Text part of Qwen3ASRThinker, "
959-    )
960-)
961:class Qwen3ASRThinkerTextModel(Qwen3ASRPreTrainedModel):
962-    config: Qwen3ASRConfig
963-    _no_split_modules = ["Qwen3ASRThinkerTextDecoderLayer"]
964-    config_class = Qwen3ASRConfig
--
1064-    The Qwen3ASRThinker model which consists of a audio backbone and a language model.
1065-    """
1066-)
1067:class Qwen3ASRThinkerForConditionalGeneration(Qwen3ASRPreTrainedModelForConditionalGeneration, GenerationMixin):
1068-    config: Qwen3ASRThinkerConfig
1069-    base_model_prefix = "thinker"
1070-    _tied_weights_keys = ["model.embed_tokens.weight", "lm_head.weight"]
--
1290-
1291-
1292-@auto_docstring
1293:class Qwen3ASRThinkerTextPreTrainedModel(PreTrainedModel):
1294-    config = Qwen3ASRConfig
1295-    base_model_prefix = "model"
1296-    supports_gradient_checkpointing = True
--
1308-    config_class = Qwen3ASRConfig
1309-
1310-
1311:class Qwen3ASRForConditionalGeneration(Qwen3ASRPreTrainedModel, GenerationMixin):
1312-    config_class = Qwen3ASRConfig
1313-
1314-    def __init__(self, config: Qwen3ASRConfig):


     1→# coding=utf-8
     2→# Copyright 2026 The Qwen team, Alibaba Group and the HuggingFace Inc. team. All rights reserved.
     3→#
     4→# Licensed under the Apache License, Version 2.0 (the "License");
     5→# you may not use this file except in compliance with the License.
     6→# You may obtain a copy of the License at
     7→#
     8→#     http://www.apache.org/licenses/LICENSE-2.0
     9→#
    10→# Unless required by applicable law or agreed to in writing, software
    11→# distributed under the License is distributed on an "AS IS" BASIS,
    12→# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13→# See the License for the specific language governing permissions and
    14→# limitations under the License.
    15→from transformers.configuration_utils import PretrainedConfig
    16→from transformers.utils import logging
    17→
    18→
    19→logger = logging.get_logger(__name__)
    20→
    21→
    22→class Qwen3ASRAudioEncoderConfig(PretrainedConfig):
    23→    r"""
    24→    This is the configuration class to store the configuration of a [`Qwen3ASRAudioEncoder`]. It is used to instantiate a
    25→    Qwen3-ASR audio encoder according to the specified arguments, defining the model architecture. Instantiating a
    26→    configuration with the defaults will yield a similar configuration to that of the audio encoder of the Qwen2-Audio
    27→    architecture.
    28→
    29→    e.g. [Qwen/Qwen3-ASR-1.7B](https://huggingface.co/Qwen/Qwen3-ASR-1.7B)
    30→
    31→    Configuration objects inherit from [`PretrainedConfig`] and can be used to control the model outputs. Read the
    32→    documentation from [`PretrainedConfig`] for more information.
    33→
    34→    Args:
    35→        num_mel_bins (`int`, *optional*, defaults to 128):
    36→            Number of mel features used per input features. Should correspond to the value used in the
    37→            `Qwen3ASRProcessor` class.
    38→        encoder_layers (`int`, *optional*, defaults to 32):
    39→            Number of encoder layers.
    40→        encoder_attention_heads (`int`, *optional*, defaults to 20):
    41→            Number of attention heads for each attention layer in the Transformer encoder.
    42→        encoder_ffn_dim (`int`, *optional*, defaults to 5120):
    43→            Dimensionality of the "intermediate" (often named feed-forward) layer in encoder.
    44→        d_model (`int`, *optional*, defaults to 1280):
    45→            Dimensionality of the layers.
    46→        dropout (`float`, *optional*, defaults to 0.0):
    47→            The dropout probability for all fully connected layers in the embeddings, encoder, and pooler.
    48→        attention_dropout (`float`, *optional*, defaults to 0.0):
    49→            The dropout ratio for the attention probabilities.
    50→        activation_function (`str`, *optional*, defaults to `"gelu"`):
    51→            The non-linear activation function (function or string) in the encoder and pooler. If string, `"gelu"`,
    52→            `"relu"`, `"silu"` and `"gelu_new"` are supported.
    53→        activation_dropout (`float`, *optional*, defaults to 0.0):
    54→            The dropout ratio for activations inside the fully connected layer.
    55→        scale_embedding (`bool`, *optional*, defaults to `False`):
    56→            Scale embeddings by diving by sqrt(d_model).
    57→        initializer_range (`float`, *optional*, defaults to 0.02):
    58→            The standard deviation of the truncated_normal_initializer for initializing all weight matrices.
    59→        max_source_positions (`int`, *optional*, defaults to 1500):
    60→            The maximum sequence length of log-mel filter-bank features that this model might ever be used with.
    61→        n_window (`int`, *optional*, defaults to 100):
    62→            The chunk for conv and flash attn in AudioEncoder.
    63→        output_dim (`int`, *optional*, defaults to 3584):
    64→            The output dimension of AudioEncoder.
    65→
    66→    Example:
    67→
    68→    ```python
    69→    >>> from transformers import Qwen3ASRAudioEncoderConfig, Qwen3ASRAudioEncoder
    70→
    71→    >>> # Initializing a Qwen3ASRAudioEncoderConfig
    72→    >>> configuration = Qwen3ASRAudioEncoderConfig()
    73→
    74→    >>> # Initializing a Qwen3ASRAudioEncoder (with random weights)
    75→    >>> model = Qwen3ASRAudioEncoder(configuration)
    76→
    77→    >>> # Accessing the model configuration
    78→    >>> configuration = model.config
    79→    ```"""
    80→
    81→    model_type = "qwen3_asr_audio_encoder"
    82→
    83→    def __init__(
    84→        self,
    85→        num_mel_bins=128,
    86→        encoder_layers=32,
    87→        encoder_attention_heads=20,
    88→        encoder_ffn_dim=5120,
    89→        d_model=1280,
    90→        dropout=0,
    91→        attention_dropout=0,
    92→        activation_function="gelu",
    93→        activation_dropout=0,
    94→        scale_embedding=False,
    95→        initializer_range=0.02,
    96→        max_source_positions=1500,
    97→        n_window=100,
    98→        output_dim=3584,
    99→        n_window_infer=400,
   100→        conv_chunksize=500,
   101→        downsample_hidden_size=480,
   102→        **kwargs,
   103→    ):
   104→        super().__init__(**kwargs)
   105→
   106→        self.num_mel_bins = num_mel_bins
   107→        self.d_model = d_model
   108→        self.encoder_layers = encoder_layers
   109→        self.encoder_attention_heads = encoder_attention_heads
   110→        self.encoder_ffn_dim = encoder_ffn_dim
   111→        self.dropout = dropout
   112→        self.attention_dropout = attention_dropout
   113→        self.activation_function = activation_function
   114→        self.activation_dropout = activation_dropout
   115→        self.num_hidden_layers = encoder_layers
   116→        self.initializer_range = initializer_range
   117→        self.scale_embedding = scale_embedding  # scale factor will be sqrt(d_model) if True
   118→        self.max_source_positions = max_source_positions
   119→        self.n_window = n_window
   120→        self.output_dim = output_dim
   121→        self.n_window_infer = n_window_infer
   122→        self.conv_chunksize = conv_chunksize
   123→        self.downsample_hidden_size = downsample_hidden_size
   124→
   125→
   126→class Qwen3ASRTextConfig(PretrainedConfig):
   127→    r"""
   128→    This is the configuration class to store the configuration of a [`Qwen3ASRTextModel`]. It is used to instantiate a
   129→    Qwen3-ASR model according to the specified arguments, defining the model architecture. Instantiating a configuration
   130→    with the defaults will yield a similar configuration to that of
   131→    Qwen3-ASR-1.7B [Qwen/Qwen3-ASR-1.7B](https://huggingface.co/Qwen/Qwen3-ASR-1.7B)
   132→
   133→    Configuration objects inherit from [`PretrainedConfig`] and can be used to control the model outputs. Read the
   134→    documentation from [`PretrainedConfig`] for more information.
   135→
   136→    Args:
   137→        vocab_size (`int`, *optional*, defaults to 151936):
   138→            Vocabulary size of the Qwen3ASR model. Defines the number of different tokens that can be represented by the
   139→            `inputs_ids` passed when calling [`Qwen3ASRModel`]
   140→        hidden_size (`int`, *optional*, defaults to 4096):
   141→            Dimension of the hidden representations.
   142→        intermediate_size (`int`, *optional*, defaults to 22016):
   143→            Dimension of the MLP representations.
   144→        num_hidden_layers (`int`, *optional*, defaults to 32):
   145→            Number of hidden layers in the Transformer encoder.
   146→        num_attention_heads (`int`, *optional*, defaults to 32):
   147→            Number of attention heads for each attention layer in the Transformer encoder.
   148→        num_key_value_heads (`int`, *optional*, defaults to 32):
   149→            This is the number of key_value heads that should be used to implement Grouped Query Attention. If
   150→            `num_key_value_heads=num_attention_heads`, the model will use Multi Head Attention (MHA), if


     1→# coding=utf-8
     2→# Copyright 2026 The Qwen team, Alibaba Group and the HuggingFace Inc. team. All rights reserved.
     3→#
     4→# Licensed under the Apache License, Version 2.0 (the "License");
     5→# you may not use this file except in compliance with the License.
     6→# You may obtain a copy of the License at
     7→#
     8→#     http://www.apache.org/licenses/LICENSE-2.0
     9→#
    10→# Unless required by applicable law or agreed to in writing, software
    11→# distributed under the License is distributed on an "AS IS" BASIS,
    12→# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13→# See the License for the specific language governing permissions and
    14→# limitations under the License.
    15→import re
    16→
    17→import numpy as np
    18→
    19→from transformers.audio_utils import AudioInput
    20→from transformers.feature_extraction_utils import BatchFeature
    21→from transformers.processing_utils import ProcessingKwargs, ProcessorMixin
    22→from transformers.tokenization_utils_base import TextInput
    23→
    24→
    25→class Qwen3ASRProcessorKwargs(ProcessingKwargs, total=False):
    26→    _defaults = {
    27→        "text_kwargs": {
    28→            "padding": False,
    29→            "padding_side": "left",
    30→        },
    31→        "audio_kwargs": {
    32→            "sampling_rate": 16000,
    33→            "padding": True,
    34→            "return_attention_mask": True,
    35→        },
    36→    }
    37→
    38→
    39→def _get_feat_extract_output_lengths(input_lengths):
    40→    """
    41→    Computes the output length of the convolutional layers and the output length of the audio encoder
    42→    """
    43→
    44→    input_lengths_leave = input_lengths % 100
    45→    feat_lengths = (input_lengths_leave - 1) // 2 + 1
    46→    output_lengths = ((feat_lengths - 1) // 2 + 1 - 1) // 2 + 1 + (input_lengths // 100) * 13
    47→    return output_lengths
    48→
    49→
    50→class Qwen3ASRProcessor(ProcessorMixin):
    51→    r"""
    52→    Constructs a Qwen3ASR processor.
    53→    [`Qwen3ASRProcessor`] offers all the functionalities of [`WhisperFeatureExtractor`], and [`Qwen2TokenizerFast`]. See the
    54→    [`~Qwen3ASRProcessor.__call__`] and [`~Qwen3ASRProcessor.decode`] for more information.
    55→
    56→    Args:
    57→        feature_extractor ([`WhisperFeatureExtractor`], *optional*):
    58→            The audio feature extractor.
    59→        tokenizer ([`Qwen2TokenizerFast`], *optional*):
    60→            The text tokenizer.
    61→        chat_template (`Optional[str]`, *optional*):
    62→            The Jinja template to use for formatting the conversation. If not provided, the default chat template is used.
    63→    """
    64→
    65→    attributes = ["feature_extractor", "tokenizer"]
    66→    feature_extractor_class = "WhisperFeatureExtractor"
    67→    tokenizer_class = ("Qwen2Tokenizer", "Qwen2TokenizerFast")
    68→
    69→    def __init__(
    70→        self, feature_extractor=None, tokenizer=None, chat_template=None
    71→    ):
    72→        super().__init__(feature_extractor, tokenizer, chat_template=chat_template)
    73→        self.audio_token = self.tokenizer.audio_token
    74→        self.audio_bos_token = self.tokenizer.audio_bos_token
    75→        self.audio_eos_token = self.tokenizer.audio_eos_token
    76→
    77→    def __call__(
    78→        self,
    79→        text: TextInput = None,
    80→        audio: AudioInput = None,
    81→        **kwargs,
    82→    ) -> BatchFeature:
    83→        """
    84→        Main method to prepare for the model one or several sequences(s) and audio(s). This method forwards the `text`
    85→        and `kwargs` arguments to Qwen2TokenizerFast's [`~Qwen2TokenizerFast.__call__`] if `text` is not `None` to encode
    86→        the text. To prepare the audio(s), this method forwards the `audio` and `kwargs` arguments to
    87→        WhisperFeatureExtractor's [`~WhisperFeatureExtractor.__call__`] if `audio` is not `None`. Please refer to the doctsring
    88→        of the above two methods for more information.
    89→
    90→        Args:
    91→            text (`str`, `List[str]`, `List[List[str]]`):
    92→                The sequence or batch of sequences to be encoded. Each sequence can be a string or a list of strings
    93→                (pretokenized string). If the sequences are provided as list of strings (pretokenized), you must set
    94→                `is_split_into_words=True` (to lift the ambiguity with a batch of sequences).
    95→            audio (`np.ndarray`, `List[np.ndarray]`):
    96→                The audio or batch of audio to be prepared. Each audio can be a NumPy array.
    97→        """
    98→
    99→        if text is None:
   100→            raise ValueError("You need to specify either a `text` input to process.")
   101→
   102→        output_kwargs = self._merge_kwargs(
   103→            Qwen3ASRProcessorKwargs,
   104→            tokenizer_init_kwargs=self.tokenizer.init_kwargs,
   105→            **kwargs,
   106→        )
   107→
   108→        if audio is not None:
   109→            output_kwargs["audio_kwargs"]["padding"] = True
   110→            output_kwargs["audio_kwargs"]["truncation"] = False
   111→            audio_inputs = self.feature_extractor(audio, **output_kwargs["audio_kwargs"])
   112→            audio_inputs["feature_attention_mask"] = audio_inputs.pop(
   113→                "attention_mask"
   114→            )  # rename feature_attention_mask to prevent conflicts later on
   115→            audio_inputs["input_features"] = audio_inputs.pop(
   116→                "input_features"
   117→            )  # rename input_features to prevent conflicts later on
   118→            audio_lengths = iter(_get_feat_extract_output_lengths(audio_inputs["feature_attention_mask"].sum(-1)))
   119→        else:
   120→            audio_inputs = {}
   121→            audio_lengths = iter([])
   122→
   123→        if not isinstance(text, list):
   124→            text = [text]
   125→
   126→        text = self.replace_multimodal_special_tokens(
   127→            text,
   128→            audio_lengths,
   129→        )
   130→
   131→        texts_inputs = self.tokenizer(text, **output_kwargs["text_kwargs"])
   132→
   133→        return BatchFeature(
   134→            data={**texts_inputs, **audio_inputs},
   135→            tensor_type=kwargs.get("return_tensors"),
   136→        )
   137→
   138→    def replace_multimodal_special_tokens(
   139→        self,
   140→        text,
   141→        audio_lengths,
   142→    ):
   143→
   144→        processed_text = []
   145→        for sample in text:
   146→            positions = []
   147→            special_tokens = [re.escape(tok) for tok in [self.audio_token]]
   148→            pattern = "|".join(special_tokens)
   149→            positions = sorted([(match.start(), match.group()) for match in re.finditer(pattern, sample)])
   150→            positions.sort(key=lambda x: x[0])


     1→# coding=utf-8
     2→# Copyright 2026 The Alibaba Qwen team.
     3→# SPDX-License-Identifier: Apache-2.0
     4→#
     5→# Licensed under the Apache License, Version 2.0 (the "License");
     6→# you may not use this file except in compliance with the License.
     7→# You may obtain a copy of the License at
     8→#
     9→#     http://www.apache.org/licenses/LICENSE-2.0
    10→#
    11→# Unless required by applicable law or agreed to in writing, software
    12→# distributed under the License is distributed on an "AS IS" BASIS,
    13→# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14→# See the License for the specific language governing permissions and
    15→# limitations under the License.
    16→from dataclasses import dataclass
    17→from typing import Any, Dict, List, Optional, Union
    18→
    19→import numpy as np
    20→import torch
    21→from qwen_asr.core.transformers_backend import (
    22→    Qwen3ASRConfig,
    23→    Qwen3ASRForConditionalGeneration,
    24→    Qwen3ASRProcessor,
    25→)
    26→from transformers import AutoConfig, AutoModel, AutoProcessor
    27→
    28→AutoConfig.register("qwen3_asr", Qwen3ASRConfig)
    29→AutoModel.register(Qwen3ASRConfig, Qwen3ASRForConditionalGeneration)
    30→AutoProcessor.register(Qwen3ASRConfig, Qwen3ASRProcessor)
    31→
    32→from .qwen3_forced_aligner import Qwen3ForcedAligner
    33→from .utils import (
    34→    MAX_ASR_INPUT_SECONDS,
    35→    MAX_FORCE_ALIGN_INPUT_SECONDS,
    36→    SAMPLE_RATE,
    37→    SUPPORTED_LANGUAGES,
    38→    AudioChunk,
    39→    AudioLike,
    40→    chunk_list,
    41→    merge_languages,
    42→    normalize_audios,
    43→    normalize_language_name,
    44→    parse_asr_output,
    45→    split_audio_into_chunks,
    46→    validate_language,
    47→)
    48→
    49→try:
    50→    from qwen_asr.core.vllm_backend import Qwen3ASRForConditionalGeneration
    51→    from vllm import ModelRegistry
    52→    ModelRegistry.register_model("Qwen3ASRForConditionalGeneration", Qwen3ASRForConditionalGeneration)
    53→except:
    54→    pass
    55→
    56→
    57→@dataclass
    58→class ASRTranscription:
    59→    """
    60→    One transcription result.
    61→
    62→    Attributes:
    63→        language (str):
    64→            Merged language string for the sample, e.g. "Chinese" or "Chinese,English".
    65→            Empty string if unknown or silent audio.
    66→        text (str):
    67→            Transcribed text.
    68→        time_stamps (Optional[Any]):
    69→            Forced aligner output (ForcedAlignResult).
    70→            Present only when return_time_stamps=True.
    71→    """
    72→    language: str
    73→    text: str
    74→    time_stamps: Optional[Any] = None
    75→
    76→
    77→@dataclass
    78→class ASRStreamingState:
    79→    """
    80→    Streaming ASR state for one audio stream (single utterance).
    81→
    82→    Attributes:
    83→        unfixed_chunk_num (int):
    84→            For the first N chunks, do not use previous ASR result as prefix prompt (reset prefix to "").
    85→        unfixed_token_num (int):
    86→            When chunk_id >= unfixed_chunk_num, rollback the last K tokens from the accumulated text
    87→            before using it as prefix prompt, to reduce boundary jitter.
    88→        chunk_size_sec (float):
    89→            Chunk size in seconds. Audio will be fed to the model in increments of this length.
    90→        chunk_size_samples (int):
    91→            Chunk size in samples at 16kHz (derived from chunk_size_sec).
    92→        chunk_id (int):
    93→            Current chunk index (0-based).
    94→        buffer (np.ndarray):
    95→            Buffered PCM samples that are not yet consumed into a full chunk.
    96→        audio_accum (np.ndarray):
    97→            Accumulated audio from the beginning of the stream up to current time (no padding).
    98→        prompt_raw (str):
    99→            Base prompt generated by chat template (with generation prompt), without appended prefix text.
   100→        context (str):
   101→            Context string.
   102→        force_language (Optional[str]):
   103→            If provided, force output to be text-only by appending "language X<asr_text>" in prompt_raw,
   104→            consistent with non-streaming transcribe().
   105→        language (str):
   106→            Latest parsed language (updated after each chunk decode). Empty if unknown/silent.
   107→        text (str):
   108→            Latest parsed transcription text (updated after each chunk decode).
   109→        _raw_decoded (str):
   110→            Internal accumulated decoded raw text (before parse_asr_output normalization).
   111→            Used for rollback/token trimming and as prefix for prompting.
   112→    """
   113→    unfixed_chunk_num: int
   114→    unfixed_token_num: int
   115→    chunk_size_sec: float
   116→    chunk_size_samples: int
   117→
   118→    chunk_id: int
   119→    buffer: np.ndarray
   120→    audio_accum: np.ndarray
   121→
   122→    prompt_raw: str
   123→    context: str
   124→    force_language: Optional[str]
   125→
   126→    language: str
   127→    text: str
   128→    _raw_decoded: str
   129→
   130→
   131→class Qwen3ASRModel:
   132→    """
   133→    Unified inference wrapper for Qwen3-ASR with two backends:
   134→      - Transformers backend
   135→      - vLLM backend
   136→
   137→    It optionally supports time stamp output via Qwen3-ForcedAligner.
   138→
   139→    Notes:
   140→      - Each request uses a context text and exactly one audio.
   141→      - If language is provided, the prompt will force the output to be text-only by appending
   142→        "language {Language}<asr_text>" to the assistant prompt.
   143→    """
   144→
   145→    def __init__(
   146→        self,
   147→        backend: str,
   148→        model: Any,
   149→        processor: Any,
   150→        sampling_params: Optional[Any] = None,
   151→        forced_aligner: Optional[Qwen3ForcedAligner] = None,
   152→        max_inference_batch_size: int = -1,
   153→        max_new_tokens: int = 512,
   154→    ):
   155→        self.backend = backend  # "transformers" | "vllm"
   156→        self.model = model
   157→        self.processor = processor
   158→        self.sampling_params = sampling_params
   159→        self.forced_aligner = forced_aligner
   160→        self.max_inference_batch_size = int(max_inference_batch_size)
   161→        self.max_new_tokens = max_new_tokens
   162→
   163→        if backend == "transformers":
   164→            self.device = getattr(model, "device", None)
   165→            if self.device is None:
   166→                try:
   167→                    self.device = next(model.parameters()).device
   168→                except StopIteration:
   169→                    self.device = torch.device("cpu")
   170→            self.dtype = getattr(model, "dtype", torch.float32)
   171→        else:
   172→            self.device = None
   173→            self.dtype = None
   174→
   175→    @classmethod
   176→    def from_pretrained(
   177→        cls,
   178→        pretrained_model_name_or_path: str,
   179→        forced_aligner: Optional[str] = None,
   180→        forced_aligner_kwargs: Optional[Dict[str, Any]] = None,
   181→        max_inference_batch_size: int = 32,
   182→        max_new_tokens: Optional[int] = 512,
   183→        **kwargs,
   184→    ) -> "Qwen3ASRModel":
   185→        """
   186→        Initialize using Transformers backend.
   187→
   188→        Args:
   189→            pretrained_model_name_or_path:
   190→                HuggingFace repo id or local directory.
   191→            forced_aligner:
   192→                Optional forced aligner model path/repo id.
   193→            forced_aligner_kwargs:
   194→                Optional kwargs forwarded to Qwen3ForcedAligner.from_pretrained(...).
   195→            max_inference_batch_size:
   196→                Batch size limit for inference. -1 means no chunking. Small values can avoid OOM.
   197→            max_new_tokens:
   198→                Maximum number of tokens to generate.
   199→            **kwargs:
   200→                Forwarded to AutoModel.from_pretrained(...).


     1→# coding=utf-8
     2→# Copyright 2026 The Alibaba Qwen team.
     3→# SPDX-License-Identifier: Apache-2.0
     4→#
     5→# Licensed under the Apache License, Version 2.0 (the "License");
     6→# you may not use this file except in compliance with the License.
     7→# You may obtain a copy of the License at
     8→#
     9→#     http://www.apache.org/licenses/LICENSE-2.0
    10→#
    11→# Unless required by applicable law or agreed to in writing, software
    12→# distributed under the License is distributed on an "AS IS" BASIS,
    13→# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14→# See the License for the specific language governing permissions and
    15→# limitations under the License.
    16→"""
    17→Examples for Qwen3ASRModel (Transformers backend).
    18→
    19→Covers:
    20→  - single-sample inference (URL audio)
    21→  - batch inference (mixed URL / base64 / (np.ndarray, sr))
    22→  - forcing language (text-only output)
    23→  - returning time_stamps (single + batch) via Qwen3ForcedAligner
    24→"""
    25→
    26→import base64
    27→import io
    28→import urllib.request
    29→from typing import Tuple
    30→
    31→import numpy as np
    32→import soundfile as sf
    33→import torch
    34→
    35→from qwen_asr import Qwen3ASRModel
    36→
    37→
    38→ASR_MODEL_PATH = "Qwen/Qwen3-ASR-1.7B"
    39→FORCED_ALIGNER_PATH = "Qwen/Qwen3-ForcedAligner-0.6B"
    40→
    41→URL_ZH = "https://qianwen-res.oss-cn-beijing.aliyuncs.com/Qwen3-ASR-Repo/asr_zh.wav"
    42→URL_EN = "https://qianwen-res.oss-cn-beijing.aliyuncs.com/Qwen3-ASR-Repo/asr_en.wav"
    43→
    44→
    45→def _download_audio_bytes(url: str, timeout: int = 30) -> bytes:
    46→    req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
    47→    with urllib.request.urlopen(req, timeout=timeout) as resp:
    48→        return resp.read()
    49→
    50→
    51→def _read_wav_from_bytes(audio_bytes: bytes) -> Tuple[np.ndarray, int]:
    52→    with io.BytesIO(audio_bytes) as f:
    53→        wav, sr = sf.read(f, dtype="float32", always_2d=False)
    54→    return np.asarray(wav, dtype=np.float32), int(sr)
    55→
    56→
    57→def _to_data_url_base64(audio_bytes: bytes, mime: str = "audio/wav") -> str:
    58→    b64 = base64.b64encode(audio_bytes).decode("utf-8")
    59→    return f"data:{mime};base64,{b64}"
    60→
    61→
    62→def _print_result(title: str, results) -> None:
    63→    print(f"\n===== {title} =====")
    64→    for i, r in enumerate(results):
    65→        print(f"[sample {i}] language={r.language!r}")
    66→        print(f"[sample {i}] text={r.text!r}")
    67→        if r.time_stamps is not None and len(r.time_stamps) > 0:
    68→            head = r.time_stamps[0]
    69→            tail = r.time_stamps[-1]
    70→            print(f"[sample {i}] ts_first: {head.text!r} {head.start_time}->{head.end_time} s")
    71→            print(f"[sample {i}] ts_last : {tail.text!r} {tail.start_time}->{tail.end_time} s")
    72→
    73→
    74→def test_single_url(asr: Qwen3ASRModel) -> None:
    75→    results = asr.transcribe(
    76→        audio=URL_ZH,
    77→        language=None,
    78→        return_time_stamps=False,
    79→    )
    80→    assert isinstance(results, list) and len(results) == 1
    81→    _print_result("single-url (no forced language, no timestamps)", results)
    82→
    83→
    84→def test_batch_mixed(asr: Qwen3ASRModel) -> None:
    85→    zh_bytes = _download_audio_bytes(URL_ZH)
    86→    en_bytes = _download_audio_bytes(URL_EN)
    87→
    88→    zh_b64 = _to_data_url_base64(zh_bytes, mime="audio/wav")
    89→    en_wav, en_sr = _read_wav_from_bytes(en_bytes)
    90→
    91→    results = asr.transcribe(
    92→        audio=[URL_ZH, zh_b64, (en_wav, en_sr)],
    93→        context=["", "交易 停滞", ""],
    94→        language=[None, "Chinese", "English"],
    95→        return_time_stamps=False,
    96→    )
    97→    assert len(results) == 3
    98→    _print_result("batch-mixed (forced language for some)", results)
    99→
   100→
   101→def test_single_with_timestamps(asr: Qwen3ASRModel) -> None:
   102→    results = asr.transcribe(
   103→        audio=URL_EN,
   104→        language="English",
   105→        return_time_stamps=True,
   106→    )
   107→    assert len(results) == 1
   108→    assert results[0].time_stamps is not None
   109→    _print_result("single-url (forced language + timestamps)", results)
   110→
   111→
   112→def test_batch_with_timestamps(asr: Qwen3ASRModel) -> None:
   113→    zh_bytes = _download_audio_bytes(URL_ZH)
   114→    zh_b64 = _to_data_url_base64(zh_bytes, mime="audio/wav")
   115→
   116→    results = asr.transcribe(
   117→        audio=[URL_ZH, zh_b64, URL_EN],
   118→        context=["", "交易 停滞", ""],
   119→        language=["Chinese", "Chinese", "English"],
   120→        return_time_stamps=True,
   121→    )
   122→    assert len(results) == 3
   123→    assert all(r.time_stamps is not None for r in results)
   124→    _print_result("batch (forced language + timestamps)", results)
   125→
   126→
   127→def main() -> None:
   128→    asr = Qwen3ASRModel.from_pretrained(
   129→        ASR_MODEL_PATH,
   130→        dtype=torch.bfloat16,
   131→        device_map="cuda:0",
   132→        # attn_implementation="flash_attention_2",
   133→        forced_aligner=FORCED_ALIGNER_PATH,
   134→        forced_aligner_kwargs=dict(
   135→            dtype=torch.bfloat16,
   136→            device_map="cuda:0",
   137→            # attn_implementation="flash_attention_2",
   138→        ),
   139→        max_inference_batch_size=32,
   140→        max_new_tokens=256,
   141→    )
   142→
   143→    test_single_url(asr)
   144→    test_batch_mixed(asr)
   145→    test_single_with_timestamps(asr)
   146→    test_batch_with_timestamps(asr)
   147→
   148→
   149→if __name__ == "__main__":
   150→    main()
   151→


     1→# coding=utf-8
     2→# Copyright 2026 The Alibaba Qwen team.
     3→# SPDX-License-Identifier: Apache-2.0
     4→#
     5→# Licensed under the Apache License, Version 2.0 (the "License");
     6→# you may not use this file except in compliance with the License.
     7→# You may obtain a copy of the License at
     8→#
     9→#     http://www.apache.org/licenses/LICENSE-2.0
    10→#
    11→# Unless required by applicable law or agreed to in writing, software
    12→# distributed under the License is distributed on an "AS IS" BASIS,
    13→# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14→# See the License for the specific language governing permissions and
    15→# limitations under the License.
    16→"""
    17→Examples for Qwen3ASRModel (vLLM backend).
    18→
    19→Covers:
    20→  - single-sample inference (URL audio)
    21→  - batch inference (mixed URL / base64 / (np.ndarray, sr))
    22→  - forcing language (text-only output)
    23→  - returning timestamps (single + batch) via Qwen3ForcedAligner
    24→
    25→Note:
    26→  Requires vLLM extra:
    27→    pip install qwen-asr[vllm]
    28→"""
    29→
    30→import base64
    31→import io
    32→import urllib.request
    33→from typing import Tuple
    34→
    35→import numpy as np
    36→import soundfile as sf
    37→import torch
    38→
    39→from qwen_asr import Qwen3ASRModel
    40→
    41→
    42→ASR_MODEL_PATH = "Qwen/Qwen3-ASR-1.7B"
    43→FORCED_ALIGNER_PATH = "Qwen/Qwen3-ForcedAligner-0.6B"
    44→
    45→URL_ZH = "https://qianwen-res.oss-cn-beijing.aliyuncs.com/Qwen3-ASR-Repo/asr_zh.wav"
    46→URL_EN = "https://qianwen-res.oss-cn-beijing.aliyuncs.com/Qwen3-ASR-Repo/asr_en.wav"
    47→
    48→
    49→def _download_audio_bytes(url: str, timeout: int = 30) -> bytes:
    50→    req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
    51→    with urllib.request.urlopen(req, timeout=timeout) as resp:
    52→        return resp.read()
    53→
    54→
    55→def _read_wav_from_bytes(audio_bytes: bytes) -> Tuple[np.ndarray, int]:
    56→    with io.BytesIO(audio_bytes) as f:
    57→        wav, sr = sf.read(f, dtype="float32", always_2d=False)
    58→    return np.asarray(wav, dtype=np.float32), int(sr)
    59→
    60→
    61→def _to_data_url_base64(audio_bytes: bytes, mime: str = "audio/wav") -> str:
    62→    b64 = base64.b64encode(audio_bytes).decode("utf-8")
    63→    return f"data:{mime};base64,{b64}"
    64→
    65→
    66→def _print_result(title: str, results) -> None:
    67→    print(f"\n===== {title} =====")
    68→    for i, r in enumerate(results):
    69→        print(f"[sample {i}] language={r.language!r}")
    70→        print(f"[sample {i}] text={r.text!r}")
    71→        if r.time_stamps is not None and len(r.time_stamps) > 0:
    72→            head = r.time_stamps[0]
    73→            tail = r.time_stamps[-1]
    74→            print(f"[sample {i}] ts_first: {head.text!r} {head.start_time}->{head.end_time} s")
    75→            print(f"[sample {i}] ts_last : {tail.text!r} {tail.start_time}->{tail.end_time} s")
    76→
    77→
    78→def test_single_url(asr: Qwen3ASRModel) -> None:
    79→    results = asr.transcribe(
    80→        audio=URL_ZH,
    81→        language=None,
    82→        return_time_stamps=False,
    83→    )
    84→    assert isinstance(results, list) and len(results) == 1
    85→    _print_result("single-url (no forced language, no timestamps)", results)
    86→
    87→
    88→def test_batch_mixed(asr: Qwen3ASRModel) -> None:
    89→    zh_bytes = _download_audio_bytes(URL_ZH)
    90→    en_bytes = _download_audio_bytes(URL_EN)
    91→
    92→    zh_b64 = _to_data_url_base64(zh_bytes, mime="audio/wav")
    93→    en_wav, en_sr = _read_wav_from_bytes(en_bytes)
    94→
    95→    results = asr.transcribe(
    96→        audio=[URL_ZH, zh_b64, (en_wav, en_sr)],
    97→        context=["", "交易 停滞", ""],
    98→        language=[None, "Chinese", "English"],
    99→        return_time_stamps=False,
   100→    )
   101→    assert len(results) == 3
   102→    _print_result("batch-mixed (forced language for some)", results)
   103→
   104→
   105→def test_single_with_timestamps(asr: Qwen3ASRModel) -> None:
   106→    results = asr.transcribe(
   107→        audio=URL_EN,
   108→        language="English",
   109→        return_time_stamps=True,
   110→    )
   111→    assert len(results) == 1
   112→    assert results[0].time_stamps is not None
   113→    _print_result("single-url (forced language + timestamps)", results)
   114→
   115→
   116→def test_batch_with_timestamps(asr: Qwen3ASRModel) -> None:
   117→    zh_bytes = _download_audio_bytes(URL_ZH)
   118→    zh_b64 = _to_data_url_base64(zh_bytes, mime="audio/wav")
   119→
   120→    results = asr.transcribe(
   121→        audio=[URL_ZH, zh_b64, URL_EN],
   122→        context=["", "交易 停滞", ""],
   123→        language=["Chinese", "Chinese", "English"],
   124→        return_time_stamps=True,
   125→    )
   126→    assert len(results) == 3
   127→    assert all(r.time_stamps is not None for r in results)
   128→    _print_result("batch (forced language + timestamps)", results)
   129→
   130→
   131→def main() -> None:
   132→    asr = Qwen3ASRModel.LLM(
   133→        model=ASR_MODEL_PATH,
   134→        gpu_memory_utilization=0.8,
   135→        forced_aligner=FORCED_ALIGNER_PATH,
   136→        forced_aligner_kwargs=dict(
   137→            dtype=torch.bfloat16,
   138→            device_map="cuda:0",
   139→            # attn_implementation="flash_attention_2",
   140→        ),
   141→        max_inference_batch_size=32,
   142→        max_new_tokens=1024,
   143→    )
   144→
   145→    test_single_url(asr)
   146→    test_batch_mixed(asr)
   147→    test_single_with_timestamps(asr)
   148→    test_batch_with_timestamps(asr)
   149→
   150→
   151→if __name__ == "__main__":
   152→    main()
   153→


     1→# coding=utf-8
     2→# Copyright 2026 The Alibaba Qwen team.
     3→# SPDX-License-Identifier: Apache-2.0
     4→#
     5→# Licensed under the Apache License, Version 2.0 (the "License");
     6→# you may not use this file except in compliance with the License.
     7→# You may obtain a copy of the License at
     8→#
     9→#     http://www.apache.org/licenses/LICENSE-2.0
    10→#
    11→# Unless required by applicable law or agreed to in writing, software
    12→# distributed under the License is distributed on an "AS IS" BASIS,
    13→# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14→# See the License for the specific language governing permissions and
    15→# limitations under the License.
    16→import base64
    17→import io
    18→import urllib.request
    19→from dataclasses import dataclass
    20→from typing import Any, Iterable, List, Optional, Tuple, Union
    21→from urllib.parse import urlparse
    22→
    23→import librosa
    24→import numpy as np
    25→import soundfile as sf
    26→
    27→AudioLike = Union[
    28→    str,                      # wav path / URL / base64
    29→    Tuple[np.ndarray, int],   # (waveform, sr)
    30→]
    31→MaybeList = Union[Any, List[Any]]
    32→
    33→SAMPLE_RATE = 16000
    34→MAX_ASR_INPUT_SECONDS = 1200
    35→MAX_FORCE_ALIGN_INPUT_SECONDS = 180
    36→MIN_ASR_INPUT_SECONDS = 0.5
    37→SUPPORTED_LANGUAGES: List[str] = [
    38→    "Chinese",
    39→    "English",
    40→    "Cantonese",
    41→    "Arabic",
    42→    "German",
    43→    "French",
    44→    "Spanish",
    45→    "Portuguese",
    46→    "Indonesian",
    47→    "Italian",
    48→    "Korean",
    49→    "Russian",
    50→    "Thai",
    51→    "Vietnamese",
    52→    "Japanese",
    53→    "Turkish",
    54→    "Hindi",
    55→    "Malay",
    56→    "Dutch",
    57→    "Swedish",
    58→    "Danish",
    59→    "Finnish",
    60→    "Polish",
    61→    "Czech",
    62→    "Filipino",
    63→    "Persian",
    64→    "Greek",
    65→    "Romanian",
    66→    "Hungarian",
    67→    "Macedonian"
    68→]
    69→_ASR_TEXT_TAG = "<asr_text>"
    70→_LANG_PREFIX = "language "
    71→
    72→
    73→def normalize_language_name(language: str) -> str:
    74→    """
    75→    Normalize language name to the canonical format used by Qwen3-ASR:
    76→    first letter uppercase, the rest lowercase (e.g., 'cHINese' -> 'Chinese').
    77→
    78→    Args:
    79→        language (str): Input language name.
    80→
    81→    Returns:
    82→        str: Normalized language name.
    83→
    84→    Raises:
    85→        ValueError: If language is empty.
    86→    """
    87→    if language is None:
    88→        raise ValueError("language is None")
    89→    s = str(language).strip()
    90→    if not s:
    91→        raise ValueError("language is empty")
    92→    return s[:1].upper() + s[1:].lower()
    93→
    94→
    95→def validate_language(language: str) -> None:
    96→    """
    97→    Validate the language is supported.
    98→
    99→    Args:
   100→        language (str): Canonical language name.
   101→
   102→    Raises:
   103→        ValueError: If unsupported.
   104→    """
   105→    if language not in SUPPORTED_LANGUAGES:
   106→        raise ValueError(f"Unsupported language: {language}. Supported: {SUPPORTED_LANGUAGES}")
   107→
   108→
   109→def ensure_list(x: MaybeList) -> List[Any]:
   110→        return x if isinstance(x, list) else [x]
   111→
   112→
   113→def is_url(s: str) -> bool:
   114→    try:
   115→        u = urlparse(s)
   116→        return u.scheme in ("http", "https") and bool(u.netloc)
   117→    except Exception:
   118→        return False
   119→
   120→
   121→def is_probably_base64(s: str) -> bool:
   122→    if s.startswith("data:audio"):
   123→        return True
   124→    if ("/" not in s and "\\" not in s) and len(s) > 256:
   125→        return True
   126→    return False
   127→
   128→
   129→def decode_base64_bytes(b64: str) -> bytes:
   130→    if "," in b64 and b64.strip().startswith("data:"):
   131→        b64 = b64.split(",", 1)[1]
   132→    return base64.b64decode(b64)
   133→
   134→
   135→def load_audio_any(x: str) -> Tuple[np.ndarray, int]:
   136→    if is_url(x):
   137→        with urllib.request.urlopen(x) as resp:
   138→            audio_bytes = resp.read()
   139→        with io.BytesIO(audio_bytes) as f:
   140→            audio, sr = sf.read(f, dtype="float32", always_2d=False)
   141→    elif is_probably_base64(x):
   142→        audio_bytes = decode_base64_bytes(x)
   143→        with io.BytesIO(audio_bytes) as f:
   144→            audio, sr = sf.read(f, dtype="float32", always_2d=False)
   145→    else:
   146→        audio, sr = librosa.load(x, sr=None, mono=False)
   147→
   148→    audio = np.asarray(audio, dtype=np.float32)
   149→    sr = int(sr)
   150→    return audio, sr
   151→
   152→
   153→def to_mono(audio: np.ndarray) -> np.ndarray:
   154→    if audio.ndim == 1:
   155→        return audio
   156→    # soundfile can return shape (T, C); some pipelines use (C, T)
   157→    if audio.ndim == 2:
   158→        if audio.shape[0] <= 8 and audio.shape[1] > audio.shape[0]:
   159→            audio = audio.T
   160→        return np.mean(audio, axis=-1).astype(np.float32)
   161→    raise ValueError(f"Unsupported audio ndim={audio.ndim}")
   162→
   163→
   164→def float_range_normalize(audio: np.ndarray) -> np.ndarray:
   165→    audio = audio.astype(np.float32)
   166→    if audio.size == 0:
   167→        return audio
   168→    peak = float(np.max(np.abs(audio)))
   169→    if peak == 0.0:
   170→        return audio
   171→    # If decoded audio is int-like scaled or out-of-range, normalize conservatively.
   172→    if peak > 1.0:
   173→        audio = audio / peak
   174→    audio = np.clip(audio, -1.0, 1.0)
   175→    return audio
   176→
   177→
   178→def normalize_audio_input(a: AudioLike) -> np.ndarray:
   179→    """
   180→    Normalize one audio input to mono 16k float32 waveform in [-1, 1].
   181→
   182→    Supported inputs:
   183→        - str: local file path / https URL / base64 audio string
   184→        - (np.ndarray, sr): waveform and sampling rate
   185→
   186→    Returns:
   187→        np.ndarray:
   188→            Mono 16k float32 waveform in [-1, 1].
   189→    """
   190→    if isinstance(a, str):
   191→        audio, sr = load_audio_any(a)
   192→    elif isinstance(a, tuple) and len(a) == 2 and isinstance(a[0], np.ndarray):
   193→        audio, sr = a[0], int(a[1])
   194→    else:
   195→        raise TypeError(f"Unsupported audio input type: {type(a)}")
   196→
   197→    audio = to_mono(np.asarray(audio))
   198→    if sr != SAMPLE_RATE:
   199→        audio = librosa.resample(audio, orig_sr=sr, target_sr=SAMPLE_RATE).astype(np.float32)
   200→    audio = float_range_normalize(audio)


   200→                Forwarded to AutoModel.from_pretrained(...).
   201→
   202→        Returns:
   203→            Qwen3ASRModel
   204→        """
   205→
   206→        model = AutoModel.from_pretrained(pretrained_model_name_or_path, **kwargs)
   207→
   208→        processor = AutoProcessor.from_pretrained(pretrained_model_name_or_path, fix_mistral_regex=True)
   209→
   210→        forced_aligner_model = None
   211→        if forced_aligner is not None:
   212→            forced_aligner_model = Qwen3ForcedAligner.from_pretrained(
   213→                forced_aligner, **(forced_aligner_kwargs or {})
   214→            )
   215→
   216→        return cls(
   217→            backend="transformers",
   218→            model=model,
   219→            processor=processor,
   220→            sampling_params=None,
   221→            forced_aligner=forced_aligner_model,
   222→            max_inference_batch_size=max_inference_batch_size,
   223→            max_new_tokens=max_new_tokens,
   224→        )
   225→
   226→    @classmethod
   227→    def LLM(
   228→        cls,
   229→        model: str,
   230→        forced_aligner: Optional[str] = None,
   231→        forced_aligner_kwargs: Optional[Dict[str, Any]] = None,
   232→        max_inference_batch_size: int = -1,
   233→        max_new_tokens: Optional[int] = 4096,
   234→        **kwargs,
   235→    ) -> "Qwen3ASRModel":
   236→        """
   237→        Initialize using vLLM backend.
   238→
   239→        Import is isolated to keep vLLM optional.
   240→
   241→        Args:
   242→            model:
   243→                Model path/repo for vLLM.
   244→            forced_aligner:
   245→                Optional forced aligner model path/repo id.
   246→            forced_aligner_kwargs:
   247→                Optional kwargs forwarded to Qwen3ForcedAligner.from_pretrained(...).
   248→            max_inference_batch_size:
   249→                Batch size limit for inference. -1 means no chunking. Small values can avoid OOM.
   250→            max_new_tokens:
   251→                Maximum number of tokens to generate.
   252→            **kwargs:
   253→                Forwarded to vllm.LLM(...).
   254→
   255→        Returns:
   256→            Qwen3ASRModel
   257→
   258→        Raises:
   259→            ImportError: If vLLM is not installed.
   260→        """
   261→        try:
   262→            from vllm import LLM as vLLM
   263→            from vllm import SamplingParams
   264→        except Exception as e:
   265→            raise ImportError(
   266→                "vLLM is not available. Install with: pip install qwen-asr[vllm]"
   267→            ) from e
   268→
   269→        llm = vLLM(model=model, **kwargs)
   270→
   271→        processor = Qwen3ASRProcessor.from_pretrained(model, fix_mistral_regex=True)
   272→        sampling_params = SamplingParams(**({"temperature": 0.0, "max_tokens": max_new_tokens}))
   273→
   274→        forced_aligner_model = None
   275→        if forced_aligner is not None:
   276→            forced_aligner_model = Qwen3ForcedAligner.from_pretrained(
   277→                forced_aligner, **(forced_aligner_kwargs or {})
   278→            )
   279→
   280→        return cls(
   281→            backend="vllm",
   282→            model=llm,
   283→            processor=processor,
   284→            sampling_params=sampling_params,
   285→            forced_aligner=forced_aligner_model,
   286→            max_inference_batch_size=max_inference_batch_size,
   287→            max_new_tokens=None,
   288→        )
   289→
   290→    def get_supported_languages(self) -> List[str]:
   291→        """
   292→        Returns the supported language list.
   293→
   294→        Returns:
   295→            List[str]: Canonical language names.
   296→        """
   297→        return list(SUPPORTED_LANGUAGES)
   298→
   299→    @torch.no_grad()
   300→    def transcribe(
   301→        self,
   302→        audio: Union[AudioLike, List[AudioLike]],
   303→        context: Union[str, List[str]] = "",
   304→        language: Optional[Union[str, List[Optional[str]]]] = None,
   305→        return_time_stamps: bool = False,
   306→    ) -> List[ASRTranscription]:
   307→        """
   308→        Transcribe audio with optional context and optional forced alignment timestamps.
   309→
   310→        Args:
   311→            audio:
   312→                Audio input(s). Supported:
   313→                  - str: local path / URL / base64 data url
   314→                  - (np.ndarray, sr)
   315→                  - list of above
   316→            context:
   317→                Context string(s). If scalar, it will be broadcast to batch size.
   318→            language:
   319→                Optional language(s). If provided, it must be in supported languages.
   320→                If scalar, it will be broadcast to batch size.
   321→                If provided, the prompt will force output to be transcription text only.
   322→            return_time_stamps:
   323→                If True, timestamps are produced via forced aligner and merged across chunks.
   324→                This requires forced_aligner initialized.
   325→
   326→        Returns:
   327→            List[ASRTranscription]: One result per input audio.
   328→
   329→        Raises:
   330→            ValueError:
   331→                - If return_time_stamps=True but forced_aligner is not provided.
   332→                - If language is unsupported.
   333→                - If batch sizes mismatch for context/language.
   334→        """
   335→        if return_time_stamps and self.forced_aligner is None:
   336→            raise ValueError("return_time_stamps=True requires `forced_aligner` to be provided at initialization.")
   337→
   338→        wavs = normalize_audios(audio)
   339→        n = len(wavs)
   340→
   341→        ctxs = context if isinstance(context, list) else [context]
   342→        if len(ctxs) == 1 and n > 1:
   343→            ctxs = ctxs * n
   344→        if len(ctxs) != n:
   345→            raise ValueError(f"Batch size mismatch: audio={n}, context={len(ctxs)}")
   346→
   347→        langs_in: List[Optional[str]]
   348→        if language is None:
   349→            langs_in = [None] * n
   350→        else:
   351→            langs_in = language if isinstance(language, list) else [language]
   352→            if len(langs_in) == 1 and n > 1:
   353→                langs_in = langs_in * n
   354→            if len(langs_in) != n:
   355→                raise ValueError(f"Batch size mismatch: audio={n}, language={len(langs_in)}")
   356→
   357→        langs_norm: List[Optional[str]] = []
   358→        for l in langs_in:
   359→            if l is None or str(l).strip() == "":
   360→                langs_norm.append(None)
   361→            else:
   362→                ln = normalize_language_name(str(l))
   363→                validate_language(ln)
   364→                langs_norm.append(ln)
   365→
   366→        max_chunk_sec = MAX_FORCE_ALIGN_INPUT_SECONDS if return_time_stamps else MAX_ASR_INPUT_SECONDS
   367→
   368→        # chunk audios and record mapping
   369→        chunks: List[AudioChunk] = []
   370→        for i, wav in enumerate(wavs):
   371→            parts = split_audio_into_chunks(
   372→                wav=wav,
   373→                sr=SAMPLE_RATE,
   374→                max_chunk_sec=max_chunk_sec,
   375→            )
   376→            for j, (cwav, offset_sec) in enumerate(parts):
   377→                chunks.append(AudioChunk(orig_index=i, chunk_index=j, wav=cwav, sr=SAMPLE_RATE, offset_sec=offset_sec))
   378→
   379→        # run ASR on chunks
   380→        chunk_ctx: List[str] = [ctxs[c.orig_index] for c in chunks]
   381→        chunk_lang: List[Optional[str]] = [langs_norm[c.orig_index] for c in chunks]
   382→        chunk_wavs: List[np.ndarray] = [c.wav for c in chunks]
   383→        raw_outputs = self._infer_asr(chunk_ctx, chunk_wavs, chunk_lang)
   384→
   385→        # parse outputs, prepare for optional alignment
   386→        per_chunk_lang: List[str] = []
   387→        per_chunk_text: List[str] = []
   388→        for out, forced_lang in zip(raw_outputs, chunk_lang):
   389→            lang, txt = parse_asr_output(out, user_language=forced_lang)
   390→            per_chunk_lang.append(lang)
   391→            per_chunk_text.append(txt)
   392→
   393→        # forced alignment (optional)
   394→        per_chunk_align: List[Optional[Any]] = [None] * len(chunks)
   395→        if return_time_stamps:
   396→            to_align_audio = []
   397→            to_align_text = []
   398→            to_align_lang = []
   399→            to_align_idx = []
   400→
   401→            for idx, (c, txt, lang_pred) in enumerate(zip(chunks, per_chunk_text, per_chunk_lang)):
   402→                if txt.strip() == "":
   403→                    continue
   404→                to_align_audio.append((c.wav, c.sr))
   405→                to_align_text.append(txt)
   406→                to_align_lang.append(lang_pred)
   407→                to_align_idx.append(idx)
   408→
   409→            # batch align with max_inference_batch_size
   410→            aligned_results: List[Any] = []
   411→            for a_chunk, t_chunk, l_chunk in zip(
   412→                chunk_list(to_align_audio, self.max_inference_batch_size),
   413→                chunk_list(to_align_text, self.max_inference_batch_size),
   414→                chunk_list(to_align_lang, self.max_inference_batch_size),
   415→            ):
   416→                aligned_results.extend(
   417→                    self.forced_aligner.align(audio=a_chunk, text=t_chunk, language=l_chunk)
   418→                )
   419→
   420→            # offset fix
   421→            for k, idx in enumerate(to_align_idx):
   422→                c = chunks[idx]
   423→                r = aligned_results[k]
   424→                per_chunk_align[idx] = self._offset_align_result(r, c.offset_sec)
   425→
   426→        # merge chunks back to original samples
   427→        out_langs: List[List[str]] = [[] for _ in range(n)]
   428→        out_texts: List[List[str]] = [[] for _ in range(n)]
   429→        out_aligns: List[List[Any]] = [[] for _ in range(n)]
   430→
   431→        for c, lang, txt, al in zip(chunks, per_chunk_lang, per_chunk_text, per_chunk_align):
   432→            out_langs[c.orig_index].append(lang)
   433→            out_texts[c.orig_index].append(txt)
   434→            if return_time_stamps and al is not None:
   435→                out_aligns[c.orig_index].append(al)
   436→
   437→        results: List[ASRTranscription] = []
   438→        for i in range(n):
   439→            merged_text = "".join([t for t in out_texts[i] if t is not None])
   440→            merged_language = merge_languages(out_langs[i])
   441→            merged_align = None
   442→            if return_time_stamps:
   443→                merged_align = self._merge_align_results(out_aligns[i])
   444→            results.append(ASRTranscription(language=merged_language, text=merged_text, time_stamps=merged_align))
   445→
   446→        return results
   447→
   448→    def _build_messages(self, context: str, audio_payload: Any) -> List[Dict[str, Any]]:
   449→        return [
   450→            {"role": "system", "content": context or ""},
   451→            {"role": "user", "content": [{"type": "audio", "audio": audio_payload}]},
   452→        ]
   453→
   454→    def _build_text_prompt(self, context: str, force_language: Optional[str]) -> str:
   455→        """
   456→        Build the string prompt for one request.
   457→
   458→        If force_language is provided, "language X<asr_text>" is appended after the generation prompt
   459→        to request text-only output.
   460→        """
   461→        msgs = self._build_messages(context=context, audio_payload="")
   462→        base = self.processor.apply_chat_template(msgs, add_generation_prompt=True, tokenize=False)
   463→        if force_language:
   464→            base = base + f"language {force_language}{'<asr_text>'}"
   465→        return base
   466→
   467→    def _infer_asr(
   468→        self,
   469→        contexts: List[str],
   470→        wavs: List[np.ndarray],
   471→        languages: List[Optional[str]],
   472→    ) -> List[str]:
   473→        """
   474→        Run backend inference for chunk-level items.
   475→
   476→        Args:
   477→            contexts: List of context strings.
   478→            wavs: List of mono waveforms (np.ndarray).
   479→            languages: List of forced languages or None.
   480→
   481→        Returns:
   482→            List[str]: Raw decoded strings (one per chunk).
   483→        """
   484→        if self.backend == "transformers":
   485→            return self._infer_asr_transformers(contexts, wavs, languages)
   486→        if self.backend == "vllm":
   487→            return self._infer_asr_vllm(contexts, wavs, languages)
   488→        raise RuntimeError(f"Unknown backend: {self.backend}")
   489→
   490→    def _infer_asr_transformers(
   491→        self,
   492→        contexts: List[str],
   493→        wavs: List[np.ndarray],
   494→        languages: List[Optional[str]],
   495→    ) -> List[str]:
   496→        outs: List[str] = []
   497→
   498→        texts = [self._build_text_prompt(context=c, force_language=fl) for c, fl in zip(contexts, languages)]
   499→


1→# coding=utf-8
     2→# Copyright 2026 The Alibaba Qwen team.
     3→# SPDX-License-Identifier: Apache-2.0
     4→#
     5→# Licensed under the Apache License, Version 2.0 (the "License");
     6→# you may not use this file except in compliance with the License.
     7→# You may obtain a copy of the License at
     8→#
     9→#     http://www.apache.org/licenses/LICENSE-2.0
    10→#
    11→# Unless required by applicable law or agreed to in writing, software
    12→# distributed under the License is distributed on an "AS IS" BASIS,
    13→# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14→# See the License for the specific language governing permissions and
    15→# limitations under the License.
    16→"""
    17→A gradio demo for Qwen3 ASR models.
    18→"""
    19→
    20→import argparse
    21→import base64
    22→import io
    23→import json
    24→import os
    25→from typing import Any, Dict, List, Optional, Tuple, Union
    26→
    27→import gradio as gr
    28→import numpy as np
    29→import torch
    30→from qwen_asr import Qwen3ASRModel
    31→from scipy.io.wavfile import write as wav_write
    32→
    33→
    34→def _title_case_display(s: str) -> str:
    35→    s = (s or "").strip()
    36→    s = s.replace("_", " ")
    37→    return " ".join([w[:1].upper() + w[1:] if w else "" for w in s.split()])
    38→
    39→
    40→def _build_choices_and_map(items: Optional[List[str]]) -> Tuple[List[str], Dict[str, str]]:
    41→    if not items:
    42→        return [], {}
    43→    display = [_title_case_display(x) for x in items]
    44→    mapping = {d: r for d, r in zip(display, items)}
    45→    return display, mapping
    46→
    47→
    48→def _dtype_from_str(s: str) -> torch.dtype:
    49→    s = (s or "").strip().lower()
    50→    if s in ("bf16", "bfloat16"):
    51→        return torch.bfloat16
    52→    if s in ("fp16", "float16", "half"):
    53→        return torch.float16
    54→    if s in ("fp32", "float32"):
    55→        return torch.float32
    56→    raise ValueError(f"Unsupported torch dtype: {s}. Use bfloat16/float16/float32.")
    57→
    58→
    59→def _normalize_audio(wav, eps=1e-12, clip=True):
    60→    x = np.asarray(wav)
    61→
    62→    if np.issubdtype(x.dtype, np.integer):
    63→        info = np.iinfo(x.dtype)
    64→        if info.min < 0:
    65→            y = x.astype(np.float32) / max(abs(info.min), info.max)
    66→        else:
    67→            mid = (info.max + 1) / 2.0
    68→            y = (x.astype(np.float32) - mid) / mid
    69→    elif np.issubdtype(x.dtype, np.floating):
    70→        y = x.astype(np.float32)
    71→        m = np.max(np.abs(y)) if y.size else 0.0
    72→        if m > 1.0 + 1e-6:
    73→            y = y / (m + eps)
    74→    else:
    75→        raise TypeError(f"Unsupported dtype: {x.dtype}")
    76→
    77→    if clip:
    78→        y = np.clip(y, -1.0, 1.0)
    79→
    80→    if y.ndim > 1:
    81→        y = np.mean(y, axis=-1).astype(np.float32)
    82→
    83→    return y
    84→
    85→
    86→def _audio_to_tuple(audio: Any) -> Optional[Tuple[np.ndarray, int]]:
    87→    """
    88→    Accept gradio audio:
    89→      - {"sampling_rate": int, "data": np.ndarray}
    90→      - (sr, np.ndarray)  [some gradio versions]
    91→    Return: (wav_float32_mono, sr)
    92→    """
    93→    if audio is None:
    94→        return None
    95→
    96→    if isinstance(audio, dict) and "sampling_rate" in audio and "data" in audio:
    97→        sr = int(audio["sampling_rate"])
    98→        wav = _normalize_audio(audio["data"])
    99→        return wav, sr
   100→
   101→    if isinstance(audio, tuple) and len(audio) == 2:
   102→        a0, a1 = audio
   103→        if isinstance(a0, int):
   104→            sr = int(a0)
   105→            wav = _normalize_audio(a1)
   106→            return wav, sr
   107→        if isinstance(a1, int):
   108→            wav = _normalize_audio(a0)
   109→            sr = int(a1)
   110→            return wav, sr
   111→
   112→    return None
   113→
   114→
   115→def _parse_audio_any(audio: Any) -> Union[str, Tuple[np.ndarray, int]]:
   116→    if audio is None:
   117→        raise ValueError("Audio is required.")
   118→    at = _audio_to_tuple(audio)
   119→    if at is not None:
   120→        return at
   121→    raise ValueError("Unsupported audio input format.")
   122→
   123→
   124→def build_parser() -> argparse.ArgumentParser:
   125→    parser = argparse.ArgumentParser(
   126→        prog="qwen-asr-demo",
   127→        description=(
   128→            "Launch a Gradio demo for Qwen3 ASR models (Transformers / vLLM).\n\n"
   129→            "Examples:\n"
   130→            "  qwen-asr-demo --asr-checkpoint Qwen/Qwen3-ASR-1.7B\n"
   131→            "  qwen-asr-demo --asr-checkpoint Qwen/Qwen3-ASR-1.7B --aligner-checkpoint Qwen/Qwen3-ForcedAligner-0.6B\n"
   132→            "  qwen-asr-demo --backend vllm --cuda-visible-devices 0\n"
   133→            "  qwen-asr-demo --backend transformers --backend-kwargs '{\"device_map\":\"cuda:0\",\"dtype\":\"bfloat16\",\"attn_implementation\":\"flash_attention_2\"}'\n"
   134→            "  qwen-asr-demo --backend vllm --backend-kwargs '{\"gpu_memory_utilization\":0.85}'\n"
   135→        ),
   136→        formatter_class=argparse.RawTextHelpFormatter,
   137→        add_help=True,
   138→    )
   139→
   140→    parser.add_argument("--asr-checkpoint", required=True, help="Qwen3-ASR model checkpoint path or HF repo id.")
   141→    parser.add_argument(
   142→        "--aligner-checkpoint",
   143→        default=None,
   144→        help="Qwen3-ForcedAligner checkpoint path or HF repo id (optional; enables timestamps when provided).",
   145→    )
   146→
   147→    parser.add_argument(
   148→        "--backend",
   149→        default="transformers",
   150→        choices=["transformers", "vllm"],
   151→        help="Backend for ASR model loading (default: transformers).",
   152→    )
   153→
   154→    parser.add_argument(
   155→        "--cuda-visible-devices",
   156→        default="0",
   157→        help=(
   158→            "Set CUDA_VISIBLE_DEVICES for the demo process (default: 0). "
   159→            "Use e.g. '0' or '1'"
   160→        ),
   161→    )
   162→
   163→    parser.add_argument(
   164→        "--backend-kwargs",
   165→        default=None,
   166→        help=(
   167→            "JSON dict for backend-specific kwargs excluding checkpoints.\n"
   168→            "Examples:\n"
   169→            "  transformers: '{\"device_map\":\"cuda:0\",\"dtype\":\"bfloat16\",\"attn_implementation\":\"flash_attention_2\",\"max_inference_batch_size\":32}'\n"
   170→            "  vllm        : '{\"gpu_memory_utilization\":0.8,\"max_inference_batch_size\":32}'\n"
   171→        ),
   172→    )
   173→    parser.add_argument(
   174→        "--aligner-kwargs",
   175→        default=None,
   176→        help=(
   177→            "JSON dict for forced aligner kwargs (only used when --aligner-checkpoint is set).\n"
   178→            "Example: '{\"dtype\":\"bfloat16\",\"device_map\":\"cuda:0\"}'\n"
   179→        ),
   180→    )
   181→
   182→    # Gradio server args
   183→    parser.add_argument("--ip", default="0.0.0.0", help="Server bind IP for Gradio (default: 0.0.0.0).")
   184→    parser.add_argument("--port", type=int, default=8000, help="Server port for Gradio (default: 8000).")
   185→    parser.add_argument(
   186→        "--share/--no-share",
   187→        dest="share",
   188→        default=False,
   189→        action=argparse.BooleanOptionalAction,
   190→        help="Whether to create a public Gradio link (default: disabled).",
   191→    )
   192→    parser.add_argument("--concurrency", type=int, default=16, help="Gradio queue concurrency (default: 16).")
   193→
   194→    # HTTPS args
   195→    parser.add_argument("--ssl-certfile", default=None, help="Path to SSL certificate file for HTTPS (optional).")
   196→    parser.add_argument("--ssl-keyfile", default=None, help="Path to SSL key file for HTTPS (optional).")
   197→    parser.add_argument(
   198→        "--ssl-verify/--no-ssl-verify",
   199→        dest="ssl_verify",
   200→        default=True,
   201→        action=argparse.BooleanOptionalAction,
   202→        help="Whether to verify SSL certificate (default: enabled).",
   203→    )
   204→
   205→    return parser
   206→
   207→
   208→def _parse_json_dict(s: Optional[str], *, name: str) -> Dict[str, Any]:
   209→    if s is None or not str(s).strip():
   210→        return {}
   211→    try:
   212→        obj = json.loads(s)
   213→    except Exception as e:
   214→        raise ValueError(f"Invalid JSON for {name}: {e}")
   215→    if not isinstance(obj, dict):
   216→        raise ValueError(f"{name} must be a JSON object (dict).")
   217→    return obj
   218→
   219→
   220→def _apply_cuda_visible_devices(cuda_visible_devices: str) -> None:
   221→    v = (cuda_visible_devices or "").strip()
   222→    if not v:
   223→        return
   224→    os.environ["CUDA_VISIBLE_DEVICES"] = v
   225→
   226→
   227→def _default_backend_kwargs(backend: str) -> Dict[str, Any]:
   228→    if backend == "transformers":
   229→        return dict(
   230→            dtype=torch.bfloat16,
   231→            device_map="cuda:0",
   232→            max_inference_batch_size=4,
   233→            max_new_tokens=512,
   234→        )
   235→    else:
   236→        return dict(
   237→            gpu_memory_utilization=0.8,
   238→            max_inference_batch_size=4,
   239→            max_new_tokens=4096,
   240→        )
   241→
   242→
   243→def _default_aligner_kwargs() -> Dict[str, Any]:
   244→    return dict(
   245→        dtype=torch.bfloat16,
   246→        device_map="cuda:0",
   247→    )
   248→
   249→
   250→def _merge_dicts(base: Dict[str, Any], override: Dict[str, Any]) -> Dict[str, Any]:
   251→    out = dict(base)
   252→    out.update(override)
   253→    return out
   254→
   255→
   256→def _coerce_special_types(d: Dict[str, Any]) -> Dict[str, Any]:
   257→    out: Dict[str, Any] = {}
   258→    for k, v in d.items():
   259→        if k == "dtype" and isinstance(v, str):
   260→            out[k] = _dtype_from_str(v)
   261→        else:
   262→            out[k] = v
   263→    return out
   264→
   265→
   266→def _make_timestamp_html(audio_upload: Any, timestamps: Any) -> str:
   267→    """
   268→    Build HTML with per-token audio slices, using base64 data URLs.
   269→    Expect timestamps as list[dict] with keys: text, start_time, end_time (ms).
   270→    """
   271→    at = _audio_to_tuple(audio_upload)
   272→    if at is None:
   273→        raise ValueError("Audio input is required for visualization.")
   274→    audio, sr = at
   275→
   276→    if not timestamps:
   277→        return "
No timestamps to visualize.
"
   278→    if not isinstance(timestamps, list):
   279→        raise ValueError("Timestamps must be a list (JSON array).")
   280→
   281→    html_content = """
   282→
   295→    """
   296→
   297→    html_content += """
   298→
   299→        Timestamps Visualization (时间戳可视化结果）
   300→

   301→    """
   302→
   303→    for item in timestamps:
   304→        if not isinstance(item, dict):
   305→            continue
   306→        word = str(item.get("text", "") or "")
   307→        start = item.get("start_time", None)
   308→        end = item.get("end_time", None)
   309→        if start is None or end is None:
   310→            continue
   311→
   312→        start = float(start)
   313→        end = float(end)
   314→        if end <= start:
   315→            continue
   316→
   317→        start_sample = max(0, int(start * sr))
   318→        end_sample = min(len(audio), int(end * sr))
   319→        if end_sample <= start_sample:
   320→            continue
   321→
   322→        seg = audio[start_sample:end_sample]
   323→        seg_i16 = (np.clip(seg, -1.0, 1.0) * 32767.0).astype(np.int16)
   324→
   325→        mem = io.BytesIO()
   326→        wav_write(mem, sr, seg_i16)
   327→        mem.seek(0)
   328→        b64 = base64.b64encode(mem.read()).decode("utf-8")
   329→        audio_src = f"data:audio/wav;base64,{b64}"
   330→
   331→        html_content += f"""
   332→

   333→
{word}

   334→
{start} - {end} s

   335→

   336→                <audio controls preload="none" src="{audio_src}"></audio>
   337→

   338→

   339→        """
   340→
   341→    html_content += "
"
   342→    return html_content
   343→
   344→
   345→def build_demo(
   346→    asr: Qwen3ASRModel,
   347→    asr_ckpt: str,
   348→    backend: str,
   349→    aligner_ckpt: Optional[str] = None,
   350→) -> gr.Blocks:
   351→    supported_langs_raw = asr.get_supported_languages()
   352→    lang_choices_disp, lang_map = _build_choices_and_map([x for x in supported_langs_raw])
   353→    lang_choices = ["Auto"] + lang_choices_disp
   354→
   355→    has_aligner = bool(aligner_ckpt)
   356→
   357→    theme = gr.themes.Soft(
   358→        font=[gr.themes.GoogleFont("Source Sans Pro"), "Arial", "sans-serif"],
   359→    )
   360→    css = ".gradio-container {max-width: none !important;}"
   361→
   362→    with gr.Blocks(theme=theme, css=css) as demo:
   363→        gr.Markdown(
   364→            f"""
   365→# Qwen3 ASR Demo
   366→**Backend:** `{backend}`
   367→**ASR Checkpoint:** `{asr_ckpt}`
   368→**Forced Aligner:** `{aligner_ckpt if aligner_ckpt else "(none)"}`
   369→"""
   370→        )
   371→
   372→        with gr.Row():
   373→            with gr.Column(scale=2):
   374→                audio_in = gr.Audio(label="Audio Input (上传音频)", type="numpy")
   375→                lang_in = gr.Dropdown(
   376→                    label="Language (语种)",
   377→                    choices=lang_choices,
   378→                    value="Auto",
   379→                    interactive=True,
   380→                )
   381→                if has_aligner:
   382→                    ts_in = gr.Checkbox(
   383→                        label="Return Timestamps (是否返回时间戳)",
   384→                        value=True,
   385→                    )
   386→                else:
   387→                    ts_in = gr.State(False)
   388→
   389→                btn = gr.Button("Transcribe (识别)", variant="primary")
   390→
   391→            with gr.Column(scale=2):
   392→                out_lang = gr.Textbox(label="Detected Language", lines=1)
   393→                out_text = gr.Textbox(label="Result Text", lines=12)
   394→
   395→            if has_aligner:
   396→                with gr.Column(scale=3):
   397→                    out_ts = gr.JSON(label="Timestamps（时间戳结果）")
   398→                    viz_btn = gr.Button("Visualize Timestamps (可视化时间戳)", variant="secondary")
   399→            else:
   400→                with gr.Column(scale=3):
   401→                    out_ts = gr.State(None)
   402→                    viz_btn = gr.State(None)
   403→
   404→        # Put the visualization panel below the three columns
   405→        if has_aligner:
   406→            with gr.Row():
   407→                out_ts_html = gr.HTML(label="Timestamps Visualization (时间戳可视化结果)")
   408→        else:
   409→            out_ts_html = gr.State("")
   410→
   411→        def run(audio_upload: Any, lang_disp: str, return_ts: bool):
   412→            audio_obj = _parse_audio_any(audio_upload)
   413→
   414→            language = None
   415→            if lang_disp and lang_disp != "Auto":
   416→                language = lang_map.get(lang_disp, lang_disp)
   417→
   418→            return_ts = bool(return_ts) and has_aligner
   419→
   420→            results = asr.transcribe(
   421→                audio=audio_obj,
   422→                language=language,
   423→                return_time_stamps=return_ts,
   424→            )
   425→            if not isinstance(results, list) or len(results) != 1:
   426→                raise RuntimeError(
   427→                    f"Unexpected result size: {type(results)} "
   428→                    f"len={len(results) if isinstance(results, list) else 'N/A'}"
   429→                )
   430→
   431→            r = results[0]
   432→
   433→            if has_aligner:
   434→                ts_payload = None
   435→                if return_ts:
   436→                    ts_payload = [
   437→                        dict(
   438→                            text=getattr(t, "text", None),
   439→                            start_time=getattr(t, "start_time", None),
   440→                            end_time=getattr(t, "end_time", None),
   441→                        )
   442→                        for t in (getattr(r, "time_stamps", None) or [])
   443→                    ]
   444→                return (
   445→                    getattr(r, "language", "") or "",
   446→                    getattr(r, "text", "") or "",
   447→                    gr.update(value=ts_payload) if return_ts else gr.update(value=None),
   448→                    gr.update(value=""),  # clear html on each transcribe
   449→                )
   450→            else:
   451→                return (
   452→                    getattr(r, "language", "") or "",
   453→                    getattr(r, "text", "") or "",
   454→                )
   455→
   456→        def visualize(audio_upload: Any, timestamps_json: Any):
   457→            return _make_timestamp_html(audio_upload, timestamps_json)
   458→
   459→        if has_aligner:
   460→            btn.click(
   461→                run,
   462→                inputs=[audio_in, lang_in, ts_in],
   463→                outputs=[out_lang, out_text, out_ts, out_ts_html],
   464→            )
   465→            viz_btn.click(
   466→                visualize,
   467→                inputs=[audio_in, out_ts],
   468→                outputs=[out_ts_html],
   469→            )
   470→        else:
   471→            btn.click(
   472→                run,
   473→                inputs=[audio_in, lang_in, ts_in],
   474→                outputs=[out_lang, out_text],
   475→            )
   476→
   477→    return demo
   478→
   479→
   480→def main(argv=None) -> int:
   481→    parser = build_parser()
   482→    args = parser.parse_args(argv)
   483→
   484→    _apply_cuda_visible_devices(args.cuda_visible_devices)
   485→
   486→    backend = args.backend
   487→    asr_ckpt = args.asr_checkpoint
   488→    aligner_ckpt = args.aligner_checkpoint
   489→
   490→    user_backend_kwargs = _parse_json_dict(args.backend_kwargs, name="--backend-kwargs")
   491→    user_aligner_kwargs = _parse_json_dict(args.aligner_kwargs, name="--aligner-kwargs")
   492→
   493→    backend_kwargs = _merge_dicts(_default_backend_kwargs(backend), user_backend_kwargs)
   494→    backend_kwargs = _coerce_special_types(backend_kwargs)
   495→
   496→    forced_aligner = None
   497→    forced_aligner_kwargs = None
   498→    if aligner_ckpt:
   499→        forced_aligner = aligner_ckpt
   500→        aligner_kwargs = _merge_dicts(_default_aligner_kwargs(), user_aligner_kwargs)
   501→        forced_aligner_kwargs = _coerce_special_types(aligner_kwargs)
   502→
   503→    if backend == "transformers":
   504→        asr = Qwen3ASRModel.from_pretrained(
   505→            asr_ckpt,
   506→            forced_aligner=forced_aligner,
   507→            forced_aligner_kwargs=forced_aligner_kwargs,
   508→            **backend_kwargs,
   509→        )
   510→    else:
   511→        asr = Qwen3ASRModel.LLM(
   512→            model=asr_ckpt,
   513→            forced_aligner=forced_aligner,
   514→            forced_aligner_kwargs=forced_aligner_kwargs,
   515→            **backend_kwargs,
   516→        )
   517→
   518→    demo = build_demo(asr, asr_ckpt, backend, aligner_ckpt=aligner_ckpt)
   519→
   520→    launch_kwargs: Dict[str, Any] = dict(
   521→        server_name=args.ip,
   522→        server_port=args.port,
   523→        share=args.share,
   524→        ssl_verify=True if args.ssl_verify else False,
   525→    )
   526→    if args.ssl_certfile is not None:
   527→        launch_kwargs["ssl_certfile"] = args.ssl_certfile
   528→    if args.ssl_keyfile is not None:
   529→        launch_kwargs["ssl_keyfile"] = args.ssl_keyfile
   530→
   531→    demo.queue(default_concurrency_limit=int(args.concurrency)).launch(**launch_kwargs)
   532→    return 0
   533→
   534→
   535→if __name__ == "__main__":
   536→    raise SystemExit(main())
   537→


1→# Qwen3-ASR
     2→
     3→

     4→
     5→

     6→
     7→

     8→
     9→

    10→&nbsp&nbsp🤗 Hugging Face&nbsp&nbsp | &nbsp&nbsp🤖 ModelScope&nbsp&nbsp | &nbsp&nbsp📑 Blog&nbsp&nbsp | &nbsp&nbsp📑 Paper&nbsp&nbsp
    11→

    12→🖥 Hugging Face Demo&nbsp&nbsp | &nbsp&nbsp 🖥 ModelScope Demo&nbsp&nbsp | &nbsp&nbsp💬 WeChat (微信)&nbsp&nbsp | &nbsp&nbsp🫨 Discord&nbsp&nbsp | &nbsp&nbsp📑 API
    13→
    14→

    15→
    16→We release **Qwen3-ASR**, a family that includes two powerful all-in-one speech recognition models that support language identification and ASR for 52 languages and dialects, as well as a novel non-autoregressive speech forced-alignment model that can align text–speech pairs in 11 languages.
    17→
    18→
    19→## News
    20→* 2026.1.29: 🎉🎉🎉 We have released the [Qwen3-ASR](https://huggingface.co/collections/Qwen/qwen3-asr) series (0.6B/1.7B) and the Qwen3-ForcedAligner-0.6B model. Please check out our [blog](https://qwen.ai/blog?id=qwen3asr)!
    21→
    22→
    23→## Contents
    24→
    25→- [Overview](#overview)
    26→  - [Introduction](#introduction)
    27→  - [Model Architecture](#model-architecture)
    28→  - [Released Models Description and Download](#released-models-description-and-download)
    29→- [Quickstart](#quickstart)
    30→  - [Environment Setup](#environment-setup)
    31→  - [Python Package Usage](#python-package-usage)
    32→    - [Quick Inference](#quick-inference)
    33→    - [vLLM Backend](#vllm-backend)
    34→    - [Streaming Inference](#streaming-inference)
    35→    - [ForcedAligner Usage](#forcedaligner-usage)
    36→  - [DashScope API Usage](#dashscope-api-usage)
    37→- [Launch Local Web UI Demo](#launch-local-web-ui-demo)
    38→  - [Gradio Demo](#gradio-demo)
    39→  - [Streaming Demo](#streaming-demo)
    40→- [Deployment with vLLM](#deployment-with-vllm)
    41→- [Fine Tuning](#fine-tuning)
    42→- [Docker](#docker)
    43→- [Evaluation](#evaluation)
    44→- [Citation](#citation)
    45→
    46→
    47→## Overview
    48→
    49→### Introduction
    50→
    51→

    52→
    53→

    54→
    55→The Qwen3-ASR family includes Qwen3-ASR-1.7B and Qwen3-ASR-0.6B, which support language identification and ASR for 52 languages and dialects. Both leverage large-scale speech training data and the strong audio understanding capability of their foundation model, Qwen3-Omni. Experiments show that the 1.7B version achieves state-of-the-art performance among open-source ASR models and is competitive with the strongest proprietary commercial APIs. Here are the main features:
    56→
    57→* **All-in-one**: Qwen3-ASR-1.7B and Qwen3-ASR-0.6B support language identification and speech recognition for 30 languages and 22 Chinese dialects, so as to English accents from multiple countries and regions.
    58→
    59→* **Excellent and Fast**: The Qwen3-ASR family ASR models maintains high-quality and robust recognition under complex acoustic environments and challenging text patterns. Qwen3-ASR-1.7B achieves strong performance on both open-sourced and internal benchmarks. While the 0.6B version achieves accuracy-efficient trade-off, it reaches 2000 times throughput at a concurrency of 128. They both achieve streaming / offline unified inference with single model and support transcribe long audio.
    60→
    61→* **Novel and strong forced alignment Solution**: We introduce Qwen3-ForcedAligner-0.6B, which supports timestamp prediction for arbitrary units within up to 5 minutes of speech in 11 languages. Evaluations show its timestamp accuracy surpasses E2E based forced-alignment models.
    62→
    63→* **Comprehensive inference toolkit**: In addition to open-sourcing the architectures and weights of the Qwen3-ASR series, we also release a powerful, full-featured inference framework that supports vLLM-based batch inference, asynchronous serving, streaming inference, timestamp prediction, and more.
    64→
    65→### Model Architecture
    66→
    67→

    68→
    69→

    70→
    71→
    72→### Released Models Description and Download
    73→
    74→Below is an introduction and download information for the Qwen3-ASR models. Please select and download the model that fits your needs.
    75→
    76→| Model | Supported Languages | Supported Dialects | Inference Mode | Audio Types |
    77→|---|---|---|---|---|
    78→| Qwen3-ASR-1.7B & Qwen3-ASR-0.6B | Chinese (zh), English (en), Cantonese (yue), Arabic (ar), German (de), French (fr), Spanish (es), Portuguese (pt), Indonesian (id), Italian (it), Korean (ko), Russian (ru), Thai (th), Vietnamese (vi), Japanese (ja), Turkish (tr), Hindi (hi), Malay (ms), Dutch (nl), Swedish (sv), Danish (da), Finnish (fi), Polish (pl), Czech (cs), Filipino (fil), Persian (fa), Greek (el), Hungarian (hu), Macedonian (mk), Romanian (ro) | Anhui, Dongbei, Fujian, Gansu, Guizhou, Hebei, Henan, Hubei, Hunan, Jiangxi, Ningxia, Shandong, Shaanxi, Shanxi, Sichuan, Tianjin, Yunnan, Zhejiang, Cantonese (Hong Kong accent), Cantonese (Guangdong accent), Wu language, Minnan language. | Offline / Streaming | Speech, Singing Voice, Songs with BGM |
    79→| Qwen3-ForcedAligner-0.6B | Chinese, English, Cantonese, French, German, Italian, Japanese, Korean, Portuguese, Russian, Spanish | -- | NAR | Speech |
    80→
    81→During model loading in the `qwen-asr` package or vLLM, model weights will be downloaded automatically based on the model name. However, if your runtime environment does not allow downloading weights during execution, you can use the following commands to manually download the model weights to a local directory:
    82→
    83→```bash
    84→# Download through ModelScope (recommended for users in Mainland China)
    85→pip install -U modelscope
    86→modelscope download --model Qwen/Qwen3-ASR-1.7B  --local_dir ./Qwen3-ASR-1.7B
    87→modelscope download --model Qwen/Qwen3-ASR-0.6B --local_dir ./Qwen3-ASR-0.6B
    88→modelscope download --model Qwen/Qwen3-ForcedAligner-0.6B --local_dir ./Qwen3-ForcedAligner-0.6B
    89→# Download through Hugging Face
    90→pip install -U "huggingface_hub[cli]"
    91→huggingface-cli download Qwen/Qwen3-ASR-1.7B --local-dir ./Qwen3-ASR-1.7B
    92→huggingface-cli download Qwen/Qwen3-ASR-0.6B --local-dir ./Qwen3-ASR-0.6B
    93→huggingface-cli download Qwen/Qwen3-ForcedAligner-0.6B --local-dir ./Qwen3-ForcedAligner-0.6B
    94→```
    95→
    96→
    97→## Quickstart
    98→
    99→### Environment Setup
   100→
   101→The easiest way to use Qwen3-ASR is to install the `qwen-asr` Python package from PyPI. This will pull in the required runtime dependencies and allow you to load any released Qwen3-ASR model. If you’d like to simplify environment setup further, you can also use our official [Docker image](#docker). The `qwen-asr` package provides two backends: the transformers backend and the vLLM backend. For usage instructions for different backends, please refer to [Python Package Usage](#python-package-usage). We recommend using a **fresh, isolated environment** to avoid dependency conflicts with existing packages. You can create a clean Python 3.12 environment like this:
   102→
   103→```bash
   104→conda create -n qwen3-asr python=3.12 -y
   105→conda activate qwen3-asr
   106→```
   107→
   108→Run the following command to get the minimal installation with transformers-backend support:
   109→
   110→```bash
   111→pip install -U qwen-asr
   112→```
   113→
   114→To enable the vLLM backend for faster inference and streaming support, run:
   115→
   116→```bash
   117→pip install -U qwen-asr[vllm]
   118→```
   119→
   120→If you want to develop or modify the code locally, install from source in editable mode:
   121→
   122→```bash
   123→git clone https://github.com/QwenLM/Qwen3-ASR.git
   124→cd Qwen3-ASR
   125→pip install -e .
   126→# support vLLM backend
   127→# pip install -e ".[vllm]"
   128→```
   129→
   130→Additionally, we recommend using FlashAttention 2 to reduce GPU memory usage and accelerate inference speed, especially for long inputs and large batch sizes.
   131→
   132→```bash
   133→pip install -U flash-attn --no-build-isolation
   134→```
   135→
   136→If your machine has less than 96GB of RAM and lots of CPU cores, run:
   137→
   138→```bash
   139→MAX_JOBS=4 pip install -U flash-attn --no-build-isolation
   140→```
   141→
   142→Also, you should have hardware that is compatible with FlashAttention 2. Read more about it in the official documentation of the [FlashAttention repository](https://github.com/Dao-AILab/flash-attention). FlashAttention 2 can only be used when a model is loaded in `torch.float16` or `torch.bfloat16`.
   143→
   144→### Python Package Usage
   145→
   146→#### Quick Inference
   147→
   148→The `qwen-asr` package provides two backends: **transformers backend** and **vLLM backend**. You can pass audio inputs as a local path, a URL, base64 data, or a `(np.ndarray, sr)` tuple, and run batch inference. To quickly try Qwen3-ASR, you can use `Qwen3ASRModel.from_pretrained(...)` for the transformers backend with the following code:
   149→
   150→```python
   151→import torch
   152→from qwen_asr import Qwen3ASRModel
   153→
   154→model = Qwen3ASRModel.from_pretrained(
   155→    "Qwen/Qwen3-ASR-1.7B",
   156→    dtype=torch.bfloat16,
   157→    device_map="cuda:0",
   158→    # attn_implementation="flash_attention_2",
   159→    max_inference_batch_size=32, # Batch size limit for inference. -1 means unlimited. Smaller values can help avoid OOM.
   160→    max_new_tokens=256, # Maximum number of tokens to generate. Set a larger value for long audio input.
   161→)
   162→
   163→results = model.transcribe(
   164→    audio="https://qianwen-res.oss-cn-beijing.aliyuncs.com/Qwen3-ASR-Repo/asr_en.wav",
   165→    language=None, # set "English" to force the language
   166→)
   167→
   168→print(results[0].language)
   169→print(results[0].text)
   170→```
   171→
   172→If you want to return timestamps, pass `forced_aligner` and its init kwargs. Here is an example of batch inference with timestamps output:
   173→
   174→```python
   175→import torch
   176→from qwen_asr import Qwen3ASRModel
   177→
   178→model = Qwen3ASRModel.from_pretrained(
   179→    "Qwen/Qwen3-ASR-1.7B",
   180→    dtype=torch.bfloat16,
   181→    device_map="cuda:0",
   182→    # attn_implementation="flash_attention_2",
   183→    max_inference_batch_size=32, # Batch size limit for inference. -1 means unlimited. Smaller values can help avoid OOM.
   184→    max_new_tokens=256, # Maximum number of tokens to generate. Set a larger value for long audio input.
   185→    forced_aligner="Qwen/Qwen3-ForcedAligner-0.6B",
   186→    forced_aligner_kwargs=dict(
   187→        dtype=torch.bfloat16,
   188→        device_map="cuda:0",
   189→        # attn_implementation="flash_attention_2",
   190→    ),
   191→)
   192→
   193→results = model.transcribe(
   194→    audio=[
   195→      "https://qianwen-res.oss-cn-beijing.aliyuncs.com/Qwen3-ASR-Repo/asr_zh.wav",
   196→      "https://qianwen-res.oss-cn-beijing.aliyuncs.com/Qwen3-ASR-Repo/asr_en.wav",
   197→    ],
   198→    language=["Chinese", "English"], # can also be set to None for automatic language detection
   199→    return_time_stamps=True,
   200→)