chua commited on
Commit
25ae981
·
0 Parent(s):

Public release

Browse files
.gitattributes ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
2
+ tokenizer.json filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.py[cod]
3
+ .pytest_cache/
4
+ .mypy_cache/
5
+ .ruff_cache/
6
+ .venv/
7
+ .venv-*/
8
+ .env
9
+ *.log
10
+ tmp/
LICENSE ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright [yyyy] [name of copyright owner]
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
README.md ADDED
@@ -0,0 +1,183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ library_name: transformers
3
+ tags:
4
+ - automatic-speech-recognition
5
+ - speech
6
+ - audio
7
+ - multilingual
8
+ - transformers
9
+ - pytorch
10
+ - safetensors
11
+ - hotword
12
+ - audio8
13
+ pipeline_tag: automatic-speech-recognition
14
+ language:
15
+ - en
16
+ - zh
17
+ - fr
18
+ - ja
19
+ - yue
20
+ license: apache-2.0
21
+ repository: https://github.com/AutoArk/open-audio-opd
22
+ ---
23
+
24
+ <div align="center">
25
+
26
+ # Audio8-ASR-0.1B
27
+
28
+ [![GitHub](https://img.shields.io/badge/GitHub-AutoArk%2Fopen--audio--opd-blue?logo=github)](https://github.com/AutoArk/open-audio-opd)
29
+ [![arXiv](https://img.shields.io/badge/arXiv-2605.28139-b31b1b?logo=arxiv)](https://arxiv.org/abs/2605.28139)
30
+ [![License](https://img.shields.io/badge/License-Apache--2.0-green)](https://www.apache.org/licenses/LICENSE-2.0)
31
+
32
+ </div>
33
+
34
+ `Audio8-ASR-0.1B` is a compact autoregressive ASR model whose language-model
35
+ component has only 0.1B parameters. It supports multilingual speech recognition
36
+ for languages including Chinese, English, French, Japanese, and Cantonese. We
37
+ position it as one of the smallest usable performance ASR models in the LLM era.
38
+
39
+ This base repository provides the Hugging Face Transformers checkpoint. We also
40
+ provide deployment-focused releases:
41
+
42
+ - [Audio8-ASR-0.1B-onnx-runtime](https://huggingface.co/AutoArk-AI/Audio8-ASR-0.1B-onnx-runtime)
43
+ - [Audio8-ASR-0.1B-iOS-ANE](https://huggingface.co/AutoArk-AI/Audio8-ASR-0.1B-iOS-ANE)
44
+
45
+ The ONNX Runtime release is designed for edge-device deployment and can run
46
+ with roughly 1.1 GB peak memory footprint, depending on device, runtime
47
+ configuration, and workload.
48
+
49
+ The iOS release is designed for local iPhone transcription with roughly 200 MB
50
+ peak runtime memory footprint, depending on device, iOS version, and workload.
51
+
52
+ ## Open ASR Leaderboard Evaluation
53
+
54
+ Coming Soon.
55
+
56
+ ## Model Overview
57
+
58
+ - **Task:** automatic speech recognition
59
+ - **Checkpoint format:** `safetensors`
60
+ - **Sampling rate:** 16 kHz
61
+ - **Decoder:** 8-layer Qwen-style causal LM
62
+ - **Audio front end:** Qwen3-ASR audio encoder plus MLP adapter/projector
63
+ - **Language-model parameters:** 103,502,336 (about 0.104B)
64
+ - **End-to-end unique parameters:** 323,990,528 (about 0.324B)
65
+ - **Runtime:** Hugging Face Transformers
66
+ - **Hotwords:** optional decode-time logit boosting, no fine-tuning required
67
+
68
+ The model should be loaded with `trust_remote_code=True`.
69
+
70
+ ## Files
71
+
72
+ - `config.json`, tokenizer files, processor files, and `model.safetensors`
73
+ - `configuration_arkasr.py`, `modeling_arkasr.py`, `processing_arkasr.py`
74
+ - `qwen3_asr_audio_config.py`, `qwen3_asr_audio_model.py`
75
+ - `hotword/`: backend-agnostic hotword trie
76
+ - `examples/`: Transformers inference examples
77
+
78
+ The root `config.json` is intentionally kept in this repository so Hugging Face
79
+ can recognize the model package and count downloads through normal model-file
80
+ queries.
81
+
82
+ ## Transformers Inference
83
+
84
+ ```python
85
+ import torch
86
+ from transformers import AutoModelForCausalLM, AutoProcessor
87
+
88
+
89
+ model_path = "AutoArk-AI/Audio8-ASR-0.1B"
90
+ audio_path = "path/to/audio.wav"
91
+
92
+ device = "cuda" if torch.cuda.is_available() else "cpu"
93
+ torch_dtype = torch.bfloat16 if device == "cuda" else torch.float32
94
+
95
+ processor = AutoProcessor.from_pretrained(model_path, trust_remote_code=True)
96
+ model = AutoModelForCausalLM.from_pretrained(
97
+ model_path,
98
+ trust_remote_code=True,
99
+ torch_dtype=torch_dtype,
100
+ attn_implementation="eager",
101
+ ).to(device)
102
+ model.eval()
103
+
104
+ conversation = [
105
+ {
106
+ "role": "user",
107
+ "content": [
108
+ {"type": "audio", "path": audio_path},
109
+ {"type": "text", "text": "Please transcribe this audio."},
110
+ ],
111
+ }
112
+ ]
113
+
114
+ batch = processor.apply_chat_template(
115
+ conversation,
116
+ return_tensors="pt",
117
+ sampling_rate=16000,
118
+ audio_padding="longest",
119
+ add_generation_prompt=True,
120
+ audio_max_length=30 * 16000,
121
+ text_kwargs={"padding": "longest", "truncation": True, "max_length": 1000},
122
+ )
123
+ batch = {key: value.to(device) if hasattr(value, "to") else value for key, value in dict(batch).items()}
124
+
125
+ with torch.inference_mode():
126
+ output_ids = model.generate(**batch, max_new_tokens=128, do_sample=False)
127
+
128
+ prompt_len = int(batch["input_ids"].shape[1])
129
+ text = processor.decode(output_ids[0, prompt_len:], skip_special_tokens=True).strip()
130
+ print(text)
131
+ ```
132
+
133
+ Equivalent script:
134
+
135
+ ```bash
136
+ python examples/transcribe.py path/to/audio.wav --model AutoArk-AI/Audio8-ASR-0.1B
137
+ ```
138
+
139
+ For local staging before upload:
140
+
141
+ ```bash
142
+ python examples/transcribe.py path/to/audio.wav --model .
143
+ ```
144
+
145
+ ## Hotword Boosting
146
+
147
+ Hotwords are applied at decode time by nudging logits for tokenizer paths that
148
+ match the requested words. This does not modify model weights and does not
149
+ inject the hotwords into the prompt.
150
+
151
+ ```bash
152
+ python examples/transcribe_hotword.py path/to/audio.wav \
153
+ --model AutoArk-AI/Audio8-ASR-0.1B \
154
+ --hotwords "Audio8,AutoArk"
155
+ ```
156
+
157
+ Main knobs:
158
+
159
+ - `--hotword_topk`: only boost tokens already inside the current top-k logits.
160
+ - `--hotword_start_boost`: boost for the first token of each hotword.
161
+ - `--hotword_continuation_boost`: boost for continuation tokens after a matched prefix.
162
+
163
+ ## Related Releases
164
+
165
+ - [ONNX Runtime package](https://huggingface.co/AutoArk-AI/Audio8-ASR-0.1B-onnx-runtime)
166
+ - [iOS ANE package](https://huggingface.co/AutoArk-AI/Audio8-ASR-0.1B-iOS-ANE)
167
+
168
+ ## Limitations
169
+
170
+ - The default examples target short-form ASR and truncate audio at 30 seconds.
171
+ - Hotword boosting can help with near-miss terms but can also over-bias decoding
172
+ when boost values are too high.
173
+ - Some Transformers/tokenizers versions emit a Qwen tokenizer regex warning. The
174
+ staged tokenizer config is kept in the loadable form used by this package; pass
175
+ explicit tokenizer regex flags only after testing your local Transformers version.
176
+
177
+ ## Acknowledgements
178
+
179
+ The audio encoder backbone is based on
180
+ [Qwen3-ASR-0.6B](https://huggingface.co/Qwen/Qwen3-ASR-0.6B), with the audio
181
+ adapter and projector trained as part of Audio8-ASR. The language-model backbone
182
+ is based on
183
+ [Ref-Pretrain-Qwen-104M](https://huggingface.co/MiniLLM/Ref-Pretrain-Qwen-104M).
added_tokens.json ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "<|assistant|>": 151650,
3
+ "<|audio|>": 151646,
4
+ "<|begin_of_audio|>": 151648,
5
+ "<|end_of_audio|>": 151649,
6
+ "<|endoftext|>": 151643,
7
+ "<|im_end|>": 151645,
8
+ "<|im_start|>": 151644,
9
+ "<|system|>": 151651,
10
+ "<|user|>": 151647
11
+ }
chat_template.jinja ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ {% for message in messages %}{% if loop.first and messages[0]['role'] != 'system' %}{{ '<|im_start|>system
2
+ You are a helpful assistant<|im_end|>
3
+ ' }}{% endif %}{{'<|im_start|>' + message['role'] + '
4
+ ' + message['content'] + '<|im_end|>' + '
5
+ '}}{% endfor %}{% if add_generation_prompt %}{{ '<|im_start|>assistant
6
+ ' }}{% endif %}
config.json ADDED
@@ -0,0 +1,286 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "adapter_type": "qwen3_asr_mlp_tower",
3
+ "architectures": [
4
+ "ArkasrForConditionalGeneration"
5
+ ],
6
+ "attention_dropout": 0.0,
7
+ "audio_token_id": 151646,
8
+ "auto_map": {
9
+ "AutoConfig": "configuration_arkasr.ArkasrConfig",
10
+ "AutoModelForCausalLM": "modeling_arkasr.ArkasrForConditionalGeneration"
11
+ },
12
+ "bos_token_id": 151643,
13
+ "dtype": "bfloat16",
14
+ "eos_token_id": 151645,
15
+ "hidden_act": "silu",
16
+ "hidden_size": 512,
17
+ "initializer_range": 0.02,
18
+ "intermediate_size": 1408,
19
+ "layer_types": [
20
+ "full_attention",
21
+ "full_attention",
22
+ "full_attention",
23
+ "full_attention",
24
+ "full_attention",
25
+ "full_attention",
26
+ "full_attention",
27
+ "full_attention"
28
+ ],
29
+ "max_position_embeddings": 32768,
30
+ "max_whisper_length": 1500,
31
+ "max_window_layers": 28,
32
+ "merge_factor": 4,
33
+ "mlp_adapter_act": "gelu",
34
+ "model_type": "arkasr",
35
+ "num_attention_heads": 8,
36
+ "num_hidden_layers": 8,
37
+ "num_key_value_heads": 8,
38
+ "pad_token_id": 151643,
39
+ "rms_norm_eps": 1e-06,
40
+ "rope_scaling": null,
41
+ "rope_theta": 1000000.0,
42
+ "sliding_window": null,
43
+ "spec_aug": false,
44
+ "tie_word_embeddings": true,
45
+ "transformers_version": "4.57.3",
46
+ "use_cache": true,
47
+ "use_rope": false,
48
+ "use_sliding_window": false,
49
+ "vocab_size": 151936,
50
+ "whisper_config": {
51
+ "_name_or_path": "openai/whisper-small",
52
+ "activation_dropout": 0.0,
53
+ "activation_function": "gelu",
54
+ "apply_spec_augment": false,
55
+ "architectures": [
56
+ "WhisperForConditionalGeneration"
57
+ ],
58
+ "attention_dropout": 0.0,
59
+ "begin_suppress_tokens": [
60
+ 220,
61
+ 50257
62
+ ],
63
+ "bos_token_id": 50257,
64
+ "classifier_proj_size": 256,
65
+ "d_model": 768,
66
+ "decoder_attention_heads": 12,
67
+ "decoder_ffn_dim": 3072,
68
+ "decoder_layerdrop": 0.0,
69
+ "decoder_layers": 12,
70
+ "decoder_start_token_id": 50258,
71
+ "dropout": 0.0,
72
+ "dtype": "float32",
73
+ "encoder_attention_heads": 12,
74
+ "encoder_ffn_dim": 3072,
75
+ "encoder_layerdrop": 0.0,
76
+ "encoder_layers": 12,
77
+ "eos_token_id": 50257,
78
+ "forced_decoder_ids": [
79
+ [
80
+ 1,
81
+ 50259
82
+ ],
83
+ [
84
+ 2,
85
+ 50359
86
+ ],
87
+ [
88
+ 3,
89
+ 50363
90
+ ]
91
+ ],
92
+ "init_std": 0.02,
93
+ "mask_feature_length": 10,
94
+ "mask_feature_min_masks": 0,
95
+ "mask_feature_prob": 0.0,
96
+ "mask_time_length": 10,
97
+ "mask_time_min_masks": 2,
98
+ "mask_time_prob": 0.05,
99
+ "max_length": 448,
100
+ "max_source_positions": 1500,
101
+ "max_target_positions": 448,
102
+ "median_filter_width": 7,
103
+ "model_type": "whisper",
104
+ "num_hidden_layers": 12,
105
+ "num_mel_bins": 80,
106
+ "pad_token_id": 50257,
107
+ "scale_embedding": false,
108
+ "suppress_tokens": [
109
+ 1,
110
+ 2,
111
+ 7,
112
+ 8,
113
+ 9,
114
+ 10,
115
+ 14,
116
+ 25,
117
+ 26,
118
+ 27,
119
+ 28,
120
+ 29,
121
+ 31,
122
+ 58,
123
+ 59,
124
+ 60,
125
+ 61,
126
+ 62,
127
+ 63,
128
+ 90,
129
+ 91,
130
+ 92,
131
+ 93,
132
+ 359,
133
+ 503,
134
+ 522,
135
+ 542,
136
+ 873,
137
+ 893,
138
+ 902,
139
+ 918,
140
+ 922,
141
+ 931,
142
+ 1350,
143
+ 1853,
144
+ 1982,
145
+ 2460,
146
+ 2627,
147
+ 3246,
148
+ 3253,
149
+ 3268,
150
+ 3536,
151
+ 3846,
152
+ 3961,
153
+ 4183,
154
+ 4667,
155
+ 6585,
156
+ 6647,
157
+ 7273,
158
+ 9061,
159
+ 9383,
160
+ 10428,
161
+ 10929,
162
+ 11938,
163
+ 12033,
164
+ 12331,
165
+ 12562,
166
+ 13793,
167
+ 14157,
168
+ 14635,
169
+ 15265,
170
+ 15618,
171
+ 16553,
172
+ 16604,
173
+ 18362,
174
+ 18956,
175
+ 20075,
176
+ 21675,
177
+ 22520,
178
+ 26130,
179
+ 26161,
180
+ 26435,
181
+ 28279,
182
+ 29464,
183
+ 31650,
184
+ 32302,
185
+ 32470,
186
+ 36865,
187
+ 42863,
188
+ 47425,
189
+ 49870,
190
+ 50254,
191
+ 50258,
192
+ 50360,
193
+ 50361,
194
+ 50362
195
+ ],
196
+ "use_cache": true,
197
+ "use_weighted_layer_sum": false,
198
+ "vocab_size": 51865
199
+ },
200
+ "qwen3_asr_audio_config": {
201
+ "_name_or_path": "",
202
+ "activation_dropout": 0,
203
+ "activation_function": "gelu",
204
+ "add_cross_attention": false,
205
+ "architectures": null,
206
+ "attention_dropout": 0,
207
+ "bad_words_ids": null,
208
+ "begin_suppress_tokens": null,
209
+ "bos_token_id": null,
210
+ "chunk_size_feed_forward": 0,
211
+ "conv_chunksize": 500,
212
+ "cross_attention_hidden_size": null,
213
+ "d_model": 896,
214
+ "decoder_start_token_id": null,
215
+ "diversity_penalty": 0.0,
216
+ "do_sample": false,
217
+ "downsample_hidden_size": 480,
218
+ "dropout": 0,
219
+ "dtype": null,
220
+ "early_stopping": false,
221
+ "encoder_attention_heads": 14,
222
+ "encoder_ffn_dim": 3584,
223
+ "encoder_layers": 18,
224
+ "encoder_no_repeat_ngram_size": 0,
225
+ "eos_token_id": null,
226
+ "exponential_decay_length_penalty": null,
227
+ "finetuning_task": null,
228
+ "forced_bos_token_id": null,
229
+ "forced_eos_token_id": null,
230
+ "id2label": {
231
+ "0": "LABEL_0",
232
+ "1": "LABEL_1"
233
+ },
234
+ "initializer_range": 0.02,
235
+ "is_decoder": false,
236
+ "is_encoder_decoder": false,
237
+ "label2id": {
238
+ "LABEL_0": 0,
239
+ "LABEL_1": 1
240
+ },
241
+ "length_penalty": 1.0,
242
+ "max_length": 20,
243
+ "max_source_positions": 1500,
244
+ "min_length": 0,
245
+ "model_type": "qwen3_asr_audio_encoder",
246
+ "n_window": 50,
247
+ "n_window_infer": 800,
248
+ "no_repeat_ngram_size": 0,
249
+ "num_beam_groups": 1,
250
+ "num_beams": 1,
251
+ "num_hidden_layers": 18,
252
+ "num_mel_bins": 128,
253
+ "num_return_sequences": 1,
254
+ "output_attentions": false,
255
+ "output_dim": 1024,
256
+ "output_hidden_states": false,
257
+ "output_scores": false,
258
+ "pad_token_id": null,
259
+ "prefix": null,
260
+ "problem_type": null,
261
+ "pruned_heads": {},
262
+ "remove_invalid_values": false,
263
+ "repetition_penalty": 1.0,
264
+ "return_dict": true,
265
+ "return_dict_in_generate": false,
266
+ "scale_embedding": false,
267
+ "sep_token_id": null,
268
+ "suppress_tokens": null,
269
+ "task_specific_params": null,
270
+ "temperature": 1.0,
271
+ "tf_legacy_loss": false,
272
+ "tie_encoder_decoder": false,
273
+ "tie_word_embeddings": true,
274
+ "tokenizer_class": null,
275
+ "top_k": 50,
276
+ "top_p": 1.0,
277
+ "torchscript": false,
278
+ "typical_p": 1.0,
279
+ "use_bfloat16": false
280
+ },
281
+ "qwen3_asr_mlp_tower_layers": 4,
282
+ "qwen3_asr_mlp_tower_hidden_size": 0,
283
+ "qwen3_asr_mlp_tower_dropout": 0.0,
284
+ "audio_backend": "qwen3_asr_mlp_tower",
285
+ "name_or_path": "audio8-asr-0.1B"
286
+ }
configuration_arkasr.py ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, Optional, Union
4
+
5
+ from transformers import Qwen2Config, WhisperConfig
6
+
7
+ from .qwen3_asr_audio_config import Qwen3ASRAudioEncoderConfig
8
+
9
+
10
+ class ArkasrConfig(Qwen2Config):
11
+ model_type = "arkasr"
12
+ is_composition = True
13
+
14
+ def __init__(
15
+ self,
16
+ whisper_config: Optional[Union[Dict[str, Any], WhisperConfig]] = None,
17
+ qwen3_asr_audio_config: Optional[Union[Dict[str, Any], Qwen3ASRAudioEncoderConfig]] = None,
18
+ adapter_type: str = "qwen3_asr_mlp_tower",
19
+ merge_factor: int = 4,
20
+ spec_aug: bool = False,
21
+ use_rope: bool = False,
22
+ max_whisper_length: int = 1500,
23
+ mlp_adapter_act: str = "gelu",
24
+ qwen3_asr_mlp_tower_layers: int = 4,
25
+ qwen3_asr_mlp_tower_hidden_size: int = 0,
26
+ qwen3_asr_mlp_tower_dropout: float = 0.0,
27
+ **kwargs,
28
+ ):
29
+ super().__init__(**kwargs)
30
+
31
+ if isinstance(whisper_config, dict):
32
+ self.whisper_config = WhisperConfig(**whisper_config)
33
+ elif isinstance(whisper_config, WhisperConfig):
34
+ self.whisper_config = whisper_config
35
+ else:
36
+ self.whisper_config = WhisperConfig()
37
+
38
+ if isinstance(qwen3_asr_audio_config, dict):
39
+ self.qwen3_asr_audio_config = Qwen3ASRAudioEncoderConfig(**qwen3_asr_audio_config)
40
+ elif isinstance(qwen3_asr_audio_config, Qwen3ASRAudioEncoderConfig):
41
+ self.qwen3_asr_audio_config = qwen3_asr_audio_config
42
+ else:
43
+ self.qwen3_asr_audio_config = Qwen3ASRAudioEncoderConfig(
44
+ num_mel_bins=128,
45
+ encoder_layers=18,
46
+ encoder_attention_heads=14,
47
+ encoder_ffn_dim=3584,
48
+ d_model=896,
49
+ max_source_positions=1500,
50
+ n_window=50,
51
+ output_dim=1024,
52
+ n_window_infer=800,
53
+ conv_chunksize=500,
54
+ downsample_hidden_size=480,
55
+ )
56
+
57
+ self.adapter_type = adapter_type
58
+ self.merge_factor = int(merge_factor)
59
+ self.spec_aug = bool(spec_aug)
60
+ self.use_rope = bool(use_rope)
61
+ self.max_whisper_length = int(max_whisper_length)
62
+ self.mlp_adapter_act = mlp_adapter_act
63
+ self.qwen3_asr_mlp_tower_layers = int(qwen3_asr_mlp_tower_layers)
64
+ self.qwen3_asr_mlp_tower_hidden_size = int(qwen3_asr_mlp_tower_hidden_size)
65
+ self.qwen3_asr_mlp_tower_dropout = float(qwen3_asr_mlp_tower_dropout)
66
+
67
+ def to_dict(self):
68
+ output = super().to_dict()
69
+ output["whisper_config"] = self.whisper_config.to_dict()
70
+ output["qwen3_asr_audio_config"] = self.qwen3_asr_audio_config.to_dict()
71
+ output["qwen3_asr_mlp_tower_layers"] = self.qwen3_asr_mlp_tower_layers
72
+ output["qwen3_asr_mlp_tower_hidden_size"] = self.qwen3_asr_mlp_tower_hidden_size
73
+ output["qwen3_asr_mlp_tower_dropout"] = self.qwen3_asr_mlp_tower_dropout
74
+ return output
75
+
76
+
77
+ __all__ = ["ArkasrConfig"]
examples/requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ torch
2
+ transformers>=4.57.0
3
+ safetensors
4
+ librosa
5
+ soundfile
6
+ numpy
examples/transcribe.py ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ from pathlib import Path
6
+
7
+ import torch
8
+ from transformers import AutoModelForCausalLM, AutoProcessor
9
+
10
+
11
+ PROMPT = "Please transcribe this audio."
12
+
13
+
14
+ def build_conversation(audio_path: Path) -> list[dict]:
15
+ return [
16
+ {
17
+ "role": "user",
18
+ "content": [
19
+ {"type": "audio", "path": str(audio_path)},
20
+ {"type": "text", "text": PROMPT},
21
+ ],
22
+ }
23
+ ]
24
+
25
+
26
+ def main() -> None:
27
+ parser = argparse.ArgumentParser(description="Transcribe one audio file with audio8-asr-0.1B.")
28
+ parser.add_argument("audio", type=Path)
29
+ parser.add_argument("--model", default=".")
30
+ parser.add_argument("--device", default="cuda" if torch.cuda.is_available() else "cpu")
31
+ parser.add_argument("--max_new_tokens", type=int, default=128)
32
+ parser.add_argument("--max_audio_seconds", type=int, default=30)
33
+ args = parser.parse_args()
34
+
35
+ device = torch.device(args.device)
36
+ dtype = torch.bfloat16 if device.type == "cuda" else torch.float32
37
+ processor = AutoProcessor.from_pretrained(args.model, trust_remote_code=True)
38
+ model = AutoModelForCausalLM.from_pretrained(
39
+ args.model,
40
+ trust_remote_code=True,
41
+ torch_dtype=dtype,
42
+ attn_implementation="eager",
43
+ ).to(device)
44
+ model.eval()
45
+
46
+ batch = processor.apply_chat_template(
47
+ build_conversation(args.audio),
48
+ return_tensors="pt",
49
+ sampling_rate=16000,
50
+ audio_padding="longest",
51
+ add_generation_prompt=True,
52
+ audio_max_length=int(args.max_audio_seconds) * 16000,
53
+ text_kwargs={"padding": "longest", "truncation": True, "max_length": 1000},
54
+ )
55
+ batch = {key: value.to(device) if hasattr(value, "to") else value for key, value in dict(batch).items()}
56
+ with torch.inference_mode():
57
+ output_ids = model.generate(**batch, max_new_tokens=args.max_new_tokens, do_sample=False)
58
+ prompt_len = int(batch["input_ids"].shape[1])
59
+ text = processor.decode(output_ids[0, prompt_len:], skip_special_tokens=True).strip()
60
+ print(text)
61
+
62
+
63
+ if __name__ == "__main__":
64
+ main()
examples/transcribe_hotword.py ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ from pathlib import Path
6
+
7
+ import torch
8
+ from transformers import LogitsProcessorList
9
+ from transformers import AutoModelForCausalLM, AutoProcessor
10
+
11
+ from hotword.hotword_trie import build_trie_from_hotwords, parse_hotwords
12
+
13
+
14
+ PROMPT = "Please transcribe this audio."
15
+
16
+
17
+ def build_conversation(audio_path: Path) -> list[dict]:
18
+ return [
19
+ {
20
+ "role": "user",
21
+ "content": [
22
+ {"type": "audio", "path": str(audio_path)},
23
+ {"type": "text", "text": PROMPT},
24
+ ],
25
+ }
26
+ ]
27
+
28
+
29
+ def make_hotword_processor(tokenizer, hotwords: str, *, topk: int, start_boost: float, continuation_boost: float):
30
+ words = parse_hotwords(hotwords)
31
+ if not words:
32
+ return None
33
+ special_ids = set(int(value) for value in tokenizer.all_special_ids if value is not None)
34
+
35
+ def encode(text: str) -> list[int]:
36
+ return tokenizer.encode(text, add_special_tokens=False)
37
+
38
+ def id_to_token(token_id: int) -> str:
39
+ return str(tokenizer.convert_ids_to_tokens(int(token_id)))
40
+
41
+ trie, _ = build_trie_from_hotwords(
42
+ words,
43
+ encode=encode,
44
+ id_to_token=id_to_token,
45
+ special_ids=special_ids,
46
+ start_boost=start_boost,
47
+ continuation_boost=continuation_boost,
48
+ )
49
+ if not trie:
50
+ return None
51
+
52
+ def processor(input_ids: torch.LongTensor, scores: torch.FloatTensor) -> torch.FloatTensor:
53
+ vocab_size = int(scores.shape[-1])
54
+ allowed_rows = None
55
+ if int(topk) > 0:
56
+ top_idx = torch.topk(scores.float(), k=min(int(topk), vocab_size), dim=-1).indices
57
+ allowed_rows = [set(int(x) for x in row.tolist()) for row in top_idx]
58
+ for batch_i in range(int(scores.shape[0])):
59
+ boosts = trie.boosts_for_generated(input_ids[batch_i].tolist())
60
+ allowed = allowed_rows[batch_i] if allowed_rows is not None else None
61
+ for token_id, boost in boosts.items():
62
+ if 0 <= int(token_id) < vocab_size and (allowed is None or int(token_id) in allowed):
63
+ scores[batch_i, int(token_id)] += float(boost)
64
+ return scores
65
+
66
+ return processor
67
+
68
+
69
+ def main() -> None:
70
+ parser = argparse.ArgumentParser(description="Transcribe one audio file with optional hotword boosting.")
71
+ parser.add_argument("audio", type=Path)
72
+ parser.add_argument("--model", default=".")
73
+ parser.add_argument("--hotwords", required=True, help="Comma-separated hotwords.")
74
+ parser.add_argument("--hotword_topk", type=int, default=50)
75
+ parser.add_argument("--hotword_start_boost", type=float, default=6.0)
76
+ parser.add_argument("--hotword_continuation_boost", type=float, default=8.0)
77
+ parser.add_argument("--device", default="cuda" if torch.cuda.is_available() else "cpu")
78
+ parser.add_argument("--max_new_tokens", type=int, default=128)
79
+ args = parser.parse_args()
80
+
81
+ device = torch.device(args.device)
82
+ dtype = torch.bfloat16 if device.type == "cuda" else torch.float32
83
+ processor = AutoProcessor.from_pretrained(args.model, trust_remote_code=True)
84
+ model = AutoModelForCausalLM.from_pretrained(
85
+ args.model,
86
+ trust_remote_code=True,
87
+ torch_dtype=dtype,
88
+ attn_implementation="eager",
89
+ ).to(device)
90
+ model.eval()
91
+
92
+ batch = processor.apply_chat_template(
93
+ build_conversation(args.audio),
94
+ return_tensors="pt",
95
+ sampling_rate=16000,
96
+ audio_padding="longest",
97
+ add_generation_prompt=True,
98
+ audio_max_length=30 * 16000,
99
+ text_kwargs={"padding": "longest", "truncation": True, "max_length": 1000},
100
+ )
101
+ batch = {key: value.to(device) if hasattr(value, "to") else value for key, value in dict(batch).items()}
102
+ logits_processor = make_hotword_processor(
103
+ processor.tokenizer,
104
+ args.hotwords,
105
+ topk=args.hotword_topk,
106
+ start_boost=args.hotword_start_boost,
107
+ continuation_boost=args.hotword_continuation_boost,
108
+ )
109
+ with torch.inference_mode():
110
+ output_ids = model.generate(
111
+ **batch,
112
+ max_new_tokens=args.max_new_tokens,
113
+ do_sample=False,
114
+ logits_processor=LogitsProcessorList([logits_processor]) if logits_processor is not None else None,
115
+ )
116
+ prompt_len = int(batch["input_ids"].shape[1])
117
+ text = processor.decode(output_ids[0, prompt_len:], skip_special_tokens=True).strip()
118
+ print(text)
119
+
120
+
121
+ if __name__ == "__main__":
122
+ main()
generation_config.json ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {
2
+ "_from_model_config": true,
3
+ "bos_token_id": 151643,
4
+ "eos_token_id": 151645,
5
+ "pad_token_id": 151643,
6
+ "transformers_version": "4.57.3"
7
+ }
hotword/__init__.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .hotword_trie import (
2
+ HotwordTrie,
3
+ build_hotword_sequences,
4
+ build_trie_from_hotwords,
5
+ flatten_sequences,
6
+ parse_hotwords,
7
+ token_is_control_or_special,
8
+ )
9
+
10
+ __all__ = [
11
+ "HotwordTrie",
12
+ "build_hotword_sequences",
13
+ "build_trie_from_hotwords",
14
+ "flatten_sequences",
15
+ "parse_hotwords",
16
+ "token_is_control_or_special",
17
+ ]
hotword/hotword_trie.py ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from typing import Any, Callable, Dict, List, Sequence, Set
5
+
6
+
7
+ _CONTROL_TOKEN_RE = re.compile(r"<\|[^>]+?\|>")
8
+ _BARE_TAG_RE = re.compile(r"</?[^>\s]+>")
9
+ _CJK_KANA_HANGUL_RE = re.compile("[\u4e00-\u9fff\u3040-\u30ff\uac00-\ud7af]")
10
+
11
+
12
+ class HotwordTrie:
13
+ """Prefix trie over hotword token-id sequences."""
14
+
15
+ def __init__(
16
+ self,
17
+ token_sequences: Sequence[Sequence[int]],
18
+ *,
19
+ start_boost: float,
20
+ continuation_boost: float,
21
+ ) -> None:
22
+ self.start_boost = float(start_boost)
23
+ self.continuation_boost = float(continuation_boost)
24
+ self.trie: Dict[int, Dict[int, Any]] = {}
25
+ self.max_sequence_len = 0
26
+ for seq in token_sequences:
27
+ ids = [int(token_id) for token_id in seq]
28
+ if not ids:
29
+ continue
30
+ node = self.trie
31
+ for token_id in ids:
32
+ node = node.setdefault(token_id, {})
33
+ self.max_sequence_len = max(self.max_sequence_len, len(ids))
34
+ self.start_token_ids = sorted(self.trie.keys())
35
+
36
+ def __bool__(self) -> bool:
37
+ return bool(self.trie)
38
+
39
+ def boosts_for_generated(self, generated_ids: Sequence[int]) -> Dict[int, float]:
40
+ boosts: Dict[int, float] = {}
41
+ if self.start_boost:
42
+ for token_id in self.start_token_ids:
43
+ boosts[token_id] = max(boosts.get(token_id, 0.0), self.start_boost)
44
+
45
+ if not generated_ids or not self.continuation_boost or self.max_sequence_len <= 1:
46
+ return boosts
47
+
48
+ max_prefix_len = min(len(generated_ids), self.max_sequence_len - 1)
49
+ for prefix_len in range(1, max_prefix_len + 1):
50
+ node: Dict[int, Any] = self.trie
51
+ matched = True
52
+ for token_id in generated_ids[-prefix_len:]:
53
+ next_node = node.get(int(token_id))
54
+ if next_node is None:
55
+ matched = False
56
+ break
57
+ node = next_node
58
+ if not matched:
59
+ continue
60
+ for next_token_id in node.keys():
61
+ boosts[int(next_token_id)] = max(
62
+ boosts.get(int(next_token_id), 0.0),
63
+ self.continuation_boost,
64
+ )
65
+ return boosts
66
+
67
+
68
+ def _has_cjk_or_kana_or_hangul(text: str) -> bool:
69
+ return bool(_CJK_KANA_HANGUL_RE.search(str(text or "")))
70
+
71
+
72
+ def _hotword_text_variants(word: str) -> List[str]:
73
+ word = str(word or "").strip()
74
+ if not word:
75
+ return []
76
+ variants = [word]
77
+ if not _has_cjk_or_kana_or_hangul(word) and re.search(r"[A-Za-z0-9_]", word):
78
+ variants.append(" " + word)
79
+ out: List[str] = []
80
+ seen: Set[str] = set()
81
+ for value in variants:
82
+ if value not in seen:
83
+ seen.add(value)
84
+ out.append(value)
85
+ return out
86
+
87
+
88
+ def token_is_control_or_special(token: str, token_id: int, special_ids: Set[int]) -> bool:
89
+ if int(token_id) in special_ids:
90
+ return True
91
+ token = str(token)
92
+ return bool(_CONTROL_TOKEN_RE.fullmatch(token) or _BARE_TAG_RE.fullmatch(token))
93
+
94
+
95
+ def build_hotword_sequences(
96
+ hotwords: Sequence[str],
97
+ *,
98
+ encode: Callable[[str], List[int]],
99
+ id_to_token: Callable[[int], str],
100
+ special_ids: Set[int],
101
+ ) -> Dict[str, List[List[int]]]:
102
+ special_ids = set(int(value) for value in special_ids if value is not None)
103
+ sequences: Dict[str, List[List[int]]] = {}
104
+ seen_global: Set[tuple[int, ...]] = set()
105
+ for word in hotwords:
106
+ word = str(word or "").strip()
107
+ if not word:
108
+ continue
109
+ variants: List[List[int]] = []
110
+ for text in _hotword_text_variants(word):
111
+ ids = [
112
+ int(token_id)
113
+ for token_id in encode(text)
114
+ if not token_is_control_or_special(id_to_token(int(token_id)), int(token_id), special_ids)
115
+ ]
116
+ key = tuple(ids)
117
+ if not key or key in seen_global:
118
+ continue
119
+ seen_global.add(key)
120
+ variants.append(ids)
121
+ if variants:
122
+ sequences[word] = variants
123
+ return sequences
124
+
125
+
126
+ def flatten_sequences(sequences_by_word: Dict[str, List[List[int]]]) -> List[List[int]]:
127
+ return [ids for variants in sequences_by_word.values() for ids in variants]
128
+
129
+
130
+ def parse_hotwords(raw: Any) -> List[str]:
131
+ values: List[str] = []
132
+ if isinstance(raw, (list, tuple)):
133
+ values = [str(value).strip() for value in raw]
134
+ elif raw:
135
+ values = [value.strip() for value in re.split(r"[,,]", str(raw))]
136
+ out: List[str] = []
137
+ seen: Set[str] = set()
138
+ for value in values:
139
+ if value and value not in seen:
140
+ seen.add(value)
141
+ out.append(value)
142
+ return out
143
+
144
+
145
+ def build_trie_from_hotwords(
146
+ hotwords: Sequence[str],
147
+ *,
148
+ encode: Callable[[str], List[int]],
149
+ id_to_token: Callable[[int], str],
150
+ special_ids: Set[int],
151
+ start_boost: float,
152
+ continuation_boost: float,
153
+ ) -> tuple[HotwordTrie, Dict[str, List[List[int]]]]:
154
+ sequences_by_word = build_hotword_sequences(
155
+ hotwords,
156
+ encode=encode,
157
+ id_to_token=id_to_token,
158
+ special_ids=special_ids,
159
+ )
160
+ trie = HotwordTrie(
161
+ flatten_sequences(sequences_by_word),
162
+ start_boost=start_boost,
163
+ continuation_boost=continuation_boost,
164
+ )
165
+ return trie, sequences_by_word
merges.txt ADDED
The diff for this file is too large to render. See raw diff
 
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:971d17f64ef5f193fca567fa3e9dc063c4eede97faabb11c0c6abf0b92b23ca4
3
+ size 648031000
modeling_arkasr.py ADDED
@@ -0,0 +1,332 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Optional
4
+
5
+ import torch
6
+ import torch.nn.functional as F
7
+ from torch import nn
8
+ from transformers import Qwen2ForCausalLM
9
+ from transformers.generation import GenerationMixin
10
+ from transformers.modeling_outputs import CausalLMOutputWithPast
11
+ from transformers.modeling_utils import PreTrainedModel
12
+
13
+ from .configuration_arkasr import ArkasrConfig
14
+ from .qwen3_asr_audio_config import Qwen3ASRAudioEncoderConfig
15
+ from .qwen3_asr_audio_model import Qwen3ASRAudioEncoder
16
+
17
+
18
+ class Qwen3AsrMlpTowerBlock(nn.Module):
19
+ def __init__(self, hidden_size: int, intermediate_size: Optional[int] = None, dropout: float = 0.0):
20
+ super().__init__()
21
+ hidden_size = int(hidden_size)
22
+ intermediate_size = int(intermediate_size or hidden_size * 4)
23
+ self.norm = nn.LayerNorm(hidden_size)
24
+ self.fc1 = nn.Linear(hidden_size, intermediate_size)
25
+ self.act = nn.GELU()
26
+ self.dropout = nn.Dropout(float(dropout))
27
+ self.fc2 = nn.Linear(intermediate_size, hidden_size)
28
+
29
+ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
30
+ residual = hidden_states
31
+ hidden_states = self.norm(hidden_states)
32
+ hidden_states = self.fc1(hidden_states)
33
+ hidden_states = self.act(hidden_states)
34
+ hidden_states = self.dropout(hidden_states)
35
+ hidden_states = self.fc2(hidden_states)
36
+ hidden_states = self.dropout(hidden_states)
37
+ return residual + hidden_states
38
+
39
+
40
+ class Qwen3AsrMlpTower(nn.Module):
41
+ def __init__(
42
+ self,
43
+ hidden_size: int,
44
+ num_layers: int = 4,
45
+ intermediate_size: Optional[int] = None,
46
+ dropout: float = 0.0,
47
+ ):
48
+ super().__init__()
49
+ hidden_size = int(hidden_size)
50
+ num_layers = int(num_layers)
51
+ intermediate_size = int(intermediate_size or hidden_size * 4)
52
+ self.hidden_size = hidden_size
53
+ self.intermediate_size = intermediate_size
54
+ self.num_layers = num_layers
55
+ self.dropout = float(dropout)
56
+ self.layers = nn.ModuleList(
57
+ [
58
+ Qwen3AsrMlpTowerBlock(
59
+ hidden_size=hidden_size,
60
+ intermediate_size=intermediate_size,
61
+ dropout=dropout,
62
+ )
63
+ for _ in range(num_layers)
64
+ ]
65
+ )
66
+ self.final_norm = nn.LayerNorm(hidden_size)
67
+
68
+ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
69
+ for layer in self.layers:
70
+ hidden_states = layer(hidden_states)
71
+ return self.final_norm(hidden_states)
72
+
73
+
74
+ class ArkasrForConditionalGeneration(PreTrainedModel, GenerationMixin):
75
+ config_class = ArkasrConfig
76
+ base_model_prefix = "language_model"
77
+ _no_split_modules = ["Qwen3ASRAudioEncoder"]
78
+ _tied_weights_keys = ["language_model.lm_head.weight", "language_model.model.embed_tokens.weight"]
79
+
80
+ def __init__(self, config: ArkasrConfig):
81
+ super().__init__(config)
82
+ self.audio_token_id = getattr(config, "audio_token_id", None)
83
+ if self.audio_token_id is None:
84
+ raise ValueError("`audio_token_id` must be defined in config.")
85
+
86
+ audio_config = getattr(config, "qwen3_asr_audio_config", None)
87
+ if isinstance(audio_config, dict):
88
+ audio_config = Qwen3ASRAudioEncoderConfig(**audio_config)
89
+ if audio_config is None:
90
+ raise ValueError("`qwen3_asr_audio_config` must be defined in config.")
91
+
92
+ self.language_model = Qwen2ForCausalLM(config)
93
+ self.audio_encoder = Qwen3ASRAudioEncoder(audio_config)
94
+ audio_dim = int(getattr(audio_config, "output_dim", 0) or 0)
95
+ if audio_dim <= 0:
96
+ raise ValueError("qwen3_asr_audio_config.output_dim must be positive.")
97
+
98
+ layers = int(getattr(config, "qwen3_asr_mlp_tower_layers", 4) or 4)
99
+ intermediate_size = int(getattr(config, "qwen3_asr_mlp_tower_hidden_size", 0) or audio_dim * 4)
100
+ dropout = float(getattr(config, "qwen3_asr_mlp_tower_dropout", 0.0) or 0.0)
101
+ self.audio_mlp_tower = Qwen3AsrMlpTower(
102
+ hidden_size=audio_dim,
103
+ num_layers=layers,
104
+ intermediate_size=intermediate_size,
105
+ dropout=dropout,
106
+ )
107
+ self.audio_projector = nn.Sequential(
108
+ nn.LayerNorm(audio_dim),
109
+ nn.Linear(audio_dim, int(config.hidden_size)),
110
+ )
111
+ self.all_tied_weights_keys = {}
112
+
113
+ def get_input_embeddings(self):
114
+ return self.language_model.get_input_embeddings()
115
+
116
+ def set_input_embeddings(self, value):
117
+ return self.language_model.set_input_embeddings(value)
118
+
119
+ def get_output_embeddings(self):
120
+ return self.language_model.get_output_embeddings()
121
+
122
+ def set_output_embeddings(self, new_embeddings):
123
+ return self.language_model.set_output_embeddings(new_embeddings)
124
+
125
+ def resize_token_embeddings(self, *args, **kwargs):
126
+ return self.language_model.resize_token_embeddings(*args, **kwargs)
127
+
128
+ @staticmethod
129
+ def _cache_seq_len(past_key_values) -> int:
130
+ if past_key_values is None:
131
+ return 0
132
+ if hasattr(past_key_values, "get_seq_length"):
133
+ try:
134
+ return int(past_key_values.get_seq_length())
135
+ except Exception:
136
+ return 0
137
+ try:
138
+ return int(past_key_values[0][0].shape[-2])
139
+ except Exception:
140
+ return 0
141
+
142
+ def _project_audio_row(
143
+ self,
144
+ input_features: torch.Tensor,
145
+ token_count: int,
146
+ dtype: torch.dtype,
147
+ feature_length: Optional[int] = None,
148
+ ) -> torch.Tensor:
149
+ token_count = int(token_count)
150
+ if token_count <= 0:
151
+ return input_features.new_zeros((0, self.get_input_embeddings().embedding_dim), dtype=dtype)
152
+ if input_features.dim() == 3 and input_features.size(0) == 1:
153
+ input_features = input_features.squeeze(0)
154
+ if input_features.dim() != 2:
155
+ raise ValueError(f"Expected audio features with shape [mel, frames], got {tuple(input_features.shape)}")
156
+
157
+ expected_mels = int(getattr(self.audio_encoder.config, "num_mel_bins", input_features.size(0)))
158
+ if input_features.size(0) != expected_mels and input_features.size(1) == expected_mels:
159
+ input_features = input_features.transpose(0, 1)
160
+ if input_features.size(0) != expected_mels:
161
+ raise ValueError(f"Audio feature bins mismatch: expected {expected_mels}, got {input_features.size(0)}")
162
+
163
+ feature_length = int(feature_length) if feature_length is not None else int(input_features.size(1))
164
+ feature_length = max(1, min(feature_length, int(input_features.size(1))))
165
+ input_features = input_features[:, :feature_length]
166
+
167
+ encoder_param = next(self.audio_encoder.parameters())
168
+ encoded = self.audio_encoder(
169
+ input_features.to(device=encoder_param.device, dtype=encoder_param.dtype),
170
+ feature_lens=torch.tensor([feature_length], dtype=torch.long, device=encoder_param.device),
171
+ )
172
+ hidden = getattr(encoded, "last_hidden_state", encoded)
173
+ if isinstance(hidden, (tuple, list)):
174
+ hidden = hidden[0]
175
+ if hidden.dim() == 3 and hidden.size(0) == 1:
176
+ hidden = hidden.squeeze(0)
177
+ if hidden.dim() != 2:
178
+ raise ValueError(f"Expected audio encoder output [time, dim], got {tuple(hidden.shape)}")
179
+
180
+ tower_param = next(self.audio_mlp_tower.parameters())
181
+ hidden = self.audio_mlp_tower(hidden.to(device=tower_param.device, dtype=tower_param.dtype))
182
+ projector_param = next(self.audio_projector.parameters())
183
+ hidden = hidden.to(device=projector_param.device, dtype=projector_param.dtype)
184
+ if int(hidden.size(0)) != token_count:
185
+ hidden = F.adaptive_avg_pool1d(
186
+ hidden.transpose(0, 1).float().unsqueeze(0),
187
+ output_size=token_count,
188
+ ).squeeze(0).transpose(0, 1).to(dtype=projector_param.dtype)
189
+ return self.audio_projector(hidden).to(dtype=dtype)
190
+
191
+ def _inject_audio_embeddings(
192
+ self,
193
+ input_ids: torch.Tensor,
194
+ input_features: Optional[torch.Tensor] = None,
195
+ feature_lens: Optional[torch.Tensor] = None,
196
+ audios: Optional[torch.Tensor] = None,
197
+ audio_feature_lengths: Optional[torch.Tensor] = None,
198
+ ) -> torch.Tensor:
199
+ input_embeddings = self.get_input_embeddings()(input_ids).clone()
200
+ if input_features is None:
201
+ input_features = audios
202
+ if feature_lens is None:
203
+ feature_lens = audio_feature_lengths
204
+ if input_features is None:
205
+ return input_embeddings
206
+
207
+ input_features = input_features.to(device=input_embeddings.device)
208
+ if input_features.dim() == 4 and input_features.size(1) == 1:
209
+ input_features = input_features.squeeze(1)
210
+ if feature_lens is not None and not torch.is_tensor(feature_lens):
211
+ feature_lens = torch.tensor(feature_lens, dtype=torch.long, device=input_embeddings.device)
212
+ if torch.is_tensor(feature_lens):
213
+ feature_lens = feature_lens.to(device=input_embeddings.device)
214
+
215
+ audio_mask = input_ids.eq(self.audio_token_id)
216
+ for batch_i in range(int(input_ids.size(0))):
217
+ positions = torch.nonzero(audio_mask[batch_i], as_tuple=False).flatten()
218
+ if positions.numel() == 0:
219
+ continue
220
+ feature_length = None
221
+ if torch.is_tensor(feature_lens) and batch_i < int(feature_lens.numel()):
222
+ feature_length = int(feature_lens[batch_i].item())
223
+ projected = self._project_audio_row(
224
+ input_features[batch_i],
225
+ int(positions.numel()),
226
+ input_embeddings.dtype,
227
+ feature_length=feature_length,
228
+ )
229
+ input_embeddings[batch_i, positions, :] = projected.to(device=input_embeddings.device)
230
+ return input_embeddings
231
+
232
+ def forward(
233
+ self,
234
+ input_ids: Optional[torch.LongTensor] = None,
235
+ input_features: Optional[torch.Tensor] = None,
236
+ feature_lens: Optional[torch.Tensor] = None,
237
+ audios: Optional[torch.Tensor] = None,
238
+ audio_feature_lengths: Optional[torch.Tensor] = None,
239
+ attention_mask: Optional[torch.Tensor] = None,
240
+ position_ids: Optional[torch.Tensor] = None,
241
+ past_key_values: Optional[Any] = None,
242
+ inputs_embeds: Optional[torch.Tensor] = None,
243
+ use_cache: Optional[bool] = None,
244
+ labels: Optional[torch.LongTensor] = None,
245
+ output_attentions: Optional[bool] = None,
246
+ output_hidden_states: Optional[bool] = None,
247
+ logits_to_keep: int | torch.Tensor = 0,
248
+ **kwargs,
249
+ ) -> CausalLMOutputWithPast:
250
+ if inputs_embeds is None:
251
+ if input_ids is None:
252
+ raise ValueError("Either `input_ids` or `inputs_embeds` must be provided.")
253
+ inputs_embeds = self.language_model.model.embed_tokens(input_ids)
254
+
255
+ past_len = self._cache_seq_len(past_key_values)
256
+ audio_inputs = input_features if input_features is not None else audios
257
+ audio_lengths = feature_lens if feature_lens is not None else audio_feature_lengths
258
+ if audio_inputs is not None and input_ids is not None and past_len == 0:
259
+ inputs_embeds = self._inject_audio_embeddings(
260
+ input_ids=input_ids,
261
+ input_features=audio_inputs,
262
+ feature_lens=audio_lengths,
263
+ )
264
+
265
+ outputs = self.language_model.model(
266
+ input_ids=None,
267
+ attention_mask=attention_mask,
268
+ position_ids=position_ids,
269
+ past_key_values=past_key_values,
270
+ inputs_embeds=inputs_embeds,
271
+ use_cache=use_cache,
272
+ output_attentions=output_attentions,
273
+ output_hidden_states=output_hidden_states,
274
+ )
275
+ hidden_states = outputs[0]
276
+
277
+ if isinstance(logits_to_keep, int) and logits_to_keep > 0:
278
+ hidden_for_logits = hidden_states[:, -logits_to_keep:, :]
279
+ elif isinstance(logits_to_keep, torch.Tensor):
280
+ hidden_for_logits = hidden_states[:, logits_to_keep, :]
281
+ else:
282
+ hidden_for_logits = hidden_states
283
+ logits = self.language_model.lm_head(hidden_for_logits)
284
+
285
+ loss = None
286
+ if labels is not None:
287
+ loss = self.language_model.loss_function(
288
+ logits=logits,
289
+ labels=labels,
290
+ vocab_size=self.config.vocab_size,
291
+ **kwargs,
292
+ )
293
+
294
+ return CausalLMOutputWithPast(
295
+ loss=loss,
296
+ logits=logits,
297
+ past_key_values=outputs.past_key_values,
298
+ hidden_states=outputs.hidden_states,
299
+ attentions=outputs.attentions,
300
+ )
301
+
302
+ def prepare_inputs_for_generation(
303
+ self,
304
+ input_ids,
305
+ past_key_values=None,
306
+ attention_mask=None,
307
+ inputs_embeds=None,
308
+ **kwargs,
309
+ ):
310
+ past_len = self._cache_seq_len(past_key_values)
311
+ if past_len > 0:
312
+ input_ids = input_ids[:, -1:]
313
+
314
+ model_inputs = {
315
+ "input_ids": input_ids,
316
+ "past_key_values": past_key_values,
317
+ "use_cache": kwargs.get("use_cache"),
318
+ "attention_mask": attention_mask,
319
+ "input_features": kwargs.get("input_features", kwargs.get("audios", None)),
320
+ "feature_lens": kwargs.get("feature_lens", kwargs.get("audio_feature_lengths", None)),
321
+ }
322
+ if inputs_embeds is not None and past_key_values is None:
323
+ model_inputs["inputs_embeds"] = inputs_embeds
324
+ del model_inputs["input_ids"]
325
+ return model_inputs
326
+
327
+
328
+ __all__ = [
329
+ "ArkasrForConditionalGeneration",
330
+ "Qwen3AsrMlpTower",
331
+ "Qwen3AsrMlpTowerBlock",
332
+ ]
preprocessor_config.json ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "auto_map": {
3
+ "AutoProcessor": "processing_arkasr.ArkasrProcessor"
4
+ },
5
+ "chunk_length": 30,
6
+ "dither": 0.0,
7
+ "feature_extractor_type": "WhisperFeatureExtractor",
8
+ "feature_size": 128,
9
+ "hop_length": 160,
10
+ "n_fft": 400,
11
+ "n_samples": 480000,
12
+ "nb_max_frames": 3000,
13
+ "padding_side": "right",
14
+ "padding_value": 0.0,
15
+ "processor_class": "ArkasrProcessor",
16
+ "return_attention_mask": false,
17
+ "sampling_rate": 16000
18
+ }
processing_arkasr.py ADDED
@@ -0,0 +1,446 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding=utf-8
2
+ from __future__ import annotations
3
+
4
+ import base64
5
+ import io
6
+ import json
7
+ import os
8
+ from typing import Any, Dict, List, Optional, Union
9
+
10
+ import numpy as np
11
+ import torch
12
+ import librosa
13
+ import soundfile as sf # Used for BytesIO fallback decoding.
14
+
15
+ from transformers import AutoTokenizer, WhisperFeatureExtractor
16
+ from transformers.feature_extraction_utils import BatchFeature
17
+ from transformers.processing_utils import ProcessorMixin
18
+ from transformers.utils import logging
19
+
20
+ logger = logging.get_logger(__name__)
21
+
22
+ _AUDIO_MARKER = "<<AUDIO_TOKENS>>"
23
+
24
+ def _normalize_dtype_name(name: str) -> str:
25
+ name = name.strip().lower()
26
+ alias = {
27
+ "fp16": "float16",
28
+ "float16": "float16",
29
+ "half": "float16",
30
+ "bf16": "bfloat16",
31
+ "bfloat16": "bfloat16",
32
+ "fp32": "float32",
33
+ "float32": "float32",
34
+ "float": "float32",
35
+ }
36
+ return alias.get(name, name)
37
+
38
+
39
+ def _resolve_torch_dtype(x: Any, default: str = "float32") -> torch.dtype:
40
+ if isinstance(x, torch.dtype):
41
+ return x
42
+ if x is None:
43
+ x = default
44
+ if isinstance(x, str):
45
+ name = _normalize_dtype_name(x)
46
+ if not hasattr(torch, name):
47
+ raise ValueError(f"Unknown torch dtype string: {x} (normalized: {name})")
48
+ return getattr(torch, name)
49
+ raise TypeError(f"audio_dtype/audio_torch_dtype must be str or torch.dtype or None, got {type(x)}")
50
+
51
+
52
+ class ArkasrProcessor(ProcessorMixin):
53
+ attributes = ["feature_extractor", "tokenizer"]
54
+ valid_kwargs = ["merge_factor", "audio_token", "audio_dtype"]
55
+ feature_extractor_class = ("WhisperFeatureExtractor", "SequenceFeatureExtractor")
56
+ tokenizer_class = ("PreTrainedTokenizerFast", "PreTrainedTokenizer")
57
+
58
+ def __init__(
59
+ self,
60
+ feature_extractor,
61
+ tokenizer,
62
+ merge_factor: int = 4,
63
+ audio_token: str = "<|audio|>",
64
+ audio_dtype: str = "float32",
65
+ **kwargs,
66
+ ):
67
+ super().__init__(feature_extractor, tokenizer)
68
+ self.merge_factor = int(merge_factor)
69
+ self.audio_token = str(audio_token)
70
+ self.audio_dtype = str(audio_dtype)
71
+
72
+ self.bos_audio_token = "<|begin_of_audio|>"
73
+ self.eos_audio_token = "<|end_of_audio|>"
74
+ self.user_token = "<|user|>"
75
+ self.assistant_token = "<|assistant|>"
76
+ self.assistant_end_token = "<|im_end|>"
77
+
78
+ @classmethod
79
+ def from_pretrained(cls, pretrained_model_name_or_path: str, **kwargs) -> "ArkasrProcessor":
80
+ trust_remote_code = bool(kwargs.pop("trust_remote_code", False))
81
+ passthrough_keys = {"cache_dir", "force_download", "local_files_only", "token", "revision", "subfolder"}
82
+ shared_kwargs = {k: kwargs[k] for k in list(kwargs.keys()) if k in passthrough_keys}
83
+
84
+ merge_factor = 4
85
+ audio_token = "<|audio|>"
86
+ audio_dtype = "float32"
87
+ tokenizer_cfg: Dict[str, Any] = {}
88
+ feat_cfg: Dict[str, Any] = {}
89
+
90
+ proc_cfg_path = os.path.join(pretrained_model_name_or_path, "processor_config.json")
91
+ if os.path.isfile(proc_cfg_path):
92
+ with open(proc_cfg_path, "r", encoding="utf-8") as f:
93
+ proc_cfg = json.load(f)
94
+ merge_factor = int(proc_cfg.get("merge_factor", merge_factor))
95
+ audio_token = str(proc_cfg.get("audio_token", audio_token))
96
+ audio_dtype = str(proc_cfg.get("audio_dtype", audio_dtype))
97
+ tokenizer_cfg = proc_cfg.get("tokenizer_config", {}) or {}
98
+ feat_cfg = proc_cfg.get("feature_extractor_config", {}) or {}
99
+
100
+ feature_extractor = WhisperFeatureExtractor.from_pretrained(pretrained_model_name_or_path, **shared_kwargs)
101
+ for k, v in feat_cfg.items():
102
+ if hasattr(feature_extractor, k):
103
+ try: setattr(feature_extractor, k, v)
104
+ except Exception: pass
105
+
106
+ tokenizer = AutoTokenizer.from_pretrained(
107
+ pretrained_model_name_or_path, use_fast=True, trust_remote_code=trust_remote_code, **shared_kwargs
108
+ )
109
+ for k, v in tokenizer_cfg.items():
110
+ if hasattr(tokenizer, k):
111
+ try: setattr(tokenizer, k, v)
112
+ except Exception: pass
113
+
114
+ return cls(
115
+ feature_extractor=feature_extractor,
116
+ tokenizer=tokenizer,
117
+ merge_factor=merge_factor,
118
+ audio_token=audio_token,
119
+ audio_dtype=audio_dtype,
120
+ )
121
+
122
+ # =========================
123
+ # Audio helpers.
124
+ # =========================
125
+ def _load_audio_file(self, path: str, sampling_rate: int = 16000, offset: float = 0.0, duration: Optional[float] = None) -> np.ndarray:
126
+ # librosa supports offset and duration for file inputs.
127
+ audio_array, _ = librosa.load(path, sr=int(sampling_rate), mono=True, offset=offset, duration=duration)
128
+ return np.asarray(audio_array, dtype=np.float32)
129
+
130
+ def _strip_data_url_prefix(self, b64: str) -> str:
131
+ if "," in b64 and b64[:30].lower().startswith("data:"):
132
+ return b64.split(",", 1)[1]
133
+ return b64
134
+
135
+ def _load_audio_base64(self, b64: str, sampling_rate: int = 16000, offset: float = 0.0, duration: Optional[float] = None) -> np.ndarray:
136
+ b64 = self._strip_data_url_prefix(b64)
137
+ raw = base64.b64decode(b64)
138
+ bio = io.BytesIO(raw)
139
+
140
+ # librosa also supports offset and duration for BytesIO inputs.
141
+ try:
142
+ wav, _sr = librosa.load(bio, sr=int(sampling_rate), mono=True, offset=offset, duration=duration)
143
+ return np.asarray(wav, dtype=np.float32)
144
+ except Exception as e:
145
+ # Fallback path with manual slicing.
146
+ try:
147
+ bio.seek(0)
148
+ data, sr = sf.read(bio, dtype="float32", always_2d=True)
149
+ wav = data.mean(axis=1)
150
+ if int(sr) != int(sampling_rate):
151
+ wav = librosa.resample(wav, orig_sr=int(sr), target_sr=int(sampling_rate))
152
+
153
+ start_sample = int(offset * sampling_rate)
154
+ end_sample = None
155
+ if duration is not None:
156
+ end_sample = start_sample + int(duration * sampling_rate)
157
+
158
+ return np.asarray(wav[start_sample:end_sample], dtype=np.float32)
159
+ except Exception as e2:
160
+ raise ValueError("Failed to decode base64 audio.") from e2
161
+
162
+ def calculate_audio_token_count(self, mel_frames: int) -> int:
163
+ downsampled = (int(mel_frames) + 1) // 2
164
+ merged = downsampled // max(self.merge_factor, 1)
165
+ return max(int(merged), 1)
166
+
167
+ def _build_templates_and_audios(
168
+ self,
169
+ conversations: List[List[dict]],
170
+ sampling_rate: int,
171
+ add_generation_prompt: bool,
172
+ ) -> tuple[List[str], List[np.ndarray], List[int]]:
173
+ prompts_template: List[str] = []
174
+ audios_raw: List[np.ndarray] = []
175
+ prompt_audio_counts: List[int] = []
176
+
177
+ for conv in conversations:
178
+ conv_str = ""
179
+ last_role = None
180
+ audio_count_this_conv = 0
181
+
182
+ for msg in conv:
183
+ role = msg["role"]
184
+ last_role = role
185
+ content = msg["content"]
186
+
187
+ if role == "user": conv_str += f"{self.user_token}"
188
+ elif role == "assistant": conv_str += f"{self.assistant_token}"
189
+ else: conv_str += f"<|{role}|>"
190
+
191
+ if isinstance(content, str):
192
+ conv_str += f"{content}"
193
+ elif isinstance(content, list):
194
+ for part in content:
195
+ ptype = part.get("type")
196
+ if ptype == "audio":
197
+ # Optional segment selection from begin_time/end_time.
198
+ begin_time = part.get("begin_time", -1)
199
+ end_time = part.get("end_time", -1)
200
+
201
+ offset = 0.0
202
+ duration = None
203
+
204
+ # Apply slicing only when begin_time is valid.
205
+ if begin_time is not None and begin_time >= 0:
206
+ offset = float(begin_time)
207
+ if end_time is not None and end_time > begin_time:
208
+ duration = float(end_time) - float(begin_time)
209
+
210
+ audio_raw_this = None
211
+ if "array" in part:
212
+ arr = part["array"]
213
+ if isinstance(arr, torch.Tensor):
214
+ arr = arr.detach().cpu().numpy()
215
+ full_arr = np.asarray(arr, dtype=np.float32).reshape(-1)
216
+
217
+ # Slice in-memory audio arrays as needed.
218
+ start_idx = int(offset * sampling_rate)
219
+ end_idx = None
220
+ if duration is not None:
221
+ end_idx = start_idx + int(duration * sampling_rate)
222
+ audio_raw_this = full_arr[start_idx:end_idx]
223
+
224
+ elif "path" in part:
225
+ audio_raw_this = self._load_audio_file(
226
+ part["path"],
227
+ sampling_rate=sampling_rate,
228
+ offset=offset,
229
+ duration=duration
230
+ )
231
+ elif "base64" in part:
232
+ audio_raw_this = self._load_audio_base64(
233
+ part["base64"],
234
+ sampling_rate=sampling_rate,
235
+ offset=offset,
236
+ duration=duration
237
+ )
238
+ else:
239
+ raise ValueError("Audio part must contain 'path' or 'array' or 'base64'.")
240
+
241
+ audios_raw.append(audio_raw_this)
242
+ audio_count_this_conv += 1
243
+ conv_str += f"{self.bos_audio_token}{_AUDIO_MARKER}{self.eos_audio_token}"
244
+
245
+ elif ptype == "text":
246
+ conv_str += f"{part.get('text', '')}"
247
+ else:
248
+ raise ValueError(f"Unknown content part type: {ptype}")
249
+ else:
250
+ raise ValueError(f"Unsupported message content type: {type(content)}")
251
+
252
+ if add_generation_prompt:
253
+ if last_role == "user": conv_str += f"{self.assistant_token}"
254
+ elif last_role == "assistant": conv_str += f"{self.assistant_end_token}"
255
+ else: conv_str += f"{self.assistant_token}"
256
+
257
+ prompts_template.append(conv_str)
258
+ prompt_audio_counts.append(audio_count_this_conv)
259
+
260
+ return prompts_template, audios_raw, prompt_audio_counts
261
+
262
+ def _calculate_audio_token_counts_per_sample(
263
+ self,
264
+ audios_raw: List[np.ndarray],
265
+ sampling_rate: int,
266
+ audio_max_length: Optional[int],
267
+ audio_pad_to_multiple_of: Optional[int],
268
+ ) -> List[int]:
269
+ del sampling_rate, audio_pad_to_multiple_of
270
+
271
+ hop_length = int(getattr(self.feature_extractor, "hop_length", 160))
272
+ max_audio_samples = int(audio_max_length) if audio_max_length is not None else None
273
+ token_counts: List[int] = []
274
+
275
+ for audio_raw in audios_raw:
276
+ audio_np = np.asarray(audio_raw, dtype=np.float32).reshape(-1)
277
+ effective_len = int(audio_np.shape[0])
278
+ if max_audio_samples is not None:
279
+ effective_len = min(effective_len, max_audio_samples)
280
+
281
+ mel_frames = effective_len // max(hop_length, 1)
282
+ token_counts.append(self.calculate_audio_token_count(int(mel_frames)))
283
+
284
+ return token_counts
285
+
286
+ # =========================
287
+ # apply_chat_template
288
+ # =========================
289
+ def apply_chat_template(
290
+ self,
291
+ conversation: Union[List[dict], List[List[dict]]],
292
+ chat_template: Optional[str] = None,
293
+ add_generation_prompt: bool = True,
294
+ **kwargs,
295
+ ) -> Union[BatchFeature, str, List[str]]:
296
+ if chat_template is not None:
297
+ logger.warning("chat_template argument is ignored.")
298
+
299
+ tokenize = kwargs.pop("tokenize", True)
300
+ return_tensors = kwargs.pop("return_tensors", "pt")
301
+ kwargs.pop("return_dict", None)
302
+
303
+ audio_torch_dtype = kwargs.pop("audio_torch_dtype", None)
304
+ audio_dtype_override = kwargs.pop("audio_dtype", None)
305
+ dtype_source = audio_torch_dtype if audio_torch_dtype is not None else audio_dtype_override
306
+ target_dtype = _resolve_torch_dtype(dtype_source, default=getattr(self, "audio_dtype", "float32"))
307
+
308
+ text_kwargs = dict(kwargs.pop("text_kwargs", {}) or {})
309
+ for k in ("padding", "truncation", "max_length", "add_special_tokens"):
310
+ if k in kwargs and k not in text_kwargs:
311
+ text_kwargs[k] = kwargs.pop(k)
312
+
313
+ sampling_rate = int(kwargs.pop("sampling_rate", 16000))
314
+ audio_padding = kwargs.pop("audio_padding", "longest")
315
+ audio_max_length = kwargs.pop("audio_max_length", None)
316
+ audio_pad_to_multiple_of = kwargs.pop("audio_pad_to_multiple_of", None)
317
+
318
+ if kwargs:
319
+ logger.warning(f"Ignored unused kwargs: {list(kwargs.keys())}")
320
+
321
+ if isinstance(conversation, list) and conversation and isinstance(conversation[0], dict):
322
+ conversations = [conversation]
323
+ is_single = True
324
+ else:
325
+ conversations = conversation
326
+ is_single = False
327
+
328
+ prompt_templates, audios_raw, prompt_audio_counts = self._build_templates_and_audios(
329
+ conversations=conversations,
330
+ sampling_rate=sampling_rate,
331
+ add_generation_prompt=add_generation_prompt,
332
+ )
333
+
334
+ input_features = None
335
+ audio_token_counts: List[int] = []
336
+
337
+ if len(audios_raw) > 0:
338
+ feat = self.feature_extractor(
339
+ audios_raw,
340
+ sampling_rate=sampling_rate,
341
+ return_tensors="np",
342
+ return_attention_mask=False,
343
+ padding=audio_padding,
344
+ max_length=audio_max_length,
345
+ pad_to_multiple_of=audio_pad_to_multiple_of,
346
+ )
347
+ input_features = feat["input_features"]
348
+ if not isinstance(input_features, np.ndarray):
349
+ input_features = np.asarray(input_features)
350
+
351
+ audio_token_counts = self._calculate_audio_token_counts_per_sample(
352
+ audios_raw=audios_raw,
353
+ sampling_rate=sampling_rate,
354
+ audio_max_length=audio_max_length,
355
+ audio_pad_to_multiple_of=audio_pad_to_multiple_of,
356
+ )
357
+
358
+ prompts: List[str] = []
359
+ audio_idx = 0
360
+ for prompt_template, audio_count in zip(prompt_templates, prompt_audio_counts):
361
+ prompt = prompt_template
362
+ for _ in range(audio_count):
363
+ if audio_idx >= len(audio_token_counts):
364
+ raise ValueError("Audio token count mismatch while building prompts.")
365
+ audio_tokens_str = "".join([self.audio_token] * audio_token_counts[audio_idx])
366
+ prompt = prompt.replace(_AUDIO_MARKER, audio_tokens_str, 1)
367
+ audio_idx += 1
368
+ if _AUDIO_MARKER in prompt:
369
+ raise ValueError("Unresolved audio marker remained in prompt.")
370
+ prompts.append(prompt)
371
+
372
+ if audio_idx != len(audio_token_counts):
373
+ raise ValueError("Unused audio token counts remained after prompt construction.")
374
+
375
+ if not tokenize:
376
+ return prompts[0] if is_single else prompts
377
+
378
+ text_kwargs.setdefault("padding", "longest")
379
+ text_kwargs.setdefault("add_special_tokens", False)
380
+ text_kwargs["return_tensors"] = return_tensors
381
+
382
+ enc = self.tokenizer(prompts, **text_kwargs)
383
+ data: Dict[str, Any] = dict(enc)
384
+
385
+ if input_features is not None:
386
+ audio_tensor = torch.tensor(input_features, dtype=target_dtype)
387
+ data["input_features"] = audio_tensor
388
+ data["audios"] = audio_tensor
389
+
390
+ return BatchFeature(data=data, tensor_type=return_tensors)
391
+
392
+ # Tokenizer proxy methods and direct audio-array path.
393
+ def batch_decode(self, *args, **kwargs):
394
+ return self.tokenizer.batch_decode(*args, **kwargs)
395
+
396
+ def decode(self, *args, **kwargs):
397
+ return self.tokenizer.decode(*args, **kwargs)
398
+
399
+ def __call__(
400
+ self,
401
+ text: Union[str, List[str]],
402
+ audios: Union[np.ndarray, torch.Tensor, List[Union[np.ndarray, torch.Tensor]]],
403
+ sampling_rate: int = 16000,
404
+ return_tensors: str = "pt",
405
+ **tokenizer_kwargs,
406
+ ) -> BatchFeature:
407
+ # Direct audio-array path; segment slicing is handled by apply_chat_template.
408
+ audios_list = []
409
+ def flatten_audios(obj):
410
+ if isinstance(obj, (list, tuple)):
411
+ if len(obj) > 0 and isinstance(obj[0], (float, int)):
412
+ audios_list.append(obj)
413
+ else:
414
+ for item in obj: flatten_audios(item)
415
+ elif isinstance(obj, (np.ndarray, torch.Tensor)):
416
+ audios_list.append(obj)
417
+ flatten_audios(audios)
418
+
419
+ audios_np: List[np.ndarray] = []
420
+ for a in audios_list:
421
+ if isinstance(a, torch.Tensor): a = a.detach().cpu().numpy()
422
+ a = np.asarray(a, dtype=np.float32).reshape(-1)
423
+ audios_np.append(a)
424
+
425
+ input_features = None
426
+ if audios_np:
427
+ feat = self.feature_extractor(audios_np, sampling_rate=int(sampling_rate), return_tensors="np", return_attention_mask=False, padding="longest")
428
+ input_features = feat["input_features"]
429
+ if not isinstance(input_features, np.ndarray): input_features = np.asarray(input_features)
430
+
431
+ tokenizer_kwargs = dict(tokenizer_kwargs or {})
432
+ tokenizer_kwargs.setdefault("padding", "longest")
433
+ tokenizer_kwargs.setdefault("add_special_tokens", False)
434
+ tokenizer_kwargs["return_tensors"] = return_tensors
435
+
436
+ enc = self.tokenizer(text, **tokenizer_kwargs)
437
+ data: Dict[str, Any] = dict(enc)
438
+ if input_features is not None:
439
+ audio_tensor = torch.tensor(input_features, dtype=_resolve_torch_dtype(getattr(self, "audio_dtype", "float32")))
440
+ data["input_features"] = audio_tensor
441
+ data["audios"] = audio_tensor
442
+ return BatchFeature(data=data, tensor_type=return_tensors)
443
+
444
+ @property
445
+ def model_input_names(self):
446
+ return ["input_ids", "attention_mask", "input_features", "audios"]
processor_config.json ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "audio_dtype": "bfloat16",
3
+ "audio_token": "<|audio|>",
4
+ "auto_map": {
5
+ "AutoProcessor": "processing_arkasr.ArkasrProcessor"
6
+ },
7
+ "merge_factor": 4,
8
+ "processor_class": "ArkasrProcessor"
9
+ }
qwen3_asr_audio_config.py ADDED
@@ -0,0 +1,425 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
151
+ `num_key_value_heads=1` the model will use Multi Query Attention (MQA) otherwise GQA is used. When
152
+ converting a multi-head checkpoint to a GQA checkpoint, each group key and value head should be constructed
153
+ by meanpooling all the original heads within that group. For more details, check out [this
154
+ paper](https://huggingface.co/papers/2305.13245). If it is not specified, will default to `32`.
155
+ head_dim (`int`, *optional*, defaults to 128):
156
+ The dimension of the head. If not specified, will default to `hidden_size // num_attention_heads`.
157
+ hidden_act (`str` or `function`, *optional*, defaults to `"silu"`):
158
+ The non-linear activation function (function or string) in the decoder.
159
+ max_position_embeddings (`int`, *optional*, defaults to 128000):
160
+ The maximum sequence length that this model might ever be used with.
161
+ initializer_range (`float`, *optional*, defaults to 0.02):
162
+ The standard deviation of the truncated_normal_initializer for initializing all weight matrices.
163
+ rms_norm_eps (`float`, *optional*, defaults to 1e-06):
164
+ The epsilon used by the rms normalization layers.
165
+ use_cache (`bool`, *optional*, defaults to `True`):
166
+ Whether or not the model should return the last key/values attentions (not used by all models). Only
167
+ relevant if `config.is_decoder=True`.
168
+ tie_word_embeddings (`bool`, *optional*, defaults to `False`):
169
+ Whether the model's input and output word embeddings should be tied.
170
+ rope_theta (`float`, *optional*, defaults to 5000000.0):
171
+ The base period of the RoPE embeddings.
172
+ rope_scaling (`Dict`, *optional*):
173
+ Dictionary containing the scaling configuration for the RoPE embeddings. NOTE: if you apply new rope type
174
+ and you expect the model to work on longer `max_position_embeddings`, we recommend you to update this value
175
+ accordingly.
176
+ Expected contents:
177
+ `rope_type` (`str`):
178
+ The sub-variant of RoPE to use. Can be one of ['default', 'linear', 'dynamic', 'yarn', 'longrope',
179
+ 'llama3'], with 'default' being the original RoPE implementation.
180
+ `factor` (`float`, *optional*):
181
+ Used with all rope types except 'default'. The scaling factor to apply to the RoPE embeddings. In
182
+ most scaling types, a `factor` of x will enable the model to handle sequences of length x *
183
+ original maximum pre-trained length.
184
+ `original_max_position_embeddings` (`int`, *optional*):
185
+ Used with 'dynamic', 'longrope' and 'llama3'. The original max position embeddings used during
186
+ pretraining.
187
+ `attention_factor` (`float`, *optional*):
188
+ Used with 'yarn' and 'longrope'. The scaling factor to be applied on the attention
189
+ computation. If unspecified, it defaults to value recommended by the implementation, using the
190
+ `factor` field to infer the suggested value.
191
+ `beta_fast` (`float`, *optional*):
192
+ Only used with 'yarn'. Parameter to set the boundary for extrapolation (only) in the linear
193
+ ramp function. If unspecified, it defaults to 32.
194
+ `beta_slow` (`float`, *optional*):
195
+ Only used with 'yarn'. Parameter to set the boundary for interpolation (only) in the linear
196
+ ramp function. If unspecified, it defaults to 1.
197
+ `short_factor` (`list[float]`, *optional*):
198
+ Only used with 'longrope'. The scaling factor to be applied to short contexts (<
199
+ `original_max_position_embeddings`). Must be a list of numbers with the same length as the hidden
200
+ size divided by the number of attention heads divided by 2
201
+ `long_factor` (`list[float]`, *optional*):
202
+ Only used with 'longrope'. The scaling factor to be applied to long contexts (<
203
+ `original_max_position_embeddings`). Must be a list of numbers with the same length as the hidden
204
+ size divided by the number of attention heads divided by 2
205
+ `low_freq_factor` (`float`, *optional*):
206
+ Only used with 'llama3'. Scaling factor applied to low frequency components of the RoPE
207
+ `high_freq_factor` (`float`, *optional*):
208
+ Only used with 'llama3'. Scaling factor applied to high frequency components of the RoPE
209
+ attention_bias (`bool`, defaults to `False`, *optional*, defaults to `False`):
210
+ Whether to use a bias in the query, key, value and output projection layers during self-attention.
211
+ attention_dropout (`float`, *optional*, defaults to 0.0):
212
+ The dropout ratio for the attention probabilities.
213
+
214
+ ```python
215
+ >>> from transformers import Qwen3ASRTextModel, Qwen3ASRTextConfig
216
+
217
+ >>> # Initializing a Qwen3ASR style configuration
218
+ >>> configuration = Qwen3ASRTextConfig()
219
+
220
+ >>> # Initializing a model from the Qwen3-VL-7B style configuration
221
+ >>> model = Qwen3ASRTextModel(configuration)
222
+
223
+ >>> # Accessing the model configuration
224
+ >>> configuration = model.config
225
+ ```"""
226
+
227
+ model_type = "qwen3_asr_text"
228
+ base_config_key = "text_config"
229
+
230
+ def __init__(
231
+ self,
232
+ vocab_size=151936,
233
+ hidden_size=4096,
234
+ intermediate_size=22016,
235
+ num_hidden_layers=32,
236
+ num_attention_heads=32,
237
+ num_key_value_heads=32,
238
+ head_dim=128,
239
+ hidden_act="silu",
240
+ max_position_embeddings=128000,
241
+ initializer_range=0.02,
242
+ rms_norm_eps=1e-6,
243
+ use_cache=True,
244
+ tie_word_embeddings=False,
245
+ rope_theta=5000000.0,
246
+ rope_scaling=None,
247
+ attention_bias=False,
248
+ attention_dropout=0.0,
249
+ **kwargs,
250
+ ):
251
+ self.vocab_size = vocab_size
252
+ self.max_position_embeddings = max_position_embeddings
253
+ self.hidden_size = hidden_size
254
+ self.intermediate_size = intermediate_size
255
+ self.num_hidden_layers = num_hidden_layers
256
+ self.num_attention_heads = num_attention_heads
257
+
258
+ # for backward compatibility
259
+ if num_key_value_heads is None:
260
+ num_key_value_heads = num_attention_heads
261
+
262
+ self.num_key_value_heads = num_key_value_heads
263
+ self.head_dim = head_dim
264
+ self.hidden_act = hidden_act
265
+ self.initializer_range = initializer_range
266
+ self.rms_norm_eps = rms_norm_eps
267
+ self.use_cache = use_cache
268
+ self.rope_theta = rope_theta
269
+ self.rope_scaling = rope_scaling
270
+ self.attention_bias = attention_bias
271
+ self.attention_dropout = attention_dropout
272
+ # Validate the correctness of rotary position embeddings parameters
273
+ # BC: if there is a 'type' field, move it to 'rope_type'.
274
+ if self.rope_scaling is not None and "type" in self.rope_scaling:
275
+ self.rope_scaling["rope_type"] = self.rope_scaling["type"]
276
+
277
+ super().__init__(tie_word_embeddings=tie_word_embeddings, **kwargs)
278
+
279
+
280
+ class Qwen3ASRThinkerConfig(PretrainedConfig):
281
+ r"""
282
+ This is the configuration class to store the configuration of a [`Qwen3ASRThinker`]. It is used to instantiate a
283
+ Qwen3-ASR-Thinker model according to the specified arguments, defining the model architecture. Instantiating a
284
+ configuration with the defaults will yield a similar configuration to that of the thinker component of the Qwen3-Omni
285
+ architecture.
286
+
287
+ e.g. [Qwen/Qwen3-ASR-1.7B](https://huggingface.co/Qwen/Qwen3-ASR-1.7B)
288
+
289
+ Configuration objects inherit from [`PretrainedConfig`] and can be used to control the model outputs. Read the
290
+ documentation from [`PretrainedConfig`] for more information.
291
+
292
+ Args:
293
+ audio_config (`dict`, *optional*):
294
+ The config dictionary of the audio backbone.
295
+ text_config (`dict`, *optional*):
296
+ The config dictionary of the text backbone.
297
+ audio_token_id (`int`, *optional*, defaults to 151646):
298
+ The audio token id to encode the audio prompt.
299
+ audio_start_token_id (`int`, *optional*, defaults to 151647):
300
+ The audio start token id to encode the audio prompt.
301
+ user_token_id (`int`, *optional*, defaults to 872):
302
+ The user token id to encode the user token.
303
+ initializer_range (`float`, *optional*, defaults to 0.02):
304
+ The standard deviation of the truncated_normal_initializer for initializing all weight matrices.
305
+
306
+ Example:
307
+
308
+ ```python
309
+ >>> from transformers import Qwen3ASRThinkerModel, Qwen3ASRThinkerConfig
310
+
311
+ >>> # Initializing a default Qwen3ASRThinkerConfig
312
+ >>> configuration = Qwen3ASRThinkerConfig()
313
+
314
+ >>> # Initializing a model (with random weights) from the default configuration
315
+ >>> model = Qwen3ASRThinkerModel(configuration)
316
+
317
+ >>> # Accessing the model configuration
318
+ >>> configuration = model.config
319
+ ```"""
320
+
321
+ model_type = "qwen3_asr_thinker"
322
+
323
+ attribute_map = {}
324
+ sub_configs = {
325
+ "audio_config": Qwen3ASRAudioEncoderConfig,
326
+ "text_config": Qwen3ASRTextConfig,
327
+ }
328
+
329
+ def __init__(
330
+ self,
331
+ audio_config=None,
332
+ text_config=None,
333
+ audio_token_id=151646,
334
+ audio_start_token_id=151647,
335
+ user_token_id=872,
336
+ initializer_range=0.02,
337
+ **kwargs,
338
+ ):
339
+ super().__init__(**kwargs)
340
+ self.user_token_id = user_token_id
341
+ self.audio_start_token_id = audio_start_token_id
342
+ self.initializer_range = initializer_range
343
+
344
+ if isinstance(audio_config, dict):
345
+ audio_config = Qwen3ASRAudioEncoderConfig(**audio_config)
346
+ elif audio_config is None:
347
+ audio_config = Qwen3ASRAudioEncoderConfig()
348
+ self.audio_config = audio_config
349
+
350
+ if isinstance(text_config, dict):
351
+ text_config = Qwen3ASRTextConfig(**text_config)
352
+ elif text_config is None:
353
+ text_config = Qwen3ASRTextConfig()
354
+ self.text_config = text_config
355
+ self.audio_token_id = audio_token_id
356
+
357
+
358
+ class Qwen3ASRConfig(PretrainedConfig):
359
+ """
360
+ This is the configuration class to store the configuration of a [`Qwen3ASRForConditionalGeneration`]. It is used to instantiate a Qwen3ASR
361
+ model according to the specified sub-models configurations, defining the model architecture.
362
+
363
+ Instantiating a configuration with the defaults will yield a similar configuration to that of the
364
+ [Qwen/Qwen3-ASR-1.7B](https://huggingface.co/Qwen/Qwen3-ASR-1.7B) architecture.
365
+
366
+ Configuration objects inherit from [`PretrainedConfig`] and can be used to control the model outputs. Read the
367
+ documentation from [`PretrainedConfig`] for more information.
368
+
369
+ Args:
370
+ thinker_config (`dict`, *optional*): Configuration of the underlying thinker sub-model.
371
+ support_languages (`List[str]`, *optional*): The languages supported by the model.
372
+
373
+ Example:
374
+
375
+ ```python
376
+ >>> from transformers import (
377
+ ... Qwen3ASRThinkerConfig,
378
+ ... Qwen3ASRForConditionalGeneration,
379
+ ... Qwen3ASRConfig,
380
+ ... )
381
+
382
+ >>> # Initializing a Qwen3ASR style configuration
383
+ >>> configuration = Qwen3ASRConfig()
384
+
385
+ >>> # Initializing a model from the configuration
386
+ >>> model = Qwen3ASRForConditionalGeneration(configuration)
387
+
388
+ >>> # Accessing the model configuration
389
+ >>> configuration = model.config
390
+ ```"""
391
+
392
+ model_type = "qwen3_asr"
393
+ sub_configs = {
394
+ "thinker_config": Qwen3ASRThinkerConfig,
395
+ }
396
+
397
+ def __init__(
398
+ self,
399
+ thinker_config=None,
400
+ support_languages=None,
401
+ **kwargs,
402
+ ):
403
+ super().__init__(**kwargs)
404
+ if thinker_config is None:
405
+ thinker_config = {}
406
+
407
+ self.thinker_config = Qwen3ASRThinkerConfig(**thinker_config)
408
+ self.support_languages = support_languages
409
+
410
+ def get_text_config(self, decoder=False) -> "PretrainedConfig":
411
+ """
412
+ Returns the config that is meant to be used with text IO. On most models, it is the original config instance
413
+ itself. On specific composite models, it is under a set of valid names.
414
+
415
+ Args:
416
+ decoder (`Optional[bool]`, *optional*, defaults to `False`):
417
+ If set to `True`, then only search for decoder config names.
418
+ """
419
+ # Overridden for deeply nested config like Qwen2.5-Omni. We don't have any omni model
420
+ # except for Qwen yet. This has to be generalized if more deeply nested configs are
421
+ # added. NOTE: currently method used only by vLLM
422
+ return self.thinker_config.get_text_config()
423
+
424
+
425
+ __all__ = ["Qwen3ASRConfig", "Qwen3ASRThinkerConfig", "Qwen3ASRAudioEncoderConfig"]
qwen3_asr_audio_model.py ADDED
@@ -0,0 +1,229 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import math
4
+ from typing import Optional
5
+
6
+ import numpy as np
7
+ import torch
8
+ import torch.nn.functional as F
9
+ from torch import nn
10
+ from transformers.activations import ACT2FN
11
+ from transformers.modeling_outputs import BaseModelOutput
12
+ from transformers.modeling_utils import PreTrainedModel
13
+
14
+ from .qwen3_asr_audio_config import Qwen3ASRAudioEncoderConfig
15
+
16
+
17
+ def _get_feat_extract_output_lengths(input_lengths: torch.Tensor) -> torch.Tensor:
18
+ input_lengths = torch.clamp(input_lengths.long(), min=1)
19
+ input_lengths_leave = input_lengths % 100
20
+ feat_lengths = (input_lengths_leave - 1) // 2 + 1
21
+ return ((feat_lengths - 1) // 2 + 1 - 1) // 2 + 1 + (input_lengths // 100) * 13
22
+
23
+
24
+ def _block_diagonal_attention_mask(hidden_states: torch.Tensor, cu_seqlens: torch.Tensor) -> torch.Tensor:
25
+ seq_length = int(hidden_states.shape[0])
26
+ mask = torch.full(
27
+ (1, 1, seq_length, seq_length),
28
+ torch.finfo(hidden_states.dtype).min,
29
+ dtype=hidden_states.dtype,
30
+ device=hidden_states.device,
31
+ )
32
+ for i in range(1, int(cu_seqlens.numel())):
33
+ start = int(cu_seqlens[i - 1].item())
34
+ end = int(cu_seqlens[i].item())
35
+ mask[..., start:end, start:end] = 0
36
+ return mask
37
+
38
+
39
+ class Qwen3ASRAudioAttention(nn.Module):
40
+ def __init__(self, config: Qwen3ASRAudioEncoderConfig):
41
+ super().__init__()
42
+ self.embed_dim = int(config.d_model)
43
+ self.num_heads = int(config.encoder_attention_heads)
44
+ self.head_dim = self.embed_dim // self.num_heads
45
+ if self.head_dim * self.num_heads != self.embed_dim:
46
+ raise ValueError("d_model must be divisible by encoder_attention_heads")
47
+ self.scaling = self.head_dim**-0.5
48
+ self.attention_dropout = float(config.attention_dropout)
49
+ self.k_proj = nn.Linear(self.embed_dim, self.embed_dim, bias=True)
50
+ self.v_proj = nn.Linear(self.embed_dim, self.embed_dim, bias=True)
51
+ self.q_proj = nn.Linear(self.embed_dim, self.embed_dim, bias=True)
52
+ self.out_proj = nn.Linear(self.embed_dim, self.embed_dim, bias=True)
53
+
54
+ def forward(
55
+ self,
56
+ hidden_states: torch.Tensor,
57
+ cu_seqlens: torch.Tensor,
58
+ attention_mask: Optional[torch.Tensor] = None,
59
+ ) -> torch.Tensor:
60
+ del attention_mask
61
+ seq_length = int(hidden_states.size(0))
62
+ query = self.q_proj(hidden_states).reshape(seq_length, self.num_heads, self.head_dim).transpose(0, 1)
63
+ key = self.k_proj(hidden_states).reshape(seq_length, self.num_heads, self.head_dim).transpose(0, 1)
64
+ value = self.v_proj(hidden_states).reshape(seq_length, self.num_heads, self.head_dim).transpose(0, 1)
65
+ query = query.unsqueeze(0)
66
+ key = key.unsqueeze(0)
67
+ value = value.unsqueeze(0)
68
+ mask = _block_diagonal_attention_mask(hidden_states, cu_seqlens)
69
+ attn_weights = torch.matmul(query, key.transpose(2, 3)) * self.scaling
70
+ attn_weights = attn_weights + mask
71
+ attn_weights = F.softmax(attn_weights, dim=-1, dtype=torch.float32).to(query.dtype)
72
+ attn_weights = F.dropout(attn_weights, p=self.attention_dropout, training=self.training)
73
+ attn_output = torch.matmul(attn_weights, value)
74
+ attn_output = attn_output.reshape(seq_length, self.embed_dim).contiguous()
75
+ return self.out_proj(attn_output)
76
+
77
+
78
+ class Qwen3ASRAudioEncoderLayer(nn.Module):
79
+ def __init__(self, config: Qwen3ASRAudioEncoderConfig):
80
+ super().__init__()
81
+ self.embed_dim = int(config.d_model)
82
+ self.self_attn = Qwen3ASRAudioAttention(config)
83
+ self.self_attn_layer_norm = nn.LayerNorm(self.embed_dim)
84
+ self.activation_fn = ACT2FN[config.activation_function]
85
+ self.fc1 = nn.Linear(self.embed_dim, int(config.encoder_ffn_dim))
86
+ self.fc2 = nn.Linear(int(config.encoder_ffn_dim), self.embed_dim)
87
+ self.final_layer_norm = nn.LayerNorm(self.embed_dim)
88
+
89
+ def forward(self, hidden_states: torch.Tensor, cu_seqlens: torch.Tensor) -> tuple[torch.Tensor]:
90
+ residual = hidden_states
91
+ hidden_states = self.self_attn_layer_norm(hidden_states)
92
+ hidden_states = self.self_attn(hidden_states=hidden_states, cu_seqlens=cu_seqlens)
93
+ hidden_states = residual + hidden_states
94
+ residual = hidden_states
95
+ hidden_states = self.final_layer_norm(hidden_states)
96
+ hidden_states = self.fc1(hidden_states)
97
+ hidden_states = self.activation_fn(hidden_states)
98
+ hidden_states = self.fc2(hidden_states)
99
+ hidden_states = residual + hidden_states
100
+ if hidden_states.dtype == torch.float16:
101
+ clamp_value = torch.finfo(hidden_states.dtype).max - 1000
102
+ hidden_states = torch.clamp(hidden_states, min=-clamp_value, max=clamp_value)
103
+ return (hidden_states,)
104
+
105
+
106
+ class SinusoidsPositionEmbedding(nn.Module):
107
+ def __init__(self, length: int, channels: int, max_timescale: int = 10000):
108
+ super().__init__()
109
+ if channels % 2 != 0:
110
+ raise ValueError("SinusoidsPositionEmbedding requires an even channel count")
111
+ log_timescale_increment = np.log(max_timescale) / (channels // 2 - 1)
112
+ inv_timescales = torch.exp(-log_timescale_increment * torch.arange(channels // 2).float())
113
+ scaled_time = torch.arange(length)[:, np.newaxis] * inv_timescales[np.newaxis, :]
114
+ self.register_buffer(
115
+ "positional_embedding",
116
+ torch.cat([torch.sin(scaled_time), torch.cos(scaled_time)], dim=1),
117
+ persistent=False,
118
+ )
119
+
120
+ def forward(self, seqlen: int):
121
+ return self.positional_embedding[:seqlen, :]
122
+
123
+
124
+ class Qwen3ASRAudioEncoder(PreTrainedModel):
125
+ config_class = Qwen3ASRAudioEncoderConfig
126
+ main_input_name = "input_features"
127
+ _no_split_modules = ["Qwen3ASRAudioEncoderLayer"]
128
+
129
+ def __init__(self, config: Qwen3ASRAudioEncoderConfig):
130
+ super().__init__(config)
131
+ embed_dim = int(config.d_model)
132
+ self.dropout = float(config.dropout)
133
+ self.num_mel_bins = int(config.num_mel_bins)
134
+ self.max_source_positions = int(config.max_source_positions)
135
+ self.embed_scale = math.sqrt(embed_dim) if bool(config.scale_embedding) else 1.0
136
+ self.n_window = int(config.n_window)
137
+ self.positional_embedding = SinusoidsPositionEmbedding(self.max_source_positions, embed_dim)
138
+ self.layers = nn.ModuleList([Qwen3ASRAudioEncoderLayer(config) for _ in range(int(config.encoder_layers))])
139
+ self.ln_post = nn.LayerNorm(embed_dim)
140
+ self.gradient_checkpointing = False
141
+ self.conv2d1 = nn.Conv2d(1, int(config.downsample_hidden_size), 3, 2, padding=1)
142
+ self.conv2d2 = nn.Conv2d(int(config.downsample_hidden_size), int(config.downsample_hidden_size), 3, 2, padding=1)
143
+ self.conv2d3 = nn.Conv2d(int(config.downsample_hidden_size), int(config.downsample_hidden_size), 3, 2, padding=1)
144
+ conv_freq = ((((int(config.num_mel_bins) + 1) // 2 + 1) // 2 + 1) // 2)
145
+ self.conv_out = nn.Linear(int(config.downsample_hidden_size) * conv_freq, embed_dim, bias=False)
146
+ self.proj1 = nn.Linear(embed_dim, embed_dim)
147
+ self.act = ACT2FN[config.activation_function]
148
+ self.proj2 = nn.Linear(embed_dim, int(config.output_dim))
149
+ self.n_window_infer = int(config.n_window_infer)
150
+ self.conv_chunksize = int(config.conv_chunksize)
151
+ self.post_init()
152
+
153
+ def _freeze_parameters(self):
154
+ for param in self.parameters():
155
+ param.requires_grad = False
156
+ self._requires_grad = False
157
+
158
+ def _prepare_attention_mask(self, inputs_tensor: torch.Tensor, cu_seqlens: torch.Tensor) -> torch.Tensor:
159
+ return _block_diagonal_attention_mask(inputs_tensor, cu_seqlens)
160
+
161
+ def forward(
162
+ self,
163
+ input_features: torch.Tensor,
164
+ feature_lens: Optional[torch.Tensor] = None,
165
+ aftercnn_lens: Optional[torch.Tensor] = None,
166
+ ):
167
+ if feature_lens is None:
168
+ feature_lens = torch.tensor([input_features.shape[-1]], dtype=torch.long, device=input_features.device)
169
+ feature_lens = feature_lens.to(device=input_features.device, dtype=torch.long)
170
+ if aftercnn_lens is None:
171
+ aftercnn_lens = _get_feat_extract_output_lengths(feature_lens)
172
+ aftercnn_lens = aftercnn_lens.to(device=input_features.device, dtype=torch.long)
173
+
174
+ chunk_num = torch.ceil(feature_lens / (self.n_window * 2)).long()
175
+ chunk_lengths = torch.tensor(
176
+ [self.n_window * 2] * int(chunk_num.sum().item()),
177
+ dtype=torch.long,
178
+ device=feature_lens.device,
179
+ )
180
+ tail_chunk_index = F.pad(chunk_num, (1, 0), value=-1).cumsum(0)[1:]
181
+ chunk_lengths[tail_chunk_index] = feature_lens % (self.n_window * 2)
182
+ chunk_lengths[chunk_lengths == 0] = self.n_window * 2
183
+
184
+ chunk_list = input_features.T.split(chunk_lengths.tolist(), dim=0)
185
+ padded_feature = nn.utils.rnn.pad_sequence(chunk_list, batch_first=True).transpose(1, 2)
186
+ feature_lens_after_cnn = _get_feat_extract_output_lengths(chunk_lengths)
187
+ padded_mask_after_cnn = nn.utils.rnn.pad_sequence(
188
+ [torch.ones(int(length.item()), dtype=torch.bool, device=padded_feature.device) for length in feature_lens_after_cnn],
189
+ batch_first=True,
190
+ )
191
+ padded_feature = padded_feature.unsqueeze(1)
192
+ padded_embeds = []
193
+ for chunk in padded_feature.split(self.conv_chunksize, dim=0):
194
+ padded_embed = F.gelu(self.conv2d1(chunk))
195
+ padded_embed = F.gelu(self.conv2d2(padded_embed))
196
+ padded_embed = F.gelu(self.conv2d3(padded_embed))
197
+ padded_embeds.append(padded_embed)
198
+ padded_embed = torch.cat(padded_embeds, dim=0)
199
+ bsz, channels, freq, time = padded_embed.size()
200
+ padded_embed = self.conv_out(padded_embed.permute(0, 3, 1, 2).contiguous().view(bsz, time, channels * freq))
201
+
202
+ positional_embedding = (
203
+ self.positional_embedding.positional_embedding[: padded_embed.shape[1], :]
204
+ .unsqueeze(0)
205
+ .to(padded_embed.dtype)
206
+ )
207
+ padded_embed = padded_embed + positional_embedding
208
+ hidden_states = padded_embed[padded_mask_after_cnn]
209
+ cu_chunk_lens = [0]
210
+ window_aftercnn = padded_mask_after_cnn.shape[-1] * (self.n_window_infer // (self.n_window * 2))
211
+ for cnn_len in aftercnn_lens:
212
+ cnn_len_int = int(cnn_len.item())
213
+ cu_chunk_lens += [window_aftercnn] * (cnn_len_int // window_aftercnn)
214
+ remainder = cnn_len_int % window_aftercnn
215
+ if remainder != 0:
216
+ cu_chunk_lens += [remainder]
217
+ cu_seqlens = torch.tensor(cu_chunk_lens, device=aftercnn_lens.device).cumsum(-1, dtype=torch.int32)
218
+
219
+ for encoder_layer in self.layers:
220
+ hidden_states = encoder_layer(hidden_states, cu_seqlens)[0]
221
+
222
+ hidden_states = self.ln_post(hidden_states)
223
+ hidden_states = self.proj1(hidden_states)
224
+ hidden_states = self.act(hidden_states)
225
+ hidden_states = self.proj2(hidden_states)
226
+ return BaseModelOutput(last_hidden_state=hidden_states)
227
+
228
+
229
+ __all__ = ["Qwen3ASRAudioEncoder", "Qwen3ASRAudioEncoderLayer", "Qwen3ASRAudioAttention"]
special_tokens_map.json ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "additional_special_tokens": [
3
+ "<|user|>",
4
+ "<|begin_of_audio|>",
5
+ "<|end_of_audio|>",
6
+ "<|assistant|>",
7
+ "<|system|>"
8
+ ],
9
+ "eos_token": {
10
+ "content": "<|im_end|>",
11
+ "lstrip": false,
12
+ "normalized": false,
13
+ "rstrip": false,
14
+ "single_word": false
15
+ },
16
+ "pad_token": {
17
+ "content": "<|endoftext|>",
18
+ "lstrip": false,
19
+ "normalized": false,
20
+ "rstrip": false,
21
+ "single_word": false
22
+ }
23
+ }
tokenizer.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:64ecaabc85a9272a8099f8aa7c097ce3a4c65a79bafbfd1450add736938ae45b
3
+ size 11419524
tokenizer_config.json ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "add_prefix_space": false,
3
+ "added_tokens_decoder": {
4
+ "151643": {
5
+ "content": "<|endoftext|>",
6
+ "lstrip": false,
7
+ "normalized": false,
8
+ "rstrip": false,
9
+ "single_word": false,
10
+ "special": true
11
+ },
12
+ "151644": {
13
+ "content": "<|im_start|>",
14
+ "lstrip": false,
15
+ "normalized": false,
16
+ "rstrip": false,
17
+ "single_word": false,
18
+ "special": true
19
+ },
20
+ "151645": {
21
+ "content": "<|im_end|>",
22
+ "lstrip": false,
23
+ "normalized": false,
24
+ "rstrip": false,
25
+ "single_word": false,
26
+ "special": true
27
+ },
28
+ "151646": {
29
+ "content": "<|audio|>",
30
+ "lstrip": false,
31
+ "normalized": true,
32
+ "rstrip": false,
33
+ "single_word": false,
34
+ "special": false
35
+ },
36
+ "151647": {
37
+ "content": "<|user|>",
38
+ "lstrip": false,
39
+ "normalized": false,
40
+ "rstrip": false,
41
+ "single_word": false,
42
+ "special": true
43
+ },
44
+ "151648": {
45
+ "content": "<|begin_of_audio|>",
46
+ "lstrip": false,
47
+ "normalized": false,
48
+ "rstrip": false,
49
+ "single_word": false,
50
+ "special": true
51
+ },
52
+ "151649": {
53
+ "content": "<|end_of_audio|>",
54
+ "lstrip": false,
55
+ "normalized": false,
56
+ "rstrip": false,
57
+ "single_word": false,
58
+ "special": true
59
+ },
60
+ "151650": {
61
+ "content": "<|assistant|>",
62
+ "lstrip": false,
63
+ "normalized": false,
64
+ "rstrip": false,
65
+ "single_word": false,
66
+ "special": true
67
+ },
68
+ "151651": {
69
+ "content": "<|system|>",
70
+ "lstrip": false,
71
+ "normalized": false,
72
+ "rstrip": false,
73
+ "single_word": false,
74
+ "special": true
75
+ }
76
+ },
77
+ "additional_special_tokens": [
78
+ "<|user|>",
79
+ "<|begin_of_audio|>",
80
+ "<|end_of_audio|>",
81
+ "<|assistant|>",
82
+ "<|system|>"
83
+ ],
84
+ "auto_map": {
85
+ "AutoProcessor": "processing_arkasr.ArkasrProcessor"
86
+ },
87
+ "bos_token": null,
88
+ "clean_up_tokenization_spaces": false,
89
+ "eos_token": "<|im_end|>",
90
+ "errors": "replace",
91
+ "extra_special_tokens": {},
92
+ "model_max_length": 32768,
93
+ "pad_token": "<|endoftext|>",
94
+ "processor_class": "ArkasrProcessor",
95
+ "split_special_tokens": false,
96
+ "tokenizer_class": "Qwen2Tokenizer",
97
+ "unk_token": null
98
+ }
vocab.json ADDED
The diff for this file is too large to render. See raw diff