Automatic Speech Recognition
NeMo
Thai
English
speech
streaming
Thai
FastConformer
Transducer
NeMo
Nemotron
Instructions to use iapp/OpenThai2.0-ASR-Streaming with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- NeMo
How to use iapp/OpenThai2.0-ASR-Streaming with NeMo:
import nemo.collections.asr as nemo_asr asr_model = nemo_asr.models.ASRModel.from_pretrained("iapp/OpenThai2.0-ASR-Streaming") transcriptions = asr_model.transcribe(["file.wav"]) - Notebooks
- Google Colab
- Kaggle
OpenThai2.0 ASR Streaming v1.0: weights, reference service, licence and NVIDIA notices (private staging; model card to follow)
Browse files- .gitattributes +2 -0
- LICENSE +50 -0
- nvidia/NVIDIA_NEMOTRON_ASR_STREAMING_README.md +763 -0
- nvidia/bias.md +9 -0
- nvidia/explainability.md +15 -0
- nvidia/privacy.md +17 -0
- nvidia/safety.md +8 -0
- openthai2.0-asr-streaming_bias-1.0.nemo +3 -0
- openthai2.0-asr-streaming_raw.nemo +3 -0
- serve/README.md +55 -0
- serve/app.py +170 -0
- serve/chinda_asr.py +817 -0
- serve/chinda_diar.py +136 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,5 @@ saved_model/**/* 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
|
|
|
|
|
|
|
|
|
| 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
|
| 36 |
+
openthai2.0-asr-streaming_bias-1.0.nemo filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
openthai2.0-asr-streaming_raw.nemo filter=lfs diff=lfs merge=lfs -text
|
LICENSE
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
OpenMDW License Agreement, version 1.1 (OpenMDW-1.1)
|
| 2 |
+
|
| 3 |
+
By exercising rights granted to you under this agreement, you accept and
|
| 4 |
+
agree to its terms.
|
| 5 |
+
|
| 6 |
+
As used in this agreement, "Model Materials" means the materials provided
|
| 7 |
+
to you under this agreement, consisting of: (1) one or more machine
|
| 8 |
+
learning models (including architecture and parameters); and (2) all
|
| 9 |
+
related artifacts (including associated data, documentation and software)
|
| 10 |
+
that are provided to you hereunder.
|
| 11 |
+
|
| 12 |
+
Subject to your compliance with this agreement, permission is hereby
|
| 13 |
+
granted, free of charge, to deal in the Model Materials without
|
| 14 |
+
restriction, including under all copyright, patent, database, and trade
|
| 15 |
+
secret rights included or embodied therein.
|
| 16 |
+
|
| 17 |
+
If you distribute any portion of the Model Materials, you shall retain in
|
| 18 |
+
your distribution (1) a copy of this agreement, and (2) all copyright
|
| 19 |
+
notices and other notices of origin included in the Model Materials that
|
| 20 |
+
are applicable to your distribution.
|
| 21 |
+
|
| 22 |
+
If you file, maintain, or voluntarily participate in a lawsuit against any
|
| 23 |
+
person or entity asserting that the Model Materials directly or indirectly
|
| 24 |
+
infringe any patent or copyright, then all rights and grants made to you
|
| 25 |
+
hereunder are terminated, unless that lawsuit was in response to a
|
| 26 |
+
corresponding lawsuit first brought against you.
|
| 27 |
+
|
| 28 |
+
This agreement does not impose any restrictions or obligations with
|
| 29 |
+
respect to any use, modification, or sharing of any outputs generated by
|
| 30 |
+
using the Model Materials.
|
| 31 |
+
|
| 32 |
+
THE MODEL MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
| 33 |
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
| 34 |
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE, NONINFRINGEMENT,
|
| 35 |
+
ACCURACY, OR THE ABSENCE OF LATENT OR OTHER DEFECTS OR ERRORS, WHETHER OR
|
| 36 |
+
NOT DISCOVERABLE, ALL TO THE GREATEST EXTENT PERMISSIBLE UNDER APPLICABLE
|
| 37 |
+
LAW.
|
| 38 |
+
|
| 39 |
+
YOU ARE SOLELY RESPONSIBLE FOR (1) CLEARING RIGHTS OF OTHER PERSONS THAT
|
| 40 |
+
MAY APPLY TO THE MODEL MATERIALS OR ANY USE THEREOF, INCLUDING WITHOUT
|
| 41 |
+
LIMITATION ANY PERSON'S COPYRIGHTS OR OTHER RIGHTS INCLUDED OR EMBODIED IN
|
| 42 |
+
THE MODEL MATERIALS; (2) OBTAINING ANY NECESSARY CONSENTS, PERMISSIONS OR
|
| 43 |
+
OTHER RIGHTS REQUIRED FOR ANY USE OF THE MODEL MATERIALS; OR (3)
|
| 44 |
+
PERFORMING ANY DUE DILIGENCE OR UNDERTAKING ANY OTHER INVESTIGATIONS INTO
|
| 45 |
+
THE MODEL MATERIALS OR ANYTHING INCORPORATED OR EMBODIED THEREIN.
|
| 46 |
+
|
| 47 |
+
IN NO EVENT SHALL THE PROVIDERS OF THE MODEL MATERIALS BE LIABLE FOR ANY
|
| 48 |
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT
|
| 49 |
+
OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE MODEL
|
| 50 |
+
MATERIALS, THE USE THEREOF OR OTHER DEALINGS THEREIN.
|
nvidia/NVIDIA_NEMOTRON_ASR_STREAMING_README.md
ADDED
|
@@ -0,0 +1,763 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: other
|
| 3 |
+
license_name: openmdw-1.1
|
| 4 |
+
license_link: >-
|
| 5 |
+
https://openmdw.ai/license/1-1/
|
| 6 |
+
library_name: nemo
|
| 7 |
+
language:
|
| 8 |
+
- en
|
| 9 |
+
- es
|
| 10 |
+
- de
|
| 11 |
+
- fr
|
| 12 |
+
- it
|
| 13 |
+
- ar
|
| 14 |
+
- ja
|
| 15 |
+
- ko
|
| 16 |
+
- pt
|
| 17 |
+
- ru
|
| 18 |
+
- hi
|
| 19 |
+
- zh
|
| 20 |
+
- vi
|
| 21 |
+
- he
|
| 22 |
+
- nl
|
| 23 |
+
- cs
|
| 24 |
+
- da
|
| 25 |
+
- pl
|
| 26 |
+
- 'no'
|
| 27 |
+
- sv
|
| 28 |
+
- th
|
| 29 |
+
- tr
|
| 30 |
+
- bg
|
| 31 |
+
- el
|
| 32 |
+
- et
|
| 33 |
+
- fi
|
| 34 |
+
- hr
|
| 35 |
+
- hu
|
| 36 |
+
- lt
|
| 37 |
+
- lv
|
| 38 |
+
- ro
|
| 39 |
+
- sk
|
| 40 |
+
- uk
|
| 41 |
+
- mt
|
| 42 |
+
- sl
|
| 43 |
+
datasets:
|
| 44 |
+
- nvidia/Granary
|
| 45 |
+
- multilingual_librispeech
|
| 46 |
+
- fleurs
|
| 47 |
+
- mozilla-foundation/common_voice_8_0
|
| 48 |
+
- voxpopuli
|
| 49 |
+
- europarl
|
| 50 |
+
thumbnail: null
|
| 51 |
+
tags:
|
| 52 |
+
- transformers
|
| 53 |
+
- speech-recognition
|
| 54 |
+
- cache-aware ASR
|
| 55 |
+
- automatic-speech-recognition
|
| 56 |
+
- streaming-asr
|
| 57 |
+
- multilingual
|
| 58 |
+
- speech
|
| 59 |
+
- audio
|
| 60 |
+
- FastConformer
|
| 61 |
+
- RNNT
|
| 62 |
+
- Parakeet
|
| 63 |
+
- ASR
|
| 64 |
+
- pytorch
|
| 65 |
+
- NeMo
|
| 66 |
+
widget:
|
| 67 |
+
- example_title: Librispeech sample 1
|
| 68 |
+
src: https://cdn-media.huggingface.co/speech_samples/sample1.flac
|
| 69 |
+
- example_title: Librispeech sample 2
|
| 70 |
+
src: https://cdn-media.huggingface.co/speech_samples/sample2.flac
|
| 71 |
+
model-index:
|
| 72 |
+
- name: nemotron-asr-streaming-multilingual-0.6b
|
| 73 |
+
results:
|
| 74 |
+
- task:
|
| 75 |
+
name: Automatic Speech Recognition
|
| 76 |
+
type: automatic-speech-recognition
|
| 77 |
+
dataset:
|
| 78 |
+
name: FLEURS (English)
|
| 79 |
+
type: google/fleurs
|
| 80 |
+
config: en_us
|
| 81 |
+
split: test
|
| 82 |
+
metrics:
|
| 83 |
+
- name: WER (1.12s frame size, LangID)
|
| 84 |
+
type: wer
|
| 85 |
+
value: 7.91
|
| 86 |
+
- task:
|
| 87 |
+
name: Automatic Speech Recognition
|
| 88 |
+
type: automatic-speech-recognition
|
| 89 |
+
dataset:
|
| 90 |
+
name: FLEURS (Spanish)
|
| 91 |
+
type: google/fleurs
|
| 92 |
+
config: es_419
|
| 93 |
+
split: test
|
| 94 |
+
metrics:
|
| 95 |
+
- name: WER (1.12s frame size, LangID)
|
| 96 |
+
type: wer
|
| 97 |
+
value: 4.11
|
| 98 |
+
- task:
|
| 99 |
+
name: Automatic Speech Recognition
|
| 100 |
+
type: automatic-speech-recognition
|
| 101 |
+
dataset:
|
| 102 |
+
name: FLEURS (French)
|
| 103 |
+
type: google/fleurs
|
| 104 |
+
config: fr_fr
|
| 105 |
+
split: test
|
| 106 |
+
metrics:
|
| 107 |
+
- name: WER (1.12s frame size, LangID)
|
| 108 |
+
type: wer
|
| 109 |
+
value: 9.03
|
| 110 |
+
- task:
|
| 111 |
+
name: Automatic Speech Recognition
|
| 112 |
+
type: automatic-speech-recognition
|
| 113 |
+
dataset:
|
| 114 |
+
name: FLEURS (Italian)
|
| 115 |
+
type: google/fleurs
|
| 116 |
+
config: it_it
|
| 117 |
+
split: test
|
| 118 |
+
metrics:
|
| 119 |
+
- name: WER (1.12s frame size, LangID)
|
| 120 |
+
type: wer
|
| 121 |
+
value: 4.25
|
| 122 |
+
- task:
|
| 123 |
+
name: Automatic Speech Recognition
|
| 124 |
+
type: automatic-speech-recognition
|
| 125 |
+
dataset:
|
| 126 |
+
name: FLEURS (Portuguese)
|
| 127 |
+
type: google/fleurs
|
| 128 |
+
config: pt_br
|
| 129 |
+
split: test
|
| 130 |
+
metrics:
|
| 131 |
+
- name: WER (1.12s frame size, LangID)
|
| 132 |
+
type: wer
|
| 133 |
+
value: 5.48
|
| 134 |
+
- task:
|
| 135 |
+
name: Automatic Speech Recognition
|
| 136 |
+
type: automatic-speech-recognition
|
| 137 |
+
dataset:
|
| 138 |
+
name: FLEURS (German)
|
| 139 |
+
type: google/fleurs
|
| 140 |
+
config: de_de
|
| 141 |
+
split: test
|
| 142 |
+
metrics:
|
| 143 |
+
- name: WER (1.12s frame size, LangID)
|
| 144 |
+
type: wer
|
| 145 |
+
value: 8.31
|
| 146 |
+
- task:
|
| 147 |
+
name: Automatic Speech Recognition
|
| 148 |
+
type: automatic-speech-recognition
|
| 149 |
+
dataset:
|
| 150 |
+
name: FLEURS (Hindi)
|
| 151 |
+
type: google/fleurs
|
| 152 |
+
config: hi_in
|
| 153 |
+
split: test
|
| 154 |
+
metrics:
|
| 155 |
+
- name: WER (1.12s frame size, LangID)
|
| 156 |
+
type: wer
|
| 157 |
+
value: 6.81
|
| 158 |
+
- task:
|
| 159 |
+
name: Automatic Speech Recognition
|
| 160 |
+
type: automatic-speech-recognition
|
| 161 |
+
dataset:
|
| 162 |
+
name: FLEURS (Korean)
|
| 163 |
+
type: google/fleurs
|
| 164 |
+
config: ko_kr
|
| 165 |
+
split: test
|
| 166 |
+
metrics:
|
| 167 |
+
- name: WER (1.12s frame size, LangID)
|
| 168 |
+
type: wer
|
| 169 |
+
value: 7.12
|
| 170 |
+
metrics:
|
| 171 |
+
- wer
|
| 172 |
+
pipeline_tag: automatic-speech-recognition
|
| 173 |
+
---
|
| 174 |
+
|
| 175 |
+
# Nemotron 3.5 ASR
|
| 176 |
+
|
| 177 |
+
<style>
|
| 178 |
+
h1, h2, h3, h4, h5, h6 {
|
| 179 |
+
color: #76b900; /* NVIDIA green */
|
| 180 |
+
font-weight: 700;
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
hr {
|
| 184 |
+
border: none;
|
| 185 |
+
border-top: 1px solid #e5e7eb;
|
| 186 |
+
margin: 2rem 0;
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
/* Improve list spacing */
|
| 190 |
+
ul, ol {
|
| 191 |
+
margin-top: 0.5rem;
|
| 192 |
+
margin-bottom: 0.5rem;
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
/* Badge alignment consistency */
|
| 196 |
+
img {
|
| 197 |
+
display: inline;
|
| 198 |
+
vertical-align: middle;
|
| 199 |
+
}
|
| 200 |
+
</style>
|
| 201 |
+
|
| 202 |
+
<p align="center">
|
| 203 |
+
<a href="#model-architecture">
|
| 204 |
+
<img src="https://img.shields.io/badge/Model_Arch-FastConformer--CacheAware--RNNT-76b900?style=flat#model-badge" alt="Model architecture"/>
|
| 205 |
+
</a>
|
| 206 |
+
|
| 207 |
+
<a href="#model-architecture">
|
| 208 |
+
<img src="https://img.shields.io/badge/Params-600M-76b900?style=flat#model-badge" alt="Model size"/>
|
| 209 |
+
</a>
|
| 210 |
+
|
| 211 |
+
<a href="#supported-languages">
|
| 212 |
+
<img src="https://img.shields.io/badge/Language-Multilingual-76b900?style=flat#model-badge" alt="Language"/>
|
| 213 |
+
</a>
|
| 214 |
+
<a href="https://developer.nvidia.com/nemotron" target="_blank" style="margin: 2px;">
|
| 215 |
+
<img alt="Homepage" src="https://img.shields.io/badge/🏠Nemotron Developer Page-Learn More Here!-536af5?color=76B900&logoColor=white" style="display: inline-block; vertical-align: middle;"/>
|
| 216 |
+
</a>
|
| 217 |
+
<a href="https://discord.gg/9xpKQtVvrk" target="_blank" style="margin: 2px;">
|
| 218 |
+
<img alt="Discord" src="https://img.shields.io/badge/Discord-NVIDIA%20AI%20Developer-7289da?logo=discord&logoColor=white&color=7289da" style="display: inline-block; vertical-align: middle;"/>
|
| 219 |
+
</a>
|
| 220 |
+
<a href="https://openmdw.ai/license/1-1/" style="margin: 2px;">
|
| 221 |
+
<img alt="License" src="https://img.shields.io/badge/License-OpenMDW--1.1-f5de53" style="display: inline-block; vertical-align: middle;"/>
|
| 222 |
+
</a>
|
| 223 |
+
</p>
|
| 224 |
+
|
| 225 |
+
<div align="center" style="margin-bottom: -20px;">
|
| 226 |
+
<img src="model_overview.png" alt="Nemotron 3.5 ASR overview: multilingual audio across 40 language-locales is transcribed by a cache-aware FastConformer-RNNT model with language-ID prompting into punctuated text with an automatic language tag" width="900"/>
|
| 227 |
+
</div>
|
| 228 |
+
<div align="center" style="margin-top: 0; margin-bottom: 0;">
|
| 229 |
+
<img src="throughput_vs_chunk.png" alt="Concurrent streams supported on a single H100: Nemotron ASR streaming vs Parakeet RNNT, across chunk sizes" width="900"/>
|
| 230 |
+
</div>
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
> [!Note]
|
| 234 |
+
> This model is the multilingual extension of [nvidia/nemotron-speech-streaming-en-0.6b](https://huggingface.co/nvidia/nemotron-speech-streaming-en-0.6b), adding language-ID prompt conditioning to support transcription across **40 language-locales** from a single model.
|
| 235 |
+
|
| 236 |
+
**Nemotron 3.5 ASR** is a multilingual, streaming Automatic Speech Recognition (ASR) model engineered to deliver high-quality multilingual transcription across both low-latency streaming and high-throughput batch workloads. Developed by NVIDIA, this 600M parameter model transcribes speech into text with native support for punctuation and capitalization, and offers runtime flexibility with configurable chunk sizes, including 80ms, 160ms, 320ms, 560ms, and 1120ms.
|
| 237 |
+
|
| 238 |
+
By leveraging a state-of-the-art **Cache-Aware FastConformer-RNNT** architecture, the model eliminates redundant overlapping computations common in traditional "buffered" streaming. This allows it to process only new audio chunks while reusing cached encoder context, significantly improving computational efficiency and minimizing end-to-end delay without sacrificing accuracy.
|
| 239 |
+
|
| 240 |
+
It was trained on a massive ASR dataset and is engineered to perform across diverse and challenging acoustic conditions.
|
| 241 |
+
|
| 242 |
+
This model is ready for commercial use.
|
| 243 |
+
|
| 244 |
+
## Release Date
|
| 245 |
+
|
| 246 |
+
- Hugging Face [06/04/2026] via https://huggingface.co/nvidia/nemotron-3.5-asr-streaming-0.6b
|
| 247 |
+
|
| 248 |
+
## Why Choose Nemotron 3.5 ASR?
|
| 249 |
+
|
| 250 |
+
- 🌍 **Single Multilingual Model:** Transcribes 40 language-locales from one model through language-ID prompt conditioning, with optional automatic language detection.
|
| 251 |
+
- ⚡ **Native Streaming Architecture:** Cache-aware design enables efficient processing of continuous audio streams, designed and optimized for low-latency voice agent applications.
|
| 252 |
+
- 💰 **Improved Operational Efficiency:** Delivers superior throughput compared to traditional buffered streaming approaches. This allows for a higher number of parallel streams within the same GPU memory constraints, directly reducing operational costs for production environments.
|
| 253 |
+
- 🎛️ **Dynamic Runtime Flexibility:** Choose the optimal operating point on the latency-accuracy Pareto curve at inference time. No re-training is required to adjust for different use-case requirements.
|
| 254 |
+
- 📝 **Punctuation & Capitalization:** Built-in support for punctuation and capitalization in output text.
|
| 255 |
+
|
| 256 |
+
- 🔧 **Fine-tuning:** Check our [blog post](https://huggingface.co/blog/nvidia/fine-tuning-nemotron-35-asr) of **how to fine-tune Nemotron 3.5 ASR to improve these languages**, including before/after results.
|
| 257 |
+
|
| 258 |
+
---
|
| 259 |
+
|
| 260 |
+
## Supported Languages
|
| 261 |
+
|
| 262 |
+
The model supports **40 language-locales** in total, across three tiers:
|
| 263 |
+
|
| 264 |
+
- **Transcription-ready (19 locales):** highest-accuracy ASR, ready out of the box.
|
| 265 |
+
- **Broad-coverage (13 locales):** production ASR across an additional 13 locales.
|
| 266 |
+
- **Adaptation-ready (8 locales):** recognized by the tokenizer; fine-tune on in-domain data to unlock full transcription.
|
| 267 |
+
|
| 268 |
+
| Tier | Languages (locales) |
|
| 269 |
+
| :--- | :--- |
|
| 270 |
+
| **Transcription-ready (19 locales)** | English (en-US, en-GB), Spanish (es-US, es-ES), French (fr-FR, fr-CA), Italian (it-IT), Portuguese (pt-BR, pt-PT), Dutch (nl-NL), German (de-DE), Turkish (tr-TR), Russian (ru-RU), Arabic (ar-AR), Hindi (hi-IN), Japanese (ja-JP), Korean (ko-KR), Vietnamese (vi-VN), Ukrainian (uk-UA) |
|
| 271 |
+
| **Broad-coverage (13 locales)** | Polish (pl-PL), Swedish (sv-SE), Czech (cs-CZ), Norwegian Bokmål (nb-NO), Danish (da-DK), Bulgarian (bg-BG), Finnish (fi-FI), Croatian (hr-HR), Slovak (sk-SK), Mandarin (zh-CN), Hungarian (hu-HU), Romanian (ro-RO), Estonian (et-EE) |
|
| 272 |
+
| **Adaptation-ready (8 locales)** | Greek (el-GR), Lithuanian (lt-LT), Latvian (lv-LV), Maltese (mt-MT), Slovenian (sl-SI), Hebrew (he-IL), Thai (th-TH), Norwegian Nynorsk (nn-NO) |
|
| 273 |
+
|
| 274 |
+
> **Note:** Transcription-ready and broad-coverage locales (**32 total**) produce ASR transcription out of the box; adaptation-ready locales require fine-tuning on in-domain data to enable full transcription. The model supports uppercase and lowercase letters, punctuation, spaces, and apostrophes.
|
| 275 |
+
|
| 276 |
+
> **Note:** We would recommend [Nemotron ASR Streaming (English)](https://huggingface.co/nvidia/nemotron-speech-streaming-en-0.6b) model for English-only transcription use cases. For all other transcription ready locales, we recommend Nemotron 3.5 ASR to leverage its expanded multilingual capabilities.
|
| 277 |
+
|
| 278 |
+
> [!Tip]
|
| 279 |
+
> **Automatic language detection / language tagging:** When run with `target_lang=auto`, the model detects the spoken language and emits the corresponding **language code/tag** in the output following the terminal punctuation. This lets a single deployment transcribe mixed-language traffic and automatically label each utterance with its detected language — no separate language-ID component required.
|
| 280 |
+
|
| 281 |
+
---
|
| 282 |
+
|
| 283 |
+
## Model Architecture
|
| 284 |
+
|
| 285 |
+
**Architecture Type:** FastConformer-CacheAware-RNNT with Prompt
|
| 286 |
+
|
| 287 |
+
This model consists of a cache-aware streaming Parakeet (FastConformer) encoder with an RNN-T decoder and language-ID prompt conditioning. It is based on the Cache-Aware [\[1\]](#ref-1) FastConformer [\[2\]](#ref-2) architecture with 24 encoder layers and an RNNT (Recurrent Neural Network Transducer) decoder. The cache-aware streaming design enables efficient processing of audio in chunks while maintaining context from previous frames. Unlike buffered inference, this model maintains caches for all encoder self-attention and convolution layers. This enables reuse of hidden states at every streaming step, where cached activations eliminate redundant computations. As a result, there are no overlapping computations; each processed frame is strictly non-overlapping. This model leverages prompts to guide the transcription process, enabling language-specific transcription from a single ASR model through language ID conditioning.
|
| 288 |
+
|
| 289 |
+
<p align="center">
|
| 290 |
+
<img src="model_architecture.png" alt="Nemotron 3.5 ASR architecture: FastConformer encoder and language-ID encoding are concatenated, projected, and fed to the RNNT decoder" width="900"/>
|
| 291 |
+
</p>
|
| 292 |
+
|
| 293 |
+
The language-ID prompt is fused with the acoustic representation as follows:
|
| 294 |
+
|
| 295 |
+
- **FastConformer encoder** processes audio into an acoustic embedding of shape (D=1024, T).
|
| 296 |
+
- **Language Encoding** expands a 128-dim one-hot language vector across the time axis → (K=128, T), broadcasting the language identity to every frame.
|
| 297 |
+
- **Concatenation** along the feature axis → fused tensor (D + K, T).
|
| 298 |
+
- **Projection layer** maps the fused features to the RNNT decoder.
|
| 299 |
+
|
| 300 |
+
**Network Architecture:**
|
| 301 |
+
- Encoder: Cache-Aware FastConformer with 24 layers
|
| 302 |
+
- Decoder: RNNT (Recurrent Neural Network Transducer)
|
| 303 |
+
- Parameters: 600M
|
| 304 |
+
|
| 305 |
+
**This model was developed based on [nvidia/nemotron-speech-streaming-en-0.6b](https://huggingface.co/nvidia/nemotron-speech-streaming-en-0.6b).**
|
| 306 |
+
|
| 307 |
+
---
|
| 308 |
+
|
| 309 |
+
## Results at a Glance
|
| 310 |
+
|
| 311 |
+
ASR performance is measured using Word Error Rate (WER) on the **FLEURS** test sets. Accuracy stays strong across both modes and improves as the chunk size grows, while remaining competitive even at the lowest-latency 80ms setting. Full tables are in [Performance](#performance).
|
| 312 |
+
|
| 313 |
+
<p align="center">
|
| 314 |
+
<img src="fleurs_wer_vs_chunk_size.png" alt="FLEURS average WER vs streaming chunk size (LangID vs Auto-detect)" width="900"/>
|
| 315 |
+
</p>
|
| 316 |
+
|
| 317 |
+
<p align="center">
|
| 318 |
+
<img src="fleurs_langid_vs_auto.png" alt="FLEURS WER by language: LangID vs Auto-detect at 320ms chunk" width="900"/>
|
| 319 |
+
</p>
|
| 320 |
+
|
| 321 |
+
> **Note:** Japanese and Korean are measured using Character Error Rate (CER) rather than WER, as is standard for these languages.
|
| 322 |
+
|
| 323 |
+
---
|
| 324 |
+
|
| 325 |
+
## Throughput & Efficiency
|
| 326 |
+
|
| 327 |
+
Despite being **roughly half the size** (0.6B vs. 1.1B), Nemotron 3.5 ASR serves **far more concurrent streams at far lower latency** than the [Parakeet RNNT 1.1B multilingual model](https://build.nvidia.com/nvidia/parakeet-1_1b-rnnt-multilingual-asr), which runs on buffered streaming. The cache-aware streaming design avoids the redundant recomputation of buffered inference, so a single H100 can sustain dramatically higher concurrency at every chunk size — directly lowering the cost per stream in production. At the lowest-latency 80ms setting, Nemotron sustains **~17× more concurrent streams** (240 vs. 14); at the 1120ms setting it sustains **6× more** (2,400 vs. 400). The latency-vs-concurrency curves tell the same story: Nemotron (solid green) holds low final-token latency well past 1,000 parallel requests, while Parakeet RNNT 1.1B (dashed blue) saturates after only a few hundred.
|
| 328 |
+
|
| 329 |
+
<p align="center">
|
| 330 |
+
<img src="throughput_vs_chunk.png" alt="Concurrent streams supported on a single H100: Nemotron ASR streaming vs Parakeet RNNT, across chunk sizes" width="900"/>
|
| 331 |
+
</p>
|
| 332 |
+
|
| 333 |
+
<p align="center">
|
| 334 |
+
<img src="latency_vs_parallel.png" alt="Median final-token latency vs number of parallel requests on a single H100, Nemotron vs Parakeet RNNT across chunk sizes" width="900"/>
|
| 335 |
+
</p>
|
| 336 |
+
|
| 337 |
+
> Measured on a single NVIDIA H100. Throughput is the number of real-time streams sustainable in parallel; latency is the median final-token latency at a given level of concurrency.
|
| 338 |
+
|
| 339 |
+
---
|
| 340 |
+
|
| 341 |
+
## Explore more from NVIDIA
|
| 342 |
+
|
| 343 |
+
For documentation, deployment guides, enterprise-ready APIs, and the latest open models—including Nemotron and other cutting-edge speech, translation, and generative AI—visit the NVIDIA Developer Portal at [developer.nvidia.com](https://developer.nvidia.com/).
|
| 344 |
+
Join the community to access tools, support, and resources to accelerate your development with NVIDIA's NeMo, Speech NIM, and foundation models.
|
| 345 |
+
|
| 346 |
+
- What is [Nemotron](https://www.nvidia.com/en-us/ai-data-science/foundation-models/nemotron/)?
|
| 347 |
+
- NVIDIA Developer [Nemotron](https://developer.nvidia.com/nemotron)
|
| 348 |
+
- [NVIDIA Speech NIM](https://docs.nvidia.com/nim/speech/latest/about/index.html)
|
| 349 |
+
- [NeMo Documentation](https://docs.nvidia.com/nemo-framework/user-guide/latest/nemotoolkit/asr/models.html)
|
| 350 |
+
|
| 351 |
+
Also, check out the following NVIDIA speech models:
|
| 352 |
+
- Nemotron ASR Streaming (English) (Nemotron 3 ASR) - https://huggingface.co/nvidia/nemotron-speech-streaming-en-0.6b
|
| 353 |
+
- Multitalker Parakeet Streaming - https://huggingface.co/nvidia/multitalker-parakeet-streaming-0.6b-v1
|
| 354 |
+
- Parakeet Realtime EOU - https://huggingface.co/nvidia/parakeet_realtime_eou_120m-v1
|
| 355 |
+
|
| 356 |
+
---
|
| 357 |
+
|
| 358 |
+
## How to Use this Model
|
| 359 |
+
|
| 360 |
+
There are several ways to use this model. Choose the one that fits your needs.
|
| 361 |
+
|
| 362 |
+
### Run locally with NeMo-Speech.cpp
|
| 363 |
+
|
| 364 |
+
[NeMo-Speech.cpp](https://github.com/NVIDIA/NeMo-Speech.cpp) provides a
|
| 365 |
+
lightweight native C++ runtime for local inference with
|
| 366 |
+
this model. After [installing the runtime](https://github.com/NVIDIA/NeMo-Speech.cpp#installation):
|
| 367 |
+
|
| 368 |
+
```bash
|
| 369 |
+
hf download nvidia/nemotron-3.5-asr-streaming-0.6b \
|
| 370 |
+
nemotron-3.5-asr-streaming-0.6b.q8_0.gguf \
|
| 371 |
+
--local-dir models
|
| 372 |
+
|
| 373 |
+
nemo-speech transcribe audio.wav \
|
| 374 |
+
--model models/nemotron-3.5-asr-streaming-0.6b.q8_0.gguf \
|
| 375 |
+
--language en-US
|
| 376 |
+
```
|
| 377 |
+
|
| 378 |
+
Use another supported locale or `--language auto` for automatic language
|
| 379 |
+
detection. See the [NeMo-Speech.cpp documentation](https://github.com/NVIDIA/NeMo-Speech.cpp)
|
| 380 |
+
for more details.
|
| 381 |
+
|
| 382 |
+
### NVIDIA NeMo
|
| 383 |
+
|
| 384 |
+
To train, fine-tune or perform inference with this model, install [NVIDIA NeMo](https://github.com/NVIDIA/NeMo) [\[4\]](#ref-4) after installing Python 3.11 or later, Cython, and a recent PyTorch version.
|
| 385 |
+
|
| 386 |
+
```bash
|
| 387 |
+
apt-get update && apt-get install -y libsndfile1 ffmpeg
|
| 388 |
+
pip install Cython packaging
|
| 389 |
+
pip install git+https://github.com/NVIDIA/NeMo.git@main#egg=nemo_toolkit[asr]
|
| 390 |
+
```
|
| 391 |
+
|
| 392 |
+
#### Loading the Model
|
| 393 |
+
|
| 394 |
+
```python
|
| 395 |
+
import nemo.collections.asr as nemo_asr
|
| 396 |
+
asr_model = nemo_asr.models.ASRModel.from_pretrained(model_name="nvidia/nemotron-3.5-asr-streaming-0.6b")
|
| 397 |
+
```
|
| 398 |
+
|
| 399 |
+
#### Streaming Inference
|
| 400 |
+
|
| 401 |
+
You can use the cache-aware streaming inference script from NeMo - [NeMo/examples/asr/asr_cache_aware_streaming/speech_to_text_cache_aware_streaming_infer.py](https://github.com/NVIDIA-NeMo/NeMo/blob/main/examples/asr/asr_cache_aware_streaming/speech_to_text_cache_aware_streaming_infer.py)
|
| 402 |
+
|
| 403 |
+
This is a prompt-conditioned multilingual model: pass the target language with `target_lang` (e.g. `en-US`, `es-ES`, `de-DE`), or use `target_lang=auto` for automatic language detection.
|
| 404 |
+
|
| 405 |
+
```bash
|
| 406 |
+
cd NeMo
|
| 407 |
+
python examples/asr/asr_cache_aware_streaming/speech_to_text_cache_aware_streaming_infer.py \
|
| 408 |
+
model_path=<model_path> \
|
| 409 |
+
dataset_manifest=<dataset_manifest> \
|
| 410 |
+
batch_size=<batch_size> \
|
| 411 |
+
target_lang=<lang_id> \ #language key (e.g. en-US) or "auto" for automatic language detection
|
| 412 |
+
att_context_size="[56,13]" \ #set the second value to the desired right context from {0,1,3,6,13}
|
| 413 |
+
strip_lang_tags=true \ #true: remove the detected language tag from the text; false: keep it in the output
|
| 414 |
+
output_path=<output_folder>
|
| 415 |
+
```
|
| 416 |
+
|
| 417 |
+
**`strip_lang_tags`** controls how the detected language tag is handled in the output. The model appends a language tag (e.g. `<en-US>`) after the transcript's terminal punctuation:
|
| 418 |
+
- `strip_lang_tags=false` (keep): the tag is left in the output, so you can read the detected language directly from each utterance — useful for mixed-language traffic and language labeling.
|
| 419 |
+
- `strip_lang_tags=true` (remove): the tag is stripped, leaving only the clean transcript text — useful when you only need the spoken words.
|
| 420 |
+
|
| 421 |
+
#### Setting up Streaming Configuration
|
| 422 |
+
|
| 423 |
+
Latency is defined by the `att_context_size` param, where att_context_size = `{num_frames_left_context, num_frame_right_context}`, all measured in **80ms frames**:
|
| 424 |
+
|
| 425 |
+
* [56, 0]: Chunk size = 1 (1 × 80ms = 0.08s)
|
| 426 |
+
* [56, 1]: Chunk size = 2 (2 × 80ms = 0.16s)
|
| 427 |
+
* [56, 3]: Chunk size = 4 (4 × 80ms = 0.32s)
|
| 428 |
+
* [56, 6]: Chunk size = 7 (7 × 80ms = 0.56s)
|
| 429 |
+
* [56, 13]: Chunk size = 14 (14 × 80ms = 1.12s)
|
| 430 |
+
|
| 431 |
+
Here, chunk size = current frame + right context; each chunk is processed in non-overlapping fashion.
|
| 432 |
+
|
| 433 |
+
### 🤗 Transformers usage
|
| 434 |
+
|
| 435 |
+
This checkpoint also runs with [🤗 Transformers](https://github.com/huggingface/transformers). The target language is passed through the processor's `language` argument: a locale such as `en-US`/`de-DE`, a bare code such as `de`, or `auto` for automatic language detection. In `auto` mode the model appends an `<xx-XX>` language tag after the transcript's terminal punctuation; it is a special token, so decoding with `skip_special_tokens=True` strips it (clean transcript) and `skip_special_tokens=False` keeps it for language labeling.
|
| 436 |
+
|
| 437 |
+
Nemotron3_5Asr is available in 🤗 Transformers starting from v5.13.0.
|
| 438 |
+
|
| 439 |
+
```bash
|
| 440 |
+
pip install "transformers>=5.13.0"
|
| 441 |
+
```
|
| 442 |
+
|
| 443 |
+
<details>
|
| 444 |
+
<summary>➡️ Pipeline</summary>
|
| 445 |
+
|
| 446 |
+
```python
|
| 447 |
+
from transformers import pipeline
|
| 448 |
+
|
| 449 |
+
pipe = pipeline("automatic-speech-recognition", model="nvidia/nemotron-3.5-asr-streaming-0.6b")
|
| 450 |
+
out = pipe("https://huggingface.co/datasets/hf-internal-testing/dummy-audio-samples/resolve/main/bcn_weather.mp3")
|
| 451 |
+
print(out)
|
| 452 |
+
```
|
| 453 |
+
|
| 454 |
+
The pipeline uses the default language prompt (index 0, `en-US`). For explicit language conditioning or automatic detection, pass the processor's `language` argument (see the AutoModel example below).
|
| 455 |
+
</details>
|
| 456 |
+
|
| 457 |
+
<details>
|
| 458 |
+
<summary>➡️ Offline transcription</summary>
|
| 459 |
+
|
| 460 |
+
```python
|
| 461 |
+
from transformers import AutoModelForRNNT, AutoProcessor
|
| 462 |
+
from transformers.audio_utils import load_audio
|
| 463 |
+
|
| 464 |
+
model_id = "nvidia/nemotron-3.5-asr-streaming-0.6b"
|
| 465 |
+
processor = AutoProcessor.from_pretrained(model_id)
|
| 466 |
+
model = AutoModelForRNNT.from_pretrained(model_id, device_map="auto")
|
| 467 |
+
|
| 468 |
+
audio = load_audio(
|
| 469 |
+
"https://huggingface.co/datasets/hf-internal-testing/dummy-audio-samples/resolve/main/bcn_weather.mp3",
|
| 470 |
+
sampling_rate=processor.feature_extractor.sampling_rate,
|
| 471 |
+
)
|
| 472 |
+
|
| 473 |
+
# Condition on a known language ...
|
| 474 |
+
inputs = processor(audio, sampling_rate=processor.feature_extractor.sampling_rate, language="en-US")
|
| 475 |
+
inputs.to(model.device, dtype=model.dtype)
|
| 476 |
+
output = model.generate(**inputs, return_dict_in_generate=True)
|
| 477 |
+
print(processor.decode(output.sequences, skip_special_tokens=True))
|
| 478 |
+
|
| 479 |
+
# ... or let the model detect it and keep the emitted <xx-XX> language tag.
|
| 480 |
+
inputs = processor(audio, sampling_rate=processor.feature_extractor.sampling_rate, language="auto")
|
| 481 |
+
inputs.to(model.device, dtype=model.dtype)
|
| 482 |
+
output = model.generate(**inputs, return_dict_in_generate=True)
|
| 483 |
+
print(processor.decode(output.sequences, skip_special_tokens=False))
|
| 484 |
+
```
|
| 485 |
+
</details>
|
| 486 |
+
|
| 487 |
+
<details>
|
| 488 |
+
<summary>➡️ Streaming transcription</summary>
|
| 489 |
+
|
| 490 |
+
```python
|
| 491 |
+
from threading import Thread
|
| 492 |
+
from transformers import AutoModelForRNNT, AutoProcessor, TextIteratorStreamer
|
| 493 |
+
from transformers.audio_utils import load_audio
|
| 494 |
+
|
| 495 |
+
model_id = "nvidia/nemotron-3.5-asr-streaming-0.6b"
|
| 496 |
+
processor = AutoProcessor.from_pretrained(model_id)
|
| 497 |
+
model = AutoModelForRNNT.from_pretrained(model_id, device_map="auto")
|
| 498 |
+
|
| 499 |
+
processor.set_num_lookahead_tokens(6)
|
| 500 |
+
print(f"Streaming latency: {processor.streaming_latency_ms} ms")
|
| 501 |
+
|
| 502 |
+
# The language prompt rides along on every chunk; use a locale (e.g. "de-DE") or "auto".
|
| 503 |
+
language = "en-US"
|
| 504 |
+
|
| 505 |
+
sampling_rate = processor.feature_extractor.sampling_rate
|
| 506 |
+
audio = load_audio(
|
| 507 |
+
"https://huggingface.co/datasets/hf-internal-testing/dummy-audio-samples/resolve/main/obama.mp3",
|
| 508 |
+
sampling_rate=sampling_rate,
|
| 509 |
+
)
|
| 510 |
+
|
| 511 |
+
first_chunk_inputs = processor(
|
| 512 |
+
audio[: processor.num_samples_first_audio_chunk],
|
| 513 |
+
sampling_rate=sampling_rate,
|
| 514 |
+
is_streaming=True,
|
| 515 |
+
is_first_audio_chunk=True,
|
| 516 |
+
language=language,
|
| 517 |
+
return_tensors="pt",
|
| 518 |
+
)
|
| 519 |
+
first_chunk_inputs = first_chunk_inputs.to(model.device, dtype=model.dtype)
|
| 520 |
+
|
| 521 |
+
|
| 522 |
+
def input_features_generator():
|
| 523 |
+
yield first_chunk_inputs.input_features[:, : processor.num_mel_frames_first_audio_chunk, :]
|
| 524 |
+
|
| 525 |
+
mel_frame_idx = processor.num_mel_frames_first_audio_chunk
|
| 526 |
+
hop_length = processor.feature_extractor.hop_length
|
| 527 |
+
n_fft = processor.feature_extractor.n_fft
|
| 528 |
+
|
| 529 |
+
start_idx = mel_frame_idx * hop_length - n_fft // 2
|
| 530 |
+
while (end_idx := start_idx + processor.num_samples_per_audio_chunk) < audio.shape[0]:
|
| 531 |
+
inputs = processor(
|
| 532 |
+
audio[start_idx:end_idx],
|
| 533 |
+
sampling_rate=sampling_rate,
|
| 534 |
+
is_streaming=True,
|
| 535 |
+
is_first_audio_chunk=False,
|
| 536 |
+
language=language,
|
| 537 |
+
return_tensors="pt",
|
| 538 |
+
)
|
| 539 |
+
inputs = inputs.to(model.device, dtype=model.dtype)
|
| 540 |
+
yield inputs.input_features
|
| 541 |
+
|
| 542 |
+
mel_frame_idx += processor.num_mel_frames_per_audio_chunk
|
| 543 |
+
start_idx = mel_frame_idx * hop_length - n_fft // 2
|
| 544 |
+
|
| 545 |
+
|
| 546 |
+
streamer = TextIteratorStreamer(processor.tokenizer, skip_special_tokens=True)
|
| 547 |
+
generate_kwargs = {
|
| 548 |
+
**first_chunk_inputs,
|
| 549 |
+
"input_features": input_features_generator(),
|
| 550 |
+
"streamer": streamer,
|
| 551 |
+
}
|
| 552 |
+
thread = Thread(target=model.generate, kwargs=generate_kwargs)
|
| 553 |
+
thread.start()
|
| 554 |
+
|
| 555 |
+
print("Model output (streaming):", end=" ", flush=True)
|
| 556 |
+
for text_chunk in streamer:
|
| 557 |
+
print(text_chunk, end="", flush=True)
|
| 558 |
+
thread.join()
|
| 559 |
+
```
|
| 560 |
+
</details>
|
| 561 |
+
|
| 562 |
+
For more details about usage, please refer to the [Transformers documentation](https://huggingface.co/docs/transformers/en/model_doc/nemotron3_5_asr).
|
| 563 |
+
|
| 564 |
+
### Input(s): <br>
|
| 565 |
+
|
| 566 |
+
**Input Type(s):** Audio, Lang ID <br>
|
| 567 |
+
|
| 568 |
+
**Input Format(s):** wav, string <br>
|
| 569 |
+
|
| 570 |
+
**Input Parameters:** One-Dimensional (1D) for audio and One-Dimensional (1D) for Lang ID <br>
|
| 571 |
+
|
| 572 |
+
**Other Properties Related to Input:** Maximum Length in seconds specific to GPU Memory, No Pre-Processing Needed, Mono channel is required.
|
| 573 |
+
|
| 574 |
+
By leveraging NVIDIA’s hardware (e.g. GPU cores) and software frameworks (e.g., CUDA libraries), the model achieves faster training and inference times compared to CPU-only solutions. <br>
|
| 575 |
+
|
| 576 |
+
### Output
|
| 577 |
+
|
| 578 |
+
**Output Type(s):** Text String in Input Language <br>
|
| 579 |
+
|
| 580 |
+
**Output Format(s):** String <br>
|
| 581 |
+
|
| 582 |
+
**Output Parameters:** One-Dimensional (1D) <br>
|
| 583 |
+
|
| 584 |
+
**Other Properties Related to Output:** No Maximum Character Length, transcribe punctuation and capitalization.
|
| 585 |
+
|
| 586 |
+
By leveraging NVIDIA’s hardware (e.g. GPU cores) and software frameworks (e.g., CUDA libraries), the model achieves faster training and inference times compared to CPU-only solutions. <br>
|
| 587 |
+
|
| 588 |
+
---
|
| 589 |
+
|
| 590 |
+
## Software Integration
|
| 591 |
+
|
| 592 |
+
**Runtime Engine:** NeMo 26.06
|
| 593 |
+
|
| 594 |
+
**Supported Hardware Microarchitecture Compatibility:**
|
| 595 |
+
- NVIDIA Ampere
|
| 596 |
+
- NVIDIA Blackwell
|
| 597 |
+
- NVIDIA Hopper
|
| 598 |
+
- NVIDIA Jetson
|
| 599 |
+
- NVIDIA Lovelace
|
| 600 |
+
- NVIDIA Turing
|
| 601 |
+
- NVIDIA Volta
|
| 602 |
+
|
| 603 |
+
**Supported Operating System(s):**
|
| 604 |
+
* Linux <br>
|
| 605 |
+
* Linux 4 Tegra <br>
|
| 606 |
+
|
| 607 |
+
The integration of foundation and fine-tuned models into AI systems requires additional testing using use-case-specific data to ensure safe and effective deployment. Following the V-model methodology, iterative testing and validation at both unit and system levels are essential to mitigate risks, meet technical and functional requirements, and ensure compliance with safety and ethical standards before deployment.<vr>
|
| 608 |
+
|
| 609 |
+
|
| 610 |
+
|
| 611 |
+
---
|
| 612 |
+
|
| 613 |
+
|
| 614 |
+
## Model Version(s):
|
| 615 |
+
nemotron-3.5-asr-streaming-0.6b-v1 <br>
|
| 616 |
+
|
| 617 |
+
## Training and Evaluation Datasets:
|
| 618 |
+
|
| 619 |
+
### Training Datasets
|
| 620 |
+
|
| 621 |
+
It was trained on speech data across 40 language-locales. The training data is a dynamic blend of public and proprietary internal datasets normalized to have spoken forms in text with punctuation and capitalization, including:
|
| 622 |
+
|
| 623 |
+
|
| 624 |
+
- NVIDIA Riva multilingual ASR training set (Proprietary)
|
| 625 |
+
- NVIDIA Granary [\[3\]](#ref-3)
|
| 626 |
+
- Multilingual LibriSpeech (MLS)
|
| 627 |
+
- Mozilla Common Voice
|
| 628 |
+
- FLEURS
|
| 629 |
+
- VoxPopuli / Europarl-ASR
|
| 630 |
+
|
| 631 |
+
** Data Modality: Audio <br>
|
| 632 |
+
|
| 633 |
+
** Audio Training Data Size: 10,000 to 1 Million Hours <br>
|
| 634 |
+
|
| 635 |
+
** Data Collection Method by dataset <br>
|
| 636 |
+
* Human <br>
|
| 637 |
+
|
| 638 |
+
** Labeling Method by dataset <br>
|
| 639 |
+
* Human <br>
|
| 640 |
+
* Synthetic: Synthetic labels were generated from an ensemble of ASR models ([NVIDIA Canary](https://build.nvidia.com/nvidia/canary-1b-asr), [Parakeet Multilingual 1.1B RNNT](https://build.nvidia.com/nvidia/parakeet-1_1b-rnnt-multilingual-asr), [Parakeet CTC 1.1B](https://build.nvidia.com/nvidia/parakeet-ctc-1_1b-asr), [OpenAI Whisper](https://huggingface.co/openai/whisper-large-v3), and [FunASR](https://github.com/modelscope/FunASR)), with punctuation and capitalization (PnC) generated from [Qwen3-32B](https://huggingface.co/Qwen/Qwen3-32B).
|
| 641 |
+
|
| 642 |
+
|
| 643 |
+
|
| 644 |
+
|
| 645 |
+
### Evaluation Datasets
|
| 646 |
+
|
| 647 |
+
The model was evaluated on multilingual ASR benchmarks:
|
| 648 |
+
|
| 649 |
+
- FLEURS
|
| 650 |
+
- Mozilla Common Voice (MCV)
|
| 651 |
+
- Multilingual LibriSpeech (MLS)
|
| 652 |
+
- NVIDIA internal multilingual evaluation sets
|
| 653 |
+
|
| 654 |
+
** Data Collection Method by dataset <br>
|
| 655 |
+
* Human <br>
|
| 656 |
+
|
| 657 |
+
** Labeling Method by dataset <br>
|
| 658 |
+
* Human <br>
|
| 659 |
+
|
| 660 |
+
---
|
| 661 |
+
|
| 662 |
+
## Performance
|
| 663 |
+
|
| 664 |
+
ASR performance is measured using the Word Error Rate (WER). The tables below report WER (%) on the **FLEURS** test sets across configurable streaming chunk sizes, in two modes:
|
| 665 |
+
- **Language Input (LangID):** the target language is provided to the model.
|
| 666 |
+
- **Auto-detect:** the model automatically detects the spoken language.
|
| 667 |
+
|
| 668 |
+
> **Note:** Japanese, Korean, and Mandarin are evaluated using Character Error Rate (CER) rather than WER, as is standard for these languages.
|
| 669 |
+
> **Note on text normalization:** WER/CER are computed after text normalization that aligns the reference and hypothesis (e.g., casing, punctuation, numerals, and formatting conventions). Normalization is not perfect across all 40 language-locales, and residual mismatches between normalized text can inflate the reported error rates — actual transcription quality may be somewhat better than the numbers suggest.
|
| 670 |
+
|
| 671 |
+
### Transcription-ready (19 locales)
|
| 672 |
+
|
| 673 |
+
_Languages are ordered by accuracy (lowest WER first)._
|
| 674 |
+
|
| 675 |
+
<table>
|
| 676 |
+
<thead>
|
| 677 |
+
<tr><th rowspan="2" align="left">Language</th><th colspan="5" align="center" style="background-color:#76b900;color:#ffffff">Language Input (LangID)</th><th colspan="5" align="center" style="background-color:#6b7280;color:#ffffff;border-left:2px solid #cbd5e1;">Auto-detect</th></tr>
|
| 678 |
+
<tr><th align="center" style="background-color:#eef6e0">80ms</th><th align="center" style="background-color:#eef6e0">160ms</th><th align="center" style="background-color:#eef6e0">320ms</th><th align="center" style="background-color:#eef6e0">560ms</th><th align="center" style="background-color:#eef6e0">1.12s</th><th align="center" style="background-color:#f3f4f6;border-left:2px solid #cbd5e1;">80ms</th><th align="center" style="background-color:#f3f4f6;">160ms</th><th align="center" style="background-color:#f3f4f6;">320ms</th><th align="center" style="background-color:#f3f4f6;">560ms</th><th align="center" style="background-color:#f3f4f6;">1.12s</th></tr>
|
| 679 |
+
</thead>
|
| 680 |
+
<tbody>
|
| 681 |
+
<tr><td align="left">Spanish (es-US, es-ES)</td><td align="center" style="background-color:#eef6e0;">4.87</td><td align="center" style="background-color:#eef6e0;">4.64</td><td align="center" style="background-color:#eef6e0;">4.39</td><td align="center" style="background-color:#eef6e0;">4.26</td><td align="center" style="background-color:#eef6e0;">4.11</td><td align="center" style="background-color:#f3f4f6;border-left:2px solid #cbd5e1;">5.04</td><td align="center" style="background-color:#f3f4f6;">4.82</td><td align="center" style="background-color:#f3f4f6;">4.48</td><td align="center" style="background-color:#f3f4f6;">4.34</td><td align="center" style="background-color:#f3f4f6;">4.13</td></tr>
|
| 682 |
+
<tr><td align="left">Italian (it-IT)</td><td align="center" style="background-color:#eef6e0;">5.23</td><td align="center" style="background-color:#eef6e0;">4.85</td><td align="center" style="background-color:#eef6e0;">4.83</td><td align="center" style="background-color:#eef6e0;">4.41</td><td align="center" style="background-color:#eef6e0;">4.25</td><td align="center" style="background-color:#f3f4f6;border-left:2px solid #cbd5e1;">5.28</td><td align="center" style="background-color:#f3f4f6;">4.89</td><td align="center" style="background-color:#f3f4f6;">4.84</td><td align="center" style="background-color:#f3f4f6;">4.47</td><td align="center" style="background-color:#f3f4f6;">4.32</td></tr>
|
| 683 |
+
<tr><td align="left">Portuguese (pt-BR, pt-PT)</td><td align="center" style="background-color:#eef6e0;">6.29</td><td align="center" style="background-color:#eef6e0;">6.10</td><td align="center" style="background-color:#eef6e0;">5.81</td><td align="center" style="background-color:#eef6e0;">5.65</td><td align="center" style="background-color:#eef6e0;">5.48</td><td align="center" style="background-color:#f3f4f6;border-left:2px solid #cbd5e1;">6.41</td><td align="center" style="background-color:#f3f4f6;">6.19</td><td align="center" style="background-color:#f3f4f6;">5.82</td><td align="center" style="background-color:#f3f4f6;">5.57</td><td align="center" style="background-color:#f3f4f6;">5.47</td></tr>
|
| 684 |
+
<tr><td align="left">Hindi (hi-IN)</td><td align="center" style="background-color:#eef6e0;">8.13</td><td align="center" style="background-color:#eef6e0;">7.97</td><td align="center" style="background-color:#eef6e0;">7.41</td><td align="center" style="background-color:#eef6e0;">7.05</td><td align="center" style="background-color:#eef6e0;">6.81</td><td align="center" style="background-color:#f3f4f6;border-left:2px solid #cbd5e1;">11.47</td><td align="center" style="background-color:#f3f4f6;">10.83</td><td align="center" style="background-color:#f3f4f6;">9.88</td><td align="center" style="background-color:#f3f4f6;">9.26</td><td align="center" style="background-color:#f3f4f6;">8.23</td></tr>
|
| 685 |
+
<tr><td align="left">Korean (ko-KR)</td><td align="center" style="background-color:#eef6e0;">7.59</td><td align="center" style="background-color:#eef6e0;">7.70</td><td align="center" style="background-color:#eef6e0;">7.27</td><td align="center" style="background-color:#eef6e0;">7.18</td><td align="center" style="background-color:#eef6e0;">7.12</td><td align="center" style="background-color:#f3f4f6;border-left:2px solid #cbd5e1;">8.31</td><td align="center" style="background-color:#f3f4f6;">8.18</td><td align="center" style="background-color:#f3f4f6;">7.81</td><td align="center" style="background-color:#f3f4f6;">7.49</td><td align="center" style="background-color:#f3f4f6;">7.30</td></tr>
|
| 686 |
+
<tr><td align="left">English (en-US, en-GB)</td><td align="center" style="background-color:#eef6e0;">9.43</td><td align="center" style="background-color:#eef6e0;">8.88</td><td align="center" style="background-color:#eef6e0;">8.27</td><td align="center" style="background-color:#eef6e0;">7.99</td><td align="center" style="background-color:#eef6e0;">7.91</td><td align="center" style="background-color:#f3f4f6;border-left:2px solid #cbd5e1;">9.72</td><td align="center" style="background-color:#f3f4f6;">9.34</td><td align="center" style="background-color:#f3f4f6;">8.84</td><td align="center" style="background-color:#f3f4f6;">8.80</td><td align="center" style="background-color:#f3f4f6;">8.84</td></tr>
|
| 687 |
+
<tr><td align="left">German (de-DE)</td><td align="center" style="background-color:#eef6e0;">9.81</td><td align="center" style="background-color:#eef6e0;">9.21</td><td align="center" style="background-color:#eef6e0;">8.83</td><td align="center" style="background-color:#eef6e0;">8.42</td><td align="center" style="background-color:#eef6e0;">8.31</td><td align="center" style="background-color:#f3f4f6;border-left:2px solid #cbd5e1;">9.90</td><td align="center" style="background-color:#f3f4f6;">9.37</td><td align="center" style="background-color:#f3f4f6;">8.87</td><td align="center" style="background-color:#f3f4f6;">8.58</td><td align="center" style="background-color:#f3f4f6;">8.22</td></tr>
|
| 688 |
+
<tr><td align="left">French (fr-FR, fr-CA)</td><td align="center" style="background-color:#eef6e0;">10.97</td><td align="center" style="background-color:#eef6e0;">10.60</td><td align="center" style="background-color:#eef6e0;">9.79</td><td align="center" style="background-color:#eef6e0;">9.45</td><td align="center" style="background-color:#eef6e0;">9.03</td><td align="center" style="background-color:#f3f4f6;border-left:2px solid #cbd5e1;">11.03</td><td align="center" style="background-color:#f3f4f6;">10.60</td><td align="center" style="background-color:#f3f4f6;">9.84</td><td align="center" style="background-color:#f3f4f6;">9.46</td><td align="center" style="background-color:#f3f4f6;">9.02</td></tr>
|
| 689 |
+
<tr><td align="left">Russian (ru-RU)</td><td align="center" style="background-color:#eef6e0;">10.84</td><td align="center" style="background-color:#eef6e0;">10.73</td><td align="center" style="background-color:#eef6e0;">9.87</td><td align="center" style="background-color:#eef6e0;">9.60</td><td align="center" style="background-color:#eef6e0;">9.17</td><td align="center" style="background-color:#f3f4f6;border-left:2px solid #cbd5e1;">12.47</td><td align="center" style="background-color:#f3f4f6;">12.09</td><td align="center" style="background-color:#f3f4f6;">11.01</td><td align="center" style="background-color:#f3f4f6;">10.57</td><td align="center" style="background-color:#f3f4f6;">10.03</td></tr>
|
| 690 |
+
<tr><td align="left">Turkish (tr-TR)</td><td align="center" style="background-color:#eef6e0;">12.34</td><td align="center" style="background-color:#eef6e0;">12.33</td><td align="center" style="background-color:#eef6e0;">12.05</td><td align="center" style="background-color:#eef6e0;">11.34</td><td align="center" style="background-color:#eef6e0;">11.17</td><td align="center" style="background-color:#f3f4f6;border-left:2px solid #cbd5e1;">12.61</td><td align="center" style="background-color:#f3f4f6;">12.28</td><td align="center" style="background-color:#f3f4f6;">11.93</td><td align="center" style="background-color:#f3f4f6;">11.51</td><td align="center" style="background-color:#f3f4f6;">11.32</td></tr>
|
| 691 |
+
<tr><td align="left">Vietnamese (vi-VN)</td><td align="center" style="background-color:#eef6e0;">13.41</td><td align="center" style="background-color:#eef6e0;">12.87</td><td align="center" style="background-color:#eef6e0;">12.29</td><td align="center" style="background-color:#eef6e0;">11.78</td><td align="center" style="background-color:#eef6e0;">11.18</td><td align="center" style="background-color:#f3f4f6;border-left:2px solid #cbd5e1;">13.59</td><td align="center" style="background-color:#f3f4f6;">13.02</td><td align="center" style="background-color:#f3f4f6;">12.40</td><td align="center" style="background-color:#f3f4f6;">12.02</td><td align="center" style="background-color:#f3f4f6;">11.22</td></tr>
|
| 692 |
+
<tr><td align="left">Dutch (nl-NL)</td><td align="center" style="background-color:#eef6e0;">14.03</td><td align="center" style="background-color:#eef6e0;">13.43</td><td align="center" style="background-color:#eef6e0;">12.17</td><td align="center" style="background-color:#eef6e0;">11.97</td><td align="center" style="background-color:#eef6e0;">11.46</td><td align="center" style="background-color:#f3f4f6;border-left:2px solid #cbd5e1;">14.09</td><td align="center" style="background-color:#f3f4f6;">13.80</td><td align="center" style="background-color:#f3f4f6;">12.62</td><td align="center" style="background-color:#f3f4f6;">12.24</td><td align="center" style="background-color:#f3f4f6;">11.70</td></tr>
|
| 693 |
+
<tr><td align="left">Japanese (ja-JP)</td><td align="center" style="background-color:#eef6e0;">13.87</td><td align="center" style="background-color:#eef6e0;">12.90</td><td align="center" style="background-color:#eef6e0;">12.22</td><td align="center" style="background-color:#eef6e0;">11.91</td><td align="center" style="background-color:#eef6e0;">11.48</td><td align="center" style="background-color:#f3f4f6;border-left:2px solid #cbd5e1;">14.97</td><td align="center" style="background-color:#f3f4f6;">13.85</td><td align="center" style="background-color:#f3f4f6;">13.00</td><td align="center" style="background-color:#f3f4f6;">12.38</td><td align="center" style="background-color:#f3f4f6;">11.66</td></tr>
|
| 694 |
+
<tr><td align="left">Arabic (ar-AR)</td><td align="center" style="background-color:#eef6e0;">13.17</td><td align="center" style="background-color:#eef6e0;">12.65</td><td align="center" style="background-color:#eef6e0;">12.55</td><td align="center" style="background-color:#eef6e0;">12.13</td><td align="center" style="background-color:#eef6e0;">12.03</td><td align="center" style="background-color:#f3f4f6;border-left:2px solid #cbd5e1;">13.47</td><td align="center" style="background-color:#f3f4f6;">12.85</td><td align="center" style="background-color:#f3f4f6;">12.67</td><td align="center" style="background-color:#f3f4f6;">12.18</td><td align="center" style="background-color:#f3f4f6;">12.06</td></tr>
|
| 695 |
+
<tr><td align="left">Ukrainian (uk-UA)</td><td align="center" style="background-color:#eef6e0;">15.70</td><td align="center" style="background-color:#eef6e0;">15.21</td><td align="center" style="background-color:#eef6e0;">14.55</td><td align="center" style="background-color:#eef6e0;">13.67</td><td align="center" style="background-color:#eef6e0;">13.07</td><td align="center" style="background-color:#f3f4f6;border-left:2px solid #cbd5e1;">18.81</td><td align="center" style="background-color:#f3f4f6;">17.96</td><td align="center" style="background-color:#f3f4f6;">16.79</td><td align="center" style="background-color:#f3f4f6;">15.60</td><td align="center" style="background-color:#f3f4f6;">14.59</td></tr>
|
| 696 |
+
<tr><td align="left"><strong>Average</strong></td><td align="center" style="background-color:#eef6e0;"><strong>10.38</strong></td><td align="center" style="background-color:#eef6e0;"><strong>10.00</strong></td><td align="center" style="background-color:#eef6e0;"><strong>9.49</strong></td><td align="center" style="background-color:#eef6e0;"><strong>9.12</strong></td><td align="center" style="background-color:#eef6e0;"><strong>8.84</strong></td><td align="center" style="background-color:#f3f4f6;border-left:2px solid #cbd5e1;"><strong>11.14</strong></td><td align="center" style="background-color:#f3f4f6;"><strong>10.67</strong></td><td align="center" style="background-color:#f3f4f6;"><strong>10.05</strong></td><td align="center" style="background-color:#f3f4f6;"><strong>9.63</strong></td><td align="center" style="background-color:#f3f4f6;"><strong>9.21</strong></td></tr>
|
| 697 |
+
</tbody>
|
| 698 |
+
</table>
|
| 699 |
+
|
| 700 |
+
### Broad-coverage (13 locales)
|
| 701 |
+
|
| 702 |
+
_Languages are ordered by accuracy (lowest WER first)._
|
| 703 |
+
|
| 704 |
+
<table>
|
| 705 |
+
<thead>
|
| 706 |
+
<tr><th rowspan="2" align="left">Language</th><th colspan="5" align="center" style="background-color:#76b900;color:#ffffff">Language Input (LangID)</th><th colspan="5" align="center" style="background-color:#6b7280;color:#ffffff;border-left:2px solid #cbd5e1;">Auto-detect</th></tr>
|
| 707 |
+
<tr><th align="center" style="background-color:#eef6e0">80ms</th><th align="center" style="background-color:#eef6e0">160ms</th><th align="center" style="background-color:#eef6e0">320ms</th><th align="center" style="background-color:#eef6e0">560ms</th><th align="center" style="background-color:#eef6e0">1.12s</th><th align="center" style="background-color:#f3f4f6;border-left:2px solid #cbd5e1;">80ms</th><th align="center" style="background-color:#f3f4f6;">160ms</th><th align="center" style="background-color:#f3f4f6;">320ms</th><th align="center" style="background-color:#f3f4f6;">560ms</th><th align="center" style="background-color:#f3f4f6;">1.12s</th></tr>
|
| 708 |
+
</thead>
|
| 709 |
+
<tbody>
|
| 710 |
+
<tr><td align="left">Polish (pl-PL)</td><td align="center" style="background-color:#eef6e0;">19.88</td><td align="center" style="background-color:#eef6e0;">18.92</td><td align="center" style="background-color:#eef6e0;">17.48</td><td align="center" style="background-color:#eef6e0;">16.61</td><td align="center" style="background-color:#eef6e0;">15.15</td><td align="center" style="background-color:#f3f4f6;border-left:2px solid #cbd5e1;">22.65</td><td align="center" style="background-color:#f3f4f6;">21.63</td><td align="center" style="background-color:#f3f4f6;">20.05</td><td align="center" style="background-color:#f3f4f6;">18.52</td><td align="center" style="background-color:#f3f4f6;">16.55</td></tr>
|
| 711 |
+
<tr><td align="left">Norwegian Bokmål (nb-NO)</td><td align="center" style="background-color:#eef6e0;">20.43</td><td align="center" style="background-color:#eef6e0;">20.07</td><td align="center" style="background-color:#eef6e0;">18.90</td><td align="center" style="background-color:#eef6e0;">18.44</td><td align="center" style="background-color:#eef6e0;">18.10</td><td align="center" style="background-color:#f3f4f6;border-left:2px solid #cbd5e1;">20.91</td><td align="center" style="background-color:#f3f4f6;">20.19</td><td align="center" style="background-color:#f3f4f6;">19.29</td><td align="center" style="background-color:#f3f4f6;">18.76</td><td align="center" style="background-color:#f3f4f6;">18.01</td></tr>
|
| 712 |
+
<tr><td align="left">Finnish (fi-FI)</td><td align="center" style="background-color:#eef6e0;">21.19</td><td align="center" style="background-color:#eef6e0;">20.57</td><td align="center" style="background-color:#eef6e0;">20.05</td><td align="center" style="background-color:#eef6e0;">18.94</td><td align="center" style="background-color:#eef6e0;">18.34</td><td align="center" style="background-color:#f3f4f6;border-left:2px solid #cbd5e1;">21.61</td><td align="center" style="background-color:#f3f4f6;">20.88</td><td align="center" style="background-color:#f3f4f6;">20.40</td><td align="center" style="background-color:#f3f4f6;">19.36</td><td align="center" style="background-color:#f3f4f6;">18.72</td></tr>
|
| 713 |
+
<tr><td align="left">Mandarin (zh-CN)</td><td align="center" style="background-color:#eef6e0;">20.56</td><td align="center" style="background-color:#eef6e0;">20.22</td><td align="center" style="background-color:#eef6e0;">20.03</td><td align="center" style="background-color:#eef6e0;">19.51</td><td align="center" style="background-color:#eef6e0;">19.28</td><td align="center" style="background-color:#f3f4f6;border-left:2px solid #cbd5e1;">22.45</td><td align="center" style="background-color:#f3f4f6;">21.07</td><td align="center" style="background-color:#f3f4f6;">20.59</td><td align="center" style="background-color:#f3f4f6;">20.40</td><td align="center" style="background-color:#f3f4f6;">19.87</td></tr>
|
| 714 |
+
<tr><td align="left">Czech (cs-CZ)</td><td align="center" style="background-color:#eef6e0;">24.18</td><td align="center" style="background-color:#eef6e0;">23.20</td><td align="center" style="background-color:#eef6e0;">22.41</td><td align="center" style="background-color:#eef6e0;">21.04</td><td align="center" style="background-color:#eef6e0;">20.41</td><td align="center" style="background-color:#f3f4f6;border-left:2px solid #cbd5e1;">25.81</td><td align="center" style="background-color:#f3f4f6;">25.12</td><td align="center" style="background-color:#f3f4f6;">23.68</td><td align="center" style="background-color:#f3f4f6;">22.55</td><td align="center" style="background-color:#f3f4f6;">21.45</td></tr>
|
| 715 |
+
<tr><td align="left">Bulgarian (bg-BG)</td><td align="center" style="background-color:#eef6e0;">24.50</td><td align="center" style="background-color:#eef6e0;">23.58</td><td align="center" style="background-color:#eef6e0;">22.80</td><td align="center" style="background-color:#eef6e0;">21.70</td><td align="center" style="background-color:#eef6e0;">20.53</td><td align="center" style="background-color:#f3f4f6;border-left:2px solid #cbd5e1;">28.28</td><td align="center" style="background-color:#f3f4f6;">27.22</td><td align="center" style="background-color:#f3f4f6;">25.54</td><td align="center" style="background-color:#f3f4f6;">24.05</td><td align="center" style="background-color:#f3f4f6;">21.84</td></tr>
|
| 716 |
+
<tr><td align="left">Slovak (sk-SK)</td><td align="center" style="background-color:#eef6e0;">25.08</td><td align="center" style="background-color:#eef6e0;">24.14</td><td align="center" style="background-color:#eef6e0;">23.73</td><td align="center" style="background-color:#eef6e0;">22.51</td><td align="center" style="background-color:#eef6e0;">21.28</td><td align="center" style="background-color:#f3f4f6;border-left:2px solid #cbd5e1;">27.59</td><td align="center" style="background-color:#f3f4f6;">26.06</td><td align="center" style="background-color:#f3f4f6;">25.61</td><td align="center" style="background-color:#f3f4f6;">24.15</td><td align="center" style="background-color:#f3f4f6;">22.68</td></tr>
|
| 717 |
+
<tr><td align="left">Swedish (sv-SE)</td><td align="center" style="background-color:#eef6e0;">25.61</td><td align="center" style="background-color:#eef6e0;">24.85</td><td align="center" style="background-color:#eef6e0;">23.63</td><td align="center" style="background-color:#eef6e0;">22.72</td><td align="center" style="background-color:#eef6e0;">22.17</td><td align="center" style="background-color:#f3f4f6;border-left:2px solid #cbd5e1;">26.28</td><td align="center" style="background-color:#f3f4f6;">25.56</td><td align="center" style="background-color:#f3f4f6;">24.18</td><td align="center" style="background-color:#f3f4f6;">23.57</td><td align="center" style="background-color:#f3f4f6;">22.53</td></tr>
|
| 718 |
+
<tr><td align="left">Croatian (hr-HR)</td><td align="center" style="background-color:#eef6e0;">27.92</td><td align="center" style="background-color:#eef6e0;">27.09</td><td align="center" style="background-color:#eef6e0;">25.79</td><td align="center" style="background-color:#eef6e0;">24.92</td><td align="center" style="background-color:#eef6e0;">23.97</td><td align="center" style="background-color:#f3f4f6;border-left:2px solid #cbd5e1;">32.13</td><td align="center" style="background-color:#f3f4f6;">31.20</td><td align="center" style="background-color:#f3f4f6;">29.65</td><td align="center" style="background-color:#f3f4f6;">28.95</td><td align="center" style="background-color:#f3f4f6;">27.46</td></tr>
|
| 719 |
+
<tr><td align="left">Romanian (ro-RO)</td><td align="center" style="background-color:#eef6e0;">31.52</td><td align="center" style="background-color:#eef6e0;">30.93</td><td align="center" style="background-color:#eef6e0;">29.04</td><td align="center" style="background-color:#eef6e0;">27.77</td><td align="center" style="background-color:#eef6e0;">25.90</td><td align="center" style="background-color:#f3f4f6;border-left:2px solid #cbd5e1;">34.22</td><td align="center" style="background-color:#f3f4f6;">33.26</td><td align="center" style="background-color:#f3f4f6;">30.97</td><td align="center" style="background-color:#f3f4f6;">29.84</td><td align="center" style="background-color:#f3f4f6;">26.88</td></tr>
|
| 720 |
+
<tr><td align="left">Estonian (et-EE)</td><td align="center" style="background-color:#eef6e0;">29.95</td><td align="center" style="background-color:#eef6e0;">29.66</td><td align="center" style="background-color:#eef6e0;">28.59</td><td align="center" style="background-color:#eef6e0;">27.37</td><td align="center" style="background-color:#eef6e0;">26.35</td><td align="center" style="background-color:#f3f4f6;border-left:2px solid #cbd5e1;">30.58</td><td align="center" style="background-color:#f3f4f6;">30.09</td><td align="center" style="background-color:#f3f4f6;">28.72</td><td align="center" style="background-color:#f3f4f6;">28.03</td><td align="center" style="background-color:#f3f4f6;">27.19</td></tr>
|
| 721 |
+
<tr><td align="left">Danish (da-DK)</td><td align="center" style="background-color:#eef6e0;">32.62</td><td align="center" style="background-color:#eef6e0;">31.51</td><td align="center" style="background-color:#eef6e0;">30.00</td><td align="center" style="background-color:#eef6e0;">28.92</td><td align="center" style="background-color:#eef6e0;">27.49</td><td align="center" style="background-color:#f3f4f6;border-left:2px solid #cbd5e1;">33.15</td><td align="center" style="background-color:#f3f4f6;">31.77</td><td align="center" style="background-color:#f3f4f6;">30.22</td><td align="center" style="background-color:#f3f4f6;">29.33</td><td align="center" style="background-color:#f3f4f6;">27.81</td></tr>
|
| 722 |
+
<tr><td align="left">Hungarian (hu-HU)</td><td align="center" style="background-color:#eef6e0;">32.70</td><td align="center" style="background-color:#eef6e0;">32.03</td><td align="center" style="background-color:#eef6e0;">30.92</td><td align="center" style="background-color:#eef6e0;">29.72</td><td align="center" style="background-color:#eef6e0;">28.68</td><td align="center" style="background-color:#f3f4f6;border-left:2px solid #cbd5e1;">33.40</td><td align="center" style="background-color:#f3f4f6;">32.39</td><td align="center" style="background-color:#f3f4f6;">31.49</td><td align="center" style="background-color:#f3f4f6;">30.20</td><td align="center" style="background-color:#f3f4f6;">29.18</td></tr>
|
| 723 |
+
<tr><td align="left"><strong>Average</strong></td><td align="center" style="background-color:#eef6e0;"><strong>25.86</strong></td><td align="center" style="background-color:#eef6e0;"><strong>25.14</strong></td><td align="center" style="background-color:#eef6e0;"><strong>24.11</strong></td><td align="center" style="background-color:#eef6e0;"><strong>23.09</strong></td><td align="center" style="background-color:#eef6e0;"><strong>22.13</strong></td><td align="center" style="background-color:#f3f4f6;border-left:2px solid #cbd5e1;"><strong>27.62</strong></td><td align="center" style="background-color:#f3f4f6;"><strong>26.65</strong></td><td align="center" style="background-color:#f3f4f6;"><strong>25.41</strong></td><td align="center" style="background-color:#f3f4f6;"><strong>24.44</strong></td><td align="center" style="background-color:#f3f4f6;"><strong>23.09</strong></td></tr>
|
| 724 |
+
</tbody>
|
| 725 |
+
</table>
|
| 726 |
+
|
| 727 |
+
### Adaptation-ready languages (fine-tune to enable)
|
| 728 |
+
|
| 729 |
+
These **8 language-locales** are recognized by the tokenizer but are not tuned for production transcription out of the box: **Greek (el-GR), Hebrew (he-IL), Lithuanian (lt-LT), Slovenian (sl-SI), Latvian (lv-LV), Maltese (mt-MT), Thai (th-TH), and Norwegian Nynorsk (nn-NO)**. Fine-tuning on in-domain data is recommended to bring them to production quality.
|
| 730 |
+
|
| 731 |
+
---
|
| 732 |
+
|
| 733 |
+
## License/Terms of Use
|
| 734 |
+
|
| 735 |
+
Governing Terms: Use of the model is governed by the [OpenMDW-1.1](https://openmdw.ai/license/1-1/) license.
|
| 736 |
+
|
| 737 |
+
## Deployment Geography
|
| 738 |
+
|
| 739 |
+
Global
|
| 740 |
+
|
| 741 |
+
## Use Case
|
| 742 |
+
|
| 743 |
+
This model is for transcription of multilingual audio.
|
| 744 |
+
|
| 745 |
+
## References
|
| 746 |
+
|
| 747 |
+
<a id="ref-1"></a>[1] [Stateful Conformer with Cache-based Inference for Streaming Automatic Speech Recognition](https://arxiv.org/abs/2312.17279)
|
| 748 |
+
|
| 749 |
+
<a id="ref-2"></a>[2] [Fast Conformer with Linearly Scalable Attention for Efficient Speech Recognition](https://arxiv.org/abs/2305.05084)
|
| 750 |
+
|
| 751 |
+
<a id="ref-3"></a>[3] [NVIDIA Granary](https://huggingface.co/datasets/nvidia/Granary)
|
| 752 |
+
|
| 753 |
+
<a id="ref-4"></a>[4] [NVIDIA NeMo Framework](https://github.com/NVIDIA/NeMo)
|
| 754 |
+
|
| 755 |
+
---
|
| 756 |
+
|
| 757 |
+
## Ethical Considerations
|
| 758 |
+
|
| 759 |
+
NVIDIA believes Trustworthy AI is a shared responsibility and we have established policies and practices to enable development for a wide array of AI applications. When downloaded or used in accordance with our terms of service, developers should work with their internal model team to ensure this model meets requirements for the relevant industry and use case and addresses unforeseen product misuse.
|
| 760 |
+
|
| 761 |
+
Please report model quality, risk, security vulnerabilities or NVIDIA AI Concerns [here](https://www.nvidia.com/en-us/support/submit-security-vulnerability/).
|
| 762 |
+
|
| 763 |
+
---
|
nvidia/bias.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Field | Response
|
| 2 |
+
:---------------------------------------------------------------------------------------------------|:---------------
|
| 3 |
+
What is the language balance of the model validation data? | en-US: 25.63%, es-ES: 10.50%, pt-PT: 5.26%, hi-IN: 5.10%, fr-FR: 4.56%, de-DE: 4.28%, pt-BR: 3.14%, mt-MT: 2.83%, hu-HU: 2.82%, ro-RO: 2.81%, bg-BG: 2.78%, el-GR: 2.77%, lt-LT: 2.26%, fi-FI: 2.26%, it-IT: 2.14%, zh-CN: 2.14%, lv-LV: 2.06%, ja-JP: 1.98%, sk-SK: 1.90%, ru-RU: 1.76%, ko-KR: 1.72%, et-EE: 1.67%, sl-SI: 1.59%, ar-AR: 1.57%, es-US: 1.43%, he-IL: 0.85%, vi-VN: 0.83%, fr-CA: 0.32%, nl-NL: 0.21%, tr-TR: 0.19%, en-GB: 0.14%, pl-PL: 0.14%, uk-UA: 0.11%, th-TH: 0.09%, hr-HR: 0.04%, cs-CZ: 0.03%, da-DK: 0.03%, nb-NO: 0.03%, sv-SE: 0.03%, nn-NO: 0.00%
|
| 4 |
+
What is the geographic origin language balance of the model validation data? | Europe: 55.01%, North America: 27.38%, Asia: 11.86%, South America: 3.14%, Middle East: 2.61%
|
| 5 |
+
What is the accent balance of the model validation data? | en-US: 25.63%, es-ES: 10.50%, pt-PT: 5.26%, hi-IN: 5.10%, fr-FR: 4.56%, de-DE: 4.28%, pt-BR: 3.14%, mt-MT: 2.83%, hu-HU: 2.82%, ro-RO: 2.81%, bg-BG: 2.78%, el-GR: 2.77%, lt-LT: 2.26%, fi-FI: 2.26%, it-IT: 2.14%, zh-CN: 2.14%, lv-LV: 2.06%, ja-JP: 1.98%, sk-SK: 1.90%, ru-RU: 1.76%, ko-KR: 1.72%, et-EE: 1.67%, sl-SI: 1.59%, ar-AR: 1.57%, es-US: 1.43%, he-IL: 0.85%, vi-VN: 0.83%, fr-CA: 0.32%, nl-NL: 0.21%, tr-TR: 0.19%, en-GB: 0.14%, pl-PL: 0.14%, uk-UA: 0.11%, th-TH: 0.09%, hr-HR: 0.04%, cs-CZ: 0.03%, da-DK: 0.03%, nb-NO: 0.03%, sv-SE: 0.03%, nn-NO: 0.00%
|
| 6 |
+
Participation considerations from adversely impacted groups ([protected classes](https://www.senate.ca.gov/protected-classes)) in model design and testing: | Age, Gender, Linguistic Background
|
| 7 |
+
Measures taken to mitigate against unwanted bias: | Used a custom dataset to evaluate model performance across genders, age groups, and linguistic backgrounds.
|
| 8 |
+
|
| 9 |
+
|
nvidia/explainability.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Field | Response
|
| 2 |
+
:------------------------------------------------------------------------------------------------------|:---------------------------------------------------------------------------------
|
| 3 |
+
Intended Task/Domain: | Speech Transcription
|
| 4 |
+
Model Type: | FastConformer-CacheAware-RNNT
|
| 5 |
+
Intended Users: | This model is intended for developers and data scientists building interactive call centers, virtual assistants, and language learning assistants.
|
| 6 |
+
Output: | Transcribed text with timestamps and confidence scores
|
| 7 |
+
Describe how the model works: | Model transcribes audio input into text for the input language
|
| 8 |
+
Name the adversely impacted groups this has been tested to deliver comparable outcomes regardless of: | Age, Gender, National Origin
|
| 9 |
+
Technical Limitations & Mitigation: | Transcripts may not be 100% accurate. Accuracy varies depending on the characteristics of the input audio, such as domain, use case, accent, noise, speech type, and speech context.
|
| 10 |
+
Verified to have met prescribed NVIDIA quality standards: | Yes
|
| 11 |
+
Performance Metrics: | Word Error Rate (WER), Silence Robustness (Characters/mins of silent audio), Latency (in milliseconds), Throughput (Total audio processed per unit of time)
|
| 12 |
+
Potential Known Risks: | Not recommended for word-for-word transcription as accuracy varies based on the characteristics of input audio (domain, use case, accent, noise, speech type, and context of speech)
|
| 13 |
+
Licensing: | Governing Terms: Use of the model is governed by the [OpenMDW-1.1](https://openmdw.ai/license/1-1/) license.
|
| 14 |
+
|
| 15 |
+
|
nvidia/privacy.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Field | Response
|
| 2 |
+
:----------------------------------------------------------------------------------------------------------------------------------|:-----------------------------------------------
|
| 3 |
+
Generatable or reverse engineerable personal data? | No
|
| 4 |
+
Personal data used to create this model? | Yes - Voice
|
| 5 |
+
Was consent obtained for any personal data used? | Yes
|
| 6 |
+
Is a mechanism in place to honor data subject right of access or deletion of personal data? | Yes
|
| 7 |
+
If personal data was collected for the development of the model, was it collected directly by NVIDIA? | Yes
|
| 8 |
+
If personal data was collected for the development of the model by NVIDIA, do you maintain or have access to disclosures made to data subjects? | Yes
|
| 9 |
+
If personal data was collected for the development of this AI model, was it minimized to only what was required? | Yes
|
| 10 |
+
Is there provenance for all datasets used in training? | Yes
|
| 11 |
+
Does data labeling (annotation, metadata) comply with privacy laws? | Yes
|
| 12 |
+
Is data compliant with data subject requests for data correction or removal, if such a request was made? | No, not possible with externally-sourced data.
|
| 13 |
+
Applicable Privacy Policy | https://www.nvidia.com/en-us/about-nvidia/privacy-policy/
|
| 14 |
+
How often is dataset reviewed? | Dataset is initially reviewed upon addition, and subsequent reviews are conducted as needed or upon request for changes.
|
| 15 |
+
Was data from user interactions with the AI model (e.g. user input and prompts) used to train the model? | No
|
| 16 |
+
|
| 17 |
+
|
nvidia/safety.md
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Field | Response
|
| 2 |
+
:---------------------------------------------------|:----------------------------------
|
| 3 |
+
Model Application Field(s): | Speech Transcription
|
| 4 |
+
Describe the life critical impact (if present). | Not Applicable
|
| 5 |
+
Use Case Restrictions: | Abide by Governing Terms: Use of the model is governed by the [OpenMDW-1.1](https://openmdw.ai/license/1-1/) license.
|
| 6 |
+
Model and dataset restrictions: | The Principle of least privilege (PoLP) is applied limiting access for dataset generation and model development. Restrictions enforce dataset access during training, and dataset license constraints adhered to.
|
| 7 |
+
|
| 8 |
+
|
openthai2.0-asr-streaming_bias-1.0.nemo
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:6094a83e9729f5bf1ddd45f459594bfef4ab3e7caa5bc58263b959b0a585f815
|
| 3 |
+
size 2562969600
|
openthai2.0-asr-streaming_raw.nemo
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:61f39dea77508e2d7656b126b8fba7d6ae42d788f0c6fe0d9f35f5fab583f5cb
|
| 3 |
+
size 2562969600
|
serve/README.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# OpenThai2.0 ASR Streaming service: reference
|
| 2 |
+
|
| 3 |
+
Install, start and example calls: see the main [README](../README.md).
|
| 4 |
+
|
| 5 |
+
## Endpoints
|
| 6 |
+
|
| 7 |
+
| endpoint | what it does |
|
| 8 |
+
|---|---|
|
| 9 |
+
| `POST /v1/transcribe` | multipart `file` + the fields below → JSON |
|
| 10 |
+
| `WS /v1/stream` | JSON config → PCM16 LE 16 kHz mono frames → text `end`; replies `partial`… then one `final` |
|
| 11 |
+
| `GET /v1/health` | loaded model and settings |
|
| 12 |
+
|
| 13 |
+
| field | values | default |
|
| 14 |
+
|---|---|---|
|
| 15 |
+
| `mode` | `standard` · `conversation` · `meeting` | `standard` |
|
| 16 |
+
| `lang` | `th-TH` · `th-mix` | `th-TH` |
|
| 17 |
+
| `latency_ms` | 80 · 320 · 560 · 1120 | 560 |
|
| 18 |
+
| `diarize` | `true` · `false` | on for `meeting` |
|
| 19 |
+
|
| 20 |
+
**Response fields:**
|
| 21 |
+
- `text` (raw, as scored) and `display_text` (for display).
|
| 22 |
+
- `duration_s`, `lang`, `latency_ms`, `mode`, `policy`, `delta`, `model`, `processing_s`.
|
| 23 |
+
- With speaker labels, also `segments` `[{speaker, start, end, text}]` and `speakers`.
|
| 24 |
+
|
| 25 |
+
**Errors:** 400 empty file · 415 unreadable audio · 422 bad option.
|
| 26 |
+
|
| 27 |
+
## What each mode does
|
| 28 |
+
|
| 29 |
+
The blank bias δ controls how readily the model writes something: the more negative it is, the fewer words get dropped.
|
| 30 |
+
|
| 31 |
+
| mode | decode |
|
| 32 |
+
|---|---|
|
| 33 |
+
| `standard` | δ −1.0; clips under 2 s re-decoded at −2.5; empty output re-decoded at −4.0 |
|
| 34 |
+
| `conversation` | δ −2.0; empty output re-decoded at −4.0 |
|
| 35 |
+
| `meeting` | δ −1.0 + speaker labels from `nvidia/Nemotron-3-Diarization` (downloaded on first use) |
|
| 36 |
+
|
| 37 |
+
## Environment variables
|
| 38 |
+
|
| 39 |
+
| var | default | meaning |
|
| 40 |
+
|---|---|---|
|
| 41 |
+
| `CHINDA_DEVICE` | `cpu` | `cuda` for GPU |
|
| 42 |
+
| `CHINDA_MODEL` | `../openthai2.0-asr-streaming_raw.nemo` | must be the raw file; `*_bias-*` files are refused (the bias would apply twice) |
|
| 43 |
+
| `CHINDA_PRELOAD` | unset | `1` = load at start-up; otherwise the first request waits about 20 s |
|
| 44 |
+
| `CHINDA_REPLICAS` | `1` | model copies, about 2.6 GB each; each copy runs one decode step at a time |
|
| 45 |
+
| `CHINDA_NUM_THREADS` | min(16, CPUs) | CPU threads |
|
| 46 |
+
| `CHINDA_DIAR_MODEL` | download | path to a local `Nemotron-3-Diarization.nemo` |
|
| 47 |
+
| `CHINDA_CUDA_GRAPHS` | `0` | leave at 0: `1` crashes on the request after speaker labels |
|
| 48 |
+
|
| 49 |
+
## Good to know
|
| 50 |
+
|
| 51 |
+
- No authentication or rate limiting.
|
| 52 |
+
- In a live stream, speaker labels arrive with the `final` message only. Speakers are anonymous (`speaker_0`, …).
|
| 53 |
+
- Live text equals decoding the finished utterance on its own. Batch decoding (the benchmark) can differ by the last word on clips under 2 s.
|
| 54 |
+
- With `ffmpeg` on the PATH, the API also accepts mp3, m4a and ogg.
|
| 55 |
+
- The decode loop is adapted from NVIDIA NeMo's `speech_to_text_cache_aware_streaming_infer.py` (Apache-2.0).
|
serve/app.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""OpenThai2.0 ASR Streaming: HTTP/WebSocket service (FastAPI) on top of serve/chinda_asr.py.
|
| 2 |
+
|
| 3 |
+
cd serve && python -m uvicorn app:app --host 127.0.0.1 --port 18081 (see README.md)
|
| 4 |
+
|
| 5 |
+
Environment:
|
| 6 |
+
CHINDA_MODEL raw (unbaked) .nemo (default ../openthai2.0-asr-streaming_raw.nemo)
|
| 7 |
+
CHINDA_DEVICE cpu (default) | cuda
|
| 8 |
+
CHINDA_NUM_THREADS torch CPU threads (default min(16, cpu_count))
|
| 9 |
+
CHINDA_REPLICAS model replicas (default 1; each ~2.6 GB)
|
| 10 |
+
CHINDA_PRELOAD 1 = load the model at startup instead of on the first request
|
| 11 |
+
CHINDA_DIAR_MODEL Nemotron-3-Diarization .nemo for speaker labels (default: downloaded from Hugging Face)
|
| 12 |
+
|
| 13 |
+
Endpoints:
|
| 14 |
+
POST /v1/transcribe multipart: file + form mode | policy, lang, latency_ms[, diarize]
|
| 15 |
+
WS /v1/stream first message JSON {mode | policy, lang, latency_ms[, diarize]}; then binary PCM16 LE 16 kHz
|
| 16 |
+
mono; text message 'end' -> {"type":"final", ...} (+ speaker segments when diarize)
|
| 17 |
+
GET /v1/health
|
| 18 |
+
"""
|
| 19 |
+
from __future__ import annotations
|
| 20 |
+
|
| 21 |
+
import asyncio
|
| 22 |
+
import json
|
| 23 |
+
import logging
|
| 24 |
+
import os
|
| 25 |
+
import time
|
| 26 |
+
|
| 27 |
+
from fastapi import FastAPI, File, Form, HTTPException, UploadFile, WebSocket, WebSocketDisconnect
|
| 28 |
+
from fastapi.concurrency import run_in_threadpool
|
| 29 |
+
from fastapi.responses import JSONResponse
|
| 30 |
+
|
| 31 |
+
from chinda_asr import LANGS, LATENCY_PRESETS, MODES, POLICIES, ChindaASR, load_audio
|
| 32 |
+
from chinda_diar import Diarizer, build_turns
|
| 33 |
+
|
| 34 |
+
logging.basicConfig(level=os.environ.get("CHINDA_LOGLEVEL", "INFO"))
|
| 35 |
+
log = logging.getLogger("chinda_app")
|
| 36 |
+
|
| 37 |
+
ASR = ChindaASR(replicas=int(os.environ.get("CHINDA_REPLICAS", "1"))) # device: CHINDA_DEVICE (default cpu)
|
| 38 |
+
DIAR = Diarizer() # speaker labels, loaded on first use
|
| 39 |
+
app = FastAPI(title="OpenThai2.0 ASR Streaming", version="1.0")
|
| 40 |
+
|
| 41 |
+
RESULT_KEYS = ("text", "duration_s", "lang", "latency_ms", "policy", "model", "processing_s")
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
@app.on_event("startup")
|
| 45 |
+
async def _startup() -> None:
|
| 46 |
+
if os.environ.get("CHINDA_PRELOAD") == "1":
|
| 47 |
+
await run_in_threadpool(ASR.load)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def _validate(lang: str, latency_ms: int, policy: str) -> None:
|
| 51 |
+
try:
|
| 52 |
+
ASR._check(lang, latency_ms, policy)
|
| 53 |
+
except ValueError as e:
|
| 54 |
+
raise HTTPException(status_code=422, detail=str(e)) from e
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def _is_true(v) -> bool:
|
| 58 |
+
return str(v).strip().lower() in ("1", "true", "yes", "on")
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
@app.get("/v1/health")
|
| 62 |
+
async def health():
|
| 63 |
+
return {"status": "ok", **ASR.info()}
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
@app.post("/v1/transcribe")
|
| 67 |
+
async def transcribe(
|
| 68 |
+
file: UploadFile = File(...),
|
| 69 |
+
lang: str = Form("th-TH"),
|
| 70 |
+
latency_ms: int = Form(560),
|
| 71 |
+
policy: str = Form("shortclip"),
|
| 72 |
+
diarize: str = Form(""),
|
| 73 |
+
mode: str = Form(""),
|
| 74 |
+
):
|
| 75 |
+
if mode: # standard | conversation | meeting -> that mode's dev-selected policy
|
| 76 |
+
if mode not in MODES:
|
| 77 |
+
raise HTTPException(status_code=422, detail=f"unknown mode {mode!r}; one of {sorted(MODES)}")
|
| 78 |
+
policy = MODES[mode]
|
| 79 |
+
_validate(lang, latency_ms, policy)
|
| 80 |
+
want_spk = _is_true(diarize) if diarize else mode == "meeting" # meeting mode labels speakers unless diarize=false
|
| 81 |
+
data = await file.read()
|
| 82 |
+
if not data:
|
| 83 |
+
raise HTTPException(status_code=400, detail="empty file")
|
| 84 |
+
try:
|
| 85 |
+
audio = await run_in_threadpool(load_audio, data)
|
| 86 |
+
except Exception as e: # noqa: BLE001
|
| 87 |
+
raise HTTPException(status_code=415, detail=f"could not decode audio: {e}") from e
|
| 88 |
+
t0 = time.time()
|
| 89 |
+
if want_spk:
|
| 90 |
+
from chinda_diar import transcribe_with_speakers
|
| 91 |
+
# speaker labels use ONE stream at the meeting-mode setting (policy standard); see serve/chinda_diar.py
|
| 92 |
+
res = await run_in_threadpool(transcribe_with_speakers, ASR, DIAR, audio, lang, latency_ms)
|
| 93 |
+
out = {k: res[k] for k in RESULT_KEYS}
|
| 94 |
+
out.update(segments=res["segments"], speakers=res["speakers"])
|
| 95 |
+
else:
|
| 96 |
+
res = await run_in_threadpool(ASR.transcribe, audio, lang, latency_ms, policy)
|
| 97 |
+
out = {k: res[k] for k in RESULT_KEYS}
|
| 98 |
+
out["processing_s"] = round(time.time() - t0, 3)
|
| 99 |
+
out["display_text"] = res["display_text"]
|
| 100 |
+
out["delta"] = res["delta"]
|
| 101 |
+
if mode:
|
| 102 |
+
out["mode"] = mode
|
| 103 |
+
return out
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
@app.websocket("/v1/stream")
|
| 107 |
+
async def stream(ws: WebSocket):
|
| 108 |
+
await ws.accept()
|
| 109 |
+
session = None
|
| 110 |
+
try:
|
| 111 |
+
first = await ws.receive_text()
|
| 112 |
+
try:
|
| 113 |
+
cfg = json.loads(first)
|
| 114 |
+
lang = cfg.get("lang", "th-TH")
|
| 115 |
+
latency_ms = int(cfg.get("latency_ms", 560))
|
| 116 |
+
policy = cfg.get("policy", "shortclip")
|
| 117 |
+
if cfg.get("mode"):
|
| 118 |
+
if cfg["mode"] not in MODES:
|
| 119 |
+
raise ValueError(f"unknown mode {cfg['mode']!r}; one of {sorted(MODES)}")
|
| 120 |
+
policy = MODES[cfg["mode"]]
|
| 121 |
+
ASR._check(lang, latency_ms, policy)
|
| 122 |
+
want_spk = _is_true(cfg["diarize"]) if "diarize" in cfg else cfg.get("mode") == "meeting"
|
| 123 |
+
except (ValueError, TypeError) as e:
|
| 124 |
+
await ws.send_json({"type": "error", "code": 422, "detail": str(e)})
|
| 125 |
+
await ws.close(code=1003)
|
| 126 |
+
return
|
| 127 |
+
session = await run_in_threadpool(ASR.stream, lang, latency_ms, policy)
|
| 128 |
+
await ws.send_json({"type": "ready", "lang": lang, "latency_ms": latency_ms, "policy": policy})
|
| 129 |
+
last = None
|
| 130 |
+
while True:
|
| 131 |
+
msg = await ws.receive()
|
| 132 |
+
if msg.get("type") == "websocket.disconnect":
|
| 133 |
+
return
|
| 134 |
+
if msg.get("bytes") is not None:
|
| 135 |
+
buf = msg["bytes"]
|
| 136 |
+
if len(buf) % 2:
|
| 137 |
+
await ws.send_json({"type": "error", "code": 400, "detail": "PCM16 frames must have an even byte count"})
|
| 138 |
+
continue
|
| 139 |
+
partial = await run_in_threadpool(session.accept, buf)
|
| 140 |
+
if partial != last:
|
| 141 |
+
await ws.send_json({"type": "partial", "text": partial})
|
| 142 |
+
last = partial
|
| 143 |
+
elif msg.get("text") is not None:
|
| 144 |
+
if msg["text"].strip() == "end":
|
| 145 |
+
res = await run_in_threadpool(session.finish)
|
| 146 |
+
final = {"type": "final", **{k: res[k] for k in RESULT_KEYS}, "display_text": res["display_text"],
|
| 147 |
+
"delta": res["delta"]}
|
| 148 |
+
if want_spk: # speaker labels for the whole stream, from the buffered audio (not per partial yet)
|
| 149 |
+
segs = await run_in_threadpool(DIAR.segments, session.audio)
|
| 150 |
+
final["segments"], _ = build_turns(session.tokens(), ASR.tokenizer, segs)
|
| 151 |
+
final["speakers"] = len({t["speaker"] for t in final["segments"]})
|
| 152 |
+
await ws.send_json(final)
|
| 153 |
+
await ws.close()
|
| 154 |
+
return
|
| 155 |
+
await ws.send_json({"type": "error", "code": 400, "detail": "text messages other than 'end' are not understood"})
|
| 156 |
+
except WebSocketDisconnect:
|
| 157 |
+
return
|
| 158 |
+
except Exception as e: # noqa: BLE001
|
| 159 |
+
log.exception("stream failed")
|
| 160 |
+
try:
|
| 161 |
+
await ws.send_json({"type": "error", "code": 500, "detail": str(e)})
|
| 162 |
+
await ws.close(code=1011)
|
| 163 |
+
except Exception: # noqa: BLE001
|
| 164 |
+
pass
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
if __name__ == "__main__":
|
| 168 |
+
import uvicorn
|
| 169 |
+
|
| 170 |
+
uvicorn.run(app, host=os.environ.get("CHINDA_HOST", "127.0.0.1"), port=int(os.environ.get("CHINDA_PORT", "18081")))
|
serve/chinda_asr.py
ADDED
|
@@ -0,0 +1,817 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""ChindaASR reference inference library.
|
| 2 |
+
|
| 3 |
+
An exact port of the benchmark decode (scripts/score_cer_ckpt.sh ->
|
| 4 |
+
NeMo examples/asr/asr_cache_aware_streaming/speech_to_text_cache_aware_streaming_infer.py):
|
| 5 |
+
|
| 6 |
+
model_path=<.nemo> batch_size=32 att_context_size=[56,6]
|
| 7 |
+
rnnt_decoding.greedy.max_symbols_per_step=2 target_lang=th-TH strip_lang_tags=true
|
| 8 |
+
|
| 9 |
+
plus the decode policies of scripts/shortclip_decode.sh (standard / shortclip / conversational).
|
| 10 |
+
|
| 11 |
+
Design (see README.md for the full rationale)
|
| 12 |
+
---------------------------------------------
|
| 13 |
+
* ONE model instance per replica (default 1 replica), loaded lazily on first use from the UNBAKED
|
| 14 |
+
(raw) model file. The blank-logit bias delta is applied per call by writing
|
| 15 |
+
joint.joint_net[-1].bias[blank] = float(orig) + delta -- the exact statement
|
| 16 |
+
scripts/bake_blank_bias.py uses to bake *_bias-<delta>.nemo files.
|
| 17 |
+
serve/tests/verify_bias_files.py proved (on v8c) that baked files differ from the raw one in exactly
|
| 18 |
+
that one scalar and that float32(orig + delta) is bit-identical to each baked value, so this is
|
| 19 |
+
the same model as loading the baked file -- at 1/4 of the memory.
|
| 20 |
+
* The language prompt (th-TH=32 / th-mix=63), the att_context_size (latency preset) and the blank
|
| 21 |
+
bias are model-wide state in NeMo. Every decode step therefore runs under the replica's lock and
|
| 22 |
+
sets all three first. Streaming sessions keep their own caches/hypotheses and take the lock only
|
| 23 |
+
for the duration of one encoder step, so many sessions interleave safely on one model.
|
| 24 |
+
* The batch decode reuses NeMo's CacheAwareStreamingAudioBuffer + conformer_stream_step loop
|
| 25 |
+
verbatim (same chunking, same keep_all_outputs / drop_extra_pre_encoded logic).
|
| 26 |
+
* Live streaming computes log-mel features incrementally, only for frames whose 32 ms STFT window
|
| 27 |
+
(plus the pre-emphasis tap) is fully inside the audio received so far, and feeds NeMo's own buffer
|
| 28 |
+
a window of those frames. A step is taken only when it is provably NOT the final step, so every
|
| 29 |
+
encoder call sees the same tensor it would see in a whole-file bs=1 decode. At finish() the
|
| 30 |
+
committed frames are re-checked against a whole-utterance feature pass (up to
|
| 31 |
+
verify_stream_max_s); on any mismatch the utterance is re-decoded from scratch, so the final text
|
| 32 |
+
always equals transcribe() on the same audio.
|
| 33 |
+
|
| 34 |
+
Tested on CPU (FP32) and on an H100 GPU (TF32, like the benchmark); see DESIGN.md for the parity results.
|
| 35 |
+
"""
|
| 36 |
+
from __future__ import annotations
|
| 37 |
+
|
| 38 |
+
import dataclasses
|
| 39 |
+
import io
|
| 40 |
+
import json
|
| 41 |
+
import logging
|
| 42 |
+
import os
|
| 43 |
+
import queue
|
| 44 |
+
import shutil
|
| 45 |
+
import subprocess
|
| 46 |
+
import tempfile
|
| 47 |
+
import threading
|
| 48 |
+
import time
|
| 49 |
+
from typing import Any, Iterable, Optional, Sequence
|
| 50 |
+
|
| 51 |
+
import numpy as np
|
| 52 |
+
|
| 53 |
+
log = logging.getLogger("chinda_asr")
|
| 54 |
+
|
| 55 |
+
SAMPLE_RATE = 16000
|
| 56 |
+
FEATURE_TOL = 1e-3 # max |log-mel difference| accepted between streamed and whole-utterance features
|
| 57 |
+
# (GPU rounding noise); larger => the stream is re-decoded from the buffered audio
|
| 58 |
+
HOP = 160 # samples per feature frame (preprocessor window_stride 0.01 s)
|
| 59 |
+
HALF_WIN = 256 # n_fft // 2: a center=True STFT frame t spans samples [t*HOP - 256, t*HOP + 256)
|
| 60 |
+
# Model file: $CHINDA_MODEL, else the raw file shipped next to this folder (Hugging Face repo layout:
|
| 61 |
+
# <repo>/openthai2.0-asr-streaming_raw.nemo + <repo>/serve/).
|
| 62 |
+
_PACKAGED_MODEL = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
| 63 |
+
"openthai2.0-asr-streaming_raw.nemo")
|
| 64 |
+
DEFAULT_MODEL_PATH = os.environ.get("CHINDA_MODEL") or _PACKAGED_MODEL
|
| 65 |
+
MODEL_NAME = os.environ.get("CHINDA_MODEL_NAME", "openthai2.0-asr-streaming-v1.0")
|
| 66 |
+
|
| 67 |
+
# latency (ms) -> encoder att_context_size [left, right]; right-context chunks of (right+1) x 80 ms
|
| 68 |
+
LATENCY_PRESETS: dict[int, tuple[int, int]] = {80: (56, 0), 320: (56, 3), 560: (56, 6), 1120: (56, 13)}
|
| 69 |
+
LANGS = ("th-TH", "th-mix") # th-TH = Thai mode (prompt slot 32), th-mix = mixed Thai-English (slot 63)
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
@dataclasses.dataclass(frozen=True)
|
| 73 |
+
class Policy:
|
| 74 |
+
"""A decode policy = scripts/shortclip_decode.sh with (BASE_D, SHORT_D, RETRY_D, T)."""
|
| 75 |
+
|
| 76 |
+
name: str
|
| 77 |
+
base_delta: float
|
| 78 |
+
short_delta: Optional[float] = None # re-decode clips shorter than short_threshold_s at this delta
|
| 79 |
+
short_threshold_s: float = 0.0
|
| 80 |
+
retry_delta: Optional[float] = None # re-decode clips whose output is still empty at this delta
|
| 81 |
+
|
| 82 |
+
def describe(self) -> str:
|
| 83 |
+
s = f"delta {self.base_delta:+.1f}"
|
| 84 |
+
if self.short_delta is not None:
|
| 85 |
+
s += f"; clips < {self.short_threshold_s:g} s re-decoded at {self.short_delta:+.1f}"
|
| 86 |
+
if self.retry_delta is not None:
|
| 87 |
+
s += f"; empty output retried at {self.retry_delta:+.1f}"
|
| 88 |
+
return s
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
POLICIES: dict[str, Policy] = {
|
| 92 |
+
"standard": Policy("standard", -1.0),
|
| 93 |
+
"shortclip": Policy("shortclip", -1.0, short_delta=-2.5, short_threshold_s=2.0, retry_delta=-4.0),
|
| 94 |
+
"conversational": Policy("conversational", -2.0, retry_delta=-4.0),
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
# MODES. Each mode = the decode policy selected on that register's development set, then reported once on its test set:
|
| 98 |
+
# standard read speech, news, dictation, lectures dev val_v1ext_sub300 -> -1.0; short-clip fix (GS2-dev)
|
| 99 |
+
# conversation TV talk shows, YouTube / social media, podcasts, interviews dev GS2-dev -> -2.0, empty -> -4.0
|
| 100 |
+
# meeting multi-speaker rooms dev LOTUSDIS validation -> -1.0; the <2 s second pass HURTS there (+0.12), so none.
|
| 101 |
+
# Speaker labels (who said what) are the meeting feature: chinda_diar.py, on by default for mode=meeting.
|
| 102 |
+
MODES: dict[str, str] = {"standard": "shortclip", "conversation": "conversational", "meeting": "standard"}
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def display_text(text: str) -> str:
|
| 106 |
+
"""The tokenizer's nmt_nfkc normalizer decomposes SARA AM (U+0E33) into NIKHAHIT+SARA AA
|
| 107 |
+
(U+0E4D U+0E32), so raw output contains the two-codepoint form. Recompose it for display.
|
| 108 |
+
(The benchmark scorer NFKC-decomposes both sides, so this does not change any CER.)"""
|
| 109 |
+
return text.replace("ํา", "ำ")
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
# ----------------------------------------------------------------------------------------------
|
| 113 |
+
# audio loading
|
| 114 |
+
# ----------------------------------------------------------------------------------------------
|
| 115 |
+
def _to_mono_16k(data: np.ndarray, sr: int) -> np.ndarray:
|
| 116 |
+
data = np.asarray(data, dtype=np.float32)
|
| 117 |
+
if data.ndim == 2:
|
| 118 |
+
# mono stays bit-identical to soundfile's 1-D read; multi-channel is averaged (NeMo's
|
| 119 |
+
# get_samples would instead hand a (channels, samples) array to the model and fail)
|
| 120 |
+
data = data[:, 0] if data.shape[1] == 1 else data.mean(axis=1, dtype=np.float32)
|
| 121 |
+
if int(sr) != SAMPLE_RATE:
|
| 122 |
+
import librosa # same resampler (librosa default soxr_hq) as NeMo's get_samples
|
| 123 |
+
|
| 124 |
+
data = librosa.resample(data, orig_sr=int(sr), target_sr=SAMPLE_RATE)
|
| 125 |
+
return np.ascontiguousarray(data, dtype=np.float32)
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def _ffmpeg_decode(src: Any) -> tuple[np.ndarray, int]:
|
| 129 |
+
"""Decode anything ffmpeg understands; keep the native rate/channels (resampling is done by
|
| 130 |
+
librosa afterwards, exactly as for soundfile input)."""
|
| 131 |
+
if shutil.which("ffmpeg") is None or shutil.which("ffprobe") is None:
|
| 132 |
+
raise RuntimeError("audio format not readable by soundfile and ffmpeg/ffprobe are not installed")
|
| 133 |
+
tmp = None
|
| 134 |
+
try:
|
| 135 |
+
if isinstance(src, (bytes, bytearray)):
|
| 136 |
+
fd, tmp = tempfile.mkstemp(suffix=".audio", dir=os.environ.get("CHINDA_TMPDIR") or None)
|
| 137 |
+
with os.fdopen(fd, "wb") as fh:
|
| 138 |
+
fh.write(src)
|
| 139 |
+
path = tmp
|
| 140 |
+
else:
|
| 141 |
+
path = os.fspath(src)
|
| 142 |
+
probe = subprocess.run(
|
| 143 |
+
["ffprobe", "-v", "error", "-select_streams", "a:0", "-show_entries",
|
| 144 |
+
"stream=sample_rate,channels", "-of", "json", path],
|
| 145 |
+
capture_output=True, check=True,
|
| 146 |
+
)
|
| 147 |
+
st = json.loads(probe.stdout)["streams"][0]
|
| 148 |
+
sr, ch = int(st["sample_rate"]), int(st["channels"])
|
| 149 |
+
out = subprocess.run(
|
| 150 |
+
["ffmpeg", "-nostdin", "-v", "error", "-i", path, "-map", "0:a:0", "-f", "f32le",
|
| 151 |
+
"-acodec", "pcm_f32le", "pipe:1"],
|
| 152 |
+
capture_output=True, check=True,
|
| 153 |
+
)
|
| 154 |
+
data = np.frombuffer(out.stdout, dtype="<f4").astype(np.float32)
|
| 155 |
+
return data.reshape(-1, ch), sr
|
| 156 |
+
finally:
|
| 157 |
+
if tmp is not None:
|
| 158 |
+
os.unlink(tmp)
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
def load_audio(src: Any) -> np.ndarray:
|
| 162 |
+
"""Path, bytes or binary file-like -> float32 mono 16 kHz numpy array.
|
| 163 |
+
|
| 164 |
+
soundfile first (the same read NeMo's get_samples does, so 16 kHz mono WAV/FLAC input is
|
| 165 |
+
bit-identical to the benchmark), ffmpeg as a fallback for everything else (mp3, m4a, ogg, ...).
|
| 166 |
+
"""
|
| 167 |
+
import soundfile as sf
|
| 168 |
+
|
| 169 |
+
raw = None
|
| 170 |
+
if isinstance(src, (bytes, bytearray, memoryview)):
|
| 171 |
+
raw = bytes(src)
|
| 172 |
+
elif hasattr(src, "read"):
|
| 173 |
+
raw = src.read()
|
| 174 |
+
try:
|
| 175 |
+
data, sr = sf.read(io.BytesIO(raw) if raw is not None else os.fspath(src), dtype="float32", always_2d=True)
|
| 176 |
+
except Exception as e: # noqa: BLE001 - any soundfile failure -> try ffmpeg
|
| 177 |
+
log.info("soundfile could not read input (%s); falling back to ffmpeg", e)
|
| 178 |
+
data, sr = _ffmpeg_decode(raw if raw is not None else src)
|
| 179 |
+
return _to_mono_16k(data, sr)
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
def pcm16_to_float32(buf: bytes) -> np.ndarray:
|
| 183 |
+
"""PCM16 little-endian mono -> float32 in [-1, 1) (the same scaling soundfile uses)."""
|
| 184 |
+
return np.frombuffer(buf, dtype="<i2").astype(np.float32) / 32768.0
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
# ----------------------------------------------------------------------------------------------
|
| 188 |
+
# one model replica
|
| 189 |
+
# ----------------------------------------------------------------------------------------------
|
| 190 |
+
_BUFFER_CLS = None
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
def _buffer_cls():
|
| 194 |
+
"""CacheAwareStreamingAudioBuffer that reuses a cached preprocessor instead of rebuilding it
|
| 195 |
+
(the parent deep-copies the whole model config on every construction). The cached preprocessor
|
| 196 |
+
is itself produced by the parent's extract_preprocessor(), so it is the identical module."""
|
| 197 |
+
global _BUFFER_CLS
|
| 198 |
+
if _BUFFER_CLS is None:
|
| 199 |
+
from nemo.collections.asr.parts.utils.streaming_utils import CacheAwareStreamingAudioBuffer
|
| 200 |
+
|
| 201 |
+
class _Buffer(CacheAwareStreamingAudioBuffer):
|
| 202 |
+
def __init__(self, model, preprocessor, normalize_type, streaming_cfg):
|
| 203 |
+
self._cached_pre = (preprocessor, normalize_type)
|
| 204 |
+
super().__init__(model=model, online_normalization=False, pad_and_drop_preencoded=False)
|
| 205 |
+
# pin the streaming config this buffer was built for (a later preset change on the shared
|
| 206 |
+
# model must not alter a live session's chunking)
|
| 207 |
+
self.streaming_cfg = streaming_cfg
|
| 208 |
+
|
| 209 |
+
def extract_preprocessor(self):
|
| 210 |
+
self.model_normalize_type = self._cached_pre[1]
|
| 211 |
+
return self._cached_pre[0]
|
| 212 |
+
|
| 213 |
+
_BUFFER_CLS = _Buffer
|
| 214 |
+
return _BUFFER_CLS
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
class _Replica:
|
| 218 |
+
"""One loaded model. Not thread-safe by itself: callers hold self.lock around configure+use."""
|
| 219 |
+
|
| 220 |
+
def __init__(self, model_path: str, device: str):
|
| 221 |
+
import copy
|
| 222 |
+
|
| 223 |
+
import torch
|
| 224 |
+
from omegaconf import OmegaConf
|
| 225 |
+
from nemo.collections.asr.models import ASRModel
|
| 226 |
+
from nemo.collections.asr.parts.submodules.rnnt_decoding import RNNTDecodingConfig
|
| 227 |
+
from nemo.collections.asr.parts.utils.streaming_utils import CacheAwareStreamingAudioBuffer
|
| 228 |
+
from nemo.utils import model_utils
|
| 229 |
+
|
| 230 |
+
self.torch = torch
|
| 231 |
+
self.lock = threading.Lock()
|
| 232 |
+
self.device = torch.device(device)
|
| 233 |
+
t0 = time.time()
|
| 234 |
+
# --- exactly transcribe_utils.setup_model() ---
|
| 235 |
+
cfg = ASRModel.restore_from(restore_path=model_path, return_config=True)
|
| 236 |
+
cls = model_utils.import_class_by_path(cfg.target)
|
| 237 |
+
model = cls.restore_from(restore_path=model_path, map_location=self.device)
|
| 238 |
+
# --- exactly the infer script's decoding setup: RNNTDecodingConfig(fused_batch_size=-1)
|
| 239 |
+
# + rnnt_decoding.greedy.max_symbols_per_step=2, then strip_lang_tags=true ---
|
| 240 |
+
dec = OmegaConf.structured(RNNTDecodingConfig(fused_batch_size=-1))
|
| 241 |
+
dec.greedy.max_symbols_per_step = 2
|
| 242 |
+
dec.fused_batch_size = -1
|
| 243 |
+
# CUDA-graph greedy decoding OFF by default (2026-09-26): the captured graph keeps pointing at buffers of a
|
| 244 |
+
# finished session; the diarizer (serve/chinda_diar.py) calls torch.cuda.empty_cache(), which releases them, and
|
| 245 |
+
# the next session's graph replay crashed with "CUDA error: an illegal memory access" (reproduced: meeting 1 ->
|
| 246 |
+
# diarize -> meeting 2; meeting 2 alone is fine). CHINDA_CUDA_GRAPHS=1 re-enables it where no diarizer shares the
|
| 247 |
+
# process. Outputs are identical either way (same kernels); only speed differs.
|
| 248 |
+
dec.greedy.use_cuda_graph_decoder = os.environ.get("CHINDA_CUDA_GRAPHS", "0") == "1"
|
| 249 |
+
model.change_decoding_strategy(dec, verbose=False)
|
| 250 |
+
model.decoding.set_strip_lang_tags(True, lang_tag_pattern=None)
|
| 251 |
+
model = model.to(device=self.device, dtype=torch.float32)
|
| 252 |
+
model.eval()
|
| 253 |
+
self.model = model
|
| 254 |
+
self.load_s = time.time() - t0
|
| 255 |
+
|
| 256 |
+
if not hasattr(model, "set_inference_prompt"):
|
| 257 |
+
raise RuntimeError(f"{type(model).__name__} is not a prompt-conditioned model")
|
| 258 |
+
self.prompt_dict = dict(model.cfg.model_defaults.prompt_dictionary)
|
| 259 |
+
self.bias = model.joint.joint_net[-1].bias
|
| 260 |
+
self.blank_id = int(self.bias.shape[0]) - 1
|
| 261 |
+
if self.blank_id != int(model.decoding.blank_id):
|
| 262 |
+
raise RuntimeError(f"blank id mismatch: bias row {self.blank_id} vs decoding {model.decoding.blank_id}")
|
| 263 |
+
self.orig_blank_bias = float(self.bias[self.blank_id].item())
|
| 264 |
+
self.greedy_cfg = OmegaConf.to_container(model.cfg.decoding.greedy)
|
| 265 |
+
|
| 266 |
+
# streaming configs per preset (copied, so sessions can pin them)
|
| 267 |
+
self.streaming_cfgs = {}
|
| 268 |
+
for ms, att in LATENCY_PRESETS.items():
|
| 269 |
+
model.encoder.set_default_att_context_size(list(att))
|
| 270 |
+
self.streaming_cfgs[att] = copy.deepcopy(model.encoder.streaming_cfg)
|
| 271 |
+
# the cached preprocessor, built by NeMo's own extract_preprocessor (dither 0, pad_to 0)
|
| 272 |
+
probe = CacheAwareStreamingAudioBuffer(model=model, online_normalization=False, pad_and_drop_preencoded=False)
|
| 273 |
+
self.preprocessor = probe.preprocessor
|
| 274 |
+
self.normalize_type = probe.model_normalize_type
|
| 275 |
+
self._cur: tuple = (None, None, None)
|
| 276 |
+
# set a defined default state: th-TH, 560 ms, delta 0 (unbaked)
|
| 277 |
+
model.set_inference_prompt("th-TH") # logs "Inference prompt set to 'th-TH' (index 32)"
|
| 278 |
+
self.configure("th-TH", LATENCY_PRESETS[560], 0.0)
|
| 279 |
+
|
| 280 |
+
# -- model-wide state; call with self.lock held --------------------------------------------
|
| 281 |
+
def configure(self, lang: str, att: tuple[int, int], delta: float) -> None:
|
| 282 |
+
torch = self.torch
|
| 283 |
+
if lang != self._cur[0]:
|
| 284 |
+
if lang not in self.prompt_dict:
|
| 285 |
+
raise ValueError(f"unknown lang {lang!r}; model prompts include {sorted(self.prompt_dict)[:10]}...")
|
| 286 |
+
# identical effect to model.set_inference_prompt(lang) (which only sets this attribute + logs)
|
| 287 |
+
self.model._inference_prompt_index = self.prompt_dict[lang]
|
| 288 |
+
if att != self._cur[1]:
|
| 289 |
+
self.model.encoder.set_default_att_context_size(list(att))
|
| 290 |
+
if delta != self._cur[2]:
|
| 291 |
+
with torch.no_grad():
|
| 292 |
+
# == scripts/bake_blank_bias.py: b[blank] = before + a.delta
|
| 293 |
+
self.bias[self.blank_id] = self.orig_blank_bias + float(delta)
|
| 294 |
+
self._cur = (lang, att, delta)
|
| 295 |
+
|
| 296 |
+
def state(self) -> dict:
|
| 297 |
+
return {
|
| 298 |
+
"prompt_index": int(self.model._inference_prompt_index),
|
| 299 |
+
"att_context_size": list(self.model.encoder.att_context_size),
|
| 300 |
+
"blank_bias": float(self.bias[self.blank_id].item()),
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
def features(self, audio: np.ndarray):
|
| 304 |
+
"""== CacheAwareStreamingAudioBuffer.preprocess_audio(audio) with the cached preprocessor."""
|
| 305 |
+
torch = self.torch
|
| 306 |
+
sig = torch.from_numpy(np.array(audio, dtype=np.float32, copy=True)).unsqueeze_(0).to(self.device)
|
| 307 |
+
ln = torch.Tensor([audio.shape[0]]).to(self.device)
|
| 308 |
+
feats, _ = self.preprocessor(input_signal=sig, length=ln)
|
| 309 |
+
return feats
|
| 310 |
+
|
| 311 |
+
def new_buffer(self, att: tuple[int, int]):
|
| 312 |
+
return _buffer_cls()(self.model, self.preprocessor, self.normalize_type, self.streaming_cfgs[att])
|
| 313 |
+
|
| 314 |
+
def stream_step(self, st: "_StepState", chunk, lengths, keep_all_outputs: bool, drop_extra: int):
|
| 315 |
+
"""One conformer_stream_step exactly as perform_streaming() calls it. Call with lock held,
|
| 316 |
+
inside torch.inference_mode(), after configure()."""
|
| 317 |
+
model = self.model
|
| 318 |
+
if st.caches is None:
|
| 319 |
+
st.caches = model.encoder.get_initial_cache_state(batch_size=int(chunk.shape[0]))
|
| 320 |
+
c1, c2, c3 = st.caches
|
| 321 |
+
(st.pred_out, texts, c1, c2, c3, st.hyps) = model.conformer_stream_step(
|
| 322 |
+
processed_signal=chunk.to(self.torch.float32),
|
| 323 |
+
processed_signal_length=lengths,
|
| 324 |
+
cache_last_channel=c1,
|
| 325 |
+
cache_last_time=c2,
|
| 326 |
+
cache_last_channel_len=c3,
|
| 327 |
+
keep_all_outputs=keep_all_outputs,
|
| 328 |
+
previous_hypotheses=st.hyps,
|
| 329 |
+
previous_pred_out=st.pred_out,
|
| 330 |
+
drop_extra_pre_encoded=drop_extra,
|
| 331 |
+
return_transcription=True,
|
| 332 |
+
)
|
| 333 |
+
st.caches = (c1, c2, c3)
|
| 334 |
+
st.texts = texts
|
| 335 |
+
st.step_num += 1
|
| 336 |
+
|
| 337 |
+
def decode_batch(self, audios: Sequence[np.ndarray], lang: str, att: tuple[int, int], delta: float) -> list[str]:
|
| 338 |
+
"""perform_streaming() on one batch. Returns the raw (unstripped) hypothesis texts."""
|
| 339 |
+
torch = self.torch
|
| 340 |
+
with self.lock:
|
| 341 |
+
self.configure(lang, att, delta)
|
| 342 |
+
cfg = self.streaming_cfgs[att]
|
| 343 |
+
with torch.inference_mode():
|
| 344 |
+
buf = self.new_buffer(att)
|
| 345 |
+
for a in audios:
|
| 346 |
+
buf.append_audio(a, stream_id=-1)
|
| 347 |
+
st = _StepState()
|
| 348 |
+
for step_num, (chunk, lengths) in enumerate(iter(buf)):
|
| 349 |
+
self.stream_step(
|
| 350 |
+
st, chunk, lengths,
|
| 351 |
+
keep_all_outputs=buf.is_buffer_empty(),
|
| 352 |
+
drop_extra=0 if step_num == 0 else cfg.drop_extra_pre_encoded,
|
| 353 |
+
)
|
| 354 |
+
if st.texts is None:
|
| 355 |
+
return [""] * len(audios)
|
| 356 |
+
return [h.text for h in st.texts] # == extract_transcriptions()
|
| 357 |
+
|
| 358 |
+
|
| 359 |
+
class _StepState:
|
| 360 |
+
__slots__ = ("caches", "hyps", "pred_out", "texts", "step_num")
|
| 361 |
+
|
| 362 |
+
def __init__(self):
|
| 363 |
+
self.caches = None
|
| 364 |
+
self.hyps = None
|
| 365 |
+
self.pred_out = None
|
| 366 |
+
self.texts = None
|
| 367 |
+
self.step_num = 0
|
| 368 |
+
|
| 369 |
+
|
| 370 |
+
# ----------------------------------------------------------------------------------------------
|
| 371 |
+
# public API
|
| 372 |
+
# ----------------------------------------------------------------------------------------------
|
| 373 |
+
class ChindaASR:
|
| 374 |
+
"""Thread-safe ChindaASR decoder.
|
| 375 |
+
|
| 376 |
+
asr = ChindaASR() # nothing is loaded yet
|
| 377 |
+
asr.transcribe(audio) # loads on first use (~20 s on CPU)
|
| 378 |
+
asr.transcribe_batch([a1, a2, ...]) # batches of 32, like the benchmark
|
| 379 |
+
s = asr.stream(); s.accept(chunk); s.finish()
|
| 380 |
+
"""
|
| 381 |
+
|
| 382 |
+
def __init__(
|
| 383 |
+
self,
|
| 384 |
+
model_path: str = DEFAULT_MODEL_PATH,
|
| 385 |
+
device: Optional[str] = None,
|
| 386 |
+
num_threads: Optional[int] = None,
|
| 387 |
+
replicas: int = 1,
|
| 388 |
+
verify_stream_max_s: float = 600.0,
|
| 389 |
+
):
|
| 390 |
+
if "_bias-" in os.path.basename(model_path):
|
| 391 |
+
raise ValueError(
|
| 392 |
+
f"{model_path} already has a blank bias baked in; pass the raw (unbaked) model file, e.g. "
|
| 393 |
+
"openthai2.0-asr-streaming_raw.nemo -- the policies apply delta at decode time"
|
| 394 |
+
)
|
| 395 |
+
self.model_path = model_path
|
| 396 |
+
self.device = device or os.environ.get("CHINDA_DEVICE", "cpu") # "cuda" / "cuda:0" on a GPU host
|
| 397 |
+
self.num_threads = num_threads or int(os.environ.get("CHINDA_NUM_THREADS", "0")) or min(16, os.cpu_count() or 1)
|
| 398 |
+
self.n_replicas = max(1, int(replicas))
|
| 399 |
+
self.verify_stream_max_s = verify_stream_max_s
|
| 400 |
+
self._load_lock = threading.Lock()
|
| 401 |
+
self._pool: Optional[queue.Queue] = None
|
| 402 |
+
self._replicas: list[_Replica] = []
|
| 403 |
+
|
| 404 |
+
# -- loading --------------------------------------------------------------------------------
|
| 405 |
+
@property
|
| 406 |
+
def loaded(self) -> bool:
|
| 407 |
+
return self._pool is not None
|
| 408 |
+
|
| 409 |
+
def load(self) -> "ChindaASR":
|
| 410 |
+
if self._pool is not None:
|
| 411 |
+
return self
|
| 412 |
+
with self._load_lock:
|
| 413 |
+
if self._pool is None:
|
| 414 |
+
import torch
|
| 415 |
+
|
| 416 |
+
if self.device == "cpu":
|
| 417 |
+
torch.set_num_threads(self.num_threads)
|
| 418 |
+
torch.set_float32_matmul_precision("high") # as the infer script (no effect on CPU matmul)
|
| 419 |
+
reps = [_Replica(self.model_path, self.device) for _ in range(self.n_replicas)]
|
| 420 |
+
pool: queue.Queue = queue.Queue()
|
| 421 |
+
for r in reps:
|
| 422 |
+
pool.put(r)
|
| 423 |
+
self._replicas = reps
|
| 424 |
+
self._pool = pool
|
| 425 |
+
log.info("loaded %s x%d on %s in %.1fs", self.model_path, len(reps), self.device, reps[0].load_s)
|
| 426 |
+
return self
|
| 427 |
+
|
| 428 |
+
def _acquire(self) -> _Replica:
|
| 429 |
+
self.load()
|
| 430 |
+
return self._pool.get()
|
| 431 |
+
|
| 432 |
+
def _release(self, rep: _Replica) -> None:
|
| 433 |
+
self._pool.put(rep)
|
| 434 |
+
|
| 435 |
+
@property
|
| 436 |
+
def tokenizer(self):
|
| 437 |
+
"""The model's SentencePiece tokenizer (identical in every replica)."""
|
| 438 |
+
self.load()
|
| 439 |
+
return self._replicas[0].model.tokenizer
|
| 440 |
+
|
| 441 |
+
def info(self) -> dict:
|
| 442 |
+
d = {
|
| 443 |
+
"model": MODEL_NAME,
|
| 444 |
+
"model_path": self.model_path,
|
| 445 |
+
"loaded": self.loaded,
|
| 446 |
+
"device": self.device,
|
| 447 |
+
"num_threads": self.num_threads,
|
| 448 |
+
"replicas": self.n_replicas,
|
| 449 |
+
"langs": list(LANGS),
|
| 450 |
+
"latency_ms": sorted(LATENCY_PRESETS),
|
| 451 |
+
"policies": {k: v.describe() for k, v in POLICIES.items()},
|
| 452 |
+
}
|
| 453 |
+
if self.loaded:
|
| 454 |
+
r = self._replicas[0]
|
| 455 |
+
d.update(
|
| 456 |
+
prompt_slots={k: r.prompt_dict.get(k) for k in LANGS},
|
| 457 |
+
blank_id=r.blank_id,
|
| 458 |
+
unbaked_blank_bias=r.orig_blank_bias,
|
| 459 |
+
greedy=dict(max_symbols_per_step=r.greedy_cfg.get("max_symbols_per_step"),
|
| 460 |
+
loop_labels=r.greedy_cfg.get("loop_labels")),
|
| 461 |
+
)
|
| 462 |
+
return d
|
| 463 |
+
|
| 464 |
+
# -- helpers --------------------------------------------------------------------------------
|
| 465 |
+
@staticmethod
|
| 466 |
+
def _check(lang: str, latency_ms: int, policy: str) -> tuple[tuple[int, int], Policy]:
|
| 467 |
+
if lang not in LANGS:
|
| 468 |
+
raise ValueError(f"lang must be one of {LANGS}, got {lang!r}")
|
| 469 |
+
if int(latency_ms) not in LATENCY_PRESETS:
|
| 470 |
+
raise ValueError(f"latency_ms must be one of {sorted(LATENCY_PRESETS)}, got {latency_ms!r}")
|
| 471 |
+
if policy not in POLICIES:
|
| 472 |
+
raise ValueError(f"policy must be one of {sorted(POLICIES)}, got {policy!r}")
|
| 473 |
+
return LATENCY_PRESETS[int(latency_ms)], POLICIES[policy]
|
| 474 |
+
|
| 475 |
+
@staticmethod
|
| 476 |
+
def _as_array(a: Any) -> np.ndarray:
|
| 477 |
+
if isinstance(a, np.ndarray):
|
| 478 |
+
if a.ndim != 1:
|
| 479 |
+
raise ValueError("audio array must be 1-D float32 mono 16 kHz (use load_audio() for files)")
|
| 480 |
+
return np.ascontiguousarray(a, dtype=np.float32)
|
| 481 |
+
return load_audio(a)
|
| 482 |
+
|
| 483 |
+
def decode_pass(
|
| 484 |
+
self, audios: Sequence[np.ndarray], lang: str = "th-TH", latency_ms: int = 560, delta: float = -1.0,
|
| 485 |
+
batch_size: int = 32,
|
| 486 |
+
) -> list[str]:
|
| 487 |
+
"""ONE decode pass at a given blank-bias delta: consecutive groups of batch_size, in order
|
| 488 |
+
(exactly the infer script's manifest loop). Returns raw, unstripped texts."""
|
| 489 |
+
att, _ = self._check(lang, latency_ms, "standard")
|
| 490 |
+
arrs = [self._as_array(a) for a in audios]
|
| 491 |
+
out = [""] * len(arrs)
|
| 492 |
+
live = [i for i, a in enumerate(arrs) if a.shape[0] > 0] # empty input -> ""
|
| 493 |
+
bs = max(1, int(batch_size))
|
| 494 |
+
for k in range(0, len(live), bs):
|
| 495 |
+
idx = live[k:k + bs]
|
| 496 |
+
rep = self._acquire()
|
| 497 |
+
try:
|
| 498 |
+
texts = rep.decode_batch([arrs[i] for i in idx], lang, att, float(delta))
|
| 499 |
+
finally:
|
| 500 |
+
self._release(rep)
|
| 501 |
+
for i, t in zip(idx, texts):
|
| 502 |
+
out[i] = t
|
| 503 |
+
return out
|
| 504 |
+
|
| 505 |
+
# -- file / batch transcription ------------------------------------------------------------
|
| 506 |
+
def transcribe(self, audio: Any, lang: str = "th-TH", latency_ms: int = 560, policy: str = "shortclip") -> dict:
|
| 507 |
+
"""One utterance, decoded alone (batch size 1)."""
|
| 508 |
+
return self.transcribe_batch([audio], lang=lang, latency_ms=latency_ms, policy=policy, batch_size=1)[0]
|
| 509 |
+
|
| 510 |
+
def transcribe_batch(
|
| 511 |
+
self, audios: Sequence[Any], lang: str = "th-TH", latency_ms: int = 560, policy: str = "shortclip",
|
| 512 |
+
batch_size: int = 32,
|
| 513 |
+
) -> list[dict]:
|
| 514 |
+
"""Batched transcription with the same pass structure as scripts/shortclip_decode.sh:
|
| 515 |
+
pass 1 = all inputs at the policy's base delta; pass 2 = inputs shorter than the threshold,
|
| 516 |
+
re-decoded together (output replaces pass 1 even if empty); pass 3 = inputs whose output is
|
| 517 |
+
still empty, re-decoded together (output used only if non-empty). Each pass batches its
|
| 518 |
+
inputs in order, batch_size at a time."""
|
| 519 |
+
att, pol = self._check(lang, latency_ms, policy)
|
| 520 |
+
t0 = time.time()
|
| 521 |
+
arrs = [self._as_array(a) for a in audios]
|
| 522 |
+
n = len(arrs)
|
| 523 |
+
durs = [a.shape[0] / SAMPLE_RATE for a in arrs]
|
| 524 |
+
raw = self.decode_pass(arrs, lang, latency_ms, pol.base_delta, batch_size)
|
| 525 |
+
cur = [r.strip() for r in raw]
|
| 526 |
+
cur_raw = list(raw)
|
| 527 |
+
cur_delta = [pol.base_delta] * n
|
| 528 |
+
passes: list[list[dict]] = [[{"delta": pol.base_delta, "reason": "base", "text": c}] for c in cur]
|
| 529 |
+
if pol.short_delta is not None:
|
| 530 |
+
idx = [i for i in range(n) if durs[i] < pol.short_threshold_s]
|
| 531 |
+
if idx:
|
| 532 |
+
raw2 = self.decode_pass([arrs[i] for i in idx], lang, latency_ms, pol.short_delta, batch_size)
|
| 533 |
+
for i, r in zip(idx, raw2):
|
| 534 |
+
cur[i], cur_raw[i], cur_delta[i] = r.strip(), r, pol.short_delta
|
| 535 |
+
passes[i].append({"delta": pol.short_delta, "reason": f"duration<{pol.short_threshold_s:g}s", "text": r.strip()})
|
| 536 |
+
if pol.retry_delta is not None:
|
| 537 |
+
idx = [i for i in range(n) if not cur[i]]
|
| 538 |
+
if idx:
|
| 539 |
+
raw3 = self.decode_pass([arrs[i] for i in idx], lang, latency_ms, pol.retry_delta, batch_size)
|
| 540 |
+
for i, r in zip(idx, raw3):
|
| 541 |
+
passes[i].append({"delta": pol.retry_delta, "reason": "empty", "text": r.strip()})
|
| 542 |
+
if r.strip():
|
| 543 |
+
cur[i], cur_raw[i], cur_delta[i] = r.strip(), r, pol.retry_delta
|
| 544 |
+
dt = time.time() - t0
|
| 545 |
+
return [
|
| 546 |
+
{
|
| 547 |
+
"text": cur[i],
|
| 548 |
+
"display_text": display_text(cur[i]),
|
| 549 |
+
"raw_text": cur_raw[i],
|
| 550 |
+
"duration_s": round(durs[i], 3),
|
| 551 |
+
"lang": lang,
|
| 552 |
+
"latency_ms": int(latency_ms),
|
| 553 |
+
"policy": policy,
|
| 554 |
+
"delta": cur_delta[i],
|
| 555 |
+
"passes": passes[i],
|
| 556 |
+
"model": MODEL_NAME,
|
| 557 |
+
"processing_s": round(dt, 3), # wall time of the whole call (shared by the batch)
|
| 558 |
+
}
|
| 559 |
+
for i in range(n)
|
| 560 |
+
]
|
| 561 |
+
|
| 562 |
+
# -- live streaming -------------------------------------------------------------------------
|
| 563 |
+
def stream(self, lang: str = "th-TH", latency_ms: int = 560, policy: str = "shortclip") -> "StreamSession":
|
| 564 |
+
self._check(lang, latency_ms, policy)
|
| 565 |
+
self.load()
|
| 566 |
+
return StreamSession(self, lang, int(latency_ms), policy)
|
| 567 |
+
|
| 568 |
+
|
| 569 |
+
class StreamSession:
|
| 570 |
+
"""A live stream of ONE utterance (batch size 1).
|
| 571 |
+
|
| 572 |
+
accept(pcm_float32) -> current partial text (first pass, base delta of the policy)
|
| 573 |
+
finish() -> final result dict, identical to asr.transcribe(all_audio, ...)
|
| 574 |
+
|
| 575 |
+
Thread-safe per session (calls are serialized); many sessions may run concurrently.
|
| 576 |
+
"""
|
| 577 |
+
|
| 578 |
+
def __init__(self, asr: ChindaASR, lang: str, latency_ms: int, policy: str):
|
| 579 |
+
self.asr = asr
|
| 580 |
+
self.lang = lang
|
| 581 |
+
self.latency_ms = latency_ms
|
| 582 |
+
self.policy_name = policy
|
| 583 |
+
self.att, self.policy = asr._check(lang, latency_ms, policy)
|
| 584 |
+
rep = asr._replicas[0]
|
| 585 |
+
self.cfg = rep.streaming_cfgs[self.att]
|
| 586 |
+
self._lock = threading.Lock()
|
| 587 |
+
self._audio = np.zeros(1 << 16, dtype=np.float32)
|
| 588 |
+
self._n = 0
|
| 589 |
+
self._committed = 0 # feature frames computed so far (exact, final values)
|
| 590 |
+
self._win = None # committed frames [self._win_base, self._committed) as a (1, F, T) tensor
|
| 591 |
+
self._win_base = 0
|
| 592 |
+
self._kept: Optional[list] = [] # all committed frames, kept for the finish() verification
|
| 593 |
+
self._buffer_idx = 0 # NeMo buffer_idx, absolute frames
|
| 594 |
+
self._st = _StepState()
|
| 595 |
+
self._partial = ""
|
| 596 |
+
self._done = False
|
| 597 |
+
self._t0 = time.time()
|
| 598 |
+
self.stats = {"steps": 0, "chunks_in": 0}
|
| 599 |
+
|
| 600 |
+
# -- audio + features -----------------------------------------------------------------------
|
| 601 |
+
def _append(self, x: np.ndarray) -> None:
|
| 602 |
+
need = self._n + x.shape[0]
|
| 603 |
+
if need > self._audio.shape[0]:
|
| 604 |
+
grown = np.zeros(max(need, 2 * self._audio.shape[0]), dtype=np.float32)
|
| 605 |
+
grown[: self._n] = self._audio[: self._n]
|
| 606 |
+
self._audio = grown
|
| 607 |
+
self._audio[self._n: need] = x
|
| 608 |
+
self._n = need
|
| 609 |
+
|
| 610 |
+
def _new_frames(self, upto: int, final: bool):
|
| 611 |
+
"""Frames [self._committed, upto) computed on an aligned audio segment.
|
| 612 |
+
Segment start s is a multiple of HOP with >= 2 frames of margin, so the segment's own
|
| 613 |
+
pre-emphasis restart and zero left-padding touch no frame we keep; with final=True the
|
| 614 |
+
segment ends at the true end of audio, so the end padding matches the whole-file pass."""
|
| 615 |
+
rep = self.asr._replicas[0]
|
| 616 |
+
F = self._committed
|
| 617 |
+
s = 0 if F < 3 else (F - 2) * HOP
|
| 618 |
+
seg = self._audio[s: self._n]
|
| 619 |
+
feats = rep.features(seg) # (1, nfeat, frames) incl. the masked last frame
|
| 620 |
+
j0, j1 = F - s // HOP, upto - s // HOP
|
| 621 |
+
new = feats[:, :, j0:j1]
|
| 622 |
+
if new.shape[-1] != upto - F:
|
| 623 |
+
raise RuntimeError(f"feature bookkeeping error: got {new.shape[-1]} frames, want {upto - F}")
|
| 624 |
+
return new
|
| 625 |
+
|
| 626 |
+
def _commit(self, final: bool) -> None:
|
| 627 |
+
torch = self.asr._replicas[0].torch
|
| 628 |
+
if final:
|
| 629 |
+
upto = self._n // HOP + 1 if self._n > 0 else 0 # == frames of a whole-file pass
|
| 630 |
+
else:
|
| 631 |
+
upto = (self._n - HALF_WIN) // HOP + 1 if self._n >= HALF_WIN else 0 # window fully received
|
| 632 |
+
if upto <= self._committed:
|
| 633 |
+
return
|
| 634 |
+
with torch.inference_mode():
|
| 635 |
+
new = self._new_frames(upto, final)
|
| 636 |
+
self._win = new if self._win is None else torch.cat((self._win, new), dim=-1)
|
| 637 |
+
if self._kept is not None:
|
| 638 |
+
self._kept.append(new)
|
| 639 |
+
if self._n > self.asr.verify_stream_max_s * SAMPLE_RATE:
|
| 640 |
+
self._kept = None # too long to re-verify cheaply; stop keeping a copy
|
| 641 |
+
self._committed = upto
|
| 642 |
+
|
| 643 |
+
# -- encoder steps --------------------------------------------------------------------------
|
| 644 |
+
def _run_steps(self, final: bool) -> None:
|
| 645 |
+
"""Run every chunk step whose inputs are final. A non-final call only takes steps that are
|
| 646 |
+
provably not the last one (keep_all_outputs=False, full-size chunk)."""
|
| 647 |
+
cfg = self.cfg
|
| 648 |
+
pick = lambda v, first: (v[0] if first else v[1]) if isinstance(v, list) else v # noqa: E731
|
| 649 |
+
torch = self.asr._replicas[0].torch
|
| 650 |
+
while True:
|
| 651 |
+
idx = self._buffer_idx
|
| 652 |
+
first = idx == 0
|
| 653 |
+
cs, ss = pick(cfg.chunk_size, first), pick(cfg.shift_size, first)
|
| 654 |
+
pre = pick(cfg.pre_encode_cache_size, first)
|
| 655 |
+
if not final and self._committed < max(idx + cs, idx + ss + 1):
|
| 656 |
+
return
|
| 657 |
+
if final and idx >= self._committed:
|
| 658 |
+
return
|
| 659 |
+
# a window of NeMo's own buffer: frames [base, committed), base = idx - pre_encode_cache
|
| 660 |
+
base = max(0, idx - pre)
|
| 661 |
+
if base < self._win_base:
|
| 662 |
+
raise RuntimeError("stream window trimmed too far")
|
| 663 |
+
win = self._win[:, :, base - self._win_base:]
|
| 664 |
+
rep = self.asr._acquire()
|
| 665 |
+
try:
|
| 666 |
+
with rep.lock:
|
| 667 |
+
rep.configure(self.lang, self.att, self.policy.base_delta)
|
| 668 |
+
with torch.inference_mode():
|
| 669 |
+
buf = rep.new_buffer(self.att)
|
| 670 |
+
buf.buffer = win
|
| 671 |
+
buf.streams_length = torch.tensor([self._committed - base], device=win.device)
|
| 672 |
+
buf.buffer_idx = idx - base
|
| 673 |
+
buf.step = self._st.step_num
|
| 674 |
+
it = iter(buf)
|
| 675 |
+
item = next(it, None)
|
| 676 |
+
if item is None: # NeMo drops a final chunk shorter than sampling_frames
|
| 677 |
+
self._buffer_idx = self._committed
|
| 678 |
+
return
|
| 679 |
+
chunk, lengths = item
|
| 680 |
+
keep_all = buf.is_buffer_empty()
|
| 681 |
+
if keep_all and not final:
|
| 682 |
+
raise RuntimeError("internal: a non-final call reached the last chunk")
|
| 683 |
+
rep.stream_step(
|
| 684 |
+
self._st, chunk, lengths, keep_all_outputs=keep_all,
|
| 685 |
+
drop_extra=0 if self._st.step_num == 0 else cfg.drop_extra_pre_encoded,
|
| 686 |
+
)
|
| 687 |
+
self._buffer_idx = base + buf.buffer_idx
|
| 688 |
+
finally:
|
| 689 |
+
self.asr._release(rep)
|
| 690 |
+
self.stats["steps"] += 1
|
| 691 |
+
self._partial = self._st.texts[0].text if self._st.texts else ""
|
| 692 |
+
# trim frames no future step can touch
|
| 693 |
+
keep_from = max(0, self._buffer_idx - pick(cfg.pre_encode_cache_size, False))
|
| 694 |
+
if keep_from > self._win_base:
|
| 695 |
+
self._win = self._win[:, :, keep_from - self._win_base:]
|
| 696 |
+
self._win_base = keep_from
|
| 697 |
+
|
| 698 |
+
# -- public ---------------------------------------------------------------------------------
|
| 699 |
+
def accept(self, pcm: Any) -> str:
|
| 700 |
+
"""Feed float32 mono 16 kHz samples (np.ndarray) -- or raw PCM16 LE bytes. Returns the partial text."""
|
| 701 |
+
with self._lock:
|
| 702 |
+
if self._done:
|
| 703 |
+
raise RuntimeError("stream already finished")
|
| 704 |
+
x = pcm16_to_float32(pcm) if isinstance(pcm, (bytes, bytearray, memoryview)) else np.asarray(pcm, dtype=np.float32).reshape(-1)
|
| 705 |
+
self.stats["chunks_in"] += 1
|
| 706 |
+
if x.shape[0]:
|
| 707 |
+
self._append(x)
|
| 708 |
+
self._commit(final=False)
|
| 709 |
+
self._run_steps(final=False)
|
| 710 |
+
return self._partial
|
| 711 |
+
|
| 712 |
+
def tokens(self) -> list:
|
| 713 |
+
"""[(token_id, time_s)] of the streamed hypothesis in emission order; time = RNNT frame index x 80 ms (97% of
|
| 714 |
+
recognised characters fall inside their true speaker turn, 99.9% within 0.25 s; measured 2026-09-26). Used by
|
| 715 |
+
chinda_diar for speaker attribution. Reflects the streamed first pass, not a policy's second-pass re-decode."""
|
| 716 |
+
with self._lock:
|
| 717 |
+
h = self._st.hyps[0] if self._st.hyps else None
|
| 718 |
+
if h is None or h.timestamp is None:
|
| 719 |
+
return []
|
| 720 |
+
ids = h.y_sequence.tolist() if hasattr(h.y_sequence, "tolist") else list(h.y_sequence)
|
| 721 |
+
ts = h.timestamp.tolist() if hasattr(h.timestamp, "tolist") else [int(t) for t in h.timestamp]
|
| 722 |
+
return [(int(i), float(t) * 0.08) for i, t in zip(ids, ts)]
|
| 723 |
+
|
| 724 |
+
@property
|
| 725 |
+
def partial(self) -> str:
|
| 726 |
+
return self._partial
|
| 727 |
+
|
| 728 |
+
@property
|
| 729 |
+
def audio(self) -> np.ndarray:
|
| 730 |
+
return self._audio[: self._n].copy()
|
| 731 |
+
|
| 732 |
+
def finish(self) -> dict:
|
| 733 |
+
"""End of utterance: flush the last chunk(s), then the policy's second pass on the buffered audio."""
|
| 734 |
+
with self._lock:
|
| 735 |
+
if self._done:
|
| 736 |
+
raise RuntimeError("stream already finished")
|
| 737 |
+
self._done = True
|
| 738 |
+
audio = self._audio[: self._n].copy()
|
| 739 |
+
fallback = None
|
| 740 |
+
if self._n > 0:
|
| 741 |
+
self._commit(final=True)
|
| 742 |
+
self._run_steps(final=True)
|
| 743 |
+
# safety net: the frames we streamed must equal a whole-utterance feature pass. Bit-identical on CPU;
|
| 744 |
+
# on GPU the chunked and the whole-utterance feature pass round differently (float noise, measured
|
| 745 |
+
# 2026-09-26), so a bit-exact test re-decoded EVERY stream. A real bug (missing, shifted or
|
| 746 |
+
# misnormalised frames) moves log-mel values by O(1); tolerate only rounding noise.
|
| 747 |
+
if self._kept is not None:
|
| 748 |
+
torch = self.asr._replicas[0].torch
|
| 749 |
+
with torch.inference_mode():
|
| 750 |
+
full = self.asr._replicas[0].features(audio)
|
| 751 |
+
mine = torch.cat(self._kept, dim=-1)
|
| 752 |
+
if full.shape != mine.shape:
|
| 753 |
+
fallback = "feature_mismatch"
|
| 754 |
+
else:
|
| 755 |
+
self.stats["feature_maxdiff"] = float((full - mine).abs().max()) if full.numel() else 0.0
|
| 756 |
+
if self.stats["feature_maxdiff"] > FEATURE_TOL:
|
| 757 |
+
fallback = "feature_mismatch"
|
| 758 |
+
first_raw = self._partial if self._n > 0 else ""
|
| 759 |
+
if fallback:
|
| 760 |
+
log.warning("stream features differed from the whole-utterance pass; re-decoding (bs=1)")
|
| 761 |
+
first_raw = self.asr.decode_pass([audio], self.lang, self.latency_ms, self.policy.base_delta, 1)[0]
|
| 762 |
+
# second pass: identical to transcribe_batch() with a single input
|
| 763 |
+
pol = self.policy
|
| 764 |
+
cur, cur_raw, cur_delta = first_raw.strip(), first_raw, pol.base_delta
|
| 765 |
+
passes = [{"delta": pol.base_delta, "reason": "base", "text": cur}]
|
| 766 |
+
dur = self._n / SAMPLE_RATE
|
| 767 |
+
if pol.short_delta is not None and dur < pol.short_threshold_s and self._n > 0:
|
| 768 |
+
r = self.asr.decode_pass([audio], self.lang, self.latency_ms, pol.short_delta, 1)[0]
|
| 769 |
+
cur, cur_raw, cur_delta = r.strip(), r, pol.short_delta
|
| 770 |
+
passes.append({"delta": pol.short_delta, "reason": f"duration<{pol.short_threshold_s:g}s", "text": cur})
|
| 771 |
+
if pol.retry_delta is not None and not cur and self._n > 0:
|
| 772 |
+
r = self.asr.decode_pass([audio], self.lang, self.latency_ms, pol.retry_delta, 1)[0]
|
| 773 |
+
passes.append({"delta": pol.retry_delta, "reason": "empty", "text": r.strip()})
|
| 774 |
+
if r.strip():
|
| 775 |
+
cur, cur_raw, cur_delta = r.strip(), r, pol.retry_delta
|
| 776 |
+
return {
|
| 777 |
+
"text": cur,
|
| 778 |
+
"display_text": display_text(cur),
|
| 779 |
+
"raw_text": cur_raw,
|
| 780 |
+
"first_pass_text": first_raw.strip(),
|
| 781 |
+
"duration_s": round(dur, 3),
|
| 782 |
+
"lang": self.lang,
|
| 783 |
+
"latency_ms": self.latency_ms,
|
| 784 |
+
"policy": self.policy_name,
|
| 785 |
+
"delta": cur_delta,
|
| 786 |
+
"passes": passes,
|
| 787 |
+
"model": MODEL_NAME,
|
| 788 |
+
"stream_steps": self.stats["steps"],
|
| 789 |
+
"stream_fallback": fallback,
|
| 790 |
+
"stream_feature_maxdiff": self.stats.get("feature_maxdiff"),
|
| 791 |
+
"processing_s": round(time.time() - self._t0, 3),
|
| 792 |
+
}
|
| 793 |
+
|
| 794 |
+
|
| 795 |
+
# ----------------------------------------------------------------------------------------------
|
| 796 |
+
# CLI: CUDA_VISIBLE_DEVICES='' python serve/chinda_asr.py a.wav [b.wav ...] --policy shortclip
|
| 797 |
+
# ----------------------------------------------------------------------------------------------
|
| 798 |
+
def _main(argv: Optional[Iterable[str]] = None) -> None:
|
| 799 |
+
import argparse
|
| 800 |
+
|
| 801 |
+
ap = argparse.ArgumentParser(description="ChindaASR file transcription (reference decoder)")
|
| 802 |
+
ap.add_argument("files", nargs="+")
|
| 803 |
+
ap.add_argument("--lang", default="th-TH", choices=LANGS)
|
| 804 |
+
ap.add_argument("--latency-ms", type=int, default=560, choices=sorted(LATENCY_PRESETS))
|
| 805 |
+
ap.add_argument("--policy", default="shortclip", choices=sorted(POLICIES))
|
| 806 |
+
ap.add_argument("--batch-size", type=int, default=32)
|
| 807 |
+
ap.add_argument("--model", default=DEFAULT_MODEL_PATH)
|
| 808 |
+
ap.add_argument("--threads", type=int, default=None)
|
| 809 |
+
a = ap.parse_args(argv)
|
| 810 |
+
logging.basicConfig(level=logging.INFO)
|
| 811 |
+
asr = ChindaASR(a.model, num_threads=a.threads)
|
| 812 |
+
for f, r in zip(a.files, asr.transcribe_batch(a.files, a.lang, a.latency_ms, a.policy, a.batch_size)):
|
| 813 |
+
print(json.dumps({"file": f, **{k: r[k] for k in ("text", "duration_s", "delta")}}, ensure_ascii=False))
|
| 814 |
+
|
| 815 |
+
|
| 816 |
+
if __name__ == "__main__":
|
| 817 |
+
_main()
|
serve/chinda_diar.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Speaker labels ("who said what") for the ChindaASR reference service (2026-09-26).
|
| 2 |
+
|
| 3 |
+
Design, chosen by measurement on 11 Thai meetings: ONE ASR stream plus the speaker
|
| 4 |
+
segments of nvidia/Nemotron-3-Diarization (OpenMDW-1.1). Every recognised token goes to the speaker the diarizer marks
|
| 5 |
+
active at the token's time; a turn breaks on a speaker change or a pause longer than `pause_s`. On Thai meetings this beat
|
| 6 |
+
NVIDIA's one-masked-ASR-stream-per-speaker design (text errors 20.7 vs 27.9; text+speaker errors 17.7 vs 19.2) and costs
|
| 7 |
+
one ASR stream however many people talk. Token times are the RNNT hypothesis frame indices x 80 ms: 97% of recognised
|
| 8 |
+
characters fall inside their true turn and 99.9% within 0.25 s (measured on LOTUSDIS S010).
|
| 9 |
+
|
| 10 |
+
Files: the diarizer runs in its OFFLINE configuration over the whole recording (best speaker labels, DER 17.5 on the Thai
|
| 11 |
+
meetings). Live streams: speaker labels are attached at the END of the stream from the buffered audio; per-partial live
|
| 12 |
+
labels are not provided yet.
|
| 13 |
+
|
| 14 |
+
from chinda_asr import ChindaASR; from chinda_diar import Diarizer, transcribe_with_speakers
|
| 15 |
+
asr = ChindaASR(device="cuda"); diar = Diarizer(device="cuda")
|
| 16 |
+
res = transcribe_with_speakers(asr, diar, audio) # res["segments"] = [{speaker, start, end, text}, ...]
|
| 17 |
+
"""
|
| 18 |
+
from __future__ import annotations
|
| 19 |
+
|
| 20 |
+
import logging
|
| 21 |
+
import os
|
| 22 |
+
import re
|
| 23 |
+
import threading
|
| 24 |
+
from typing import Any, Optional
|
| 25 |
+
|
| 26 |
+
import numpy as np
|
| 27 |
+
|
| 28 |
+
log = logging.getLogger("chinda_diar")
|
| 29 |
+
|
| 30 |
+
# $CHINDA_DIAR_MODEL, else downloaded from Hugging Face on first use (nvidia/Nemotron-3-Diarization, OpenMDW-1.1).
|
| 31 |
+
DIAR_MODEL_PATH = os.environ.get("CHINDA_DIAR_MODEL") or ""
|
| 32 |
+
DIAR_HF_REPO, DIAR_HF_FILE = "nvidia/Nemotron-3-Diarization", "Nemotron-3-Diarization.nemo"
|
| 33 |
+
# offline configuration = the model card's 30.4 s-latency row (spkcache 264, fifo 40, chunk 340, right context 40, update 300)
|
| 34 |
+
OFFLINE = dict(chunk_len=340, chunk_right_context=40, fifo_len=40, spkcache_update_period=300, spkcache_len=264)
|
| 35 |
+
FRAME_S = 0.08
|
| 36 |
+
LANGTAG = re.compile(r"^<[a-z]{2,3}(-[A-Za-z]{2,4})?>$")
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class Diarizer:
|
| 40 |
+
"""Thread-safe, lazily loaded Nemotron-3-Diarization (needs NeMo Speech at the commit pinned in the README)."""
|
| 41 |
+
|
| 42 |
+
def __init__(self, device: Optional[str] = None, model_path: str = DIAR_MODEL_PATH):
|
| 43 |
+
self.device = device or os.environ.get("CHINDA_DEVICE", "cpu")
|
| 44 |
+
self.model_path = model_path
|
| 45 |
+
self._model = None
|
| 46 |
+
self._lock = threading.Lock()
|
| 47 |
+
|
| 48 |
+
def load(self) -> "Diarizer":
|
| 49 |
+
with self._lock:
|
| 50 |
+
if self._model is None:
|
| 51 |
+
from nemo.collections.asr.models import SortformerEncLabelModel
|
| 52 |
+
|
| 53 |
+
if not os.path.exists(self.model_path):
|
| 54 |
+
from huggingface_hub import hf_hub_download
|
| 55 |
+
|
| 56 |
+
self.model_path = hf_hub_download(DIAR_HF_REPO, DIAR_HF_FILE)
|
| 57 |
+
m = SortformerEncLabelModel.restore_from(self.model_path, map_location=self.device).eval()
|
| 58 |
+
for k, v in OFFLINE.items():
|
| 59 |
+
setattr(m.sortformer_modules, k, v)
|
| 60 |
+
self._model = m
|
| 61 |
+
log.info("diarizer loaded from %s on %s", self.model_path, self.device)
|
| 62 |
+
return self
|
| 63 |
+
|
| 64 |
+
def segments(self, audio: np.ndarray, sample_rate: int = 16000) -> list[tuple[float, float, str]]:
|
| 65 |
+
"""[(start_s, end_s, 'speaker_N')] for one recording (float32 mono)."""
|
| 66 |
+
self.load()
|
| 67 |
+
if audio.shape[0] < int(0.5 * sample_rate): # too short to say anything about speakers
|
| 68 |
+
return [(0.0, audio.shape[0] / sample_rate, "speaker_0")] if audio.shape[0] else []
|
| 69 |
+
with self._lock:
|
| 70 |
+
out = self._model.diarize(audio=[np.asarray(audio, dtype=np.float32)], sample_rate=sample_rate, batch_size=1)
|
| 71 |
+
segs = []
|
| 72 |
+
for line in out[0]:
|
| 73 |
+
s, e, spk = line.split()
|
| 74 |
+
segs.append((float(s), float(e), spk))
|
| 75 |
+
return sorted(segs)
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def speaker_at(t: float, segs: list[tuple[float, float, str]], prev: Optional[str]) -> Optional[str]:
|
| 79 |
+
"""Speaker active at time t: the covering segment (the longest if several overlap), else the nearest one within
|
| 80 |
+
1 s, else the previous token's speaker (identical to scripts/diar_thai.py, which produced the measurements)."""
|
| 81 |
+
cover = [s for s in segs if s[0] - 0.2 <= t <= s[1] + 0.2]
|
| 82 |
+
if cover:
|
| 83 |
+
return max(cover, key=lambda s: s[1] - s[0])[2]
|
| 84 |
+
near = min(segs, key=lambda s: min(abs(t - s[0]), abs(t - s[1])), default=None)
|
| 85 |
+
if near is not None and min(abs(t - near[0]), abs(t - near[1])) <= 1.0:
|
| 86 |
+
return near[2]
|
| 87 |
+
return prev
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def build_turns(tokens: list[tuple[int, float]], tokenizer: Any, segs: list[tuple[float, float, str]],
|
| 91 |
+
pause_s: float = 1.0) -> tuple[list[dict], list[dict]]:
|
| 92 |
+
"""tokens = [(token_id, time_s)] of ONE stream in emission order -> (turns, per-token records)."""
|
| 93 |
+
recs, prev = [], None
|
| 94 |
+
for i, t in tokens:
|
| 95 |
+
piece = tokenizer.ids_to_tokens([i])[0]
|
| 96 |
+
if i == 0 or LANGTAG.match(piece):
|
| 97 |
+
continue
|
| 98 |
+
prev = speaker_at(t, segs, prev) or "speaker_0"
|
| 99 |
+
recs.append({"speaker": prev, "t": round(t, 3), "id": int(i), "piece": piece})
|
| 100 |
+
turns, cur = [], None
|
| 101 |
+
for r in recs:
|
| 102 |
+
if cur is None or r["speaker"] != cur["speaker"] or r["t"] - cur["end"] > pause_s:
|
| 103 |
+
if cur is not None:
|
| 104 |
+
turns.append(cur)
|
| 105 |
+
cur = {"speaker": r["speaker"], "start": r["t"], "end": r["t"], "ids": []}
|
| 106 |
+
cur["ids"].append(r["id"])
|
| 107 |
+
cur["end"] = r["t"]
|
| 108 |
+
if cur is not None:
|
| 109 |
+
turns.append(cur)
|
| 110 |
+
out = []
|
| 111 |
+
for c in turns:
|
| 112 |
+
text = tokenizer.ids_to_text(c["ids"]).strip()
|
| 113 |
+
if text:
|
| 114 |
+
out.append({"speaker": c["speaker"], "start": round(c["start"], 2), "end": round(c["end"] + FRAME_S, 2), "text": text})
|
| 115 |
+
return out, recs
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def transcribe_with_speakers(asr: Any, diar: Diarizer, audio: Any, lang: str = "th-TH", latency_ms: int = 560,
|
| 119 |
+
pause_s: float = 1.0, include_tokens: bool = False) -> dict:
|
| 120 |
+
"""Who said what for one recording: one ChindaASR stream (policy 'standard', the meeting-mode setting) + offline
|
| 121 |
+
diarization. Returns the stream's final result plus `segments` (turns with speaker, start, end, text)."""
|
| 122 |
+
from chinda_asr import load_audio
|
| 123 |
+
|
| 124 |
+
x = load_audio(audio) if not isinstance(audio, np.ndarray) else np.asarray(audio, dtype=np.float32)
|
| 125 |
+
s = asr.stream(lang=lang, latency_ms=latency_ms, policy="standard")
|
| 126 |
+
step = 16000
|
| 127 |
+
for k in range(0, x.shape[0], step):
|
| 128 |
+
s.accept(x[k:k + step])
|
| 129 |
+
res = s.finish()
|
| 130 |
+
segs = diar.segments(x)
|
| 131 |
+
turns, recs = build_turns(s.tokens(), asr.tokenizer, segs, pause_s=pause_s)
|
| 132 |
+
res.update(segments=turns, speakers=len({t["speaker"] for t in turns}),
|
| 133 |
+
diarization=[{"start": a, "end": b, "speaker": c} for a, b, c in segs])
|
| 134 |
+
if include_tokens:
|
| 135 |
+
res["tokens"] = recs
|
| 136 |
+
return res
|