Publish Audio8 ASR ONNX runtime
Browse files- .gitattributes +4 -33
- .gitignore +13 -0
- LICENSE +201 -0
- README.md +306 -0
- asr_onnx_runtime.py +735 -0
- hotword/__init__.py +1 -0
- hotword/hotword_trie.py +163 -0
- measure_precision_memory.py +147 -0
- model_bundle/added_tokens.json +11 -0
- model_bundle/audio8_audio_wrapper_config.json +11 -0
- model_bundle/audio_hidden.onnx +3 -0
- model_bundle/audio_hidden_int8.onnx +3 -0
- model_bundle/chat_template.jinja +6 -0
- model_bundle/config.json +196 -0
- model_bundle/generation_config.json +7 -0
- model_bundle/lm_cache_decode.onnx +3 -0
- model_bundle/lm_cache_decode_int4.onnx +3 -0
- model_bundle/lm_cache_decode_int4.onnx.data +3 -0
- model_bundle/lm_cache_decode_int8.onnx +3 -0
- model_bundle/lm_cache_decode_int8.onnx.data +3 -0
- model_bundle/lm_cache_prefill.onnx +3 -0
- model_bundle/lm_cache_prefill_int4.onnx +3 -0
- model_bundle/lm_cache_prefill_int4.onnx.data +3 -0
- model_bundle/lm_cache_prefill_int8.onnx +3 -0
- model_bundle/lm_cache_prefill_int8.onnx.data +3 -0
- model_bundle/lm_logits.onnx +3 -0
- model_bundle/merges.txt +0 -0
- model_bundle/metadata.json +105 -0
- model_bundle/preprocessor_config.json +15 -0
- model_bundle/processor_config.json +6 -0
- model_bundle/qwen3_asr_feature_extractor/preprocessor_config.json +15 -0
- model_bundle/special_tokens_map.json +23 -0
- model_bundle/tokenizer.json +3 -0
- model_bundle/tokenizer_config.json +96 -0
- model_bundle/vocab.json +0 -0
- model_bundle/weights/audio_projector.npz +3 -0
- model_bundle/weights/token_embedding.npy +3 -0
- requirements-onnx.txt +11 -0
- run_local.sh +28 -0
- server.py +359 -0
- smoke_test.sh +25 -0
- static/app.js +459 -0
- static/index.html +121 -0
- static/styles.css +391 -0
- transcribe_file.py +119 -0
.gitattributes
CHANGED
|
@@ -1,35 +1,6 @@
|
|
| 1 |
-
*.
|
| 2 |
-
*.
|
| 3 |
-
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
-
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
-
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
-
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
-
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
-
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
-
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
-
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
-
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
-
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
-
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
-
*.
|
| 17 |
-
|
| 18 |
-
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
-
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
-
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
-
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
-
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
-
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
-
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
-
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
-
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
-
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
-
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
-
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
-
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
-
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
-
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
-
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
-
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
-
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 1 |
+
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.data filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 4 |
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.wav filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
model_bundle/tokenizer.json filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.gitignore
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.DS_Store
|
| 2 |
+
__MACOSX/
|
| 3 |
+
._*
|
| 4 |
+
__pycache__/
|
| 5 |
+
*.pyc
|
| 6 |
+
*.pyo
|
| 7 |
+
*.log
|
| 8 |
+
*.pid
|
| 9 |
+
.env
|
| 10 |
+
.venv/
|
| 11 |
+
venv/
|
| 12 |
+
dist/
|
| 13 |
+
build/
|
LICENSE
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Apache License
|
| 2 |
+
Version 2.0, January 2004
|
| 3 |
+
http://www.apache.org/licenses/
|
| 4 |
+
|
| 5 |
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
| 6 |
+
|
| 7 |
+
1. Definitions.
|
| 8 |
+
|
| 9 |
+
"License" shall mean the terms and conditions for use, reproduction,
|
| 10 |
+
and distribution as defined by Sections 1 through 9 of this document.
|
| 11 |
+
|
| 12 |
+
"Licensor" shall mean the copyright owner or entity authorized by
|
| 13 |
+
the copyright owner that is granting the License.
|
| 14 |
+
|
| 15 |
+
"Legal Entity" shall mean the union of the acting entity and all
|
| 16 |
+
other entities that control, are controlled by, or are under common
|
| 17 |
+
control with that entity. For the purposes of this definition,
|
| 18 |
+
"control" means (i) the power, direct or indirect, to cause the
|
| 19 |
+
direction or management of such entity, whether by contract or
|
| 20 |
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
| 21 |
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
| 22 |
+
|
| 23 |
+
"You" (or "Your") shall mean an individual or Legal Entity
|
| 24 |
+
exercising permissions granted by this License.
|
| 25 |
+
|
| 26 |
+
"Source" form shall mean the preferred form for making modifications,
|
| 27 |
+
including but not limited to software source code, documentation
|
| 28 |
+
source, and configuration files.
|
| 29 |
+
|
| 30 |
+
"Object" form shall mean any form resulting from mechanical
|
| 31 |
+
transformation or translation of a Source form, including but
|
| 32 |
+
not limited to compiled object code, generated documentation,
|
| 33 |
+
and conversions to other media types.
|
| 34 |
+
|
| 35 |
+
"Work" shall mean the work of authorship, whether in Source or
|
| 36 |
+
Object form, made available under the License, as indicated by a
|
| 37 |
+
copyright notice that is included in or attached to the work
|
| 38 |
+
(an example is provided in the Appendix below).
|
| 39 |
+
|
| 40 |
+
"Derivative Works" shall mean any work, whether in Source or Object
|
| 41 |
+
form, that is based on (or derived from) the Work and for which the
|
| 42 |
+
editorial revisions, annotations, elaborations, or other modifications
|
| 43 |
+
represent, as a whole, an original work of authorship. For the purposes
|
| 44 |
+
of this License, Derivative Works shall not include works that remain
|
| 45 |
+
separable from, or merely link (or bind by name) to the interfaces of,
|
| 46 |
+
the Work and Derivative Works thereof.
|
| 47 |
+
|
| 48 |
+
"Contribution" shall mean any work of authorship, including
|
| 49 |
+
the original version of the Work and any modifications or additions
|
| 50 |
+
to that Work or Derivative Works thereof, that is intentionally
|
| 51 |
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
| 52 |
+
or by an individual or Legal Entity authorized to submit on behalf of
|
| 53 |
+
the copyright owner. For the purposes of this definition, "submitted"
|
| 54 |
+
means any form of electronic, verbal, or written communication sent
|
| 55 |
+
to the Licensor or its representatives, including but not limited to
|
| 56 |
+
communication on electronic mailing lists, source code control systems,
|
| 57 |
+
and issue tracking systems that are managed by, or on behalf of, the
|
| 58 |
+
Licensor for the purpose of discussing and improving the Work, but
|
| 59 |
+
excluding communication that is conspicuously marked or otherwise
|
| 60 |
+
designated in writing by the copyright owner as "Not a Contribution."
|
| 61 |
+
|
| 62 |
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
| 63 |
+
on behalf of whom a Contribution has been received by Licensor and
|
| 64 |
+
subsequently incorporated within the Work.
|
| 65 |
+
|
| 66 |
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
| 67 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 68 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 69 |
+
copyright license to reproduce, prepare Derivative Works of,
|
| 70 |
+
publicly display, publicly perform, sublicense, and distribute the
|
| 71 |
+
Work and such Derivative Works in Source or Object form.
|
| 72 |
+
|
| 73 |
+
3. Grant of Patent License. Subject to the terms and conditions of
|
| 74 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 75 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 76 |
+
(except as stated in this section) patent license to make, have made,
|
| 77 |
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
| 78 |
+
where such license applies only to those patent claims licensable
|
| 79 |
+
by such Contributor that are necessarily infringed by their
|
| 80 |
+
Contribution(s) alone or by combination of their Contribution(s)
|
| 81 |
+
with the Work to which such Contribution(s) was submitted. If You
|
| 82 |
+
institute patent litigation against any entity (including a
|
| 83 |
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
| 84 |
+
or a Contribution incorporated within the Work constitutes direct
|
| 85 |
+
or contributory patent infringement, then any patent licenses
|
| 86 |
+
granted to You under this License for that Work shall terminate
|
| 87 |
+
as of the date such litigation is filed.
|
| 88 |
+
|
| 89 |
+
4. Redistribution. You may reproduce and distribute copies of the
|
| 90 |
+
Work or Derivative Works thereof in any medium, with or without
|
| 91 |
+
modifications, and in Source or Object form, provided that You
|
| 92 |
+
meet the following conditions:
|
| 93 |
+
|
| 94 |
+
(a) You must give any other recipients of the Work or
|
| 95 |
+
Derivative Works a copy of this License; and
|
| 96 |
+
|
| 97 |
+
(b) You must cause any modified files to carry prominent notices
|
| 98 |
+
stating that You changed the files; and
|
| 99 |
+
|
| 100 |
+
(c) You must retain, in the Source form of any Derivative Works
|
| 101 |
+
that You distribute, all copyright, patent, trademark, and
|
| 102 |
+
attribution notices from the Source form of the Work,
|
| 103 |
+
excluding those notices that do not pertain to any part of
|
| 104 |
+
the Derivative Works; and
|
| 105 |
+
|
| 106 |
+
(d) If the Work includes a "NOTICE" text file as part of its
|
| 107 |
+
distribution, then any Derivative Works that You distribute must
|
| 108 |
+
include a readable copy of the attribution notices contained
|
| 109 |
+
within such NOTICE file, excluding those notices that do not
|
| 110 |
+
pertain to any part of the Derivative Works, in at least one
|
| 111 |
+
of the following places: within a NOTICE text file distributed
|
| 112 |
+
as part of the Derivative Works; within the Source form or
|
| 113 |
+
documentation, if provided along with the Derivative Works; or,
|
| 114 |
+
within a display generated by the Derivative Works, if and
|
| 115 |
+
wherever such third-party notices normally appear. The contents
|
| 116 |
+
of the NOTICE file are for informational purposes only and
|
| 117 |
+
do not modify the License. You may add Your own attribution
|
| 118 |
+
notices within Derivative Works that You distribute, alongside
|
| 119 |
+
or as an addendum to the NOTICE text from the Work, provided
|
| 120 |
+
that such additional attribution notices cannot be construed
|
| 121 |
+
as modifying the License.
|
| 122 |
+
|
| 123 |
+
You may add Your own copyright statement to Your modifications and
|
| 124 |
+
may provide additional or different license terms and conditions
|
| 125 |
+
for use, reproduction, or distribution of Your modifications, or
|
| 126 |
+
for any such Derivative Works as a whole, provided Your use,
|
| 127 |
+
reproduction, and distribution of the Work otherwise complies with
|
| 128 |
+
the conditions stated in this License.
|
| 129 |
+
|
| 130 |
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
| 131 |
+
any Contribution intentionally submitted for inclusion in the Work
|
| 132 |
+
by You to the Licensor shall be under the terms and conditions of
|
| 133 |
+
this License, without any additional terms or conditions.
|
| 134 |
+
Notwithstanding the above, nothing herein shall supersede or modify
|
| 135 |
+
the terms of any separate license agreement you may have executed
|
| 136 |
+
with Licensor regarding such Contributions.
|
| 137 |
+
|
| 138 |
+
6. Trademarks. This License does not grant permission to use the trade
|
| 139 |
+
names, trademarks, service marks, or product names of the Licensor,
|
| 140 |
+
except as required for reasonable and customary use in describing the
|
| 141 |
+
origin of the Work and reproducing the content of the NOTICE file.
|
| 142 |
+
|
| 143 |
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
| 144 |
+
agreed to in writing, Licensor provides the Work (and each
|
| 145 |
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
| 146 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
| 147 |
+
implied, including, without limitation, any warranties or conditions
|
| 148 |
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
| 149 |
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
| 150 |
+
appropriateness of using or redistributing the Work and assume any
|
| 151 |
+
risks associated with Your exercise of permissions under this License.
|
| 152 |
+
|
| 153 |
+
8. Limitation of Liability. In no event and under no legal theory,
|
| 154 |
+
whether in tort (including negligence), contract, or otherwise,
|
| 155 |
+
unless required by applicable law (such as deliberate and grossly
|
| 156 |
+
negligent acts) or agreed to in writing, shall any Contributor be
|
| 157 |
+
liable to You for damages, including any direct, indirect, special,
|
| 158 |
+
incidental, or consequential damages of any character arising as a
|
| 159 |
+
result of this License or out of the use or inability to use the
|
| 160 |
+
Work (including but not limited to damages for loss of goodwill,
|
| 161 |
+
work stoppage, computer failure or malfunction, or any and all
|
| 162 |
+
other commercial damages or losses), even if such Contributor
|
| 163 |
+
has been advised of the possibility of such damages.
|
| 164 |
+
|
| 165 |
+
9. Accepting Warranty or Additional Liability. While redistributing
|
| 166 |
+
the Work or Derivative Works thereof, You may choose to offer,
|
| 167 |
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
| 168 |
+
or other liability obligations and/or rights consistent with this
|
| 169 |
+
License. However, in accepting such obligations, You may act only
|
| 170 |
+
on Your own behalf and on Your sole responsibility, not on behalf
|
| 171 |
+
of any other Contributor, and only if You agree to indemnify,
|
| 172 |
+
defend, and hold each Contributor harmless for any liability
|
| 173 |
+
incurred by, or claims asserted against, such Contributor by reason
|
| 174 |
+
of your accepting any such warranty or additional liability.
|
| 175 |
+
|
| 176 |
+
END OF TERMS AND CONDITIONS
|
| 177 |
+
|
| 178 |
+
APPENDIX: How to apply the Apache License to your work.
|
| 179 |
+
|
| 180 |
+
To apply the Apache License to your work, attach the following
|
| 181 |
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
| 182 |
+
replaced with your own identifying information. (Do not include
|
| 183 |
+
the brackets!) The text should be enclosed in the appropriate
|
| 184 |
+
comment syntax for the file format. We also recommend that a
|
| 185 |
+
file or class name and description of purpose be included on the
|
| 186 |
+
same "printed page" as the copyright notice for easier
|
| 187 |
+
identification within third-party archives.
|
| 188 |
+
|
| 189 |
+
Copyright [yyyy] [name of copyright owner]
|
| 190 |
+
|
| 191 |
+
Licensed under the Apache License, Version 2.0 (the "License");
|
| 192 |
+
you may not use this file except in compliance with the License.
|
| 193 |
+
You may obtain a copy of the License at
|
| 194 |
+
|
| 195 |
+
http://www.apache.org/licenses/LICENSE-2.0
|
| 196 |
+
|
| 197 |
+
Unless required by applicable law or agreed to in writing, software
|
| 198 |
+
distributed under the License is distributed on an "AS IS" BASIS,
|
| 199 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 200 |
+
See the License for the specific language governing permissions and
|
| 201 |
+
limitations under the License.
|
README.md
CHANGED
|
@@ -1,3 +1,309 @@
|
|
| 1 |
---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
license: apache-2.0
|
|
|
|
| 3 |
---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
tags:
|
| 3 |
+
- automatic-speech-recognition
|
| 4 |
+
- audio
|
| 5 |
+
- asr
|
| 6 |
+
- onnx
|
| 7 |
+
- onnxruntime
|
| 8 |
+
- quantized
|
| 9 |
+
- int8
|
| 10 |
+
- int4
|
| 11 |
+
language:
|
| 12 |
+
- zh
|
| 13 |
+
library_name: onnx
|
| 14 |
+
pipeline_tag: automatic-speech-recognition
|
| 15 |
license: apache-2.0
|
| 16 |
+
repository: https://github.com/AutoArk/open-audio-opd
|
| 17 |
---
|
| 18 |
+
|
| 19 |
+
<div align="center">
|
| 20 |
+
|
| 21 |
+
# Audio8-ASR-0.1B ONNX Runtime
|
| 22 |
+
|
| 23 |
+
[](https://github.com/AutoArk/open-audio-opd)
|
| 24 |
+
[](https://arxiv.org/abs/2605.28139)
|
| 25 |
+
[](https://www.apache.org/licenses/LICENSE-2.0)
|
| 26 |
+
|
| 27 |
+
</div>
|
| 28 |
+
|
| 29 |
+
Audio8-ASR-0.1B ONNX Runtime is a self-contained local inference package for
|
| 30 |
+
Chinese automatic speech recognition. It includes ONNX Runtime inference code,
|
| 31 |
+
a browser UI, a local HTTP API, tokenizer/config files, and decoder/audio-head
|
| 32 |
+
precision variants.
|
| 33 |
+
|
| 34 |
+
This repository does not require the original training repository or a separate
|
| 35 |
+
source checkpoint at runtime. Everything needed for CPU ONNX inference is in
|
| 36 |
+
`model_bundle/`.
|
| 37 |
+
|
| 38 |
+
This repository is intended to be used through the included ONNX Runtime code.
|
| 39 |
+
It is not a Transformers `AutoModel` source release.
|
| 40 |
+
|
| 41 |
+
## Related Repositories
|
| 42 |
+
|
| 43 |
+
- [Audio8-ASR-0.1B](https://huggingface.co/AutoArk-AI/Audio8-ASR-0.1B): main open-source model repository.
|
| 44 |
+
- [Audio8-ASR-0.1B-iOS-ANE](https://huggingface.co/AutoArk-AI/Audio8-ASR-0.1B-iOS-ANE): iPhone-ready, out-of-the-box ASR demo and Swift SDK. The demo is designed to keep runtime memory footprint around 200 MB on device.
|
| 45 |
+
- [AutoArk/open-audio-opd](https://github.com/AutoArk/open-audio-opd): shared GitHub project for Audio8 open-source releases.
|
| 46 |
+
|
| 47 |
+
## Contents
|
| 48 |
+
|
| 49 |
+
- `model_bundle/`: tokenizer, feature extractor config, ONNX graphs, and numpy weights.
|
| 50 |
+
- `asr_onnx_runtime.py`: ONNX Runtime ASR engine.
|
| 51 |
+
- `server.py`: FastAPI local web/API server.
|
| 52 |
+
- `static/`: browser UI with file upload, microphone recording, precision switching, hotwords, and memory panels.
|
| 53 |
+
- `transcribe_file.py`: single-file CLI and minimal Python helper.
|
| 54 |
+
- `hotword/`: optional decode-time hotword trie boosting helper.
|
| 55 |
+
- `run_local.sh`: local WebUI launch helper.
|
| 56 |
+
- `smoke_test.sh`: health + ASR API smoke test for a user-provided audio file.
|
| 57 |
+
- `measure_precision_memory.py`: optional fresh-process RSS measurement helper.
|
| 58 |
+
|
| 59 |
+
## Included ONNX Variants
|
| 60 |
+
|
| 61 |
+
Decoder cache graphs:
|
| 62 |
+
|
| 63 |
+
- `fp32`: `lm_cache_prefill.onnx`, `lm_cache_decode.onnx`
|
| 64 |
+
- `int8`: `lm_cache_prefill_int8.onnx`, `lm_cache_decode_int8.onnx`
|
| 65 |
+
- `int4`: `lm_cache_prefill_int4.onnx`, `lm_cache_decode_int4.onnx`
|
| 66 |
+
|
| 67 |
+
Audio tower graphs:
|
| 68 |
+
|
| 69 |
+
- `fp32`: `audio_hidden.onnx`
|
| 70 |
+
- `int8`: `audio_hidden_int8.onnx`
|
| 71 |
+
|
| 72 |
+
The default runtime path is decoder `int8` plus audio tower `int8`. Decoder
|
| 73 |
+
`int4` is included for lower peak memory, while decoder `fp32` is included as a
|
| 74 |
+
full-precision reference path.
|
| 75 |
+
|
| 76 |
+
## Install
|
| 77 |
+
|
| 78 |
+
Use Python 3.10+. Python 3.12 is recommended.
|
| 79 |
+
|
| 80 |
+
```bash
|
| 81 |
+
python3.12 -m venv .venv
|
| 82 |
+
source .venv/bin/activate
|
| 83 |
+
python3 -m pip install --upgrade pip
|
| 84 |
+
python3 -m pip install -r requirements-onnx.txt
|
| 85 |
+
```
|
| 86 |
+
|
| 87 |
+
With `uv`:
|
| 88 |
+
|
| 89 |
+
```bash
|
| 90 |
+
uv venv --python 3.12 .venv
|
| 91 |
+
uv pip install --python .venv/bin/python -r requirements-onnx.txt
|
| 92 |
+
source .venv/bin/activate
|
| 93 |
+
```
|
| 94 |
+
|
| 95 |
+
With conda:
|
| 96 |
+
|
| 97 |
+
```bash
|
| 98 |
+
conda create -n audio8-asr-onnx python=3.12
|
| 99 |
+
conda activate audio8-asr-onnx
|
| 100 |
+
python3 -m pip install -r requirements-onnx.txt
|
| 101 |
+
```
|
| 102 |
+
|
| 103 |
+
## Run WebUI
|
| 104 |
+
|
| 105 |
+
```bash
|
| 106 |
+
./run_local.sh
|
| 107 |
+
```
|
| 108 |
+
|
| 109 |
+
Open:
|
| 110 |
+
|
| 111 |
+
```text
|
| 112 |
+
http://127.0.0.1:7860
|
| 113 |
+
```
|
| 114 |
+
|
| 115 |
+
If the port is busy:
|
| 116 |
+
|
| 117 |
+
```bash
|
| 118 |
+
PORT=7870 ./run_local.sh
|
| 119 |
+
```
|
| 120 |
+
|
| 121 |
+
## Command Line
|
| 122 |
+
|
| 123 |
+
Transcribe one local audio file without starting the WebUI:
|
| 124 |
+
|
| 125 |
+
```bash
|
| 126 |
+
python3 transcribe_file.py /path/to/audio.wav --max_new_tokens 128
|
| 127 |
+
```
|
| 128 |
+
|
| 129 |
+
Print the full result JSON:
|
| 130 |
+
|
| 131 |
+
```bash
|
| 132 |
+
python3 transcribe_file.py /path/to/audio.wav --json
|
| 133 |
+
```
|
| 134 |
+
|
| 135 |
+
Force a precision combination:
|
| 136 |
+
|
| 137 |
+
```bash
|
| 138 |
+
python3 transcribe_file.py /path/to/audio.wav \
|
| 139 |
+
--cache_precision int8 \
|
| 140 |
+
--audio_precision int8
|
| 141 |
+
```
|
| 142 |
+
|
| 143 |
+
Enable optional hotword biasing:
|
| 144 |
+
|
| 145 |
+
```bash
|
| 146 |
+
python3 transcribe_file.py /path/to/audio.wav \
|
| 147 |
+
--hotwords "term_one,term_two" \
|
| 148 |
+
--json
|
| 149 |
+
```
|
| 150 |
+
|
| 151 |
+
## Use From Python
|
| 152 |
+
|
| 153 |
+
```python
|
| 154 |
+
from pathlib import Path
|
| 155 |
+
|
| 156 |
+
from asr_onnx_runtime import OnnxCacheAsrEngine
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
engine = OnnxCacheAsrEngine(
|
| 160 |
+
"model_bundle",
|
| 161 |
+
cache_precision="int8",
|
| 162 |
+
audio_precision="int8",
|
| 163 |
+
)
|
| 164 |
+
result = engine.transcribe(
|
| 165 |
+
Path("/path/to/audio.wav").read_bytes(),
|
| 166 |
+
language="Chinese",
|
| 167 |
+
max_new_tokens=128,
|
| 168 |
+
hotwords=None,
|
| 169 |
+
)
|
| 170 |
+
print(result["text"])
|
| 171 |
+
```
|
| 172 |
+
|
| 173 |
+
The lower-level `OnnxAsrEngine` class is available for the full-context
|
| 174 |
+
fallback graph. Prefer `OnnxCacheAsrEngine` for normal local inference.
|
| 175 |
+
|
| 176 |
+
## HTTP API
|
| 177 |
+
|
| 178 |
+
Start the server with `./run_local.sh`, then call `POST /asr` with multipart
|
| 179 |
+
form data:
|
| 180 |
+
|
| 181 |
+
```bash
|
| 182 |
+
curl --noproxy "*" -fsS -X POST http://127.0.0.1:7860/asr \
|
| 183 |
+
-F "audio=@/path/to/audio.wav" \
|
| 184 |
+
-F "language=Chinese" \
|
| 185 |
+
-F "max_new_tokens=128" \
|
| 186 |
+
-F "cache_precision=int8" \
|
| 187 |
+
-F "audio_precision=int8" \
|
| 188 |
+
| python3 -m json.tool
|
| 189 |
+
```
|
| 190 |
+
|
| 191 |
+
Form fields:
|
| 192 |
+
|
| 193 |
+
- `audio`: required audio file. WAV is recommended; `librosa`/`soundfile` handle common formats.
|
| 194 |
+
- `language`: optional, currently intended for `Chinese`.
|
| 195 |
+
- `max_new_tokens`: optional generation cap; default is `128`.
|
| 196 |
+
- `cache_precision`: optional decoder precision, one of `fp32`, `int8`, `int4`, `auto`.
|
| 197 |
+
- `audio_precision`: optional audio tower precision, one of `fp32`, `int8`, `auto`.
|
| 198 |
+
- `hotwords`: optional comma-separated hotwords. Omit or leave empty to disable.
|
| 199 |
+
- `hotword_topk`: optional top-k gate for applying boosts; default is `50`.
|
| 200 |
+
- `hotword_start_boost`: optional first-token boost; default is `6.0`.
|
| 201 |
+
- `hotword_continuation_boost`: optional continuation-token boost; default is `8.0`.
|
| 202 |
+
|
| 203 |
+
Useful endpoints:
|
| 204 |
+
|
| 205 |
+
- `GET /health`: readiness and selected runtime.
|
| 206 |
+
- `GET /api/runtime`: selected graphs, provider, and available precision variants.
|
| 207 |
+
- `POST /api/reload`: switch backend/precision without restarting the process.
|
| 208 |
+
- `GET /metrics`: process/system memory metrics plus runtime info.
|
| 209 |
+
|
| 210 |
+
Important response fields:
|
| 211 |
+
|
| 212 |
+
- `text`: normalized transcript for application use.
|
| 213 |
+
- `raw`: raw decoded model text before normalization.
|
| 214 |
+
- `elapsed_seconds`: inference time inside the runtime.
|
| 215 |
+
- `audio_seconds`: decoded audio duration after loading/resampling.
|
| 216 |
+
- `generated_tokens`, `hit_stop`, `stop_token_id`: generation diagnostics.
|
| 217 |
+
- `backend`, `cache_precision`, `audio_precision`, `providers`: selected runtime path.
|
| 218 |
+
- `request_peak_rss_bytes`: latest request RSS high-water mark.
|
| 219 |
+
- `hotword`: hotword tokenization/boost metadata when hotwords are enabled, otherwise `null`.
|
| 220 |
+
|
| 221 |
+
## Hotwords
|
| 222 |
+
|
| 223 |
+
Hotwords are an opt-in decode-time feature. They do not change model weights,
|
| 224 |
+
ONNX graphs, or the prompt. The runtime tokenizes each hotword with the bundled
|
| 225 |
+
tokenizer, builds a prefix trie, and adds a top-k gated logit boost during
|
| 226 |
+
decoding. If no hotwords are provided, the decode path is unchanged except that
|
| 227 |
+
the response includes `"hotword": null`.
|
| 228 |
+
|
| 229 |
+
The WebUI exposes two hotword strength levels:
|
| 230 |
+
|
| 231 |
+
- `Normal`: default logit boost.
|
| 232 |
+
- `Strong`: stronger biasing for difficult names or rare terms.
|
| 233 |
+
|
| 234 |
+
Strong hotword biasing may force incorrect hotwords, hallucinate, or repeat
|
| 235 |
+
text. Use it only when the target terms are known in advance.
|
| 236 |
+
|
| 237 |
+
## Runtime Defaults
|
| 238 |
+
|
| 239 |
+
```text
|
| 240 |
+
ASR_BACKEND=auto
|
| 241 |
+
ASR_CACHE_PRECISION=int8
|
| 242 |
+
ASR_AUDIO_PRECISION=int8
|
| 243 |
+
```
|
| 244 |
+
|
| 245 |
+
Available variants:
|
| 246 |
+
|
| 247 |
+
- decoder: `fp32`, `int8`, `int4`
|
| 248 |
+
- audio tower: `fp32`, `int8`
|
| 249 |
+
|
| 250 |
+
Force a specific combination:
|
| 251 |
+
|
| 252 |
+
```bash
|
| 253 |
+
ASR_BACKEND=onnx_cache ASR_CACHE_PRECISION=fp32 ASR_AUDIO_PRECISION=fp32 ./run_local.sh
|
| 254 |
+
ASR_BACKEND=onnx_cache ASR_CACHE_PRECISION=int8 ASR_AUDIO_PRECISION=int8 ./run_local.sh
|
| 255 |
+
ASR_BACKEND=onnx_cache ASR_CACHE_PRECISION=int4 ASR_AUDIO_PRECISION=int8 ./run_local.sh
|
| 256 |
+
```
|
| 257 |
+
|
| 258 |
+
## Runtime Limits
|
| 259 |
+
|
| 260 |
+
- Audio is loaded as mono and resampled to 16 kHz.
|
| 261 |
+
- Audio longer than 30 seconds is truncated by the runtime bundle metadata.
|
| 262 |
+
- Cached decoder context is capped at 512 total tokens. If prompt audio tokens
|
| 263 |
+
plus `max_new_tokens` exceed that limit, the runtime raises an error.
|
| 264 |
+
- CPU ONNX Runtime is the verified default path. GPU use requires installing a
|
| 265 |
+
compatible ONNX Runtime GPU package and selecting an available provider.
|
| 266 |
+
|
| 267 |
+
## License
|
| 268 |
+
|
| 269 |
+
This project is released under the Apache License 2.0. See `LICENSE`.
|
| 270 |
+
|
| 271 |
+
## Notes
|
| 272 |
+
|
| 273 |
+
- `requirements-onnx.txt` is pinned for reproducible local behavior.
|
| 274 |
+
- Runtime audio loading tries `librosa.load` first for consistent decoding.
|
| 275 |
+
- `run_local.sh` sets `NO_PROXY/no_proxy` for localhost inside the service
|
| 276 |
+
process only; it does not change system proxy settings.
|
| 277 |
+
- Browser recording uploads WAV/RIFF audio. The UI records PCM with Web Audio,
|
| 278 |
+
waits a short flush after Stop, then appends silence before encoding WAV.
|
| 279 |
+
- The UI memory panels report process RSS for CPU ONNX inference. `Peak RSS` is
|
| 280 |
+
the service high-water mark; `Request Peak` is the latest request peak.
|
| 281 |
+
|
| 282 |
+
## Quick Checks
|
| 283 |
+
|
| 284 |
+
Syntax/import check:
|
| 285 |
+
|
| 286 |
+
```bash
|
| 287 |
+
python3 -m py_compile \
|
| 288 |
+
asr_onnx_runtime.py \
|
| 289 |
+
server.py \
|
| 290 |
+
measure_precision_memory.py \
|
| 291 |
+
transcribe_file.py
|
| 292 |
+
```
|
| 293 |
+
|
| 294 |
+
API smoke test with your own audio file:
|
| 295 |
+
|
| 296 |
+
```bash
|
| 297 |
+
./run_local.sh
|
| 298 |
+
./smoke_test.sh 127.0.0.1 7860 /path/to/audio.wav
|
| 299 |
+
```
|
| 300 |
+
|
| 301 |
+
Run one precision memory measurement:
|
| 302 |
+
|
| 303 |
+
```bash
|
| 304 |
+
python3 measure_precision_memory.py \
|
| 305 |
+
--bundle_dir model_bundle \
|
| 306 |
+
--audio /path/to/audio.wav \
|
| 307 |
+
--cache_precision int8 \
|
| 308 |
+
--audio_precision int8
|
| 309 |
+
```
|
asr_onnx_runtime.py
ADDED
|
@@ -0,0 +1,735 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import io
|
| 4 |
+
import json
|
| 5 |
+
import math
|
| 6 |
+
import os
|
| 7 |
+
import re
|
| 8 |
+
import subprocess
|
| 9 |
+
import time
|
| 10 |
+
import wave
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
from typing import Any
|
| 13 |
+
|
| 14 |
+
import numpy as np
|
| 15 |
+
import onnxruntime as ort
|
| 16 |
+
import psutil
|
| 17 |
+
from tokenizers import Tokenizer
|
| 18 |
+
from transformers import WhisperFeatureExtractor
|
| 19 |
+
|
| 20 |
+
from hotword.hotword_trie import build_trie_from_hotwords, parse_hotwords
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
SPECIAL_TOKEN_PATTERN = re.compile(
|
| 24 |
+
r"<\|(?:"
|
| 25 |
+
r"bicodec_(?:semantic|global)_\d+|"
|
| 26 |
+
r"(?:start|end)_(?:global_token|glm_token|semantic_token|content)|"
|
| 27 |
+
r"[^>]+"
|
| 28 |
+
r")\|>"
|
| 29 |
+
)
|
| 30 |
+
TURN_END_MARKERS = ("<|user|>", "<|assistant|>", "<|im_end|>")
|
| 31 |
+
LEADING_NOISE_PATTERN = re.compile(r"^[\s,.;:!?-]+")
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def _resample_linear(audio: np.ndarray, orig_sr: int, target_sr: int) -> np.ndarray:
|
| 35 |
+
if int(orig_sr) == int(target_sr):
|
| 36 |
+
return audio.astype(np.float32, copy=False)
|
| 37 |
+
if audio.size == 0:
|
| 38 |
+
return audio.astype(np.float32, copy=False)
|
| 39 |
+
duration = float(audio.shape[0]) / float(orig_sr)
|
| 40 |
+
target_len = max(1, int(round(duration * float(target_sr))))
|
| 41 |
+
old_x = np.linspace(0.0, duration, num=audio.shape[0], endpoint=False)
|
| 42 |
+
new_x = np.linspace(0.0, duration, num=target_len, endpoint=False)
|
| 43 |
+
return np.interp(new_x, old_x, audio).astype(np.float32, copy=False)
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def load_audio_bytes(audio_bytes: bytes, sampling_rate: int) -> np.ndarray:
|
| 47 |
+
try:
|
| 48 |
+
import librosa
|
| 49 |
+
|
| 50 |
+
audio, _ = librosa.load(io.BytesIO(audio_bytes), sr=int(sampling_rate), mono=True)
|
| 51 |
+
return np.asarray(audio, dtype=np.float32)
|
| 52 |
+
except Exception:
|
| 53 |
+
pass
|
| 54 |
+
|
| 55 |
+
try:
|
| 56 |
+
import soundfile as sf
|
| 57 |
+
|
| 58 |
+
audio, sr = sf.read(io.BytesIO(audio_bytes), dtype="float32", always_2d=False)
|
| 59 |
+
if audio.ndim > 1:
|
| 60 |
+
audio = audio.mean(axis=-1)
|
| 61 |
+
return _resample_linear(np.asarray(audio, dtype=np.float32), int(sr), int(sampling_rate))
|
| 62 |
+
except Exception:
|
| 63 |
+
pass
|
| 64 |
+
|
| 65 |
+
with wave.open(io.BytesIO(audio_bytes), "rb") as wav:
|
| 66 |
+
sr = int(wav.getframerate())
|
| 67 |
+
channels = int(wav.getnchannels())
|
| 68 |
+
sample_width = int(wav.getsampwidth())
|
| 69 |
+
raw = wav.readframes(wav.getnframes())
|
| 70 |
+
if sample_width != 2:
|
| 71 |
+
raise ValueError(f"Fallback wave loader only supports 16-bit PCM WAV, got sample_width={sample_width}")
|
| 72 |
+
audio = np.frombuffer(raw, dtype="<i2").astype(np.float32) / 32768.0
|
| 73 |
+
if channels > 1:
|
| 74 |
+
audio = audio.reshape(-1, channels).mean(axis=-1)
|
| 75 |
+
return _resample_linear(audio, sr, int(sampling_rate))
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def truncate_generation_text(text: str) -> str:
|
| 79 |
+
cut = len(text)
|
| 80 |
+
for marker in TURN_END_MARKERS:
|
| 81 |
+
index = text.find(marker)
|
| 82 |
+
if index != -1 and index < cut:
|
| 83 |
+
cut = index
|
| 84 |
+
return text[:cut].strip()
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def normalize_prediction_text(text: str) -> str:
|
| 88 |
+
if not text:
|
| 89 |
+
return ""
|
| 90 |
+
text = truncate_generation_text(text)
|
| 91 |
+
if "<|text|>" in text:
|
| 92 |
+
text = text.split("<|text|>", 1)[1]
|
| 93 |
+
if "<asr_text>" in text:
|
| 94 |
+
text = text.split("<asr_text>", 1)[1]
|
| 95 |
+
text = re.sub(r"^\s*language\s+[A-Za-z]+\s+", "", text)
|
| 96 |
+
text = SPECIAL_TOKEN_PATTERN.sub("", text).strip()
|
| 97 |
+
text = re.sub(r"\s+", " ", text).strip()
|
| 98 |
+
return LEADING_NOISE_PATTERN.sub("", text).strip()
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def create_ort_session_options(intra_op_num_threads: int | None = None) -> ort.SessionOptions:
|
| 102 |
+
options = ort.SessionOptions()
|
| 103 |
+
if intra_op_num_threads is not None and int(intra_op_num_threads) > 0:
|
| 104 |
+
options.intra_op_num_threads = int(intra_op_num_threads)
|
| 105 |
+
options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
|
| 106 |
+
if os.environ.get("ORT_CPU_MEM_ARENA", "0").lower() not in {"1", "true", "yes", "on"}:
|
| 107 |
+
options.enable_cpu_mem_arena = False
|
| 108 |
+
if os.environ.get("ORT_MEM_PATTERN", "0").lower() not in {"1", "true", "yes", "on"}:
|
| 109 |
+
options.enable_mem_pattern = False
|
| 110 |
+
return options
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
def ark_audio_token_count(sample_count: int, *, hop_length: int, merge_factor: int) -> int:
|
| 114 |
+
mel_frames = int(sample_count) // max(int(hop_length), 1)
|
| 115 |
+
downsampled = (int(mel_frames) + 1) // 2
|
| 116 |
+
merged = downsampled // max(int(merge_factor), 1)
|
| 117 |
+
return max(int(merged), 1)
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def layer_norm(x: np.ndarray, weight: np.ndarray, bias: np.ndarray, eps: float = 1e-5) -> np.ndarray:
|
| 121 |
+
x32 = x.astype(np.float32, copy=False)
|
| 122 |
+
mean = x32.mean(axis=-1, keepdims=True)
|
| 123 |
+
var = ((x32 - mean) ** 2).mean(axis=-1, keepdims=True)
|
| 124 |
+
return ((x32 - mean) / np.sqrt(var + eps)) * weight + bias
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
def adaptive_avg_pool_time(x: np.ndarray, output_size: int) -> np.ndarray:
|
| 128 |
+
input_size = int(x.shape[0])
|
| 129 |
+
output_size = int(output_size)
|
| 130 |
+
if input_size == output_size:
|
| 131 |
+
return x.astype(np.float32, copy=False)
|
| 132 |
+
pooled = np.empty((output_size, x.shape[1]), dtype=np.float32)
|
| 133 |
+
for out_i in range(output_size):
|
| 134 |
+
start = int(math.floor(out_i * input_size / output_size))
|
| 135 |
+
end = int(math.ceil((out_i + 1) * input_size / output_size))
|
| 136 |
+
end = max(end, start + 1)
|
| 137 |
+
pooled[out_i] = x[start:end].mean(axis=0)
|
| 138 |
+
return pooled
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def apply_repetition_penalty(logits: np.ndarray, token_ids: list[int], penalty: float) -> np.ndarray:
|
| 142 |
+
if penalty == 1.0:
|
| 143 |
+
return logits
|
| 144 |
+
for token_id in set(int(value) for value in token_ids):
|
| 145 |
+
if 0 <= token_id < logits.shape[-1]:
|
| 146 |
+
logits[token_id] = logits[token_id] * penalty if logits[token_id] < 0 else logits[token_id] / penalty
|
| 147 |
+
return logits
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
def softmax(x: np.ndarray) -> np.ndarray:
|
| 151 |
+
shifted = x - np.max(x)
|
| 152 |
+
exp = np.exp(shifted)
|
| 153 |
+
return exp / np.sum(exp)
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
class OnnxAsrEngine:
|
| 157 |
+
def __init__(
|
| 158 |
+
self,
|
| 159 |
+
bundle_dir: str | Path,
|
| 160 |
+
*,
|
| 161 |
+
provider: str = "CPUExecutionProvider",
|
| 162 |
+
intra_op_num_threads: int | None = None,
|
| 163 |
+
load_lm_session: bool = True,
|
| 164 |
+
audio_precision: str = "fp32",
|
| 165 |
+
) -> None:
|
| 166 |
+
self.bundle_dir = Path(bundle_dir).expanduser().resolve()
|
| 167 |
+
with (self.bundle_dir / "metadata.json").open("r", encoding="utf-8") as handle:
|
| 168 |
+
self.metadata = json.load(handle)
|
| 169 |
+
self.audio_precision = str(audio_precision or "fp32").lower().strip()
|
| 170 |
+
|
| 171 |
+
options = create_ort_session_options(intra_op_num_threads)
|
| 172 |
+
providers = [provider] if provider in ort.get_available_providers() else ["CPUExecutionProvider"]
|
| 173 |
+
if "CPUExecutionProvider" not in providers:
|
| 174 |
+
providers.append("CPUExecutionProvider")
|
| 175 |
+
audio_graph_key = "audio_hidden"
|
| 176 |
+
if self.audio_precision in {"int8", "auto"} and "audio_hidden_int8" in self.metadata.get("graphs", {}):
|
| 177 |
+
audio_graph_key = "audio_hidden_int8"
|
| 178 |
+
self.audio_precision = "int8"
|
| 179 |
+
else:
|
| 180 |
+
self.audio_precision = "fp32"
|
| 181 |
+
self.audio_graph = self.metadata["graphs"][audio_graph_key]
|
| 182 |
+
self.audio_graph_path = self.bundle_dir / self.audio_graph["path"]
|
| 183 |
+
|
| 184 |
+
self.audio_session = ort.InferenceSession(
|
| 185 |
+
str(self.audio_graph_path),
|
| 186 |
+
sess_options=options,
|
| 187 |
+
providers=providers,
|
| 188 |
+
)
|
| 189 |
+
self.lm_session = None
|
| 190 |
+
if load_lm_session:
|
| 191 |
+
self.lm_session = ort.InferenceSession(
|
| 192 |
+
str(self.bundle_dir / self.metadata["graphs"]["lm_logits"]["path"]),
|
| 193 |
+
sess_options=options,
|
| 194 |
+
providers=providers,
|
| 195 |
+
)
|
| 196 |
+
self.providers = {
|
| 197 |
+
"audio": self.audio_session.get_providers(),
|
| 198 |
+
"lm": self.lm_session.get_providers() if self.lm_session is not None else None,
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
self.tokenizer = Tokenizer.from_file(str(self.bundle_dir / "tokenizer.json"))
|
| 202 |
+
feature_dir = self.bundle_dir / "qwen3_asr_feature_extractor"
|
| 203 |
+
if not feature_dir.exists():
|
| 204 |
+
feature_dir = self.bundle_dir
|
| 205 |
+
self.feature_extractor = WhisperFeatureExtractor.from_pretrained(str(feature_dir))
|
| 206 |
+
self.token_embedding = np.load(
|
| 207 |
+
self.bundle_dir / self.metadata["weights"]["token_embedding"],
|
| 208 |
+
mmap_mode="r",
|
| 209 |
+
)
|
| 210 |
+
if self.token_embedding.dtype != np.float32:
|
| 211 |
+
self.token_embedding = self.token_embedding.astype(np.float32, copy=False)
|
| 212 |
+
projector = np.load(self.bundle_dir / self.metadata["weights"]["audio_projector"])
|
| 213 |
+
self.projector = {key: projector[key].astype(np.float32) for key in projector.files}
|
| 214 |
+
|
| 215 |
+
tokens = self.metadata["tokens"]
|
| 216 |
+
self.audio_token_id = int(tokens["audio_token_id"])
|
| 217 |
+
self.pad_token_id = int(tokens["pad_token_id"])
|
| 218 |
+
self.eos_token_ids = set(int(value) for value in tokens["eos_token_ids"])
|
| 219 |
+
self.asr_block_token_id_from = int(tokens.get("asr_block_token_id_from", -1))
|
| 220 |
+
self.extra_block_token_ids = set(int(value) for value in tokens.get("extra_block_token_ids", []))
|
| 221 |
+
self.sampling_rate = int(self.metadata["sampling_rate"])
|
| 222 |
+
self.max_audio_seconds = int(self.metadata["max_audio_seconds"])
|
| 223 |
+
prompt_audio = self.metadata.get("prompt_audio", {})
|
| 224 |
+
self.prompt_merge_factor = int(prompt_audio.get("merge_factor") or 4)
|
| 225 |
+
|
| 226 |
+
def _token_to_id(self, token: str) -> int:
|
| 227 |
+
token_id = self.tokenizer.token_to_id(token)
|
| 228 |
+
if token_id is None:
|
| 229 |
+
raise KeyError(f"Token not found in tokenizer: {token}")
|
| 230 |
+
return int(token_id)
|
| 231 |
+
|
| 232 |
+
def _build_prompt(self, audio_token_count: int, language: str | None = None) -> str:
|
| 233 |
+
del language
|
| 234 |
+
tokens = self.metadata["tokens"]
|
| 235 |
+
audio_tokens = tokens["audio_token"] * int(audio_token_count)
|
| 236 |
+
return (
|
| 237 |
+
f"{tokens['user_token']}"
|
| 238 |
+
f"{tokens['bos_audio_token']}{audio_tokens}{tokens['eos_audio_token']}"
|
| 239 |
+
"Please transcribe this audio."
|
| 240 |
+
f"{tokens['assistant_token']}"
|
| 241 |
+
f"{self.metadata.get('response_prefix', '') or ''}"
|
| 242 |
+
)
|
| 243 |
+
|
| 244 |
+
def _extract_features(self, audio: np.ndarray) -> tuple[np.ndarray, int, int, int]:
|
| 245 |
+
max_samples = int(self.max_audio_seconds * self.sampling_rate)
|
| 246 |
+
if audio.shape[0] > max_samples:
|
| 247 |
+
audio = audio[:max_samples]
|
| 248 |
+
sample_count = int(max(1, audio.shape[0]))
|
| 249 |
+
feature = self.feature_extractor(
|
| 250 |
+
[audio],
|
| 251 |
+
sampling_rate=self.sampling_rate,
|
| 252 |
+
return_tensors="np",
|
| 253 |
+
return_attention_mask=False,
|
| 254 |
+
padding="longest",
|
| 255 |
+
max_length=max_samples,
|
| 256 |
+
)["input_features"].astype(np.float32)
|
| 257 |
+
hop_length = int(getattr(self.feature_extractor, "hop_length", 160))
|
| 258 |
+
encoder_feature_len = int(math.ceil(float(sample_count) / float(max(hop_length, 1))))
|
| 259 |
+
encoder_feature_len = min(max(1, encoder_feature_len), int(feature.shape[-1]))
|
| 260 |
+
|
| 261 |
+
frames_padded = int(self.audio_graph["frames_padded"])
|
| 262 |
+
if feature.shape[-1] < frames_padded:
|
| 263 |
+
feature = np.pad(feature, ((0, 0), (0, 0), (0, frames_padded - feature.shape[-1])), mode="constant")
|
| 264 |
+
elif feature.shape[-1] > frames_padded:
|
| 265 |
+
feature = feature[:, :, :frames_padded]
|
| 266 |
+
return feature.astype(np.float32), sample_count, encoder_feature_len, hop_length
|
| 267 |
+
|
| 268 |
+
def _audio_embeddings(
|
| 269 |
+
self,
|
| 270 |
+
feature: np.ndarray,
|
| 271 |
+
sample_count: int,
|
| 272 |
+
encoder_feature_len: int,
|
| 273 |
+
hop_length: int,
|
| 274 |
+
) -> np.ndarray:
|
| 275 |
+
audio_token_count = ark_audio_token_count(
|
| 276 |
+
sample_count,
|
| 277 |
+
hop_length=hop_length,
|
| 278 |
+
merge_factor=self.prompt_merge_factor,
|
| 279 |
+
)
|
| 280 |
+
hidden, valid_mask = self.audio_session.run(
|
| 281 |
+
None,
|
| 282 |
+
{
|
| 283 |
+
"audios": feature.astype(np.float32, copy=False),
|
| 284 |
+
"audio_feature_lengths": np.asarray([encoder_feature_len], dtype=np.int64),
|
| 285 |
+
},
|
| 286 |
+
)
|
| 287 |
+
hidden = hidden.astype(np.float32, copy=False)
|
| 288 |
+
valid_mask = valid_mask.astype(bool)
|
| 289 |
+
valid_hidden = hidden[valid_mask]
|
| 290 |
+
if valid_hidden.shape[0] != audio_token_count:
|
| 291 |
+
valid_hidden = adaptive_avg_pool_time(valid_hidden, audio_token_count)
|
| 292 |
+
projected = layer_norm(
|
| 293 |
+
valid_hidden,
|
| 294 |
+
self.projector["norm_weight"],
|
| 295 |
+
self.projector["norm_bias"],
|
| 296 |
+
)
|
| 297 |
+
projected = projected @ self.projector["linear_weight"].T + self.projector["linear_bias"]
|
| 298 |
+
return projected.astype(np.float32, copy=False)
|
| 299 |
+
|
| 300 |
+
def _initial_embeddings(self, audio_embeddings: np.ndarray, language: str | None) -> tuple[list[int], np.ndarray]:
|
| 301 |
+
prompt = self._build_prompt(audio_embeddings.shape[0], language=language)
|
| 302 |
+
input_ids = self.tokenizer.encode(prompt, add_special_tokens=False).ids
|
| 303 |
+
embeds = self.token_embedding[np.asarray(input_ids, dtype=np.int64)].astype(np.float32)
|
| 304 |
+
audio_positions = [index for index, token_id in enumerate(input_ids) if int(token_id) == self.audio_token_id]
|
| 305 |
+
if len(audio_positions) != audio_embeddings.shape[0]:
|
| 306 |
+
raise RuntimeError(
|
| 307 |
+
f"Prompt has {len(audio_positions)} audio tokens, but audio graph returned {audio_embeddings.shape[0]}"
|
| 308 |
+
)
|
| 309 |
+
embeds[np.asarray(audio_positions, dtype=np.int64)] = audio_embeddings
|
| 310 |
+
return [int(value) for value in input_ids], embeds
|
| 311 |
+
|
| 312 |
+
def _mask_logits(self, logits: np.ndarray) -> None:
|
| 313 |
+
if self.asr_block_token_id_from >= 0 and self.asr_block_token_id_from < logits.shape[0]:
|
| 314 |
+
logits[self.asr_block_token_id_from :] = -np.inf
|
| 315 |
+
for token_id in self.extra_block_token_ids:
|
| 316 |
+
if 0 <= token_id < logits.shape[0]:
|
| 317 |
+
logits[token_id] = -np.inf
|
| 318 |
+
|
| 319 |
+
def _build_hotword_trie(self, hotwords, start_boost: float, continuation_boost: float):
|
| 320 |
+
special_ids = set(self.eos_token_ids)
|
| 321 |
+
special_ids.add(self.pad_token_id)
|
| 322 |
+
special_ids.update(self.extra_block_token_ids)
|
| 323 |
+
|
| 324 |
+
def encode(text: str) -> list[int]:
|
| 325 |
+
return list(self.tokenizer.encode(text, add_special_tokens=False).ids)
|
| 326 |
+
|
| 327 |
+
def id_to_token(token_id: int) -> str:
|
| 328 |
+
return str(self.tokenizer.id_to_token(int(token_id)) or "")
|
| 329 |
+
|
| 330 |
+
trie, sequences_by_word = build_trie_from_hotwords(
|
| 331 |
+
hotwords,
|
| 332 |
+
encode=encode,
|
| 333 |
+
id_to_token=id_to_token,
|
| 334 |
+
special_ids=special_ids,
|
| 335 |
+
start_boost=float(start_boost),
|
| 336 |
+
continuation_boost=float(continuation_boost),
|
| 337 |
+
)
|
| 338 |
+
meta = {
|
| 339 |
+
"hotwords": list(hotwords),
|
| 340 |
+
"hotword_token_ids": {word: variants for word, variants in sequences_by_word.items()},
|
| 341 |
+
"hotword_start_boost": float(start_boost),
|
| 342 |
+
"hotword_continuation_boost": float(continuation_boost),
|
| 343 |
+
}
|
| 344 |
+
return trie, meta
|
| 345 |
+
|
| 346 |
+
@staticmethod
|
| 347 |
+
def _apply_hotword_boost(logits: np.ndarray, generated: list[int], trie, topk: int) -> None:
|
| 348 |
+
if not trie:
|
| 349 |
+
return
|
| 350 |
+
boosts = trie.boosts_for_generated(generated)
|
| 351 |
+
if not boosts:
|
| 352 |
+
return
|
| 353 |
+
allowed: set[int] | None = None
|
| 354 |
+
if topk and int(topk) > 0:
|
| 355 |
+
k = min(int(topk), int(logits.shape[-1]))
|
| 356 |
+
allowed = set(int(i) for i in np.argpartition(logits, -k)[-k:])
|
| 357 |
+
vocab = int(logits.shape[-1])
|
| 358 |
+
for token_id, boost in boosts.items():
|
| 359 |
+
if allowed is not None and token_id not in allowed:
|
| 360 |
+
continue
|
| 361 |
+
if 0 <= token_id < vocab:
|
| 362 |
+
logits[token_id] += boost
|
| 363 |
+
|
| 364 |
+
def transcribe(
|
| 365 |
+
self,
|
| 366 |
+
audio_bytes: bytes,
|
| 367 |
+
*,
|
| 368 |
+
language: str | None = None,
|
| 369 |
+
max_new_tokens: int = 128,
|
| 370 |
+
temperature: float = 0.5,
|
| 371 |
+
repetition_penalty: float = 1.0,
|
| 372 |
+
do_sample: bool = False,
|
| 373 |
+
hotwords: str | list | None = None,
|
| 374 |
+
hotword_topk: int = 50,
|
| 375 |
+
hotword_start_boost: float = 6.0,
|
| 376 |
+
hotword_continuation_boost: float = 8.0,
|
| 377 |
+
) -> dict[str, Any]:
|
| 378 |
+
started = time.perf_counter()
|
| 379 |
+
audio = load_audio_bytes(audio_bytes, self.sampling_rate)
|
| 380 |
+
feature, sample_count, encoder_feature_len, hop_length = self._extract_features(audio)
|
| 381 |
+
audio_embeddings = self._audio_embeddings(feature, sample_count, encoder_feature_len, hop_length)
|
| 382 |
+
token_ids, embeds = self._initial_embeddings(audio_embeddings, language=language)
|
| 383 |
+
hotword_list = parse_hotwords(hotwords)
|
| 384 |
+
hot_trie, hot_meta = (
|
| 385 |
+
self._build_hotword_trie(hotword_list, hotword_start_boost, hotword_continuation_boost)
|
| 386 |
+
if hotword_list
|
| 387 |
+
else (None, None)
|
| 388 |
+
)
|
| 389 |
+
|
| 390 |
+
generated: list[int] = []
|
| 391 |
+
hit_stop = False
|
| 392 |
+
stop_token_id: int | None = None
|
| 393 |
+
rng = np.random.default_rng()
|
| 394 |
+
for _ in range(int(max_new_tokens)):
|
| 395 |
+
if self.lm_session is None:
|
| 396 |
+
raise RuntimeError("ONNX LM session is not loaded")
|
| 397 |
+
attention_mask = np.ones((1, embeds.shape[0]), dtype=np.int64)
|
| 398 |
+
logits = self.lm_session.run(
|
| 399 |
+
None,
|
| 400 |
+
{
|
| 401 |
+
"inputs_embeds": embeds[None, :, :].astype(np.float32, copy=False),
|
| 402 |
+
"attention_mask": attention_mask,
|
| 403 |
+
},
|
| 404 |
+
)[0][0].astype(np.float32)
|
| 405 |
+
apply_repetition_penalty(logits, token_ids + generated, float(repetition_penalty))
|
| 406 |
+
self._mask_logits(logits)
|
| 407 |
+
if hot_trie:
|
| 408 |
+
self._apply_hotword_boost(logits, generated, hot_trie, hotword_topk)
|
| 409 |
+
if do_sample:
|
| 410 |
+
probs = softmax(logits / max(float(temperature), 1e-6))
|
| 411 |
+
next_token = int(rng.choice(np.arange(probs.shape[0]), p=probs))
|
| 412 |
+
else:
|
| 413 |
+
next_token = int(np.argmax(logits))
|
| 414 |
+
if next_token in self.eos_token_ids or next_token == self.pad_token_id:
|
| 415 |
+
hit_stop = True
|
| 416 |
+
stop_token_id = next_token
|
| 417 |
+
break
|
| 418 |
+
generated.append(next_token)
|
| 419 |
+
token_embed = self.token_embedding[np.asarray([next_token], dtype=np.int64)].astype(np.float32)
|
| 420 |
+
embeds = np.concatenate([embeds, token_embed], axis=0)
|
| 421 |
+
|
| 422 |
+
raw = self.tokenizer.decode(generated, skip_special_tokens=False)
|
| 423 |
+
text = normalize_prediction_text(raw)
|
| 424 |
+
elapsed = time.perf_counter() - started
|
| 425 |
+
return {
|
| 426 |
+
"text": text,
|
| 427 |
+
"raw": raw,
|
| 428 |
+
"generated_tokens": len(generated),
|
| 429 |
+
"hit_stop": hit_stop,
|
| 430 |
+
"stop_token_id": stop_token_id,
|
| 431 |
+
"elapsed_seconds": elapsed,
|
| 432 |
+
"audio_seconds": float(audio.shape[0]) / float(self.sampling_rate),
|
| 433 |
+
"audio_token_count": int(audio_embeddings.shape[0]),
|
| 434 |
+
"providers": self.providers,
|
| 435 |
+
"backend": "onnx",
|
| 436 |
+
"audio_precision": self.audio_precision,
|
| 437 |
+
"hotword": hot_meta,
|
| 438 |
+
}
|
| 439 |
+
|
| 440 |
+
|
| 441 |
+
class OnnxCacheAsrEngine(OnnxAsrEngine):
|
| 442 |
+
def __init__(
|
| 443 |
+
self,
|
| 444 |
+
bundle_dir: str | Path,
|
| 445 |
+
*,
|
| 446 |
+
provider: str = "CPUExecutionProvider",
|
| 447 |
+
intra_op_num_threads: int | None = None,
|
| 448 |
+
cache_precision: str = "int8",
|
| 449 |
+
audio_precision: str | None = None,
|
| 450 |
+
) -> None:
|
| 451 |
+
selected_audio_precision = audio_precision or "fp32"
|
| 452 |
+
super().__init__(
|
| 453 |
+
bundle_dir,
|
| 454 |
+
provider=provider,
|
| 455 |
+
intra_op_num_threads=intra_op_num_threads,
|
| 456 |
+
load_lm_session=False,
|
| 457 |
+
audio_precision=selected_audio_precision,
|
| 458 |
+
)
|
| 459 |
+
prefill_graph = self.metadata.get("graphs", {}).get("lm_cache_prefill")
|
| 460 |
+
graph = self.metadata.get("graphs", {}).get("lm_cache_decode")
|
| 461 |
+
if not graph:
|
| 462 |
+
raise FileNotFoundError("Bundle metadata has no graphs.lm_cache_decode entry")
|
| 463 |
+
if not prefill_graph:
|
| 464 |
+
raise FileNotFoundError("Bundle metadata has no graphs.lm_cache_prefill entry")
|
| 465 |
+
cache_precision = str(cache_precision or "fp32").lower().strip()
|
| 466 |
+
if cache_precision not in {"fp32", "int8", "int4", "auto"}:
|
| 467 |
+
raise ValueError(f"Unsupported cache_precision={cache_precision!r}; use fp32, int8, int4, or auto")
|
| 468 |
+
graph_path = self.bundle_dir / graph["path"]
|
| 469 |
+
prefill_graph_path = self.bundle_dir / prefill_graph["path"]
|
| 470 |
+
int8_path = graph_path.with_name(f"{graph_path.stem}_int8{graph_path.suffix}")
|
| 471 |
+
prefill_int8_path = prefill_graph_path.with_name(f"{prefill_graph_path.stem}_int8{prefill_graph_path.suffix}")
|
| 472 |
+
int4_path = graph_path.with_name(f"{graph_path.stem}_int4{graph_path.suffix}")
|
| 473 |
+
prefill_int4_path = prefill_graph_path.with_name(f"{prefill_graph_path.stem}_int4{prefill_graph_path.suffix}")
|
| 474 |
+
if cache_precision in {"int8", "int4"}:
|
| 475 |
+
requested_paths = (prefill_int8_path, int8_path) if cache_precision == "int8" else (prefill_int4_path, int4_path)
|
| 476 |
+
missing = [str(path) for path in requested_paths if not path.exists()]
|
| 477 |
+
if missing:
|
| 478 |
+
raise FileNotFoundError(f"Requested {cache_precision} cache graph(s) do not exist: {missing}")
|
| 479 |
+
prefill_graph_path, graph_path = requested_paths
|
| 480 |
+
elif cache_precision == "auto":
|
| 481 |
+
if int8_path.exists() and prefill_int8_path.exists():
|
| 482 |
+
graph_path = int8_path
|
| 483 |
+
prefill_graph_path = prefill_int8_path
|
| 484 |
+
elif int4_path.exists() and prefill_int4_path.exists():
|
| 485 |
+
graph_path = int4_path
|
| 486 |
+
prefill_graph_path = prefill_int4_path
|
| 487 |
+
|
| 488 |
+
options = create_ort_session_options(intra_op_num_threads)
|
| 489 |
+
providers = [provider] if provider in ort.get_available_providers() else ["CPUExecutionProvider"]
|
| 490 |
+
if "CPUExecutionProvider" not in providers:
|
| 491 |
+
providers.append("CPUExecutionProvider")
|
| 492 |
+
self.prefill_lm_session = ort.InferenceSession(
|
| 493 |
+
str(prefill_graph_path),
|
| 494 |
+
sess_options=options,
|
| 495 |
+
providers=providers,
|
| 496 |
+
)
|
| 497 |
+
self.cache_lm_session = ort.InferenceSession(
|
| 498 |
+
str(graph_path),
|
| 499 |
+
sess_options=options,
|
| 500 |
+
providers=providers,
|
| 501 |
+
)
|
| 502 |
+
self.prefill_graph = prefill_graph
|
| 503 |
+
self.prefill_graph_path = prefill_graph_path
|
| 504 |
+
self.cache_graph = graph
|
| 505 |
+
self.cache_graph_path = graph_path
|
| 506 |
+
if graph_path == int8_path and prefill_graph_path == prefill_int8_path:
|
| 507 |
+
self.cache_precision = "int8"
|
| 508 |
+
elif graph_path == int4_path and prefill_graph_path == prefill_int4_path:
|
| 509 |
+
self.cache_precision = "int4"
|
| 510 |
+
else:
|
| 511 |
+
self.cache_precision = "fp32"
|
| 512 |
+
self.providers["lm"] = self.cache_lm_session.get_providers()
|
| 513 |
+
first_input = self.prefill_lm_session.get_inputs()[0]
|
| 514 |
+
self.lm_embed_dtype = self._ort_type_to_numpy(first_input.type)
|
| 515 |
+
cache_key_input = next(inp for inp in self.cache_lm_session.get_inputs() if inp.name == "cache_key_0")
|
| 516 |
+
self.lm_cache_dtype = self._ort_type_to_numpy(cache_key_input.type)
|
| 517 |
+
|
| 518 |
+
@staticmethod
|
| 519 |
+
def _ort_type_to_numpy(ort_type: str) -> np.dtype:
|
| 520 |
+
mapping = {
|
| 521 |
+
"tensor(float)": np.float32,
|
| 522 |
+
"tensor(float16)": np.float16,
|
| 523 |
+
"tensor(double)": np.float64,
|
| 524 |
+
"tensor(int64)": np.int64,
|
| 525 |
+
"tensor(int32)": np.int32,
|
| 526 |
+
}
|
| 527 |
+
if ort_type not in mapping:
|
| 528 |
+
raise ValueError(f"Unsupported ONNX Runtime tensor type: {ort_type}")
|
| 529 |
+
return mapping[ort_type]
|
| 530 |
+
|
| 531 |
+
def _new_cache(self) -> list[np.ndarray]:
|
| 532 |
+
graph = self.cache_graph
|
| 533 |
+
num_layers = int(graph["num_layers"])
|
| 534 |
+
max_total_len = int(graph["max_total_len"])
|
| 535 |
+
num_kv_heads = int(graph["num_key_value_heads"])
|
| 536 |
+
head_dim = int(graph["head_dim"])
|
| 537 |
+
caches: list[np.ndarray] = []
|
| 538 |
+
for _ in range(num_layers):
|
| 539 |
+
caches.extend(
|
| 540 |
+
[
|
| 541 |
+
np.zeros((1, num_kv_heads, max_total_len, head_dim), dtype=self.lm_cache_dtype),
|
| 542 |
+
np.zeros((1, num_kv_heads, max_total_len, head_dim), dtype=self.lm_cache_dtype),
|
| 543 |
+
]
|
| 544 |
+
)
|
| 545 |
+
return caches
|
| 546 |
+
|
| 547 |
+
def _run_cache_prefill(self, embeds: np.ndarray, caches: list[np.ndarray]) -> np.ndarray:
|
| 548 |
+
graph = self.cache_graph
|
| 549 |
+
num_layers = int(graph["num_layers"])
|
| 550 |
+
max_total_len = int(graph["max_total_len"])
|
| 551 |
+
prompt_len = int(embeds.shape[0])
|
| 552 |
+
if prompt_len > max_total_len:
|
| 553 |
+
raise ValueError(f"prompt_len exceeds ONNX cache max_total_len: {prompt_len} > {max_total_len}")
|
| 554 |
+
feeds: dict[str, np.ndarray] = {
|
| 555 |
+
"inputs_embeds": embeds[None, :, :].astype(self.lm_embed_dtype, copy=False),
|
| 556 |
+
"cache_position": np.arange(prompt_len, dtype=np.int64),
|
| 557 |
+
}
|
| 558 |
+
outputs = self.prefill_lm_session.run(None, feeds)
|
| 559 |
+
logits = outputs[0][0, -1, :].astype(np.float32, copy=False)
|
| 560 |
+
for i in range(num_layers):
|
| 561 |
+
output_base = 1 + 2 * i
|
| 562 |
+
cache_base = 2 * i
|
| 563 |
+
caches[cache_base][:, :, :prompt_len, :] = outputs[output_base]
|
| 564 |
+
caches[cache_base + 1][:, :, :prompt_len, :] = outputs[output_base + 1]
|
| 565 |
+
return logits
|
| 566 |
+
|
| 567 |
+
def _run_cache_token(
|
| 568 |
+
self,
|
| 569 |
+
token_embed: np.ndarray,
|
| 570 |
+
caches: list[np.ndarray],
|
| 571 |
+
*,
|
| 572 |
+
position: int,
|
| 573 |
+
valid_len: int,
|
| 574 |
+
) -> np.ndarray:
|
| 575 |
+
graph = self.cache_graph
|
| 576 |
+
num_layers = int(graph["num_layers"])
|
| 577 |
+
max_total_len = int(graph["max_total_len"])
|
| 578 |
+
if valid_len > max_total_len:
|
| 579 |
+
raise ValueError(f"valid_len exceeds ONNX cache max_total_len: {valid_len} > {max_total_len}")
|
| 580 |
+
attention_mask = np.zeros((1, max_total_len), dtype=np.int64)
|
| 581 |
+
attention_mask[:, :valid_len] = 1
|
| 582 |
+
feeds: dict[str, np.ndarray] = {
|
| 583 |
+
"inputs_embeds": token_embed.reshape(1, 1, -1).astype(self.lm_embed_dtype, copy=False),
|
| 584 |
+
"attention_mask": attention_mask,
|
| 585 |
+
"cache_position": np.asarray([position], dtype=np.int64),
|
| 586 |
+
}
|
| 587 |
+
for i in range(num_layers):
|
| 588 |
+
base = 2 * i
|
| 589 |
+
feeds[f"cache_key_{i}"] = caches[base]
|
| 590 |
+
feeds[f"cache_value_{i}"] = caches[base + 1]
|
| 591 |
+
|
| 592 |
+
outputs = self.cache_lm_session.run(None, feeds)
|
| 593 |
+
logits = outputs[0][0, -1, :].astype(np.float32, copy=False)
|
| 594 |
+
for i in range(num_layers):
|
| 595 |
+
output_base = 1 + 2 * i
|
| 596 |
+
cache_base = 2 * i
|
| 597 |
+
caches[cache_base][:, :, position : position + 1, :] = outputs[output_base]
|
| 598 |
+
caches[cache_base + 1][:, :, position : position + 1, :] = outputs[output_base + 1]
|
| 599 |
+
return logits
|
| 600 |
+
|
| 601 |
+
def transcribe(
|
| 602 |
+
self,
|
| 603 |
+
audio_bytes: bytes,
|
| 604 |
+
*,
|
| 605 |
+
language: str | None = None,
|
| 606 |
+
max_new_tokens: int = 128,
|
| 607 |
+
temperature: float = 0.5,
|
| 608 |
+
repetition_penalty: float = 1.0,
|
| 609 |
+
do_sample: bool = False,
|
| 610 |
+
hotwords: str | list | None = None,
|
| 611 |
+
hotword_topk: int = 50,
|
| 612 |
+
hotword_start_boost: float = 6.0,
|
| 613 |
+
hotword_continuation_boost: float = 8.0,
|
| 614 |
+
) -> dict[str, Any]:
|
| 615 |
+
started = time.perf_counter()
|
| 616 |
+
audio = load_audio_bytes(audio_bytes, self.sampling_rate)
|
| 617 |
+
feature, sample_count, encoder_feature_len, hop_length = self._extract_features(audio)
|
| 618 |
+
audio_embeddings = self._audio_embeddings(feature, sample_count, encoder_feature_len, hop_length)
|
| 619 |
+
token_ids, embeds = self._initial_embeddings(audio_embeddings, language=language)
|
| 620 |
+
hotword_list = parse_hotwords(hotwords)
|
| 621 |
+
hot_trie, hot_meta = (
|
| 622 |
+
self._build_hotword_trie(hotword_list, hotword_start_boost, hotword_continuation_boost)
|
| 623 |
+
if hotword_list
|
| 624 |
+
else (None, None)
|
| 625 |
+
)
|
| 626 |
+
|
| 627 |
+
max_total_len = int(self.cache_graph["max_total_len"])
|
| 628 |
+
if embeds.shape[0] + int(max_new_tokens) > max_total_len:
|
| 629 |
+
raise ValueError(
|
| 630 |
+
f"Prompt + max_new_tokens exceeds cache max_total_len: "
|
| 631 |
+
f"{embeds.shape[0]} + {max_new_tokens} > {max_total_len}"
|
| 632 |
+
)
|
| 633 |
+
|
| 634 |
+
caches = self._new_cache()
|
| 635 |
+
if embeds.shape[0] <= 0:
|
| 636 |
+
raise RuntimeError("Empty prompt")
|
| 637 |
+
logits = self._run_cache_prefill(embeds, caches)
|
| 638 |
+
|
| 639 |
+
generated: list[int] = []
|
| 640 |
+
hit_stop = False
|
| 641 |
+
stop_token_id: int | None = None
|
| 642 |
+
rng = np.random.default_rng()
|
| 643 |
+
current_position = embeds.shape[0]
|
| 644 |
+
for _ in range(int(max_new_tokens)):
|
| 645 |
+
step_logits = logits.copy()
|
| 646 |
+
apply_repetition_penalty(step_logits, token_ids + generated, float(repetition_penalty))
|
| 647 |
+
self._mask_logits(step_logits)
|
| 648 |
+
if hot_trie:
|
| 649 |
+
self._apply_hotword_boost(step_logits, generated, hot_trie, hotword_topk)
|
| 650 |
+
if do_sample:
|
| 651 |
+
probs = softmax(step_logits / max(float(temperature), 1e-6))
|
| 652 |
+
next_token = int(rng.choice(np.arange(probs.shape[0]), p=probs))
|
| 653 |
+
else:
|
| 654 |
+
next_token = int(np.argmax(step_logits))
|
| 655 |
+
if next_token in self.eos_token_ids or next_token == self.pad_token_id:
|
| 656 |
+
hit_stop = True
|
| 657 |
+
stop_token_id = next_token
|
| 658 |
+
break
|
| 659 |
+
generated.append(next_token)
|
| 660 |
+
token_embed = self.token_embedding[np.asarray([next_token], dtype=np.int64)][0].astype(np.float32)
|
| 661 |
+
logits = self._run_cache_token(
|
| 662 |
+
token_embed,
|
| 663 |
+
caches,
|
| 664 |
+
position=current_position,
|
| 665 |
+
valid_len=current_position + 1,
|
| 666 |
+
)
|
| 667 |
+
current_position += 1
|
| 668 |
+
|
| 669 |
+
raw = self.tokenizer.decode(generated, skip_special_tokens=False)
|
| 670 |
+
text = normalize_prediction_text(raw)
|
| 671 |
+
elapsed = time.perf_counter() - started
|
| 672 |
+
return {
|
| 673 |
+
"text": text,
|
| 674 |
+
"raw": raw,
|
| 675 |
+
"generated_tokens": len(generated),
|
| 676 |
+
"hit_stop": hit_stop,
|
| 677 |
+
"stop_token_id": stop_token_id,
|
| 678 |
+
"elapsed_seconds": elapsed,
|
| 679 |
+
"audio_seconds": float(audio.shape[0]) / float(self.sampling_rate),
|
| 680 |
+
"audio_token_count": int(audio_embeddings.shape[0]),
|
| 681 |
+
"providers": self.providers,
|
| 682 |
+
"backend": "onnx_cache",
|
| 683 |
+
"cache_precision": self.cache_precision,
|
| 684 |
+
"audio_precision": self.audio_precision,
|
| 685 |
+
"hotword": hot_meta,
|
| 686 |
+
}
|
| 687 |
+
|
| 688 |
+
|
| 689 |
+
def collect_metrics() -> dict[str, Any]:
|
| 690 |
+
process = psutil.Process()
|
| 691 |
+
vm = psutil.virtual_memory()
|
| 692 |
+
metrics: dict[str, Any] = {
|
| 693 |
+
"process": {
|
| 694 |
+
"pid": process.pid,
|
| 695 |
+
"rss_bytes": int(process.memory_info().rss),
|
| 696 |
+
"cpu_percent": process.cpu_percent(interval=None),
|
| 697 |
+
},
|
| 698 |
+
"system": {
|
| 699 |
+
"total_bytes": int(vm.total),
|
| 700 |
+
"available_bytes": int(vm.available),
|
| 701 |
+
"used_bytes": int(vm.used),
|
| 702 |
+
"percent": float(vm.percent),
|
| 703 |
+
},
|
| 704 |
+
"gpu": {
|
| 705 |
+
"nvidia": None,
|
| 706 |
+
"apple": None,
|
| 707 |
+
},
|
| 708 |
+
}
|
| 709 |
+
try:
|
| 710 |
+
output = subprocess.check_output(
|
| 711 |
+
[
|
| 712 |
+
"nvidia-smi",
|
| 713 |
+
"--query-gpu=index,name,memory.used,memory.total,utilization.gpu",
|
| 714 |
+
"--format=csv,noheader,nounits",
|
| 715 |
+
],
|
| 716 |
+
text=True,
|
| 717 |
+
timeout=1.5,
|
| 718 |
+
)
|
| 719 |
+
rows = []
|
| 720 |
+
for line in output.splitlines():
|
| 721 |
+
parts = [part.strip() for part in line.split(",")]
|
| 722 |
+
if len(parts) >= 5:
|
| 723 |
+
rows.append(
|
| 724 |
+
{
|
| 725 |
+
"index": int(parts[0]),
|
| 726 |
+
"name": parts[1],
|
| 727 |
+
"memory_used_mb": float(parts[2]),
|
| 728 |
+
"memory_total_mb": float(parts[3]),
|
| 729 |
+
"utilization_percent": float(parts[4]),
|
| 730 |
+
}
|
| 731 |
+
)
|
| 732 |
+
metrics["gpu"]["nvidia"] = rows
|
| 733 |
+
except Exception:
|
| 734 |
+
metrics["gpu"]["nvidia"] = []
|
| 735 |
+
return metrics
|
hotword/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Hotword helpers for Audio8 ASR ONNX runtime."""
|
hotword/hotword_trie.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Backend-agnostic hotword trie logit-boosting core."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import re
|
| 6 |
+
from typing import Any, Callable, Dict, List, Sequence, Set
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
_CONTROL_TOKEN_RE = re.compile(r"<\|[^>]+?\|>")
|
| 10 |
+
_BARE_TAG_RE = re.compile(r"</?[^>\s]+>")
|
| 11 |
+
_CJK_KANA_HANGUL_RE = re.compile("[\u4e00-\u9fff\u3040-\u30ff\uac00-\ud7af]")
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class HotwordTrie:
|
| 15 |
+
"""Prefix trie over hotword token sequences with per-step boost lookup."""
|
| 16 |
+
|
| 17 |
+
def __init__(
|
| 18 |
+
self,
|
| 19 |
+
token_sequences: Sequence[Sequence[int]],
|
| 20 |
+
*,
|
| 21 |
+
start_boost: float,
|
| 22 |
+
continuation_boost: float,
|
| 23 |
+
) -> None:
|
| 24 |
+
self.start_boost = float(start_boost)
|
| 25 |
+
self.continuation_boost = float(continuation_boost)
|
| 26 |
+
self.trie: Dict[int, Dict[int, Any]] = {}
|
| 27 |
+
self.max_sequence_len = 0
|
| 28 |
+
for seq in token_sequences:
|
| 29 |
+
ids = [int(token_id) for token_id in seq]
|
| 30 |
+
if not ids:
|
| 31 |
+
continue
|
| 32 |
+
node = self.trie
|
| 33 |
+
for token_id in ids:
|
| 34 |
+
node = node.setdefault(token_id, {})
|
| 35 |
+
self.max_sequence_len = max(self.max_sequence_len, len(ids))
|
| 36 |
+
self.start_token_ids = sorted(self.trie.keys())
|
| 37 |
+
|
| 38 |
+
def __bool__(self) -> bool:
|
| 39 |
+
return bool(self.trie)
|
| 40 |
+
|
| 41 |
+
def boosts_for_generated(self, generated_ids: Sequence[int]) -> Dict[int, float]:
|
| 42 |
+
boosts: Dict[int, float] = {}
|
| 43 |
+
if self.start_boost:
|
| 44 |
+
for token_id in self.start_token_ids:
|
| 45 |
+
boosts[token_id] = max(boosts.get(token_id, 0.0), self.start_boost)
|
| 46 |
+
|
| 47 |
+
if not generated_ids or not self.continuation_boost or self.max_sequence_len <= 1:
|
| 48 |
+
return boosts
|
| 49 |
+
|
| 50 |
+
max_prefix_len = min(len(generated_ids), self.max_sequence_len - 1)
|
| 51 |
+
for prefix_len in range(1, max_prefix_len + 1):
|
| 52 |
+
node: Dict[int, Any] = self.trie
|
| 53 |
+
matched = True
|
| 54 |
+
for token_id in generated_ids[-prefix_len:]:
|
| 55 |
+
next_node = node.get(int(token_id))
|
| 56 |
+
if next_node is None:
|
| 57 |
+
matched = False
|
| 58 |
+
break
|
| 59 |
+
node = next_node
|
| 60 |
+
if not matched:
|
| 61 |
+
continue
|
| 62 |
+
for next_token_id in node.keys():
|
| 63 |
+
boosts[int(next_token_id)] = max(
|
| 64 |
+
boosts.get(int(next_token_id), 0.0), self.continuation_boost
|
| 65 |
+
)
|
| 66 |
+
return boosts
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def _has_cjk_or_kana_or_hangul(text: str) -> bool:
|
| 70 |
+
return bool(_CJK_KANA_HANGUL_RE.search(str(text or "")))
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def _hotword_text_variants(word: str) -> List[str]:
|
| 74 |
+
word = str(word or "").strip()
|
| 75 |
+
if not word:
|
| 76 |
+
return []
|
| 77 |
+
variants = [word]
|
| 78 |
+
if not _has_cjk_or_kana_or_hangul(word) and re.search(r"[A-Za-z0-9_]", word):
|
| 79 |
+
variants.append(" " + word)
|
| 80 |
+
out: List[str] = []
|
| 81 |
+
seen: Set[str] = set()
|
| 82 |
+
for value in variants:
|
| 83 |
+
if value not in seen:
|
| 84 |
+
seen.add(value)
|
| 85 |
+
out.append(value)
|
| 86 |
+
return out
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def token_is_control_or_special(token: str, token_id: int, special_ids: Set[int]) -> bool:
|
| 90 |
+
if int(token_id) in special_ids:
|
| 91 |
+
return True
|
| 92 |
+
token = str(token)
|
| 93 |
+
return bool(_CONTROL_TOKEN_RE.fullmatch(token) or _BARE_TAG_RE.fullmatch(token))
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def build_hotword_sequences(
|
| 97 |
+
hotwords: Sequence[str],
|
| 98 |
+
*,
|
| 99 |
+
encode: Callable[[str], List[int]],
|
| 100 |
+
id_to_token: Callable[[int], str],
|
| 101 |
+
special_ids: Set[int],
|
| 102 |
+
) -> Dict[str, List[List[int]]]:
|
| 103 |
+
special_ids = set(int(x) for x in special_ids if x is not None)
|
| 104 |
+
sequences: Dict[str, List[List[int]]] = {}
|
| 105 |
+
seen_global: Set[tuple[int, ...]] = set()
|
| 106 |
+
for word in hotwords:
|
| 107 |
+
word = str(word or "").strip()
|
| 108 |
+
if not word:
|
| 109 |
+
continue
|
| 110 |
+
variants: List[List[int]] = []
|
| 111 |
+
for text in _hotword_text_variants(word):
|
| 112 |
+
ids = [
|
| 113 |
+
int(token_id)
|
| 114 |
+
for token_id in encode(text)
|
| 115 |
+
if not token_is_control_or_special(id_to_token(int(token_id)), int(token_id), special_ids)
|
| 116 |
+
]
|
| 117 |
+
key = tuple(ids)
|
| 118 |
+
if not key or key in seen_global:
|
| 119 |
+
continue
|
| 120 |
+
seen_global.add(key)
|
| 121 |
+
variants.append(ids)
|
| 122 |
+
if variants:
|
| 123 |
+
sequences[word] = variants
|
| 124 |
+
return sequences
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
def flatten_sequences(sequences_by_word: Dict[str, List[List[int]]]) -> List[List[int]]:
|
| 128 |
+
return [ids for variants in sequences_by_word.values() for ids in variants]
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
def parse_hotwords(raw: Any) -> List[str]:
|
| 132 |
+
values: List[str] = []
|
| 133 |
+
if isinstance(raw, (list, tuple)):
|
| 134 |
+
values = [str(x).strip() for x in raw]
|
| 135 |
+
elif raw:
|
| 136 |
+
values = [x.strip() for x in re.split(r"[,,]", str(raw))]
|
| 137 |
+
out: List[str] = []
|
| 138 |
+
seen: Set[str] = set()
|
| 139 |
+
for value in values:
|
| 140 |
+
if value and value not in seen:
|
| 141 |
+
seen.add(value)
|
| 142 |
+
out.append(value)
|
| 143 |
+
return out
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
def build_trie_from_hotwords(
|
| 147 |
+
hotwords: Sequence[str],
|
| 148 |
+
*,
|
| 149 |
+
encode: Callable[[str], List[int]],
|
| 150 |
+
id_to_token: Callable[[int], str],
|
| 151 |
+
special_ids: Set[int],
|
| 152 |
+
start_boost: float,
|
| 153 |
+
continuation_boost: float,
|
| 154 |
+
) -> tuple[HotwordTrie, Dict[str, List[List[int]]]]:
|
| 155 |
+
sequences_by_word = build_hotword_sequences(
|
| 156 |
+
hotwords, encode=encode, id_to_token=id_to_token, special_ids=special_ids
|
| 157 |
+
)
|
| 158 |
+
trie = HotwordTrie(
|
| 159 |
+
flatten_sequences(sequences_by_word),
|
| 160 |
+
start_boost=start_boost,
|
| 161 |
+
continuation_boost=continuation_boost,
|
| 162 |
+
)
|
| 163 |
+
return trie, sequences_by_word
|
measure_precision_memory.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import argparse
|
| 5 |
+
import json
|
| 6 |
+
import subprocess
|
| 7 |
+
import sys
|
| 8 |
+
import threading
|
| 9 |
+
import time
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
from typing import Any
|
| 12 |
+
|
| 13 |
+
import psutil
|
| 14 |
+
|
| 15 |
+
APP_DIR = Path(__file__).resolve().parent
|
| 16 |
+
if str(APP_DIR) not in sys.path:
|
| 17 |
+
sys.path.insert(0, str(APP_DIR))
|
| 18 |
+
|
| 19 |
+
from asr_onnx_runtime import OnnxCacheAsrEngine # noqa: E402
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def nvidia_total_used_mb() -> tuple[float | None, list[float]]:
|
| 23 |
+
try:
|
| 24 |
+
out = subprocess.check_output(
|
| 25 |
+
["nvidia-smi", "--query-gpu=memory.used", "--format=csv,noheader,nounits"],
|
| 26 |
+
text=True,
|
| 27 |
+
timeout=2,
|
| 28 |
+
)
|
| 29 |
+
values = [float(line.strip()) for line in out.splitlines() if line.strip()]
|
| 30 |
+
return sum(values), values
|
| 31 |
+
except Exception:
|
| 32 |
+
return None, []
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class RequestSampler:
|
| 36 |
+
def __init__(self, process: psutil.Process, interval: float = 0.01) -> None:
|
| 37 |
+
self.process = process
|
| 38 |
+
self.interval = float(interval)
|
| 39 |
+
self.peak_rss_bytes = process.memory_info().rss
|
| 40 |
+
self._stop = threading.Event()
|
| 41 |
+
self._thread = threading.Thread(target=self._run, daemon=True)
|
| 42 |
+
|
| 43 |
+
def _run(self) -> None:
|
| 44 |
+
while not self._stop.wait(self.interval):
|
| 45 |
+
try:
|
| 46 |
+
self.peak_rss_bytes = max(self.peak_rss_bytes, self.process.memory_info().rss)
|
| 47 |
+
except psutil.Error:
|
| 48 |
+
break
|
| 49 |
+
|
| 50 |
+
def __enter__(self) -> "RequestSampler":
|
| 51 |
+
self.peak_rss_bytes = self.process.memory_info().rss
|
| 52 |
+
self._thread.start()
|
| 53 |
+
return self
|
| 54 |
+
|
| 55 |
+
def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
|
| 56 |
+
self._stop.set()
|
| 57 |
+
self._thread.join(timeout=1.0)
|
| 58 |
+
try:
|
| 59 |
+
self.peak_rss_bytes = max(self.peak_rss_bytes, self.process.memory_info().rss)
|
| 60 |
+
except psutil.Error:
|
| 61 |
+
pass
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def measure_one(
|
| 65 |
+
*,
|
| 66 |
+
bundle_dir: Path,
|
| 67 |
+
audio_path: Path,
|
| 68 |
+
cache_precision: str,
|
| 69 |
+
audio_precision: str,
|
| 70 |
+
max_new_tokens: int,
|
| 71 |
+
provider: str,
|
| 72 |
+
threads: int | None,
|
| 73 |
+
) -> dict[str, Any]:
|
| 74 |
+
process = psutil.Process()
|
| 75 |
+
gpu_before_total, gpu_before = nvidia_total_used_mb()
|
| 76 |
+
started = time.perf_counter()
|
| 77 |
+
engine = OnnxCacheAsrEngine(
|
| 78 |
+
bundle_dir,
|
| 79 |
+
provider=provider,
|
| 80 |
+
intra_op_num_threads=threads,
|
| 81 |
+
cache_precision=cache_precision,
|
| 82 |
+
audio_precision=audio_precision,
|
| 83 |
+
)
|
| 84 |
+
static_rss = process.memory_info().rss
|
| 85 |
+
static_gpu_total, static_gpu = nvidia_total_used_mb()
|
| 86 |
+
load_seconds = time.perf_counter() - started
|
| 87 |
+
|
| 88 |
+
with RequestSampler(process) as sampler:
|
| 89 |
+
result = engine.transcribe(audio_path.read_bytes(), language="Chinese", max_new_tokens=max_new_tokens)
|
| 90 |
+
post_rss = process.memory_info().rss
|
| 91 |
+
post_gpu_total, post_gpu = nvidia_total_used_mb()
|
| 92 |
+
|
| 93 |
+
return {
|
| 94 |
+
"cache_precision": cache_precision,
|
| 95 |
+
"audio_precision": audio_precision,
|
| 96 |
+
"selected_cache_precision": engine.cache_precision,
|
| 97 |
+
"selected_audio_precision": engine.audio_precision,
|
| 98 |
+
"static_rss_bytes": int(static_rss),
|
| 99 |
+
"infer_peak_rss_bytes": int(sampler.peak_rss_bytes),
|
| 100 |
+
"post_rss_bytes": int(post_rss),
|
| 101 |
+
"load_seconds": float(load_seconds),
|
| 102 |
+
"elapsed_seconds": float(result.get("elapsed_seconds") or 0.0),
|
| 103 |
+
"text": result.get("text"),
|
| 104 |
+
"generated_tokens": result.get("generated_tokens"),
|
| 105 |
+
"hit_stop": result.get("hit_stop"),
|
| 106 |
+
"gpu_total_used_mb_before": gpu_before_total,
|
| 107 |
+
"gpu_total_used_mb_static": static_gpu_total,
|
| 108 |
+
"gpu_total_used_mb_post": post_gpu_total,
|
| 109 |
+
"gpu_used_mb_before": gpu_before,
|
| 110 |
+
"gpu_used_mb_static": static_gpu,
|
| 111 |
+
"gpu_used_mb_post": post_gpu,
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def main() -> None:
|
| 116 |
+
parser = argparse.ArgumentParser(description="Measure Audio8 ASR ONNX precision memory in a fresh process.")
|
| 117 |
+
parser.add_argument("--bundle_dir", type=Path, default=APP_DIR / "model_bundle")
|
| 118 |
+
parser.add_argument(
|
| 119 |
+
"--audio",
|
| 120 |
+
type=Path,
|
| 121 |
+
required=True,
|
| 122 |
+
help="Audio file path for the measurement.",
|
| 123 |
+
)
|
| 124 |
+
parser.add_argument("--cache_precision", choices=["fp32", "int8", "int4"], required=True)
|
| 125 |
+
parser.add_argument("--audio_precision", choices=["fp32", "int8"], required=True)
|
| 126 |
+
parser.add_argument("--max_new_tokens", type=int, default=64)
|
| 127 |
+
parser.add_argument("--provider", default="CPUExecutionProvider")
|
| 128 |
+
parser.add_argument("--threads", type=int, default=0)
|
| 129 |
+
args = parser.parse_args()
|
| 130 |
+
print(
|
| 131 |
+
json.dumps(
|
| 132 |
+
measure_one(
|
| 133 |
+
bundle_dir=args.bundle_dir,
|
| 134 |
+
audio_path=args.audio,
|
| 135 |
+
cache_precision=args.cache_precision,
|
| 136 |
+
audio_precision=args.audio_precision,
|
| 137 |
+
max_new_tokens=args.max_new_tokens,
|
| 138 |
+
provider=args.provider,
|
| 139 |
+
threads=args.threads if args.threads > 0 else None,
|
| 140 |
+
),
|
| 141 |
+
ensure_ascii=False,
|
| 142 |
+
)
|
| 143 |
+
)
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
if __name__ == "__main__":
|
| 147 |
+
main()
|
model_bundle/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 |
+
}
|
model_bundle/audio8_audio_wrapper_config.json
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"audio_backend": "qwen3_asr_mlp_tower",
|
| 3 |
+
"audio_token_id": 151646,
|
| 4 |
+
"audio_encoder_output_dim": 1024,
|
| 5 |
+
"audio_projector_file": "weights/audio_projector.npz",
|
| 6 |
+
"audio_mlp_tower_file": "audio_hidden.onnx",
|
| 7 |
+
"language_model_type": "arkasr",
|
| 8 |
+
"language_model_hidden_size": 512,
|
| 9 |
+
"native_audio_encoder_pruned": true,
|
| 10 |
+
"removed_native_audio_encoder_params": 91825664
|
| 11 |
+
}
|
model_bundle/audio_hidden.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:5f21a8db5a8aee6c57150f3aa7464585b1a1c0ad7ea7a68e54c693df8e9511f5
|
| 3 |
+
size 880238173
|
model_bundle/audio_hidden_int8.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:1d30ee323fbe69514ca197f02881ceb174229523258f225acab94ebee53682f3
|
| 3 |
+
size 234699007
|
model_bundle/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 %}
|
model_bundle/config.json
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"adapter_type": "mlp",
|
| 3 |
+
"architectures": [
|
| 4 |
+
"ArkasrForConditionalGeneration"
|
| 5 |
+
],
|
| 6 |
+
"attention_dropout": 0.0,
|
| 7 |
+
"audio_token_id": 151646,
|
| 8 |
+
"bos_token_id": 151643,
|
| 9 |
+
"dtype": "bfloat16",
|
| 10 |
+
"eos_token_id": 151645,
|
| 11 |
+
"hidden_act": "silu",
|
| 12 |
+
"hidden_size": 512,
|
| 13 |
+
"initializer_range": 0.02,
|
| 14 |
+
"intermediate_size": 1408,
|
| 15 |
+
"layer_types": [
|
| 16 |
+
"full_attention",
|
| 17 |
+
"full_attention",
|
| 18 |
+
"full_attention",
|
| 19 |
+
"full_attention",
|
| 20 |
+
"full_attention",
|
| 21 |
+
"full_attention",
|
| 22 |
+
"full_attention",
|
| 23 |
+
"full_attention"
|
| 24 |
+
],
|
| 25 |
+
"max_position_embeddings": 32768,
|
| 26 |
+
"max_whisper_length": 1500,
|
| 27 |
+
"max_window_layers": 28,
|
| 28 |
+
"merge_factor": 4,
|
| 29 |
+
"mlp_adapter_act": "gelu",
|
| 30 |
+
"model_type": "arkasr",
|
| 31 |
+
"num_attention_heads": 8,
|
| 32 |
+
"num_hidden_layers": 8,
|
| 33 |
+
"num_key_value_heads": 8,
|
| 34 |
+
"pad_token_id": 151643,
|
| 35 |
+
"rms_norm_eps": 1e-06,
|
| 36 |
+
"rope_scaling": null,
|
| 37 |
+
"rope_theta": 1000000.0,
|
| 38 |
+
"sliding_window": null,
|
| 39 |
+
"spec_aug": false,
|
| 40 |
+
"tie_word_embeddings": true,
|
| 41 |
+
"transformers_version": "4.57.3",
|
| 42 |
+
"use_cache": true,
|
| 43 |
+
"use_rope": false,
|
| 44 |
+
"use_sliding_window": false,
|
| 45 |
+
"vocab_size": 151936,
|
| 46 |
+
"whisper_config": {
|
| 47 |
+
"_name_or_path": "openai/whisper-small",
|
| 48 |
+
"activation_dropout": 0.0,
|
| 49 |
+
"activation_function": "gelu",
|
| 50 |
+
"apply_spec_augment": false,
|
| 51 |
+
"architectures": [
|
| 52 |
+
"WhisperForConditionalGeneration"
|
| 53 |
+
],
|
| 54 |
+
"attention_dropout": 0.0,
|
| 55 |
+
"begin_suppress_tokens": [
|
| 56 |
+
220,
|
| 57 |
+
50257
|
| 58 |
+
],
|
| 59 |
+
"bos_token_id": 50257,
|
| 60 |
+
"classifier_proj_size": 256,
|
| 61 |
+
"d_model": 768,
|
| 62 |
+
"decoder_attention_heads": 12,
|
| 63 |
+
"decoder_ffn_dim": 3072,
|
| 64 |
+
"decoder_layerdrop": 0.0,
|
| 65 |
+
"decoder_layers": 12,
|
| 66 |
+
"decoder_start_token_id": 50258,
|
| 67 |
+
"dropout": 0.0,
|
| 68 |
+
"dtype": "float32",
|
| 69 |
+
"encoder_attention_heads": 12,
|
| 70 |
+
"encoder_ffn_dim": 3072,
|
| 71 |
+
"encoder_layerdrop": 0.0,
|
| 72 |
+
"encoder_layers": 12,
|
| 73 |
+
"eos_token_id": 50257,
|
| 74 |
+
"forced_decoder_ids": [
|
| 75 |
+
[
|
| 76 |
+
1,
|
| 77 |
+
50259
|
| 78 |
+
],
|
| 79 |
+
[
|
| 80 |
+
2,
|
| 81 |
+
50359
|
| 82 |
+
],
|
| 83 |
+
[
|
| 84 |
+
3,
|
| 85 |
+
50363
|
| 86 |
+
]
|
| 87 |
+
],
|
| 88 |
+
"init_std": 0.02,
|
| 89 |
+
"mask_feature_length": 10,
|
| 90 |
+
"mask_feature_min_masks": 0,
|
| 91 |
+
"mask_feature_prob": 0.0,
|
| 92 |
+
"mask_time_length": 10,
|
| 93 |
+
"mask_time_min_masks": 2,
|
| 94 |
+
"mask_time_prob": 0.05,
|
| 95 |
+
"max_length": 448,
|
| 96 |
+
"max_source_positions": 1500,
|
| 97 |
+
"max_target_positions": 448,
|
| 98 |
+
"median_filter_width": 7,
|
| 99 |
+
"model_type": "whisper",
|
| 100 |
+
"num_hidden_layers": 12,
|
| 101 |
+
"num_mel_bins": 80,
|
| 102 |
+
"pad_token_id": 50257,
|
| 103 |
+
"scale_embedding": false,
|
| 104 |
+
"suppress_tokens": [
|
| 105 |
+
1,
|
| 106 |
+
2,
|
| 107 |
+
7,
|
| 108 |
+
8,
|
| 109 |
+
9,
|
| 110 |
+
10,
|
| 111 |
+
14,
|
| 112 |
+
25,
|
| 113 |
+
26,
|
| 114 |
+
27,
|
| 115 |
+
28,
|
| 116 |
+
29,
|
| 117 |
+
31,
|
| 118 |
+
58,
|
| 119 |
+
59,
|
| 120 |
+
60,
|
| 121 |
+
61,
|
| 122 |
+
62,
|
| 123 |
+
63,
|
| 124 |
+
90,
|
| 125 |
+
91,
|
| 126 |
+
92,
|
| 127 |
+
93,
|
| 128 |
+
359,
|
| 129 |
+
503,
|
| 130 |
+
522,
|
| 131 |
+
542,
|
| 132 |
+
873,
|
| 133 |
+
893,
|
| 134 |
+
902,
|
| 135 |
+
918,
|
| 136 |
+
922,
|
| 137 |
+
931,
|
| 138 |
+
1350,
|
| 139 |
+
1853,
|
| 140 |
+
1982,
|
| 141 |
+
2460,
|
| 142 |
+
2627,
|
| 143 |
+
3246,
|
| 144 |
+
3253,
|
| 145 |
+
3268,
|
| 146 |
+
3536,
|
| 147 |
+
3846,
|
| 148 |
+
3961,
|
| 149 |
+
4183,
|
| 150 |
+
4667,
|
| 151 |
+
6585,
|
| 152 |
+
6647,
|
| 153 |
+
7273,
|
| 154 |
+
9061,
|
| 155 |
+
9383,
|
| 156 |
+
10428,
|
| 157 |
+
10929,
|
| 158 |
+
11938,
|
| 159 |
+
12033,
|
| 160 |
+
12331,
|
| 161 |
+
12562,
|
| 162 |
+
13793,
|
| 163 |
+
14157,
|
| 164 |
+
14635,
|
| 165 |
+
15265,
|
| 166 |
+
15618,
|
| 167 |
+
16553,
|
| 168 |
+
16604,
|
| 169 |
+
18362,
|
| 170 |
+
18956,
|
| 171 |
+
20075,
|
| 172 |
+
21675,
|
| 173 |
+
22520,
|
| 174 |
+
26130,
|
| 175 |
+
26161,
|
| 176 |
+
26435,
|
| 177 |
+
28279,
|
| 178 |
+
29464,
|
| 179 |
+
31650,
|
| 180 |
+
32302,
|
| 181 |
+
32470,
|
| 182 |
+
36865,
|
| 183 |
+
42863,
|
| 184 |
+
47425,
|
| 185 |
+
49870,
|
| 186 |
+
50254,
|
| 187 |
+
50258,
|
| 188 |
+
50360,
|
| 189 |
+
50361,
|
| 190 |
+
50362
|
| 191 |
+
],
|
| 192 |
+
"use_cache": true,
|
| 193 |
+
"use_weighted_layer_sum": false,
|
| 194 |
+
"vocab_size": 51865
|
| 195 |
+
}
|
| 196 |
+
}
|
model_bundle/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 |
+
}
|
model_bundle/lm_cache_decode.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:1926e5016196ac80e27c6ec51ea6affb3b5e21852c02b5425058d33a91cf903c
|
| 3 |
+
size 414145009
|
model_bundle/lm_cache_decode_int4.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:0eb8ccd60f042a50c0d12d1a7fdf476fd448d5247dbb50091c3866a8ab26760c
|
| 3 |
+
size 133747
|
model_bundle/lm_cache_decode_int4.onnx.data
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:fcd14cf0b65449621c6ddf052c938aa2acd72fb272bb8e0157decbf4d28eb4fd
|
| 3 |
+
size 55497472
|
model_bundle/lm_cache_decode_int8.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d7dd4e20e38ab6e453001225854b33b522569db402f54a33366f212fb271143c
|
| 3 |
+
size 1255083
|
model_bundle/lm_cache_decode_int8.onnx.data
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a2953e6835e8b7c3a36beb061122dbcf2329e6a9e2e612208645610cd095a9ee
|
| 3 |
+
size 103598080
|
model_bundle/lm_cache_prefill.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e8095de55b95e6b2303ae70a8f28b8c0092dbee2500aba61878b815b3bfab29b
|
| 3 |
+
size 414144056
|
model_bundle/lm_cache_prefill_int4.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:23c5e89eed9781d465dd235cdef371f778aadf1340f863cd02cbe616db5d6ca5
|
| 3 |
+
size 165158
|
model_bundle/lm_cache_prefill_int4.onnx.data
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:f7ffe71cc716a7b2db49fd9a39cca9b714b5d30680d93760003c0b63109bf8e8
|
| 3 |
+
size 55464704
|
model_bundle/lm_cache_prefill_int8.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:410b87e57f4834b7b64529154dab8d761300f9afe46479fe5834ef642797d402
|
| 3 |
+
size 1308788
|
model_bundle/lm_cache_prefill_int8.onnx.data
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:1cf8f2333339d9e3b8902cde04928feab9c76c74aea57a440aff301bfbc2e92a
|
| 3 |
+
size 103565312
|
model_bundle/lm_logits.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:4fa77550bac03603ac239e7c1634208617b8399c93eaeda3d64cde8c9d55e988
|
| 3 |
+
size 414132615
|
model_bundle/merges.txt
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
model_bundle/metadata.json
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"format": "audio8_asr_onnx_bundle",
|
| 3 |
+
"format_version": 1,
|
| 4 |
+
"sampling_rate": 16000,
|
| 5 |
+
"max_audio_seconds": 30,
|
| 6 |
+
"prompt_mode": "arkasr_chat",
|
| 7 |
+
"response_prefix": "",
|
| 8 |
+
"language_hint_enabled": false,
|
| 9 |
+
"runtime_notes": {
|
| 10 |
+
"audio_loader": "librosa_first_for_hf_eval_parity"
|
| 11 |
+
},
|
| 12 |
+
"prompt_audio": {
|
| 13 |
+
"token_count_rule": "arkasr_processor_floor_mel_downsample_merge",
|
| 14 |
+
"merge_factor": 4,
|
| 15 |
+
"hop_length": 160
|
| 16 |
+
},
|
| 17 |
+
"tokens": {
|
| 18 |
+
"user_token": "<|user|>",
|
| 19 |
+
"assistant_token": "<|assistant|>",
|
| 20 |
+
"bos_audio_token": "<|begin_of_audio|>",
|
| 21 |
+
"audio_token": "<|audio|>",
|
| 22 |
+
"eos_audio_token": "<|end_of_audio|>",
|
| 23 |
+
"assistant_end_token": "<|im_end|>",
|
| 24 |
+
"audio_token_id": 151646,
|
| 25 |
+
"pad_token_id": 151643,
|
| 26 |
+
"eos_token_ids": [
|
| 27 |
+
151645
|
| 28 |
+
],
|
| 29 |
+
"asr_block_token_id_from": -1,
|
| 30 |
+
"extra_block_token_ids": [
|
| 31 |
+
151647,
|
| 32 |
+
151650,
|
| 33 |
+
151648,
|
| 34 |
+
151646,
|
| 35 |
+
151649
|
| 36 |
+
],
|
| 37 |
+
"vocab_size": 151652
|
| 38 |
+
},
|
| 39 |
+
"audio_encoder": {
|
| 40 |
+
"type": "qwen3_asr_mlp_tower",
|
| 41 |
+
"qwen3_asr_audio_output_mode": "mlp_tower",
|
| 42 |
+
"prompt_token_count_uses_arkasr_processor_rule": true
|
| 43 |
+
},
|
| 44 |
+
"graphs": {
|
| 45 |
+
"audio_hidden": {
|
| 46 |
+
"path": "audio_hidden.onnx",
|
| 47 |
+
"max_frames": 3000,
|
| 48 |
+
"frames_padded": 3000,
|
| 49 |
+
"feature_size": 128,
|
| 50 |
+
"max_audio_tokens": 390,
|
| 51 |
+
"hidden_size": 1024,
|
| 52 |
+
"tokens_per_chunk": 13,
|
| 53 |
+
"valid_mask_sum_at_max": 390
|
| 54 |
+
},
|
| 55 |
+
"audio_hidden_int8": {
|
| 56 |
+
"path": "audio_hidden_int8.onnx",
|
| 57 |
+
"quantization": "dynamic_int8_matmul_quint8_per_channel",
|
| 58 |
+
"source_graph": "audio_hidden.onnx",
|
| 59 |
+
"max_frames": 3000,
|
| 60 |
+
"frames_padded": 3000,
|
| 61 |
+
"feature_size": 128,
|
| 62 |
+
"max_audio_tokens": 390,
|
| 63 |
+
"hidden_size": 1024,
|
| 64 |
+
"tokens_per_chunk": 13,
|
| 65 |
+
"valid_mask_sum_at_max": 390
|
| 66 |
+
},
|
| 67 |
+
"lm_logits": {
|
| 68 |
+
"path": "lm_logits.onnx",
|
| 69 |
+
"hidden_size": 512,
|
| 70 |
+
"dummy_sequence_length": 16,
|
| 71 |
+
"patched_negative_transpose_perms": 0
|
| 72 |
+
},
|
| 73 |
+
"lm_cache_prefill": {
|
| 74 |
+
"path": "lm_cache_prefill.onnx",
|
| 75 |
+
"hidden_size": 512,
|
| 76 |
+
"num_layers": 8,
|
| 77 |
+
"num_attention_heads": 8,
|
| 78 |
+
"num_key_value_heads": 8,
|
| 79 |
+
"head_dim": 64,
|
| 80 |
+
"max_total_len": 512,
|
| 81 |
+
"dummy_sequence_length": 16,
|
| 82 |
+
"patched_negative_transpose_perms": 0
|
| 83 |
+
},
|
| 84 |
+
"lm_cache_decode": {
|
| 85 |
+
"path": "lm_cache_decode.onnx",
|
| 86 |
+
"hidden_size": 512,
|
| 87 |
+
"num_layers": 8,
|
| 88 |
+
"num_attention_heads": 8,
|
| 89 |
+
"num_key_value_heads": 8,
|
| 90 |
+
"head_dim": 64,
|
| 91 |
+
"max_total_len": 512,
|
| 92 |
+
"patched_negative_transpose_perms": 0
|
| 93 |
+
}
|
| 94 |
+
},
|
| 95 |
+
"weights": {
|
| 96 |
+
"token_embedding": "weights/token_embedding.npy",
|
| 97 |
+
"audio_projector": "weights/audio_projector.npz"
|
| 98 |
+
},
|
| 99 |
+
"generation_defaults": {
|
| 100 |
+
"max_new_tokens": 128,
|
| 101 |
+
"do_sample": false,
|
| 102 |
+
"temperature": 0.5,
|
| 103 |
+
"repetition_penalty": 1.0
|
| 104 |
+
}
|
| 105 |
+
}
|
model_bundle/preprocessor_config.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"chunk_length": 30,
|
| 3 |
+
"dither": 0.0,
|
| 4 |
+
"feature_extractor_type": "WhisperFeatureExtractor",
|
| 5 |
+
"feature_size": 80,
|
| 6 |
+
"hop_length": 160,
|
| 7 |
+
"n_fft": 400,
|
| 8 |
+
"n_samples": 480000,
|
| 9 |
+
"nb_max_frames": 3000,
|
| 10 |
+
"padding_side": "right",
|
| 11 |
+
"padding_value": 0.0,
|
| 12 |
+
"processor_class": "ArkasrProcessor",
|
| 13 |
+
"return_attention_mask": false,
|
| 14 |
+
"sampling_rate": 16000
|
| 15 |
+
}
|
model_bundle/processor_config.json
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"audio_dtype": "bfloat16",
|
| 3 |
+
"audio_token": "<|audio|>",
|
| 4 |
+
"merge_factor": 4,
|
| 5 |
+
"processor_class": "ArkasrProcessor"
|
| 6 |
+
}
|
model_bundle/qwen3_asr_feature_extractor/preprocessor_config.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"chunk_length": 30,
|
| 3 |
+
"dither": 0.0,
|
| 4 |
+
"feature_extractor_type": "WhisperFeatureExtractor",
|
| 5 |
+
"feature_size": 128,
|
| 6 |
+
"hop_length": 160,
|
| 7 |
+
"n_fft": 400,
|
| 8 |
+
"n_samples": 480000,
|
| 9 |
+
"nb_max_frames": 3000,
|
| 10 |
+
"padding_side": "right",
|
| 11 |
+
"padding_value": 0.0,
|
| 12 |
+
"processor_class": "Qwen3ASRProcessor",
|
| 13 |
+
"return_attention_mask": true,
|
| 14 |
+
"sampling_rate": 16000
|
| 15 |
+
}
|
model_bundle/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 |
+
}
|
model_bundle/tokenizer.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:64ecaabc85a9272a8099f8aa7c097ce3a4c65a79bafbfd1450add736938ae45b
|
| 3 |
+
size 11419524
|
model_bundle/tokenizer_config.json
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
"bos_token": null,
|
| 85 |
+
"clean_up_tokenization_spaces": false,
|
| 86 |
+
"eos_token": "<|im_end|>",
|
| 87 |
+
"errors": "replace",
|
| 88 |
+
"extra_special_tokens": {},
|
| 89 |
+
"fix_mistral_regex": true,
|
| 90 |
+
"model_max_length": 32768,
|
| 91 |
+
"pad_token": "<|endoftext|>",
|
| 92 |
+
"processor_class": "ArkasrProcessor",
|
| 93 |
+
"split_special_tokens": false,
|
| 94 |
+
"tokenizer_class": "Qwen2Tokenizer",
|
| 95 |
+
"unk_token": null
|
| 96 |
+
}
|
model_bundle/vocab.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
model_bundle/weights/audio_projector.npz
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:c15eec79b817abab7de6c593845de150f984e0afec9ec41a92d1f89ab75470a5
|
| 3 |
+
size 2108430
|
model_bundle/weights/token_embedding.npy
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:b8d6949be746c1e11335250a67ef9c58f7b01e2fcc89edf265743a8484114ac5
|
| 3 |
+
size 311165056
|
requirements-onnx.txt
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Pinned for reproducible CPU ONNX Runtime behavior.
|
| 2 |
+
fastapi==0.139.0
|
| 3 |
+
uvicorn[standard]==0.49.0
|
| 4 |
+
python-multipart==0.0.32
|
| 5 |
+
numpy==1.26.4
|
| 6 |
+
onnxruntime==1.22.0
|
| 7 |
+
psutil==7.0.0
|
| 8 |
+
librosa==0.11.0
|
| 9 |
+
soundfile==0.14.0
|
| 10 |
+
tokenizers==0.22.2
|
| 11 |
+
transformers==4.57.6
|
run_local.sh
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
set -euo pipefail
|
| 3 |
+
|
| 4 |
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
| 5 |
+
BUNDLE_DIR="${1:-${AUDIO8_ASR_BUNDLE:-$SCRIPT_DIR/model_bundle}}"
|
| 6 |
+
HOST="${HOST:-127.0.0.1}"
|
| 7 |
+
PORT="${PORT:-7860}"
|
| 8 |
+
ORT_PROVIDER="${ORT_PROVIDER:-CPUExecutionProvider}"
|
| 9 |
+
ASR_BACKEND="${ASR_BACKEND:-auto}"
|
| 10 |
+
ASR_CACHE_PRECISION="${ASR_CACHE_PRECISION:-int8}"
|
| 11 |
+
ASR_AUDIO_PRECISION="${ASR_AUDIO_PRECISION:-int8}"
|
| 12 |
+
PYTHON_BIN="${PYTHON:-python3}"
|
| 13 |
+
|
| 14 |
+
cd "$SCRIPT_DIR"
|
| 15 |
+
export NO_PROXY="${NO_PROXY:+$NO_PROXY,}127.0.0.1,localhost,::1"
|
| 16 |
+
export no_proxy="${no_proxy:+$no_proxy,}127.0.0.1,localhost,::1"
|
| 17 |
+
if ! "$PYTHON_BIN" -c 'import sys; raise SystemExit(0 if sys.version_info >= (3, 10) else 1)'; then
|
| 18 |
+
echo "Python 3.10+ is required. Create/activate a newer venv, then rerun this script." >&2
|
| 19 |
+
exit 1
|
| 20 |
+
fi
|
| 21 |
+
exec "$PYTHON_BIN" server.py \
|
| 22 |
+
--bundle_dir "$BUNDLE_DIR" \
|
| 23 |
+
--backend "$ASR_BACKEND" \
|
| 24 |
+
--host "$HOST" \
|
| 25 |
+
--port "$PORT" \
|
| 26 |
+
--provider "$ORT_PROVIDER" \
|
| 27 |
+
--cache_precision "$ASR_CACHE_PRECISION" \
|
| 28 |
+
--audio_precision "$ASR_AUDIO_PRECISION"
|
server.py
ADDED
|
@@ -0,0 +1,359 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
import gc
|
| 5 |
+
import json
|
| 6 |
+
import os
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
import threading
|
| 9 |
+
from typing import Any
|
| 10 |
+
|
| 11 |
+
from fastapi import FastAPI, File, Form, HTTPException, UploadFile
|
| 12 |
+
from fastapi.responses import FileResponse
|
| 13 |
+
from fastapi.staticfiles import StaticFiles
|
| 14 |
+
import psutil
|
| 15 |
+
|
| 16 |
+
from asr_onnx_runtime import OnnxAsrEngine, OnnxCacheAsrEngine, collect_metrics
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
APP_DIR = Path(__file__).resolve().parent
|
| 20 |
+
STATIC_DIR = APP_DIR / "static"
|
| 21 |
+
ENGINE: OnnxAsrEngine | None = None
|
| 22 |
+
ENGINE_INFO: dict[str, Any] = {}
|
| 23 |
+
ENGINE_ARGS: argparse.Namespace | None = None
|
| 24 |
+
ENGINE_LOCK = threading.RLock()
|
| 25 |
+
PROCESS = psutil.Process()
|
| 26 |
+
SERVICE_PEAK_RSS = PROCESS.memory_info().rss
|
| 27 |
+
LAST_REQUEST_PEAK_RSS = SERVICE_PEAK_RSS
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
app = FastAPI(title="Audio8 ASR Local")
|
| 31 |
+
app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def release_engine() -> None:
|
| 35 |
+
global ENGINE
|
| 36 |
+
ENGINE = None
|
| 37 |
+
gc.collect()
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
class RequestMemorySampler:
|
| 41 |
+
def __init__(self, sample_interval_s: float = 0.05) -> None:
|
| 42 |
+
self.sample_interval_s = float(sample_interval_s)
|
| 43 |
+
self.peak_rss_bytes = PROCESS.memory_info().rss
|
| 44 |
+
self._stop = threading.Event()
|
| 45 |
+
self._thread: threading.Thread | None = None
|
| 46 |
+
|
| 47 |
+
def _run(self) -> None:
|
| 48 |
+
while not self._stop.wait(self.sample_interval_s):
|
| 49 |
+
self.peak_rss_bytes = max(self.peak_rss_bytes, PROCESS.memory_info().rss)
|
| 50 |
+
|
| 51 |
+
def start(self) -> None:
|
| 52 |
+
self.peak_rss_bytes = PROCESS.memory_info().rss
|
| 53 |
+
self._thread = threading.Thread(target=self._run, daemon=True)
|
| 54 |
+
self._thread.start()
|
| 55 |
+
|
| 56 |
+
def stop(self) -> int:
|
| 57 |
+
self.peak_rss_bytes = max(self.peak_rss_bytes, PROCESS.memory_info().rss)
|
| 58 |
+
self._stop.set()
|
| 59 |
+
if self._thread is not None:
|
| 60 |
+
self._thread.join(timeout=1.0)
|
| 61 |
+
return self.peak_rss_bytes
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def update_service_peak(candidate: int | None = None) -> None:
|
| 65 |
+
global SERVICE_PEAK_RSS
|
| 66 |
+
current = PROCESS.memory_info().rss
|
| 67 |
+
values = [SERVICE_PEAK_RSS, current]
|
| 68 |
+
if candidate is not None:
|
| 69 |
+
values.append(int(candidate))
|
| 70 |
+
SERVICE_PEAK_RSS = max(values)
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def available_cache_precisions(bundle_dir: str | Path) -> list[str]:
|
| 74 |
+
bundle_path = Path(bundle_dir).expanduser().resolve()
|
| 75 |
+
metadata_path = bundle_path / "metadata.json"
|
| 76 |
+
if not metadata_path.exists():
|
| 77 |
+
return []
|
| 78 |
+
metadata = json.loads(metadata_path.read_text(encoding="utf-8"))
|
| 79 |
+
prefill_graph = metadata.get("graphs", {}).get("lm_cache_prefill")
|
| 80 |
+
graph = metadata.get("graphs", {}).get("lm_cache_decode")
|
| 81 |
+
if not graph or not prefill_graph:
|
| 82 |
+
return []
|
| 83 |
+
prefill_graph_path = bundle_path / prefill_graph["path"]
|
| 84 |
+
graph_path = bundle_path / graph["path"]
|
| 85 |
+
values = []
|
| 86 |
+
if prefill_graph_path.exists() and graph_path.exists():
|
| 87 |
+
values.append("fp32")
|
| 88 |
+
prefill_int8_path = prefill_graph_path.with_name(f"{prefill_graph_path.stem}_int8{prefill_graph_path.suffix}")
|
| 89 |
+
int8_path = graph_path.with_name(f"{graph_path.stem}_int8{graph_path.suffix}")
|
| 90 |
+
if prefill_int8_path.exists() and int8_path.exists():
|
| 91 |
+
values.append("int8")
|
| 92 |
+
prefill_int4_path = prefill_graph_path.with_name(f"{prefill_graph_path.stem}_int4{prefill_graph_path.suffix}")
|
| 93 |
+
int4_path = graph_path.with_name(f"{graph_path.stem}_int4{graph_path.suffix}")
|
| 94 |
+
if prefill_int4_path.exists() and int4_path.exists():
|
| 95 |
+
values.append("int4")
|
| 96 |
+
return values
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
def available_audio_precisions(bundle_dir: str | Path) -> list[str]:
|
| 100 |
+
bundle_path = Path(bundle_dir).expanduser().resolve()
|
| 101 |
+
metadata_path = bundle_path / "metadata.json"
|
| 102 |
+
if not metadata_path.exists():
|
| 103 |
+
return []
|
| 104 |
+
metadata = json.loads(metadata_path.read_text(encoding="utf-8"))
|
| 105 |
+
graphs = metadata.get("graphs", {})
|
| 106 |
+
values = []
|
| 107 |
+
fp32_graph = graphs.get("audio_hidden")
|
| 108 |
+
if fp32_graph and (bundle_path / fp32_graph["path"]).exists():
|
| 109 |
+
values.append("fp32")
|
| 110 |
+
int8_graph = graphs.get("audio_hidden_int8")
|
| 111 |
+
if int8_graph and (bundle_path / int8_graph["path"]).exists():
|
| 112 |
+
values.append("int8")
|
| 113 |
+
return values
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def runtime_snapshot() -> dict[str, Any]:
|
| 117 |
+
update_service_peak()
|
| 118 |
+
bundle_dir = ENGINE_INFO.get("bundle_dir")
|
| 119 |
+
return {
|
| 120 |
+
**ENGINE_INFO,
|
| 121 |
+
"runtime_loaded": ENGINE is not None,
|
| 122 |
+
"available_cache_precisions": available_cache_precisions(bundle_dir) if bundle_dir else [],
|
| 123 |
+
"available_audio_precisions": available_audio_precisions(bundle_dir) if bundle_dir else [],
|
| 124 |
+
"service_peak_rss_bytes": int(SERVICE_PEAK_RSS),
|
| 125 |
+
"last_request_peak_rss_bytes": int(LAST_REQUEST_PEAK_RSS),
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def load_engine(
|
| 130 |
+
args: argparse.Namespace,
|
| 131 |
+
*,
|
| 132 |
+
backend: str | None = None,
|
| 133 |
+
cache_precision: str | None = None,
|
| 134 |
+
audio_precision: str | None = None,
|
| 135 |
+
release_existing: bool = False,
|
| 136 |
+
) -> None:
|
| 137 |
+
global ENGINE, ENGINE_INFO
|
| 138 |
+
if release_existing:
|
| 139 |
+
release_engine()
|
| 140 |
+
selected_backend = backend or args.backend
|
| 141 |
+
selected_precision = cache_precision or args.cache_precision
|
| 142 |
+
selected_audio_precision = audio_precision if audio_precision is not None else getattr(args, "audio_precision", None)
|
| 143 |
+
threads = args.threads if args.threads > 0 else None
|
| 144 |
+
|
| 145 |
+
if selected_backend in {"auto", "onnx_cache"}:
|
| 146 |
+
try:
|
| 147 |
+
engine = OnnxCacheAsrEngine(
|
| 148 |
+
args.bundle_dir,
|
| 149 |
+
provider=args.provider,
|
| 150 |
+
intra_op_num_threads=threads,
|
| 151 |
+
cache_precision=selected_precision,
|
| 152 |
+
audio_precision=selected_audio_precision,
|
| 153 |
+
)
|
| 154 |
+
ENGINE = engine
|
| 155 |
+
ENGINE_INFO = {
|
| 156 |
+
"bundle_dir": str(engine.bundle_dir),
|
| 157 |
+
"backend": "onnx_cache",
|
| 158 |
+
"cache_precision": engine.cache_precision,
|
| 159 |
+
"cache_prefill_graph": str(engine.prefill_graph_path),
|
| 160 |
+
"cache_graph": str(engine.cache_graph_path),
|
| 161 |
+
"audio_precision": engine.audio_precision,
|
| 162 |
+
"audio_graph": str(engine.audio_graph_path),
|
| 163 |
+
"providers": engine.providers,
|
| 164 |
+
}
|
| 165 |
+
update_service_peak()
|
| 166 |
+
return
|
| 167 |
+
except Exception:
|
| 168 |
+
if selected_backend == "onnx_cache":
|
| 169 |
+
raise
|
| 170 |
+
|
| 171 |
+
engine = OnnxAsrEngine(
|
| 172 |
+
args.bundle_dir,
|
| 173 |
+
provider=args.provider,
|
| 174 |
+
intra_op_num_threads=threads,
|
| 175 |
+
audio_precision=selected_audio_precision or "fp32",
|
| 176 |
+
)
|
| 177 |
+
ENGINE = engine
|
| 178 |
+
ENGINE_INFO = {
|
| 179 |
+
"bundle_dir": str(engine.bundle_dir),
|
| 180 |
+
"backend": "onnx",
|
| 181 |
+
"cache_precision": None,
|
| 182 |
+
"cache_graph": None,
|
| 183 |
+
"audio_precision": engine.audio_precision,
|
| 184 |
+
"audio_graph": str(engine.audio_graph_path),
|
| 185 |
+
"providers": engine.providers,
|
| 186 |
+
}
|
| 187 |
+
update_service_peak()
|
| 188 |
+
|
| 189 |
+
|
| 190 |
+
@app.get("/")
|
| 191 |
+
async def index() -> FileResponse:
|
| 192 |
+
return FileResponse(STATIC_DIR / "index.html")
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
@app.get("/health")
|
| 196 |
+
async def health() -> dict[str, Any]:
|
| 197 |
+
if ENGINE is None:
|
| 198 |
+
return {"ok": False, "error": "engine not loaded"}
|
| 199 |
+
return {"ok": True, **runtime_snapshot()}
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
@app.get("/api/runtime")
|
| 203 |
+
async def runtime() -> dict[str, Any]:
|
| 204 |
+
return {"ok": ENGINE is not None, "runtime": runtime_snapshot()}
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
@app.post("/api/reload")
|
| 208 |
+
async def reload_runtime(
|
| 209 |
+
backend: str = Form("onnx_cache"),
|
| 210 |
+
cache_precision: str | None = Form(None),
|
| 211 |
+
audio_precision: str | None = Form(None),
|
| 212 |
+
) -> dict[str, Any]:
|
| 213 |
+
if ENGINE_ARGS is None:
|
| 214 |
+
raise HTTPException(status_code=503, detail="Server arguments are not initialized")
|
| 215 |
+
if backend not in {"onnx_cache", "onnx", "auto"}:
|
| 216 |
+
raise HTTPException(status_code=400, detail=f"Unsupported backend: {backend}")
|
| 217 |
+
if cache_precision:
|
| 218 |
+
cache_precision = cache_precision.lower().strip()
|
| 219 |
+
if cache_precision not in {"fp32", "int8", "int4", "auto"}:
|
| 220 |
+
raise HTTPException(status_code=400, detail=f"Unsupported cache precision: {cache_precision}")
|
| 221 |
+
else:
|
| 222 |
+
cache_precision = str(ENGINE_INFO.get("cache_precision") or "").lower().strip() or ENGINE_ARGS.cache_precision
|
| 223 |
+
if audio_precision:
|
| 224 |
+
audio_precision = audio_precision.lower().strip()
|
| 225 |
+
if audio_precision not in {"fp32", "int8", "auto"}:
|
| 226 |
+
raise HTTPException(status_code=400, detail=f"Unsupported audio precision: {audio_precision}")
|
| 227 |
+
else:
|
| 228 |
+
audio_precision = str(ENGINE_INFO.get("audio_precision") or "").lower().strip() or None
|
| 229 |
+
with ENGINE_LOCK:
|
| 230 |
+
try:
|
| 231 |
+
load_engine(
|
| 232 |
+
ENGINE_ARGS,
|
| 233 |
+
backend=backend,
|
| 234 |
+
cache_precision=cache_precision,
|
| 235 |
+
audio_precision=audio_precision,
|
| 236 |
+
release_existing=True,
|
| 237 |
+
)
|
| 238 |
+
except Exception as exc:
|
| 239 |
+
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
| 240 |
+
return {"ok": True, "runtime": runtime_snapshot()}
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
@app.get("/metrics")
|
| 244 |
+
async def metrics() -> dict[str, Any]:
|
| 245 |
+
data = collect_metrics()
|
| 246 |
+
data["runtime"] = runtime_snapshot()
|
| 247 |
+
return data
|
| 248 |
+
|
| 249 |
+
|
| 250 |
+
@app.post("/asr")
|
| 251 |
+
async def asr(
|
| 252 |
+
audio: UploadFile = File(...),
|
| 253 |
+
language: str | None = Form(None),
|
| 254 |
+
max_new_tokens: int = Form(128),
|
| 255 |
+
cache_precision: str | None = Form(None),
|
| 256 |
+
audio_precision: str | None = Form(None),
|
| 257 |
+
hotwords: str | None = Form(None),
|
| 258 |
+
hotword_topk: int = Form(50),
|
| 259 |
+
hotword_start_boost: float = Form(6.0),
|
| 260 |
+
hotword_continuation_boost: float = Form(8.0),
|
| 261 |
+
) -> dict[str, Any]:
|
| 262 |
+
global LAST_REQUEST_PEAK_RSS
|
| 263 |
+
if ENGINE is None:
|
| 264 |
+
raise HTTPException(status_code=503, detail="ASR engine is not loaded")
|
| 265 |
+
audio_bytes = await audio.read()
|
| 266 |
+
if not audio_bytes:
|
| 267 |
+
raise HTTPException(status_code=400, detail="Empty audio upload")
|
| 268 |
+
if ENGINE_ARGS is not None:
|
| 269 |
+
requested_cache = cache_precision.lower().strip() if cache_precision else None
|
| 270 |
+
requested_audio = audio_precision.lower().strip() if audio_precision else None
|
| 271 |
+
if requested_cache and requested_cache not in {"fp32", "int8", "int4", "auto"}:
|
| 272 |
+
raise HTTPException(status_code=400, detail=f"Unsupported cache precision: {requested_cache}")
|
| 273 |
+
if requested_audio and requested_audio not in {"fp32", "int8", "auto"}:
|
| 274 |
+
raise HTTPException(status_code=400, detail=f"Unsupported audio precision: {requested_audio}")
|
| 275 |
+
current_cache = str(ENGINE_INFO.get("cache_precision") or "").lower().strip()
|
| 276 |
+
current_audio = str(ENGINE_INFO.get("audio_precision") or "").lower().strip()
|
| 277 |
+
target_cache = requested_cache or current_cache or ENGINE_ARGS.cache_precision
|
| 278 |
+
target_audio = requested_audio or current_audio or getattr(ENGINE_ARGS, "audio_precision", None)
|
| 279 |
+
should_reload = (
|
| 280 |
+
ENGINE_INFO.get("backend") == "onnx_cache"
|
| 281 |
+
and (
|
| 282 |
+
(requested_cache is not None and target_cache != current_cache)
|
| 283 |
+
or (requested_audio is not None and target_audio != current_audio)
|
| 284 |
+
)
|
| 285 |
+
)
|
| 286 |
+
if should_reload:
|
| 287 |
+
with ENGINE_LOCK:
|
| 288 |
+
try:
|
| 289 |
+
load_engine(
|
| 290 |
+
ENGINE_ARGS,
|
| 291 |
+
backend="onnx_cache",
|
| 292 |
+
cache_precision=target_cache,
|
| 293 |
+
audio_precision=target_audio,
|
| 294 |
+
release_existing=True,
|
| 295 |
+
)
|
| 296 |
+
except Exception as exc:
|
| 297 |
+
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
| 298 |
+
sampler = RequestMemorySampler()
|
| 299 |
+
sampler.start()
|
| 300 |
+
try:
|
| 301 |
+
with ENGINE_LOCK:
|
| 302 |
+
if ENGINE is None:
|
| 303 |
+
raise HTTPException(status_code=503, detail="ASR engine is not loaded")
|
| 304 |
+
result = ENGINE.transcribe(
|
| 305 |
+
audio_bytes,
|
| 306 |
+
language=language or None,
|
| 307 |
+
max_new_tokens=max_new_tokens,
|
| 308 |
+
hotwords=hotwords or None,
|
| 309 |
+
hotword_topk=int(hotword_topk),
|
| 310 |
+
hotword_start_boost=float(hotword_start_boost),
|
| 311 |
+
hotword_continuation_boost=float(hotword_continuation_boost),
|
| 312 |
+
)
|
| 313 |
+
LAST_REQUEST_PEAK_RSS = max(int(sampler.stop()), PROCESS.memory_info().rss)
|
| 314 |
+
update_service_peak(LAST_REQUEST_PEAK_RSS)
|
| 315 |
+
result["request_peak_rss_bytes"] = int(LAST_REQUEST_PEAK_RSS)
|
| 316 |
+
result["runtime"] = runtime_snapshot()
|
| 317 |
+
return result
|
| 318 |
+
except Exception as exc:
|
| 319 |
+
LAST_REQUEST_PEAK_RSS = max(int(sampler.stop()), PROCESS.memory_info().rss)
|
| 320 |
+
update_service_peak(LAST_REQUEST_PEAK_RSS)
|
| 321 |
+
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
| 322 |
+
|
| 323 |
+
|
| 324 |
+
def parse_args() -> argparse.Namespace:
|
| 325 |
+
parser = argparse.ArgumentParser(description="Run the local Audio8 ASR ONNX web UI.")
|
| 326 |
+
parser.add_argument(
|
| 327 |
+
"--bundle_dir",
|
| 328 |
+
default=os.environ.get("AUDIO8_ASR_BUNDLE", "model_bundle"),
|
| 329 |
+
)
|
| 330 |
+
parser.add_argument(
|
| 331 |
+
"--backend",
|
| 332 |
+
choices=["auto", "onnx", "onnx_cache"],
|
| 333 |
+
default=os.environ.get("ASR_BACKEND", "auto"),
|
| 334 |
+
)
|
| 335 |
+
parser.add_argument("--host", default=os.environ.get("HOST", "127.0.0.1"))
|
| 336 |
+
parser.add_argument("--port", type=int, default=int(os.environ.get("PORT", "7860")))
|
| 337 |
+
parser.add_argument("--provider", default=os.environ.get("ORT_PROVIDER", "CPUExecutionProvider"))
|
| 338 |
+
parser.add_argument("--cache_precision", default=os.environ.get("ASR_CACHE_PRECISION", "auto"))
|
| 339 |
+
parser.add_argument("--audio_precision", choices=["fp32", "int8", "auto"], default=os.environ.get("ASR_AUDIO_PRECISION", "auto"))
|
| 340 |
+
parser.add_argument("--threads", type=int, default=int(os.environ.get("ORT_THREADS", "0")))
|
| 341 |
+
return parser.parse_args()
|
| 342 |
+
|
| 343 |
+
|
| 344 |
+
def main() -> None:
|
| 345 |
+
global ENGINE_ARGS
|
| 346 |
+
args = parse_args()
|
| 347 |
+
ENGINE_ARGS = args
|
| 348 |
+
load_engine(args)
|
| 349 |
+
_run_uvicorn(args)
|
| 350 |
+
|
| 351 |
+
|
| 352 |
+
def _run_uvicorn(args: argparse.Namespace) -> None:
|
| 353 |
+
import uvicorn
|
| 354 |
+
|
| 355 |
+
uvicorn.run(app, host=args.host, port=args.port)
|
| 356 |
+
|
| 357 |
+
|
| 358 |
+
if __name__ == "__main__":
|
| 359 |
+
main()
|
smoke_test.sh
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
set -euo pipefail
|
| 3 |
+
|
| 4 |
+
HOST="${1:-127.0.0.1}"
|
| 5 |
+
PORT="${2:-7860}"
|
| 6 |
+
if [[ $# -lt 3 ]]; then
|
| 7 |
+
echo "Usage: $0 [host] [port] /path/to/audio.wav" >&2
|
| 8 |
+
exit 2
|
| 9 |
+
fi
|
| 10 |
+
AUDIO_PATH="$3"
|
| 11 |
+
BASE_URL="http://${HOST}:${PORT}"
|
| 12 |
+
PYTHON_BIN="${PYTHON:-python3}"
|
| 13 |
+
CURL_LOCAL=(curl --noproxy "*" -fsS)
|
| 14 |
+
|
| 15 |
+
echo "[smoke] health ${BASE_URL}/health"
|
| 16 |
+
"${CURL_LOCAL[@]}" "${BASE_URL}/health" | "$PYTHON_BIN" -m json.tool
|
| 17 |
+
|
| 18 |
+
echo "[smoke] asr ${AUDIO_PATH}"
|
| 19 |
+
"${CURL_LOCAL[@]}" -X POST "${BASE_URL}/asr" \
|
| 20 |
+
-F "audio=@${AUDIO_PATH}" \
|
| 21 |
+
-F "language=Chinese" \
|
| 22 |
+
-F "max_new_tokens=64" \
|
| 23 |
+
-F "cache_precision=int8" \
|
| 24 |
+
-F "audio_precision=int8" \
|
| 25 |
+
| "$PYTHON_BIN" -m json.tool
|
static/app.js
ADDED
|
@@ -0,0 +1,459 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const recordBtn = document.getElementById("recordBtn");
|
| 2 |
+
const stopBtn = document.getElementById("stopBtn");
|
| 3 |
+
const uploadBtn = document.getElementById("uploadBtn");
|
| 4 |
+
const fileInput = document.getElementById("fileInput");
|
| 5 |
+
const preview = document.getElementById("preview");
|
| 6 |
+
const transcript = document.getElementById("transcript");
|
| 7 |
+
const raw = document.getElementById("raw");
|
| 8 |
+
const health = document.getElementById("health");
|
| 9 |
+
const recordStatus = document.getElementById("recordStatus");
|
| 10 |
+
const maxTokens = document.getElementById("maxTokens");
|
| 11 |
+
const hotwords = document.getElementById("hotwords");
|
| 12 |
+
const hotwordStrengthSegments = document.getElementById("hotwordStrengthSegments");
|
| 13 |
+
const latency = document.getElementById("latency");
|
| 14 |
+
const precisionSegments = document.getElementById("precisionSegments");
|
| 15 |
+
const precisionNote = document.getElementById("precisionNote");
|
| 16 |
+
const audioPrecisionSegments = document.getElementById("audioPrecisionSegments");
|
| 17 |
+
const audioPrecisionNote = document.getElementById("audioPrecisionNote");
|
| 18 |
+
|
| 19 |
+
let stream = null;
|
| 20 |
+
let audioContext = null;
|
| 21 |
+
let sourceNode = null;
|
| 22 |
+
let processorNode = null;
|
| 23 |
+
let monitorGain = null;
|
| 24 |
+
let recordedBuffers = [];
|
| 25 |
+
let recordingSampleRate = 16000;
|
| 26 |
+
let isStoppingRecording = false;
|
| 27 |
+
const stopTailCaptureMs = 80;
|
| 28 |
+
const wavTailSilenceMs = 300;
|
| 29 |
+
let selectedPrecision = "int8";
|
| 30 |
+
let availablePrecisions = ["fp32", "int8", "int4"];
|
| 31 |
+
let selectedAudioPrecision = "int8";
|
| 32 |
+
let availableAudioPrecisions = ["fp32", "int8"];
|
| 33 |
+
let selectedHotwordStrength = "normal";
|
| 34 |
+
const hotwordStrengths = {
|
| 35 |
+
normal: {
|
| 36 |
+
topk: 50,
|
| 37 |
+
startBoost: 6.0,
|
| 38 |
+
continuationBoost: 8.0,
|
| 39 |
+
},
|
| 40 |
+
strong: {
|
| 41 |
+
topk: 50,
|
| 42 |
+
startBoost: 7.0,
|
| 43 |
+
continuationBoost: 9.0,
|
| 44 |
+
},
|
| 45 |
+
};
|
| 46 |
+
|
| 47 |
+
function apiUrl(path) {
|
| 48 |
+
if (window.location.protocol === "file:") {
|
| 49 |
+
return `http://127.0.0.1:7860${path}`;
|
| 50 |
+
}
|
| 51 |
+
return path;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
function formatBytes(bytes) {
|
| 55 |
+
if (!Number.isFinite(bytes)) return "--";
|
| 56 |
+
const units = ["B", "KB", "MB", "GB", "TB"];
|
| 57 |
+
let value = bytes;
|
| 58 |
+
let unit = 0;
|
| 59 |
+
while (value >= 1024 && unit < units.length - 1) {
|
| 60 |
+
value /= 1024;
|
| 61 |
+
unit += 1;
|
| 62 |
+
}
|
| 63 |
+
return `${value.toFixed(unit === 0 ? 0 : 1)} ${units[unit]}`;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
function formatSeconds(value) {
|
| 67 |
+
const number = Number(value);
|
| 68 |
+
return Number.isFinite(number) ? `${number.toFixed(2)}s` : "--";
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
function setBusy(isBusy, label = "Ready") {
|
| 72 |
+
recordStatus.textContent = label;
|
| 73 |
+
recordBtn.disabled = isBusy;
|
| 74 |
+
uploadBtn.disabled = isBusy;
|
| 75 |
+
precisionSegments.querySelectorAll("button").forEach((button) => {
|
| 76 |
+
button.disabled = isBusy || !availablePrecisions.includes(button.dataset.precision);
|
| 77 |
+
});
|
| 78 |
+
audioPrecisionSegments.querySelectorAll("button").forEach((button) => {
|
| 79 |
+
button.disabled = isBusy || !availableAudioPrecisions.includes(button.dataset.audioPrecision);
|
| 80 |
+
});
|
| 81 |
+
hotwordStrengthSegments.querySelectorAll("button").forEach((button) => {
|
| 82 |
+
button.disabled = isBusy;
|
| 83 |
+
});
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
function setPrecisionButtons() {
|
| 87 |
+
precisionSegments.querySelectorAll("button").forEach((button) => {
|
| 88 |
+
const precision = button.dataset.precision;
|
| 89 |
+
button.classList.toggle("active", precision === selectedPrecision);
|
| 90 |
+
button.disabled = !availablePrecisions.includes(precision);
|
| 91 |
+
});
|
| 92 |
+
precisionNote.textContent = availablePrecisions.length ? `${availablePrecisions.join(" / ")} available` : "cache unavailable";
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
function setAudioPrecisionButtons() {
|
| 96 |
+
audioPrecisionSegments.querySelectorAll("button").forEach((button) => {
|
| 97 |
+
const precision = button.dataset.audioPrecision;
|
| 98 |
+
button.classList.toggle("active", precision === selectedAudioPrecision);
|
| 99 |
+
button.disabled = !availableAudioPrecisions.includes(precision);
|
| 100 |
+
});
|
| 101 |
+
audioPrecisionNote.textContent = availableAudioPrecisions.length
|
| 102 |
+
? `${availableAudioPrecisions.join(" / ")} available`
|
| 103 |
+
: "audio quant unavailable";
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
function setHotwordStrengthButtons() {
|
| 107 |
+
hotwordStrengthSegments.querySelectorAll("button").forEach((button) => {
|
| 108 |
+
const strength = button.dataset.hotwordStrength;
|
| 109 |
+
button.classList.toggle("active", strength === selectedHotwordStrength);
|
| 110 |
+
});
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
function applyRuntime(runtime) {
|
| 114 |
+
if (!runtime) return;
|
| 115 |
+
availablePrecisions = Array.isArray(runtime.available_cache_precisions)
|
| 116 |
+
? runtime.available_cache_precisions
|
| 117 |
+
: ["fp32"];
|
| 118 |
+
availableAudioPrecisions = Array.isArray(runtime.available_audio_precisions)
|
| 119 |
+
? runtime.available_audio_precisions
|
| 120 |
+
: ["fp32"];
|
| 121 |
+
selectedPrecision = runtime.cache_precision || selectedPrecision || "fp32";
|
| 122 |
+
selectedAudioPrecision = runtime.audio_precision || selectedAudioPrecision || "fp32";
|
| 123 |
+
setPrecisionButtons();
|
| 124 |
+
setAudioPrecisionButtons();
|
| 125 |
+
health.textContent = `${runtime.backend || "--"} · ${runtime.cache_precision || "n/a"} · audio ${runtime.audio_precision || "n/a"}`;
|
| 126 |
+
document.getElementById("backend").textContent = runtime.backend || "--";
|
| 127 |
+
document.getElementById("precision").textContent = `${runtime.cache_precision || "n/a"} / ${runtime.audio_precision || "n/a"}`;
|
| 128 |
+
document.getElementById("peakRss").textContent = formatBytes(runtime.service_peak_rss_bytes);
|
| 129 |
+
document.getElementById("requestPeak").textContent = formatBytes(runtime.last_request_peak_rss_bytes);
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
async function checkRuntime() {
|
| 133 |
+
try {
|
| 134 |
+
const response = await fetch(apiUrl("/api/runtime"));
|
| 135 |
+
const data = await response.json();
|
| 136 |
+
applyRuntime(data.runtime);
|
| 137 |
+
} catch (error) {
|
| 138 |
+
health.textContent = `runtime check failed: ${error}`;
|
| 139 |
+
}
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
async function switchPrecision(precision) {
|
| 143 |
+
if (precision === selectedPrecision || !availablePrecisions.includes(precision)) return;
|
| 144 |
+
setBusy(true, "Loading");
|
| 145 |
+
const form = new FormData();
|
| 146 |
+
form.append("backend", "onnx_cache");
|
| 147 |
+
form.append("cache_precision", precision);
|
| 148 |
+
form.append("audio_precision", selectedAudioPrecision);
|
| 149 |
+
try {
|
| 150 |
+
const response = await fetch(apiUrl("/api/reload"), { method: "POST", body: form });
|
| 151 |
+
const data = await response.json();
|
| 152 |
+
if (!response.ok) {
|
| 153 |
+
throw new Error(data.detail || response.statusText);
|
| 154 |
+
}
|
| 155 |
+
applyRuntime(data.runtime);
|
| 156 |
+
recordStatus.textContent = "Ready";
|
| 157 |
+
} catch (error) {
|
| 158 |
+
transcript.textContent = `Runtime switch error: ${error}`;
|
| 159 |
+
recordStatus.textContent = "Error";
|
| 160 |
+
} finally {
|
| 161 |
+
setBusy(false, recordStatus.textContent);
|
| 162 |
+
}
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
async function switchAudioPrecision(precision) {
|
| 166 |
+
if (precision === selectedAudioPrecision || !availableAudioPrecisions.includes(precision)) return;
|
| 167 |
+
setBusy(true, "Loading");
|
| 168 |
+
const form = new FormData();
|
| 169 |
+
form.append("backend", "onnx_cache");
|
| 170 |
+
form.append("cache_precision", selectedPrecision);
|
| 171 |
+
form.append("audio_precision", precision);
|
| 172 |
+
try {
|
| 173 |
+
const response = await fetch(apiUrl("/api/reload"), { method: "POST", body: form });
|
| 174 |
+
const data = await response.json();
|
| 175 |
+
if (!response.ok) {
|
| 176 |
+
throw new Error(data.detail || response.statusText);
|
| 177 |
+
}
|
| 178 |
+
applyRuntime(data.runtime);
|
| 179 |
+
recordStatus.textContent = "Ready";
|
| 180 |
+
} catch (error) {
|
| 181 |
+
transcript.textContent = `Runtime switch error: ${error}`;
|
| 182 |
+
recordStatus.textContent = "Error";
|
| 183 |
+
} finally {
|
| 184 |
+
setBusy(false, recordStatus.textContent);
|
| 185 |
+
}
|
| 186 |
+
}
|
| 187 |
+
|
| 188 |
+
async function uploadBlob(blob, name = "recording.wav") {
|
| 189 |
+
try {
|
| 190 |
+
setBusy(true, "Transcribing");
|
| 191 |
+
transcript.textContent = "Working...";
|
| 192 |
+
raw.textContent = "";
|
| 193 |
+
latency.textContent = "--";
|
| 194 |
+
const form = new FormData();
|
| 195 |
+
form.append("audio", blob, name);
|
| 196 |
+
form.append("language", "Chinese");
|
| 197 |
+
form.append("max_new_tokens", maxTokens.value || "128");
|
| 198 |
+
form.append("cache_precision", selectedPrecision);
|
| 199 |
+
form.append("audio_precision", selectedAudioPrecision);
|
| 200 |
+
if (hotwords && hotwords.value.trim()) {
|
| 201 |
+
form.append("hotwords", hotwords.value.trim());
|
| 202 |
+
const strength = hotwordStrengths[selectedHotwordStrength] || hotwordStrengths.normal;
|
| 203 |
+
form.append("hotword_topk", String(strength.topk));
|
| 204 |
+
form.append("hotword_start_boost", String(strength.startBoost));
|
| 205 |
+
form.append("hotword_continuation_boost", String(strength.continuationBoost));
|
| 206 |
+
}
|
| 207 |
+
const response = await fetch(apiUrl("/asr"), { method: "POST", body: form });
|
| 208 |
+
const text = await response.text();
|
| 209 |
+
let data = {};
|
| 210 |
+
try {
|
| 211 |
+
data = text ? JSON.parse(text) : {};
|
| 212 |
+
} catch (error) {
|
| 213 |
+
throw new Error(`Invalid ASR response: ${text.slice(0, 240)}`);
|
| 214 |
+
}
|
| 215 |
+
if (!response.ok) {
|
| 216 |
+
throw new Error(data.detail || response.statusText);
|
| 217 |
+
}
|
| 218 |
+
transcript.textContent = data.text || "(empty)";
|
| 219 |
+
latency.textContent = `${formatSeconds(data.elapsed_seconds)} · ${data.cache_precision || selectedPrecision}`;
|
| 220 |
+
raw.textContent = JSON.stringify(data, null, 2);
|
| 221 |
+
applyRuntime(data.runtime);
|
| 222 |
+
setBusy(false, "Ready");
|
| 223 |
+
} catch (error) {
|
| 224 |
+
transcript.textContent = `Transcription error: ${error}`;
|
| 225 |
+
raw.textContent = "";
|
| 226 |
+
setBusy(false, "Error");
|
| 227 |
+
throw error;
|
| 228 |
+
}
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
function mergeAudioBuffers(buffers) {
|
| 232 |
+
const totalLength = buffers.reduce((sum, buffer) => sum + buffer.length, 0);
|
| 233 |
+
const merged = new Float32Array(totalLength);
|
| 234 |
+
let offset = 0;
|
| 235 |
+
for (const buffer of buffers) {
|
| 236 |
+
merged.set(buffer, offset);
|
| 237 |
+
offset += buffer.length;
|
| 238 |
+
}
|
| 239 |
+
return merged;
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
function appendSilence(samples, sampleRate, durationMs) {
|
| 243 |
+
const silenceLength = Math.max(0, Math.round((sampleRate * durationMs) / 1000));
|
| 244 |
+
if (silenceLength === 0) return samples;
|
| 245 |
+
const padded = new Float32Array(samples.length + silenceLength);
|
| 246 |
+
padded.set(samples);
|
| 247 |
+
return padded;
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
function delay(ms) {
|
| 251 |
+
return new Promise((resolve) => {
|
| 252 |
+
window.setTimeout(resolve, ms);
|
| 253 |
+
});
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
function writeString(view, offset, value) {
|
| 257 |
+
for (let index = 0; index < value.length; index += 1) {
|
| 258 |
+
view.setUint8(offset + index, value.charCodeAt(index));
|
| 259 |
+
}
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
function encodeWav(samples, sampleRate) {
|
| 263 |
+
const bytesPerSample = 2;
|
| 264 |
+
const channelCount = 1;
|
| 265 |
+
const buffer = new ArrayBuffer(44 + samples.length * bytesPerSample);
|
| 266 |
+
const view = new DataView(buffer);
|
| 267 |
+
|
| 268 |
+
writeString(view, 0, "RIFF");
|
| 269 |
+
view.setUint32(4, 36 + samples.length * bytesPerSample, true);
|
| 270 |
+
writeString(view, 8, "WAVE");
|
| 271 |
+
writeString(view, 12, "fmt ");
|
| 272 |
+
view.setUint32(16, 16, true);
|
| 273 |
+
view.setUint16(20, 1, true);
|
| 274 |
+
view.setUint16(22, channelCount, true);
|
| 275 |
+
view.setUint32(24, sampleRate, true);
|
| 276 |
+
view.setUint32(28, sampleRate * channelCount * bytesPerSample, true);
|
| 277 |
+
view.setUint16(32, channelCount * bytesPerSample, true);
|
| 278 |
+
view.setUint16(34, 8 * bytesPerSample, true);
|
| 279 |
+
writeString(view, 36, "data");
|
| 280 |
+
view.setUint32(40, samples.length * bytesPerSample, true);
|
| 281 |
+
|
| 282 |
+
let offset = 44;
|
| 283 |
+
for (const sample of samples) {
|
| 284 |
+
const clamped = Math.max(-1, Math.min(1, sample));
|
| 285 |
+
view.setInt16(offset, clamped < 0 ? clamped * 0x8000 : clamped * 0x7fff, true);
|
| 286 |
+
offset += bytesPerSample;
|
| 287 |
+
}
|
| 288 |
+
return new Blob([view], { type: "audio/wav" });
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
async function stopRecording() {
|
| 292 |
+
if (!audioContext || isStoppingRecording) return;
|
| 293 |
+
isStoppingRecording = true;
|
| 294 |
+
recordStatus.textContent = "Finishing";
|
| 295 |
+
stopBtn.disabled = true;
|
| 296 |
+
await delay(stopTailCaptureMs);
|
| 297 |
+
recordStatus.textContent = "Preparing";
|
| 298 |
+
|
| 299 |
+
if (processorNode) {
|
| 300 |
+
processorNode.disconnect();
|
| 301 |
+
processorNode.onaudioprocess = null;
|
| 302 |
+
}
|
| 303 |
+
if (sourceNode) sourceNode.disconnect();
|
| 304 |
+
if (monitorGain) monitorGain.disconnect();
|
| 305 |
+
if (stream) stream.getTracks().forEach((track) => track.stop());
|
| 306 |
+
|
| 307 |
+
const samples = appendSilence(mergeAudioBuffers(recordedBuffers), recordingSampleRate, wavTailSilenceMs);
|
| 308 |
+
const wav = encodeWav(samples, recordingSampleRate);
|
| 309 |
+
preview.src = URL.createObjectURL(wav);
|
| 310 |
+
|
| 311 |
+
await audioContext.close();
|
| 312 |
+
audioContext = null;
|
| 313 |
+
sourceNode = null;
|
| 314 |
+
processorNode = null;
|
| 315 |
+
monitorGain = null;
|
| 316 |
+
stream = null;
|
| 317 |
+
recordedBuffers = [];
|
| 318 |
+
isStoppingRecording = false;
|
| 319 |
+
|
| 320 |
+
await uploadBlob(wav, "recording.wav");
|
| 321 |
+
}
|
| 322 |
+
|
| 323 |
+
recordBtn.addEventListener("click", async () => {
|
| 324 |
+
try {
|
| 325 |
+
stream = await navigator.mediaDevices.getUserMedia({
|
| 326 |
+
audio: {
|
| 327 |
+
channelCount: 1,
|
| 328 |
+
echoCancellation: true,
|
| 329 |
+
noiseSuppression: true,
|
| 330 |
+
},
|
| 331 |
+
});
|
| 332 |
+
const AudioContextClass = window.AudioContext || window.webkitAudioContext;
|
| 333 |
+
audioContext = new AudioContextClass();
|
| 334 |
+
recordingSampleRate = audioContext.sampleRate;
|
| 335 |
+
recordedBuffers = [];
|
| 336 |
+
isStoppingRecording = false;
|
| 337 |
+
sourceNode = audioContext.createMediaStreamSource(stream);
|
| 338 |
+
processorNode = audioContext.createScriptProcessor(4096, 1, 1);
|
| 339 |
+
monitorGain = audioContext.createGain();
|
| 340 |
+
monitorGain.gain.value = 0;
|
| 341 |
+
processorNode.onaudioprocess = (event) => {
|
| 342 |
+
recordedBuffers.push(new Float32Array(event.inputBuffer.getChannelData(0)));
|
| 343 |
+
};
|
| 344 |
+
sourceNode.connect(processorNode);
|
| 345 |
+
processorNode.connect(monitorGain);
|
| 346 |
+
monitorGain.connect(audioContext.destination);
|
| 347 |
+
recordStatus.textContent = "Recording";
|
| 348 |
+
recordBtn.disabled = true;
|
| 349 |
+
stopBtn.disabled = false;
|
| 350 |
+
uploadBtn.disabled = true;
|
| 351 |
+
} catch (error) {
|
| 352 |
+
transcript.textContent = `Microphone error: ${error}`;
|
| 353 |
+
recordStatus.textContent = "Error";
|
| 354 |
+
}
|
| 355 |
+
});
|
| 356 |
+
|
| 357 |
+
stopBtn.addEventListener("click", () => {
|
| 358 |
+
stopRecording().catch((error) => {
|
| 359 |
+
transcript.textContent = String(error);
|
| 360 |
+
setBusy(false, "Error");
|
| 361 |
+
});
|
| 362 |
+
});
|
| 363 |
+
|
| 364 |
+
uploadBtn.addEventListener("click", async () => {
|
| 365 |
+
const file = fileInput.files && fileInput.files[0];
|
| 366 |
+
if (!file) {
|
| 367 |
+
transcript.textContent = "Choose an audio file first.";
|
| 368 |
+
return;
|
| 369 |
+
}
|
| 370 |
+
preview.src = URL.createObjectURL(file);
|
| 371 |
+
try {
|
| 372 |
+
await uploadBlob(file, file.name);
|
| 373 |
+
} catch (error) {
|
| 374 |
+
transcript.textContent = String(error);
|
| 375 |
+
setBusy(false, "Error");
|
| 376 |
+
}
|
| 377 |
+
});
|
| 378 |
+
|
| 379 |
+
precisionSegments.addEventListener("click", (event) => {
|
| 380 |
+
const button = event.target.closest("button[data-precision]");
|
| 381 |
+
if (button) {
|
| 382 |
+
switchPrecision(button.dataset.precision);
|
| 383 |
+
}
|
| 384 |
+
});
|
| 385 |
+
|
| 386 |
+
audioPrecisionSegments.addEventListener("click", (event) => {
|
| 387 |
+
const button = event.target.closest("button[data-audio-precision]");
|
| 388 |
+
if (button) {
|
| 389 |
+
switchAudioPrecision(button.dataset.audioPrecision);
|
| 390 |
+
}
|
| 391 |
+
});
|
| 392 |
+
|
| 393 |
+
hotwordStrengthSegments.addEventListener("click", (event) => {
|
| 394 |
+
const button = event.target.closest("button[data-hotword-strength]");
|
| 395 |
+
if (!button || !hotwordStrengths[button.dataset.hotwordStrength]) return;
|
| 396 |
+
selectedHotwordStrength = button.dataset.hotwordStrength;
|
| 397 |
+
setHotwordStrengthButtons();
|
| 398 |
+
});
|
| 399 |
+
|
| 400 |
+
function renderGpuList(gpus) {
|
| 401 |
+
const gpuList = document.getElementById("gpuList");
|
| 402 |
+
const gpuSummary = document.getElementById("gpuSummary");
|
| 403 |
+
gpuList.innerHTML = "";
|
| 404 |
+
if (!Array.isArray(gpus) || gpus.length === 0) {
|
| 405 |
+
gpuSummary.textContent = "unified / n/a";
|
| 406 |
+
gpuList.innerHTML = "<div class='empty'>No discrete GPU telemetry.</div>";
|
| 407 |
+
return;
|
| 408 |
+
}
|
| 409 |
+
const totalUsed = gpus.reduce((sum, gpu) => sum + Number(gpu.memory_used_mb || 0), 0);
|
| 410 |
+
const total = gpus.reduce((sum, gpu) => sum + Number(gpu.memory_total_mb || 0), 0);
|
| 411 |
+
gpuSummary.textContent = `${totalUsed.toFixed(0)} / ${total.toFixed(0)} MB`;
|
| 412 |
+
for (const gpu of gpus) {
|
| 413 |
+
const used = Number(gpu.memory_used_mb || 0);
|
| 414 |
+
const capacity = Number(gpu.memory_total_mb || 0);
|
| 415 |
+
const percent = capacity > 0 ? Math.min(100, Math.max(0, (used / capacity) * 100)) : 0;
|
| 416 |
+
const item = document.createElement("div");
|
| 417 |
+
item.className = "gpu-item";
|
| 418 |
+
item.innerHTML = `
|
| 419 |
+
<div class="gpu-line">
|
| 420 |
+
<strong>GPU ${gpu.index}</strong>
|
| 421 |
+
<span>${used.toFixed(0)} / ${capacity.toFixed(0)} MB · ${Number(gpu.utilization_percent || 0).toFixed(0)}%</span>
|
| 422 |
+
</div>
|
| 423 |
+
<div class="bar"><i style="width: ${percent.toFixed(1)}%"></i></div>
|
| 424 |
+
<small>${gpu.name || ""}</small>
|
| 425 |
+
`;
|
| 426 |
+
gpuList.appendChild(item);
|
| 427 |
+
}
|
| 428 |
+
}
|
| 429 |
+
|
| 430 |
+
async function refreshMetrics() {
|
| 431 |
+
try {
|
| 432 |
+
const response = await fetch(apiUrl("/metrics"));
|
| 433 |
+
const data = await response.json();
|
| 434 |
+
document.getElementById("metricStamp").textContent = new Date().toLocaleTimeString();
|
| 435 |
+
document.getElementById("rss").textContent = formatBytes(data.process.rss_bytes);
|
| 436 |
+
document.getElementById("sysmem").textContent = `${formatBytes(data.system.used_bytes)} / ${formatBytes(
|
| 437 |
+
data.system.total_bytes,
|
| 438 |
+
)}`;
|
| 439 |
+
renderGpuList(data.gpu && data.gpu.nvidia);
|
| 440 |
+
applyRuntime(data.runtime);
|
| 441 |
+
document.getElementById("metricsRaw").textContent = JSON.stringify(
|
| 442 |
+
{
|
| 443 |
+
providers: data.runtime && data.runtime.providers,
|
| 444 |
+
cache_graph: data.runtime && data.runtime.cache_graph,
|
| 445 |
+
audio_graph: data.runtime && data.runtime.audio_graph,
|
| 446 |
+
cpu_percent: data.process.cpu_percent,
|
| 447 |
+
},
|
| 448 |
+
null,
|
| 449 |
+
2,
|
| 450 |
+
);
|
| 451 |
+
} catch (error) {
|
| 452 |
+
document.getElementById("metricsRaw").textContent = String(error);
|
| 453 |
+
}
|
| 454 |
+
}
|
| 455 |
+
|
| 456 |
+
checkRuntime();
|
| 457 |
+
setHotwordStrengthButtons();
|
| 458 |
+
refreshMetrics();
|
| 459 |
+
setInterval(refreshMetrics, 2000);
|
static/index.html
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="zh-CN">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8" />
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 6 |
+
<title>Audio8 ASR</title>
|
| 7 |
+
<link rel="stylesheet" href="/static/styles.css" />
|
| 8 |
+
</head>
|
| 9 |
+
<body>
|
| 10 |
+
<main class="shell">
|
| 11 |
+
<section class="workspace">
|
| 12 |
+
<header class="topbar">
|
| 13 |
+
<div>
|
| 14 |
+
<h1>Audio8 ASR</h1>
|
| 15 |
+
<p id="health">loading runtime...</p>
|
| 16 |
+
</div>
|
| 17 |
+
<div class="runtime-pill" id="recordStatus">Ready</div>
|
| 18 |
+
</header>
|
| 19 |
+
|
| 20 |
+
<div class="toolbar">
|
| 21 |
+
<button id="recordBtn" class="primary" type="button">Record</button>
|
| 22 |
+
<button id="stopBtn" type="button" disabled>Stop</button>
|
| 23 |
+
<label>
|
| 24 |
+
<span>Max Tokens</span>
|
| 25 |
+
<input id="maxTokens" type="number" min="8" max="512" value="128" />
|
| 26 |
+
</label>
|
| 27 |
+
</div>
|
| 28 |
+
|
| 29 |
+
<div class="controls">
|
| 30 |
+
<div class="hotword-panel">
|
| 31 |
+
<label>
|
| 32 |
+
<span>Hotwords</span>
|
| 33 |
+
<input id="hotwords" placeholder="Enter hotwords, separated by commas" />
|
| 34 |
+
</label>
|
| 35 |
+
<div class="hotword-options">
|
| 36 |
+
<span>Hotword strength</span>
|
| 37 |
+
<div class="segments compact" id="hotwordStrengthSegments">
|
| 38 |
+
<button type="button" data-hotword-strength="normal">Normal</button>
|
| 39 |
+
<button type="button" data-hotword-strength="strong">Strong</button>
|
| 40 |
+
</div>
|
| 41 |
+
<strong class="risk-note">Strong may force incorrect hotwords, hallucinate, or repeat text.</strong>
|
| 42 |
+
</div>
|
| 43 |
+
</div>
|
| 44 |
+
</div>
|
| 45 |
+
|
| 46 |
+
<div class="precision-row">
|
| 47 |
+
<span>Decoder</span>
|
| 48 |
+
<div class="segments" id="precisionSegments">
|
| 49 |
+
<button type="button" data-precision="fp32">FP32</button>
|
| 50 |
+
<button type="button" data-precision="int8">INT8</button>
|
| 51 |
+
<button type="button" data-precision="int4">INT4</button>
|
| 52 |
+
</div>
|
| 53 |
+
<span class="precision-note" id="precisionNote">--</span>
|
| 54 |
+
</div>
|
| 55 |
+
|
| 56 |
+
<div class="precision-row">
|
| 57 |
+
<span>Audio tower</span>
|
| 58 |
+
<div class="segments" id="audioPrecisionSegments">
|
| 59 |
+
<button type="button" data-audio-precision="fp32">FP32</button>
|
| 60 |
+
<button type="button" data-audio-precision="int8">INT8</button>
|
| 61 |
+
</div>
|
| 62 |
+
<span class="precision-note" id="audioPrecisionNote">--</span>
|
| 63 |
+
</div>
|
| 64 |
+
|
| 65 |
+
<div class="upload-row">
|
| 66 |
+
<input id="fileInput" type="file" accept="audio/*" />
|
| 67 |
+
<button id="uploadBtn" type="button">Transcribe</button>
|
| 68 |
+
</div>
|
| 69 |
+
|
| 70 |
+
<audio id="preview" controls></audio>
|
| 71 |
+
|
| 72 |
+
<section class="result-surface">
|
| 73 |
+
<div class="result-head">
|
| 74 |
+
<h2>Transcript</h2>
|
| 75 |
+
<span id="latency">--</span>
|
| 76 |
+
</div>
|
| 77 |
+
<div class="transcript" id="transcript">Transcript will appear here.</div>
|
| 78 |
+
</section>
|
| 79 |
+
|
| 80 |
+
<details class="raw-box">
|
| 81 |
+
<summary>Raw response</summary>
|
| 82 |
+
<pre id="raw"></pre>
|
| 83 |
+
</details>
|
| 84 |
+
</section>
|
| 85 |
+
|
| 86 |
+
<aside class="monitor">
|
| 87 |
+
<section class="panel">
|
| 88 |
+
<div class="panel-head">
|
| 89 |
+
<h2>Runtime</h2>
|
| 90 |
+
<span id="metricStamp">--</span>
|
| 91 |
+
</div>
|
| 92 |
+
<div class="kv-list">
|
| 93 |
+
<div><span>Backend</span><strong id="backend">--</strong></div>
|
| 94 |
+
<div><span>Precision</span><strong id="precision">--</strong></div>
|
| 95 |
+
<div><span>Process RSS</span><strong id="rss">--</strong></div>
|
| 96 |
+
<div><span>Peak RSS</span><strong id="peakRss">--</strong></div>
|
| 97 |
+
<div><span>Request Peak</span><strong id="requestPeak">--</strong></div>
|
| 98 |
+
<div><span>System Memory</span><strong id="sysmem">--</strong></div>
|
| 99 |
+
</div>
|
| 100 |
+
</section>
|
| 101 |
+
|
| 102 |
+
<section class="panel">
|
| 103 |
+
<div class="panel-head">
|
| 104 |
+
<h2>GPU Memory</h2>
|
| 105 |
+
<span id="gpuSummary">--</span>
|
| 106 |
+
</div>
|
| 107 |
+
<div class="gpu-list" id="gpuList"></div>
|
| 108 |
+
</section>
|
| 109 |
+
|
| 110 |
+
<section class="panel">
|
| 111 |
+
<div class="panel-head">
|
| 112 |
+
<h2>Providers</h2>
|
| 113 |
+
</div>
|
| 114 |
+
<pre id="metricsRaw"></pre>
|
| 115 |
+
</section>
|
| 116 |
+
</aside>
|
| 117 |
+
</main>
|
| 118 |
+
|
| 119 |
+
<script src="/static/app.js?v=hf-staging-error-handling-v1"></script>
|
| 120 |
+
</body>
|
| 121 |
+
</html>
|
static/styles.css
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
:root {
|
| 2 |
+
color-scheme: light;
|
| 3 |
+
--bg: #eef1f5;
|
| 4 |
+
--panel: #ffffff;
|
| 5 |
+
--panel-soft: #f7f9fb;
|
| 6 |
+
--ink: #16202a;
|
| 7 |
+
--muted: #687484;
|
| 8 |
+
--line: #d8dee7;
|
| 9 |
+
--accent: #0b766d;
|
| 10 |
+
--accent-strong: #075c55;
|
| 11 |
+
--accent-ink: #ffffff;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
* {
|
| 15 |
+
box-sizing: border-box;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
body {
|
| 19 |
+
margin: 0;
|
| 20 |
+
min-height: 100vh;
|
| 21 |
+
background: var(--bg);
|
| 22 |
+
color: var(--ink);
|
| 23 |
+
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
.shell {
|
| 27 |
+
width: min(1240px, calc(100vw - 32px));
|
| 28 |
+
margin: 20px auto;
|
| 29 |
+
display: grid;
|
| 30 |
+
grid-template-columns: minmax(0, 1fr) 380px;
|
| 31 |
+
gap: 14px;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
.workspace,
|
| 35 |
+
.panel {
|
| 36 |
+
background: var(--panel);
|
| 37 |
+
border: 1px solid var(--line);
|
| 38 |
+
border-radius: 8px;
|
| 39 |
+
box-shadow: 0 1px 2px rgba(24, 32, 42, 0.05);
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
.workspace {
|
| 43 |
+
padding: 18px;
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
.panel {
|
| 47 |
+
padding: 14px;
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
.monitor {
|
| 51 |
+
display: grid;
|
| 52 |
+
align-content: start;
|
| 53 |
+
gap: 12px;
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
.topbar,
|
| 57 |
+
.panel-head,
|
| 58 |
+
.result-head,
|
| 59 |
+
.precision-row {
|
| 60 |
+
display: flex;
|
| 61 |
+
align-items: center;
|
| 62 |
+
justify-content: space-between;
|
| 63 |
+
gap: 12px;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
.topbar {
|
| 67 |
+
align-items: flex-start;
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
h1,
|
| 71 |
+
h2,
|
| 72 |
+
p {
|
| 73 |
+
margin: 0;
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
h1 {
|
| 77 |
+
font-size: 26px;
|
| 78 |
+
font-weight: 720;
|
| 79 |
+
letter-spacing: 0;
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
h2 {
|
| 83 |
+
font-size: 16px;
|
| 84 |
+
font-weight: 700;
|
| 85 |
+
letter-spacing: 0;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
p,
|
| 89 |
+
span,
|
| 90 |
+
label,
|
| 91 |
+
small,
|
| 92 |
+
#metricStamp,
|
| 93 |
+
#latency {
|
| 94 |
+
color: var(--muted);
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
.runtime-pill {
|
| 98 |
+
min-width: 86px;
|
| 99 |
+
padding: 6px 10px;
|
| 100 |
+
border-radius: 8px;
|
| 101 |
+
background: #e9f7f4;
|
| 102 |
+
color: var(--accent-strong);
|
| 103 |
+
text-align: center;
|
| 104 |
+
font-size: 13px;
|
| 105 |
+
font-weight: 700;
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
.toolbar,
|
| 109 |
+
.upload-row,
|
| 110 |
+
.controls {
|
| 111 |
+
margin-top: 16px;
|
| 112 |
+
display: flex;
|
| 113 |
+
flex-wrap: wrap;
|
| 114 |
+
align-items: end;
|
| 115 |
+
gap: 10px;
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
.controls label {
|
| 119 |
+
flex: 1;
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
.controls input {
|
| 123 |
+
width: 100%;
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
.hotword-panel {
|
| 127 |
+
width: 100%;
|
| 128 |
+
display: grid;
|
| 129 |
+
gap: 10px;
|
| 130 |
+
}
|
| 131 |
+
|
| 132 |
+
.hotword-options {
|
| 133 |
+
display: grid;
|
| 134 |
+
grid-template-columns: max-content max-content minmax(220px, 1fr);
|
| 135 |
+
align-items: center;
|
| 136 |
+
gap: 10px;
|
| 137 |
+
padding: 10px;
|
| 138 |
+
border: 1px solid #e6b7b7;
|
| 139 |
+
border-radius: 8px;
|
| 140 |
+
background: #fff7f7;
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
.hotword-options > span {
|
| 144 |
+
font-size: 12px;
|
| 145 |
+
font-weight: 750;
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
.risk-note {
|
| 149 |
+
color: #a23b35;
|
| 150 |
+
font-size: 13px;
|
| 151 |
+
line-height: 1.35;
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
.precision-row {
|
| 155 |
+
justify-content: flex-start;
|
| 156 |
+
margin-top: 16px;
|
| 157 |
+
padding: 10px;
|
| 158 |
+
border: 1px solid var(--line);
|
| 159 |
+
border-radius: 8px;
|
| 160 |
+
background: var(--panel-soft);
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
.precision-note {
|
| 164 |
+
margin-left: auto;
|
| 165 |
+
font-size: 13px;
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
button,
|
| 169 |
+
input {
|
| 170 |
+
height: 38px;
|
| 171 |
+
border-radius: 7px;
|
| 172 |
+
border: 1px solid var(--line);
|
| 173 |
+
font: inherit;
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
button {
|
| 177 |
+
padding: 0 14px;
|
| 178 |
+
background: #ffffff;
|
| 179 |
+
color: var(--ink);
|
| 180 |
+
cursor: pointer;
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
button.primary {
|
| 184 |
+
background: var(--accent);
|
| 185 |
+
border-color: var(--accent);
|
| 186 |
+
color: var(--accent-ink);
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
button:hover:not(:disabled) {
|
| 190 |
+
border-color: var(--accent);
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
button:disabled {
|
| 194 |
+
opacity: 0.45;
|
| 195 |
+
cursor: not-allowed;
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
label {
|
| 199 |
+
display: grid;
|
| 200 |
+
gap: 5px;
|
| 201 |
+
font-size: 12px;
|
| 202 |
+
font-weight: 650;
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
input {
|
| 206 |
+
min-width: 128px;
|
| 207 |
+
padding: 0 10px;
|
| 208 |
+
color: var(--ink);
|
| 209 |
+
background: #ffffff;
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
input[type="file"] {
|
| 213 |
+
min-width: min(360px, 100%);
|
| 214 |
+
padding: 7px 10px;
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
.segments {
|
| 218 |
+
display: inline-grid;
|
| 219 |
+
grid-auto-flow: column;
|
| 220 |
+
grid-auto-columns: 74px;
|
| 221 |
+
gap: 4px;
|
| 222 |
+
padding: 3px;
|
| 223 |
+
border: 1px solid var(--line);
|
| 224 |
+
border-radius: 8px;
|
| 225 |
+
background: #ffffff;
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
.segments.compact {
|
| 229 |
+
grid-auto-columns: 88px;
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
.segments button {
|
| 233 |
+
height: 30px;
|
| 234 |
+
padding: 0;
|
| 235 |
+
border-color: transparent;
|
| 236 |
+
border-radius: 6px;
|
| 237 |
+
font-size: 13px;
|
| 238 |
+
font-weight: 750;
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
+
.segments button.active {
|
| 242 |
+
background: var(--accent);
|
| 243 |
+
color: var(--accent-ink);
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
audio {
|
| 247 |
+
width: 100%;
|
| 248 |
+
margin-top: 16px;
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
.result-surface {
|
| 252 |
+
margin-top: 16px;
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
.transcript {
|
| 256 |
+
min-height: 176px;
|
| 257 |
+
margin-top: 10px;
|
| 258 |
+
padding: 16px;
|
| 259 |
+
border: 1px solid var(--line);
|
| 260 |
+
border-radius: 8px;
|
| 261 |
+
background: #fbfcfe;
|
| 262 |
+
font-size: 22px;
|
| 263 |
+
line-height: 1.55;
|
| 264 |
+
word-break: break-word;
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
.raw-box {
|
| 268 |
+
margin-top: 12px;
|
| 269 |
+
}
|
| 270 |
+
|
| 271 |
+
.raw-box summary {
|
| 272 |
+
cursor: pointer;
|
| 273 |
+
color: var(--muted);
|
| 274 |
+
font-size: 13px;
|
| 275 |
+
font-weight: 650;
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
pre {
|
| 279 |
+
max-height: 240px;
|
| 280 |
+
overflow: auto;
|
| 281 |
+
margin: 10px 0 0;
|
| 282 |
+
padding: 12px;
|
| 283 |
+
border-radius: 8px;
|
| 284 |
+
background: #101820;
|
| 285 |
+
color: #d8e7ef;
|
| 286 |
+
font-size: 12px;
|
| 287 |
+
line-height: 1.45;
|
| 288 |
+
white-space: pre-wrap;
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
.kv-list {
|
| 292 |
+
margin-top: 12px;
|
| 293 |
+
display: grid;
|
| 294 |
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
| 295 |
+
gap: 8px;
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
.kv-list div {
|
| 299 |
+
min-height: 62px;
|
| 300 |
+
display: grid;
|
| 301 |
+
align-content: center;
|
| 302 |
+
gap: 4px;
|
| 303 |
+
padding: 10px;
|
| 304 |
+
border: 1px solid var(--line);
|
| 305 |
+
border-radius: 8px;
|
| 306 |
+
background: var(--panel-soft);
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
.kv-list span,
|
| 310 |
+
.gpu-line span {
|
| 311 |
+
font-size: 12px;
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
+
.kv-list strong {
|
| 315 |
+
font-size: 14px;
|
| 316 |
+
overflow-wrap: anywhere;
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
+
.gpu-list {
|
| 320 |
+
display: grid;
|
| 321 |
+
gap: 10px;
|
| 322 |
+
margin-top: 12px;
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
.gpu-item {
|
| 326 |
+
display: grid;
|
| 327 |
+
gap: 6px;
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
.gpu-line {
|
| 331 |
+
display: flex;
|
| 332 |
+
justify-content: space-between;
|
| 333 |
+
gap: 10px;
|
| 334 |
+
}
|
| 335 |
+
|
| 336 |
+
.bar {
|
| 337 |
+
height: 8px;
|
| 338 |
+
border-radius: 999px;
|
| 339 |
+
background: #e4e9f0;
|
| 340 |
+
overflow: hidden;
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
.bar i {
|
| 344 |
+
display: block;
|
| 345 |
+
height: 100%;
|
| 346 |
+
background: var(--accent);
|
| 347 |
+
}
|
| 348 |
+
|
| 349 |
+
.empty {
|
| 350 |
+
color: var(--muted);
|
| 351 |
+
font-size: 13px;
|
| 352 |
+
}
|
| 353 |
+
|
| 354 |
+
@media (max-width: 920px) {
|
| 355 |
+
.shell {
|
| 356 |
+
grid-template-columns: 1fr;
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
+
.monitor {
|
| 360 |
+
grid-template-columns: 1fr;
|
| 361 |
+
}
|
| 362 |
+
}
|
| 363 |
+
|
| 364 |
+
@media (max-width: 560px) {
|
| 365 |
+
.shell {
|
| 366 |
+
width: calc(100vw - 18px);
|
| 367 |
+
margin: 9px auto;
|
| 368 |
+
}
|
| 369 |
+
|
| 370 |
+
.workspace,
|
| 371 |
+
.panel {
|
| 372 |
+
padding: 12px;
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
h1 {
|
| 376 |
+
font-size: 22px;
|
| 377 |
+
}
|
| 378 |
+
|
| 379 |
+
.transcript {
|
| 380 |
+
font-size: 18px;
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
+
.hotword-options {
|
| 384 |
+
grid-template-columns: 1fr;
|
| 385 |
+
align-items: stretch;
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
.segments.compact {
|
| 389 |
+
grid-auto-columns: minmax(0, 1fr);
|
| 390 |
+
}
|
| 391 |
+
}
|
transcribe_file.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
|
| 4 |
+
import argparse
|
| 5 |
+
import json
|
| 6 |
+
import os
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
from typing import Any
|
| 9 |
+
|
| 10 |
+
from asr_onnx_runtime import OnnxAsrEngine, OnnxCacheAsrEngine
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
APP_DIR = Path(__file__).resolve().parent
|
| 14 |
+
BUNDLE_CANDIDATES = (
|
| 15 |
+
"model_bundle",
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def default_bundle_dir() -> Path:
|
| 20 |
+
env_value = os.environ.get("AUDIO8_ASR_BUNDLE")
|
| 21 |
+
if env_value:
|
| 22 |
+
return Path(env_value).expanduser()
|
| 23 |
+
for name in BUNDLE_CANDIDATES:
|
| 24 |
+
candidate = APP_DIR / name
|
| 25 |
+
if (candidate / "metadata.json").exists():
|
| 26 |
+
return candidate
|
| 27 |
+
return APP_DIR / "model_bundle"
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def transcribe_file(
|
| 31 |
+
audio_path: str | Path,
|
| 32 |
+
*,
|
| 33 |
+
bundle_dir: str | Path | None = None,
|
| 34 |
+
backend: str = "onnx_cache",
|
| 35 |
+
cache_precision: str = "int8",
|
| 36 |
+
audio_precision: str = "int8",
|
| 37 |
+
language: str | None = "Chinese",
|
| 38 |
+
max_new_tokens: int = 128,
|
| 39 |
+
provider: str = "CPUExecutionProvider",
|
| 40 |
+
threads: int | None = None,
|
| 41 |
+
hotwords: str | list[str] | None = None,
|
| 42 |
+
hotword_topk: int = 50,
|
| 43 |
+
hotword_start_boost: float = 6.0,
|
| 44 |
+
hotword_continuation_boost: float = 8.0,
|
| 45 |
+
) -> dict[str, Any]:
|
| 46 |
+
bundle_path = Path(bundle_dir).expanduser() if bundle_dir is not None else default_bundle_dir()
|
| 47 |
+
audio_file = Path(audio_path).expanduser()
|
| 48 |
+
if backend == "onnx":
|
| 49 |
+
engine = OnnxAsrEngine(
|
| 50 |
+
bundle_path,
|
| 51 |
+
provider=provider,
|
| 52 |
+
intra_op_num_threads=threads,
|
| 53 |
+
audio_precision=audio_precision,
|
| 54 |
+
)
|
| 55 |
+
elif backend == "onnx_cache":
|
| 56 |
+
engine = OnnxCacheAsrEngine(
|
| 57 |
+
bundle_path,
|
| 58 |
+
provider=provider,
|
| 59 |
+
intra_op_num_threads=threads,
|
| 60 |
+
cache_precision=cache_precision,
|
| 61 |
+
audio_precision=audio_precision,
|
| 62 |
+
)
|
| 63 |
+
else:
|
| 64 |
+
raise ValueError(f"Unsupported backend: {backend}")
|
| 65 |
+
return engine.transcribe(
|
| 66 |
+
audio_file.read_bytes(),
|
| 67 |
+
language=language,
|
| 68 |
+
max_new_tokens=max_new_tokens,
|
| 69 |
+
hotwords=hotwords,
|
| 70 |
+
hotword_topk=hotword_topk,
|
| 71 |
+
hotword_start_boost=hotword_start_boost,
|
| 72 |
+
hotword_continuation_boost=hotword_continuation_boost,
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def parse_args() -> argparse.Namespace:
|
| 77 |
+
parser = argparse.ArgumentParser(description="Transcribe one audio file with the Audio8 ASR ONNX runtime.")
|
| 78 |
+
parser.add_argument("audio", type=Path, help="Audio file path. WAV is recommended; librosa/soundfile handle common formats.")
|
| 79 |
+
parser.add_argument("--bundle_dir", type=Path, default=default_bundle_dir())
|
| 80 |
+
parser.add_argument("--backend", choices=["onnx_cache", "onnx"], default=os.environ.get("ASR_BACKEND", "onnx_cache"))
|
| 81 |
+
parser.add_argument("--cache_precision", choices=["fp32", "int8", "int4", "auto"], default=os.environ.get("ASR_CACHE_PRECISION", "int8"))
|
| 82 |
+
parser.add_argument("--audio_precision", choices=["fp32", "int8", "auto"], default=os.environ.get("ASR_AUDIO_PRECISION", "int8"))
|
| 83 |
+
parser.add_argument("--language", default="Chinese")
|
| 84 |
+
parser.add_argument("--max_new_tokens", type=int, default=128)
|
| 85 |
+
parser.add_argument("--provider", default=os.environ.get("ORT_PROVIDER", "CPUExecutionProvider"))
|
| 86 |
+
parser.add_argument("--threads", type=int, default=int(os.environ.get("ORT_THREADS", "0")))
|
| 87 |
+
parser.add_argument("--hotwords", default=None, help="Comma or Chinese-comma separated hotwords, disabled when omitted.")
|
| 88 |
+
parser.add_argument("--hotword_topk", type=int, default=50)
|
| 89 |
+
parser.add_argument("--hotword_start_boost", type=float, default=6.0)
|
| 90 |
+
parser.add_argument("--hotword_continuation_boost", type=float, default=8.0)
|
| 91 |
+
parser.add_argument("--json", action="store_true", help="Print the full result JSON instead of only transcript text.")
|
| 92 |
+
return parser.parse_args()
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def main() -> None:
|
| 96 |
+
args = parse_args()
|
| 97 |
+
result = transcribe_file(
|
| 98 |
+
args.audio,
|
| 99 |
+
bundle_dir=args.bundle_dir,
|
| 100 |
+
backend=args.backend,
|
| 101 |
+
cache_precision=args.cache_precision,
|
| 102 |
+
audio_precision=args.audio_precision,
|
| 103 |
+
language=args.language or None,
|
| 104 |
+
max_new_tokens=args.max_new_tokens,
|
| 105 |
+
provider=args.provider,
|
| 106 |
+
threads=args.threads if args.threads > 0 else None,
|
| 107 |
+
hotwords=args.hotwords or None,
|
| 108 |
+
hotword_topk=args.hotword_topk,
|
| 109 |
+
hotword_start_boost=args.hotword_start_boost,
|
| 110 |
+
hotword_continuation_boost=args.hotword_continuation_boost,
|
| 111 |
+
)
|
| 112 |
+
if args.json:
|
| 113 |
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
| 114 |
+
else:
|
| 115 |
+
print(result.get("text", ""))
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
if __name__ == "__main__":
|
| 119 |
+
main()
|