Upload Model
Browse files- .gitattributes +14 -0
- LICENSE +34 -0
- README.md +770 -0
- assets/asr_condition_gap.jpg +3 -0
- assets/banner.jpg +3 -0
- assets/benchmarks.jpg +3 -0
- assets/method_vs_architecture.jpg +3 -0
- assets/per_class.jpg +3 -0
- assets/per_language_wit3.jpg +3 -0
- assets/size_vs_score.jpg +3 -0
- dewpoint.py +596 -0
- ensemble_config.json +116 -0
- gazetteer.json +0 -0
- handler.py +51 -0
- mmbert-base/config.json +78 -0
- mmbert-base/model.safetensors +3 -0
- mmbert-base/tokenizer.json +3 -0
- mmbert-base/tokenizer_config.json +25 -0
- mmbert-base/vpunct_config.json +27 -0
- requirements.txt +7 -0
- results/rt2_voices.json +166 -0
- results/scores.json +0 -0
- serve.py +84 -0
- xlm-roberta-large/config.json +30 -0
- xlm-roberta-large/model.safetensors +3 -0
- xlm-roberta-large/tokenizer.json +3 -0
- xlm-roberta-large/tokenizer_config.json +15 -0
- xlm-roberta-large/vpunct_config.json +91 -0
.gitattributes
CHANGED
|
@@ -21,6 +21,11 @@
|
|
| 21 |
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
*.pth filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
@@ -28,8 +33,17 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 28 |
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
*.tgz filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 31 |
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
+
*.png filter=lfs diff=lfs merge=lfs -text
|
| 25 |
+
*.jpg filter=lfs diff=lfs merge=lfs -text
|
| 26 |
+
*.gif filter=lfs diff=lfs merge=lfs -text
|
| 27 |
+
*.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 28 |
+
*.svg filter=lfs diff=lfs merge=lfs -text
|
| 29 |
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 30 |
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 31 |
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 33 |
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
tokenizer.json filter=lfs diff=lfs merge=lfs -text
|
| 37 |
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 38 |
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 39 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 40 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 41 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 42 |
+
|
| 43 |
+
# LF-pinned: manifest integrity hashes must match git-served bytes
|
| 44 |
+
*.py text eol=lf
|
| 45 |
+
*.json text eol=lf
|
| 46 |
+
*.md text eol=lf
|
| 47 |
+
*.jinja text eol=lf
|
| 48 |
+
LICENSE text eol=lf
|
| 49 |
+
README text eol=lf
|
LICENSE
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2026 valkayuh
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
| 22 |
+
|
| 23 |
+
-------------------------------------------------------------------------------
|
| 24 |
+
Third-party material
|
| 25 |
+
|
| 26 |
+
The encoders are fine-tuned from jhu-clsp/mmBERT-base (MIT) and
|
| 27 |
+
FacebookAI/xlm-roberta-large (MIT).
|
| 28 |
+
|
| 29 |
+
Training data included Wikipedia (CC-BY-SA-4.0), Tatoeba sentences
|
| 30 |
+
(CC-BY-2.0 FR, (c) Tatoeba contributors, https://tatoeba.org), Europarl,
|
| 31 |
+
HPLT 2.0 (CC0-1.0), VoxPopuli transcripts (CC0-1.0) and the Aya dataset
|
| 32 |
+
(Apache-2.0), plus Whisper transcripts of synthetic speech produced with
|
| 33 |
+
Kokoro-82M (Apache-2.0) and facebook/mms-tts (CC-BY-NC-4.0). None of that
|
| 34 |
+
data is redistributed here. See README.md, "Licence and data provenance".
|
README.md
ADDED
|
@@ -0,0 +1,770 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: mit
|
| 3 |
+
language:
|
| 4 |
+
- ar
|
| 5 |
+
- az
|
| 6 |
+
- bg
|
| 7 |
+
- bn
|
| 8 |
+
- bs
|
| 9 |
+
- ca
|
| 10 |
+
- cs
|
| 11 |
+
- da
|
| 12 |
+
- de
|
| 13 |
+
- el
|
| 14 |
+
- en
|
| 15 |
+
- eo
|
| 16 |
+
- es
|
| 17 |
+
- et
|
| 18 |
+
- fa
|
| 19 |
+
- fi
|
| 20 |
+
- fr
|
| 21 |
+
- he
|
| 22 |
+
- hi
|
| 23 |
+
- hr
|
| 24 |
+
- hu
|
| 25 |
+
- hy
|
| 26 |
+
- id
|
| 27 |
+
- it
|
| 28 |
+
- ja
|
| 29 |
+
- ka
|
| 30 |
+
- kk
|
| 31 |
+
- ko
|
| 32 |
+
- ku
|
| 33 |
+
- lt
|
| 34 |
+
- lv
|
| 35 |
+
- mk
|
| 36 |
+
- mn
|
| 37 |
+
- mr
|
| 38 |
+
- ms
|
| 39 |
+
- my
|
| 40 |
+
- nb
|
| 41 |
+
- nl
|
| 42 |
+
- pl
|
| 43 |
+
- pt
|
| 44 |
+
- ro
|
| 45 |
+
- ru
|
| 46 |
+
- sk
|
| 47 |
+
- sl
|
| 48 |
+
- sq
|
| 49 |
+
- sr
|
| 50 |
+
- sv
|
| 51 |
+
- sw
|
| 52 |
+
- ta
|
| 53 |
+
- th
|
| 54 |
+
- tr
|
| 55 |
+
- uk
|
| 56 |
+
- ur
|
| 57 |
+
- vi
|
| 58 |
+
- zh
|
| 59 |
+
- af
|
| 60 |
+
- be
|
| 61 |
+
- cy
|
| 62 |
+
- eu
|
| 63 |
+
- ga
|
| 64 |
+
- gl
|
| 65 |
+
- is
|
| 66 |
+
- kn
|
| 67 |
+
- ml
|
| 68 |
+
- ne
|
| 69 |
+
- nn
|
| 70 |
+
- si
|
| 71 |
+
- te
|
| 72 |
+
- tl
|
| 73 |
+
- uz
|
| 74 |
+
- gu
|
| 75 |
+
- pa
|
| 76 |
+
- km
|
| 77 |
+
- la
|
| 78 |
+
- lb
|
| 79 |
+
- mt
|
| 80 |
+
- so
|
| 81 |
+
- am
|
| 82 |
+
- ky
|
| 83 |
+
pipeline_tag: token-classification
|
| 84 |
+
tags:
|
| 85 |
+
- punctuation
|
| 86 |
+
- punctuation-restoration
|
| 87 |
+
- truecasing
|
| 88 |
+
- capitalization
|
| 89 |
+
- asr
|
| 90 |
+
- speech-recognition-postprocessing
|
| 91 |
+
- multilingual
|
| 92 |
+
- ensemble
|
| 93 |
+
- modernbert
|
| 94 |
+
- xlm-roberta
|
| 95 |
+
base_model:
|
| 96 |
+
- jhu-clsp/mmBERT-base
|
| 97 |
+
- valkayuh/dewpoint
|
| 98 |
+
base_model_relation: quantized
|
| 99 |
+
datasets:
|
| 100 |
+
- wikimedia/wikipedia
|
| 101 |
+
- Helsinki-NLP/europarl
|
| 102 |
+
- HPLT/HPLT2.0_cleaned
|
| 103 |
+
- facebook/voxpopuli
|
| 104 |
+
- CohereLabs/aya_dataset
|
| 105 |
+
metrics:
|
| 106 |
+
- f1
|
| 107 |
+
model-index:
|
| 108 |
+
- name: Dewpoint
|
| 109 |
+
results:
|
| 110 |
+
- task:
|
| 111 |
+
type: token-classification
|
| 112 |
+
name: Punctuation restoration
|
| 113 |
+
dataset:
|
| 114 |
+
name: IWSLT 2017 TED, official tst2010-2015 (9 languages)
|
| 115 |
+
type: IWSLT/iwslt2017
|
| 116 |
+
metrics:
|
| 117 |
+
- type: f1
|
| 118 |
+
value: 0.7559
|
| 119 |
+
name: Macro-F1 (COMMA, PERIOD, QUESTION), mean over languages
|
| 120 |
+
- type: f1
|
| 121 |
+
value: 0.6557
|
| 122 |
+
name: Comma F1
|
| 123 |
+
- type: f1
|
| 124 |
+
value: 0.7923
|
| 125 |
+
name: Period F1
|
| 126 |
+
- type: f1
|
| 127 |
+
value: 0.8197
|
| 128 |
+
name: Question F1
|
| 129 |
+
- task:
|
| 130 |
+
type: token-classification
|
| 131 |
+
name: Punctuation restoration
|
| 132 |
+
dataset:
|
| 133 |
+
name: WIT3 TED, same 90 talks (57 language tracks)
|
| 134 |
+
type: wit3
|
| 135 |
+
metrics:
|
| 136 |
+
- type: f1
|
| 137 |
+
value: 0.7303
|
| 138 |
+
name: Macro-F1 (COMMA, PERIOD, QUESTION), mean over languages
|
| 139 |
+
- type: f1
|
| 140 |
+
value: 0.6482
|
| 141 |
+
name: Comma F1
|
| 142 |
+
- type: f1
|
| 143 |
+
value: 0.7794
|
| 144 |
+
name: Period F1
|
| 145 |
+
- type: f1
|
| 146 |
+
value: 0.7632
|
| 147 |
+
name: Question F1
|
| 148 |
+
- task:
|
| 149 |
+
type: token-classification
|
| 150 |
+
name: Punctuation restoration
|
| 151 |
+
dataset:
|
| 152 |
+
name: IWSLT TED through TTS and Whisper ASR (33 languages)
|
| 153 |
+
type: iwslt-ted-asr
|
| 154 |
+
metrics:
|
| 155 |
+
- type: f1
|
| 156 |
+
value: 0.6332
|
| 157 |
+
name: Macro-F1 (COMMA, PERIOD, QUESTION), mean over languages
|
| 158 |
+
- type: f1
|
| 159 |
+
value: 0.5656
|
| 160 |
+
name: Comma F1
|
| 161 |
+
- type: f1
|
| 162 |
+
value: 0.7102
|
| 163 |
+
name: Period F1
|
| 164 |
+
- type: f1
|
| 165 |
+
value: 0.6238
|
| 166 |
+
name: Question F1
|
| 167 |
+
---
|
| 168 |
+
|
| 169 |
+
# About
|
| 170 |
+
|
| 171 |
+
This model was quantized to BF16 from valkayuh/dewpoint. Refer to the [original model card](https://huggingface.co/valkayuh/dewpoint) for more details on the model.
|
| 172 |
+
|
| 173 |
+
# Original Model Card
|
| 174 |
+
|
| 175 |
+

|
| 176 |
+
|
| 177 |
+
# Dewpoint
|
| 178 |
+
|
| 179 |
+
**Punctuation and truecasing for speech-recognition output, in 79 languages.** Speech
|
| 180 |
+
recognisers hand back a stream of lowercase words with no punctuation, and Dewpoint
|
| 181 |
+
restores it: it adds the commas, full stops and question marks, and the capitals.
|
| 182 |
+
Open weights, MIT licence, free for everyone.
|
| 183 |
+
|
| 184 |
+
```text
|
| 185 |
+
so i said meet at three thirty tuesday what do you think the iphone was announced by steve jobs in san francisco
|
| 186 |
+
→ So I said meet at three thirty Tuesday. What do you think? The iPhone was announced by Steve Jobs in San Francisco.
|
| 187 |
+
|
| 188 |
+
ich weiß nicht ob wir das schaffen aber wir sollten es versuchen was meinst du
|
| 189 |
+
→ Ich weiß nicht, ob wir das schaffen, aber wir sollten es versuchen. Was meinst du?
|
| 190 |
+
|
| 191 |
+
πού είναι ο σταθμός ξέρεις να μου πεις
|
| 192 |
+
→ Πού είναι ο σταθμός; Ξέρεις να μου πεις;
|
| 193 |
+
```
|
| 194 |
+
|
| 195 |
+
These are unedited outputs of this checkpoint. Dewpoint is a **tagger, not a
|
| 196 |
+
generator**. For each word it decides which mark follows (none, `,` `.` `?` `!`) and how
|
| 197 |
+
the word is cased (lower, Capitalised, UPPER). It never adds, drops, reorders or rewrites
|
| 198 |
+
a word, so it cannot invent content.
|
| 199 |
+
|
| 200 |
+
## Results
|
| 201 |
+
|
| 202 |
+
On the IWSLT TED benchmark, Dewpoint scores higher than every other multilingual
|
| 203 |
+
punctuation model we tested, under all three test conditions. On the two text test
|
| 204 |
+
sets it is behind no other model in any language: **0 of 9** on the official
|
| 205 |
+
set and **0 of 57** on the extended one. Under ASR noise it trails in
|
| 206 |
+
**7 of 33**, 5 of them to raw Whisper.
|
| 207 |
+
|
| 208 |
+

|
| 209 |
+
|
| 210 |
+
| system | params | official, 9 lang | WIT3, 57 lang | ASR, 33 lang |
|
| 211 |
+
|---|---|---|---|---|
|
| 212 |
+
| **Dewpoint** | 869M | **0.7559** | **0.7303** | **0.6332** |
|
| 213 |
+
| Dewpoint, mmBERT-base half alone | 307M | 0.7392 | 0.7098 | 0.6197 |
|
| 214 |
+
| Dewpoint, XLM-R-large half alone | 561M | 0.7443 | 0.7241 | 0.6208 |
|
| 215 |
+
| [oliverguhr/fullstop-punctuation-multilang-large](https://huggingface.co/oliverguhr/fullstop-punctuation-multilang-large) | 559M | 0.6438 | 0.5984 | 0.5301 |
|
| 216 |
+
| [kredor/punctuate-all](https://huggingface.co/kredor/punctuate-all) | 278M | 0.6344 | 0.5950 | 0.5333 |
|
| 217 |
+
| [oliverguhr/fullstop-punctuation-multilingual-sonar-base](https://huggingface.co/oliverguhr/fullstop-punctuation-multilingual-sonar-base) | 278M | 0.6381 | 0.5653 | 0.5205 |
|
| 218 |
+
| [1-800-BAD-CODE/xlm-roberta_punctuation_fullstop_truecase](https://huggingface.co/1-800-BAD-CODE/xlm-roberta_punctuation_fullstop_truecase) | 278M | 0.6204 | 0.5710 | 0.5346 |
|
| 219 |
+
| raw [openai/whisper-large-v3-turbo](https://huggingface.co/openai/whisper-large-v3-turbo) punctuation | 809M | — | — | 0.4436 |
|
| 220 |
+
|
| 221 |
+
Macro-F1 over COMMA, PERIOD and QUESTION, averaged over languages. Every system runs
|
| 222 |
+
through the same harness and gets the same dev-set threshold calibration.
|
| 223 |
+
|
| 224 |
+
**Head to head on each model's own languages.** The averages above include languages the
|
| 225 |
+
other models were never trained on, which pulls their means down. Scored only on the
|
| 226 |
+
languages each model lists on its own model card, the margins are smaller:
|
| 227 |
+
|
| 228 |
+
| other model | languages it supports | official | WIT3 | ASR |
|
| 229 |
+
|---|---|---|---|---|
|
| 230 |
+
| [oliverguhr/fullstop-punctuation-multilang-large](https://huggingface.co/oliverguhr/fullstop-punctuation-multilang-large) | 4 | 0.755 vs **0.791** (4) | 0.758 vs **0.793** (5) | 0.741 vs **0.772** (4) |
|
| 231 |
+
| [oliverguhr/fullstop-punctuation-multilingual-sonar-base](https://huggingface.co/oliverguhr/fullstop-punctuation-multilingual-sonar-base) | 5 | 0.734 vs **0.786** (5) | 0.752 vs **0.797** (6) | 0.702 vs **0.753** (5) |
|
| 232 |
+
| [kredor/punctuate-all](https://huggingface.co/kredor/punctuate-all) | 12 | 0.726 vs **0.786** (5) | 0.733 vs **0.797** (14) | 0.695 vs **0.755** (9) |
|
| 233 |
+
| [1-800-BAD-CODE/xlm-roberta_punctuation_fullstop_truecase](https://huggingface.co/1-800-BAD-CODE/xlm-roberta_punctuation_fullstop_truecase) | 47 | 0.620 vs **0.756** (9) | 0.598 vs **0.744** (37) | 0.540 vs **0.636** (27) |
|
| 234 |
+
|
| 235 |
+
Dewpoint is ahead in every one of these languages except Bengali on Whisper ASR output against xlm-r truecase.
|
| 236 |
+
|
| 237 |
+
Each cell is that model's mean macro-F1 vs Dewpoint's on the same languages, with the
|
| 238 |
+
number of languages in brackets. This is the fairer comparison.
|
| 239 |
+
|
| 240 |
+
---
|
| 241 |
+
|
| 242 |
+
## Quick start
|
| 243 |
+
|
| 244 |
+
```bash
|
| 245 |
+
pip install torch transformers safetensors huggingface_hub numpy # GPU or CPU
|
| 246 |
+
# or, with no torch at all:
|
| 247 |
+
pip install onnxruntime tokenizers huggingface_hub numpy
|
| 248 |
+
```
|
| 249 |
+
|
| 250 |
+
```python
|
| 251 |
+
import os, sys
|
| 252 |
+
from huggingface_hub import hf_hub_download
|
| 253 |
+
sys.path.insert(0, os.path.dirname(hf_hub_download("valkayuh/dewpoint", "dewpoint.py")))
|
| 254 |
+
from dewpoint import Punctuator
|
| 255 |
+
|
| 256 |
+
p = Punctuator.from_pretrained("valkayuh/dewpoint") # torch if installed, else ONNX
|
| 257 |
+
p.restore("what time is it in tokyo right now", lang="en")
|
| 258 |
+
# 'What time is it in Tokyo right now?'
|
| 259 |
+
```
|
| 260 |
+
|
| 261 |
+
`from_pretrained` downloads only what the chosen backend needs. The torch backend
|
| 262 |
+
fetches the safetensors (3.5 GB, or 1.2 GB with `members=["mmbert-base"]`), the ONNX
|
| 263 |
+
backend fetches the ONNX graphs, and neither fetches the other.
|
| 264 |
+
|
| 265 |
+
**Always pass the language.** It selects the calibrated decision thresholds, turns
|
| 266 |
+
casing off for scripts that have none, and picks the right marks to write: `؟` for
|
| 267 |
+
Arabic, `;` for the Greek question mark, `。` and `,` for Chinese, `।` for Hindi.
|
| 268 |
+
|
| 269 |
+
**Single-model mode.** Pass `members=["mmbert-base"]` to download and run only the
|
| 270 |
+
307M-parameter mmBERT-base half. It uses its own calibration, runs in about half the
|
| 271 |
+
GPU time, and still beats every other model we tested on every benchmark:
|
| 272 |
+
|
| 273 |
+
```python
|
| 274 |
+
p = Punctuator.from_pretrained("valkayuh/dewpoint", members=["mmbert-base"])
|
| 275 |
+
```
|
| 276 |
+
|
| 277 |
+
**Streaming.** For live captions, each word is released only once it has right-hand
|
| 278 |
+
context and a stable label, so marks don't flicker:
|
| 279 |
+
|
| 280 |
+
```python
|
| 281 |
+
s = p.stream(lang="en", lag=6)
|
| 282 |
+
for chunk in asr_chunks: # partial transcripts as they arrive
|
| 283 |
+
print(s.push(chunk), end="", flush=True)
|
| 284 |
+
print(s.finish())
|
| 285 |
+
```
|
| 286 |
+
|
| 287 |
+
**Labels and probabilities.** `p.predict(words, lang)` returns per-word labels plus
|
| 288 |
+
the averaged posteriors (`punct_probs`, `case_probs`), in case you want confidence
|
| 289 |
+
scores or your own decision rule.
|
| 290 |
+
|
| 291 |
+
**Command line.**
|
| 292 |
+
|
| 293 |
+
```bash
|
| 294 |
+
echo "i called the office this morning but nobody answered did you get my message" | python dewpoint.py --lang en
|
| 295 |
+
# I called the office this morning, but nobody answered. Did you get my message?
|
| 296 |
+
```
|
| 297 |
+
|
| 298 |
+
## Formats and integration
|
| 299 |
+
|
| 300 |
+
| you want | use |
|
| 301 |
+
|---|---|
|
| 302 |
+
| Python with a GPU | `Punctuator(..., backend="torch")`: bf16 on CUDA, fp32 on CPU |
|
| 303 |
+
| Python without torch, e.g. a slim CPU server | `Punctuator(..., backend="onnx")`: needs only `onnxruntime`, `tokenizers`, `numpy` |
|
| 304 |
+
| a local HTTP API | `python serve.py` gives `POST /restore {"text", "lang"}` (standard library only) |
|
| 305 |
+
| a hosted API | deploy this repo as a [Hugging Face Inference Endpoint](https://huggingface.co/inference-endpoints); `handler.py` serves `{"inputs", "parameters": {"lang"}}` |
|
| 306 |
+
| the shell | `python dewpoint.py --lang de "…"` or pipe text on stdin |
|
| 307 |
+
| C#, Java, JavaScript, C++, mobile | the ONNX graphs in `onnx/`, with the pre- and post-processing below |
|
| 308 |
+
|
| 309 |
+
**ONNX graphs.** There is one per member, in `onnx/mmbert-base/` and
|
| 310 |
+
`onnx/xlm-roberta-large/` (the latter keeps its weights in `model.onnx.data` beside
|
| 311 |
+
the graph). Both are fp32, opset 17, with dynamic batch and sequence axes:
|
| 312 |
+
|
| 313 |
+
- inputs: `input_ids`, `attention_mask`, int64 `[batch, seq]`, from that member's
|
| 314 |
+
`tokenizer.json` with the words passed pre-tokenised
|
| 315 |
+
- outputs: `punct_logits` `[batch, seq, 5]` and `case_logits` `[batch, seq, 3]`
|
| 316 |
+
|
| 317 |
+
Around the graph, `dewpoint.py` does four things, and a port needs the same four:
|
| 318 |
+
1. split the input into overlapping windows of at most 508 subwords, keeping each
|
| 319 |
+
window's centre (`plan_windows`)
|
| 320 |
+
2. read each word's label from its first subword
|
| 321 |
+
3. average the two members' softmax posteriors
|
| 322 |
+
4. add the calibrated per-class bias from `ensemble_config.json` for the language, then
|
| 323 |
+
take the argmax
|
| 324 |
+
|
| 325 |
+
**Verified.** Run over the whole official test set on CPU, the ONNX backend scores **0.7558** macro-F1 against the published 0.7559; the largest difference in any scored language is 0.0011, the gap between fp32 ONNX and the bf16 GPU run the published numbers come from. Its tokenisation matches transformers exactly on 323 documents in 57 languages.
|
| 326 |
+
|
| 327 |
+
### Speed
|
| 328 |
+
|
| 329 |
+
Measured on a 584-word TED passage, after warm-up, with the code in this repo:
|
| 330 |
+
|
| 331 |
+
| hardware | Dewpoint (869M) | mmBERT half (307M) |
|
| 332 |
+
|---|---|---|
|
| 333 |
+
| RTX 5080, torch bf16 | 45 ms (13k words/s) | 23 ms (25k words/s) |
|
| 334 |
+
| Ryzen 7 5800X3D, torch fp32 | 2.18 s | 632 ms (1k words/s) |
|
| 335 |
+
| Ryzen 7 5800X3D, ONNX Runtime fp32 | 2.13 s | 606 ms (1k words/s) |
|
| 336 |
+
|
| 337 |
+
Input of any length is handled with overlapping 512-token windows. Only the centre of
|
| 338 |
+
each window commits its predictions, because words near a window edge lack context and
|
| 339 |
+
score measurably worse.
|
| 340 |
+
|
| 341 |
+
---
|
| 342 |
+
|
| 343 |
+
## Model
|
| 344 |
+
|
| 345 |
+
Dewpoint is an **ensemble of two dual-head token classifiers**. Both members read the
|
| 346 |
+
same word list and produce one posterior per word, and those posteriors are averaged.
|
| 347 |
+
|
| 348 |
+
| member | encoder | architecture | parameters | of which embeddings |
|
| 349 |
+
|---|---|---|---|---|
|
| 350 |
+
| `mmbert-base/` | [jhu-clsp/mmBERT-base](https://huggingface.co/jhu-clsp/mmBERT-base) | ModernBERT | 307.5M | 196.6M |
|
| 351 |
+
| `xlm-roberta-large/` | [FacebookAI/xlm-roberta-large](https://huggingface.co/FacebookAI/xlm-roberta-large) | XLM-RoBERTa | 561.0M | 256.5M |
|
| 352 |
+
|
| 353 |
+
**Each member** is an encoder, then a residual shared trunk (Linear, GELU, LayerNorm),
|
| 354 |
+
then two linear heads:
|
| 355 |
+
|
| 356 |
+
- **punctuation**, 5 classes: `O` `COMMA` `PERIOD` `QUESTION` `EXCLAM`
|
| 357 |
+
- **case**, 3 classes: `LOWER` (*meeting*), `CAP` for a capitalised first letter
|
| 358 |
+
(*Tuesday*, *Tokyo*, a sentence's first word), and `UPPER` for a word written
|
| 359 |
+
entirely in capitals (*NASA*, *BBC*, *DNA*). Mixed-case words such as *iPhone* are
|
| 360 |
+
handled by a word list, described below.
|
| 361 |
+
|
| 362 |
+
There is one decision per word, not per subword: the label sits on each word's first
|
| 363 |
+
subword. Punctuation and case are learned together because they are correlated: a
|
| 364 |
+
sentence end means a capital comes next.
|
| 365 |
+
|
| 366 |
+
**Combining the members.** Both members label the same words, so their outputs line up
|
| 367 |
+
without any tokenizer reconciliation. The average is taken in **probability space**, not
|
| 368 |
+
log space. With a geometric mean, one confident but wrong member could veto a class;
|
| 369 |
+
the arithmetic mean degrades gracefully instead.
|
| 370 |
+
|
| 371 |
+
**Calibration.** Class weighting stops the model predicting "no punctuation"
|
| 372 |
+
everywhere, but it buys recall at the cost of precision, so a plain argmax is not the
|
| 373 |
+
F1-optimal rule. Instead, a per-class additive bias on the log-posterior is fitted on
|
| 374 |
+
IWSLT dev2010, which is held out of every test table. Whether to add per-language
|
| 375 |
+
biases on top, and how far to trust them, is chosen by cross-validation *inside*
|
| 376 |
+
dev2010: its talks are split in half five times, and each strategy is fitted on one
|
| 377 |
+
half and scored on the other. An unshrunk bias per language, fitted on eight talks,
|
| 378 |
+
overfits: it was never the winning strategy, and for Dewpoint it scored worst. Dewpoint
|
| 379 |
+
uses per-language biases pulled three quarters of the way back to the pooled one. The ensemble is
|
| 380 |
+
calibrated as a system in its own right, and every other model in the tables went
|
| 381 |
+
through the identical procedure, each choosing its own strategy.
|
| 382 |
+
|
| 383 |
+
**Post-processing**, in `restore()` only (`predict()` returns the raw decision):
|
| 384 |
+
- a capital after every sentence-final mark
|
| 385 |
+
- a gazetteer of 4,896 mixed-case forms such as *iPhone*, *McDonald* and *GHz*
|
| 386 |
+
- a closing mark if the text would otherwise end on none
|
| 387 |
+
|
| 388 |
+
---
|
| 389 |
+
|
| 390 |
+
## Evaluation
|
| 391 |
+
|
| 392 |
+
- **Three test conditions:**
|
| 393 |
+
- the IWSLT 2017 official tst2010–2015 sets, with 9 scorable languages
|
| 394 |
+
- the WIT3 releases of the *same 90 talks* in 57 language tracks, so only the
|
| 395 |
+
language changes
|
| 396 |
+
- an ASR condition: TED text spoken by TTS, transcribed by Whisper
|
| 397 |
+
large-v3-turbo, and aligned back to the reference, in 33 languages
|
| 398 |
+
- **Metric.** Macro-F1 over COMMA, PERIOD and QUESTION, averaged over languages.
|
| 399 |
+
EXCLAM is folded into PERIOD, because no other model emits it. Accuracy is never
|
| 400 |
+
reported: about 87% of words carry no mark, so a model that predicts nothing is
|
| 401 |
+
"87% accurate".
|
| 402 |
+
- **Exclusions.** Japanese and Thai are excluded, and named here. Their TED references
|
| 403 |
+
carry almost no punctuation (0.38% and 0.12% of tokens).
|
| 404 |
+
- **Reference audit.** Two errors in the reference's label maps were fixed for every
|
| 405 |
+
system alike. Armenian writes its full stop as an ASCII colon; fixing that doubled
|
| 406 |
+
*every* system's Armenian score. And the Burmese comma U+104A had been mapped to a
|
| 407 |
+
full stop.
|
| 408 |
+
- **No TED data was used for training.** TED is the test set.
|
| 409 |
+
|
| 410 |
+
### Every language on the extended set
|
| 411 |
+
|
| 412 |
+

|
| 413 |
+
|
| 414 |
+
### Under ASR noise
|
| 415 |
+
|
| 416 |
+

|
| 417 |
+
|
| 418 |
+
5 of the 7 are to raw Whisper (English, Spanish, French, Italian, Portuguese). Whisper punctuates from the audio itself, so it can use pauses and intonation that a text-only tagger never sees. The rest: Bengali (−0.044 to fullstop-sonar), Albanian (−0.025 to xlm-r truecase). Languages marked * in the chart have under 3,000 scored words, so their gaps are noisy.
|
| 419 |
+
|
| 420 |
+
### Per class, and truecasing
|
| 421 |
+
|
| 422 |
+

|
| 423 |
+
|
| 424 |
+
Mean per-class F1 against the strongest other model:
|
| 425 |
+
|
| 426 |
+
| test set | system | COMMA | PERIOD | QUESTION |
|
| 427 |
+
|---|---|---|---|---|
|
| 428 |
+
| IWSLT 2017 official (9 languages) | Dewpoint | **0.6557** | **0.7923** | **0.8197** |
|
| 429 |
+
| IWSLT 2017 official (9 languages) | fullstop-large | 0.5334 | 0.7206 | 0.6772 |
|
| 430 |
+
| WIT3, same 90 talks (57 language tracks) | Dewpoint | **0.6482** | **0.7794** | **0.7632** |
|
| 431 |
+
| WIT3, same 90 talks (57 language tracks) | fullstop-large | 0.5067 | 0.6988 | 0.5896 |
|
| 432 |
+
| Whisper ASR output (33 languages) | Dewpoint | **0.5656** | **0.7102** | **0.6238** |
|
| 433 |
+
| Whisper ASR output (33 languages) | fullstop-large | 0.4489 | 0.6556 | 0.4858 |
|
| 434 |
+
|
| 435 |
+
Truecasing F1 against the only other model that restores case, on cased scripts:
|
| 436 |
+
|
| 437 |
+
| test set | cased languages | Dewpoint CAP | UPPER | xlm-r truecase CAP | UPPER | languages behind |
|
| 438 |
+
|---|---|---|---|---|---|---|
|
| 439 |
+
| IWSLT 2017 official (9 languages) | 6 | **0.8772** | **0.8748** | 0.7708 | 0.7888 | 0 |
|
| 440 |
+
| WIT3, same 90 talks (57 language tracks) | 43 | **0.8533** | **0.8599** | 0.7173 | 0.8005 | 0 |
|
| 441 |
+
| Whisper ASR output (33 languages) | 25 | **0.8007** | **0.7584** | 0.7136 | 0.7083 | 0 |
|
| 442 |
+
|
| 443 |
+
**Reading the truecasing table.** `CAP` scores words whose first letter should be a
|
| 444 |
+
capital: names, places, and the first word of every sentence. `UPPER` scores words
|
| 445 |
+
written entirely in capitals, which in practice means acronyms and initialisms such as
|
| 446 |
+
*NASA*, *BBC* or *DNA*. These are much rarer, so they get their own score and are not
|
| 447 |
+
swamped by ordinary capitals. A single capital letter such as *I* counts as `CAP`.
|
| 448 |
+
|
| 449 |
+
The truecasing comparison covers cased scripts only. On caseless scripts (Arabic,
|
| 450 |
+
Chinese, Korean, Hindi and others) the case head is masked by design, so Latin loanwords
|
| 451 |
+
inside them are never capitalised. `xlm-roberta_punctuation_fullstop_truecase` does
|
| 452 |
+
capitalise them, so it wins on those words by construction. The native EXCLAM class
|
| 453 |
+
scores 0.11 F1 on the official set and 0.07 on WIT3. No other
|
| 454 |
+
model emits it at all.
|
| 455 |
+
|
| 456 |
+
### Size
|
| 457 |
+
|
| 458 |
+

|
| 459 |
+
|
| 460 |
+
### Where the gain comes from
|
| 461 |
+
|
| 462 |
+
Every other model in these tables is an XLM-RoBERTa model, so a win could come from the
|
| 463 |
+
architecture or from the training recipe. To separate the two, we trained
|
| 464 |
+
**XLM-R-large, the encoder `fullstop-punctuation-multilang-large` uses, on this corpus
|
| 465 |
+
with this recipe**.
|
| 466 |
+
|
| 467 |
+

|
| 468 |
+
|
| 469 |
+
The recipe is worth **+0.08 to +0.13** macro-F1. Swapping the encoder is worth −0.004
|
| 470 |
+
to +0.016. The corpus, the orthography fixes and the decision rule matter roughly an
|
| 471 |
+
order of magnitude more than the encoder. Neither encoder dominated the other, and
|
| 472 |
+
averaging exploits exactly that: members that do not make the same errors.
|
| 473 |
+
|
| 474 |
+
### Per-language tables
|
| 475 |
+
|
| 476 |
+
<details>
|
| 477 |
+
<summary>IWSLT 2017 official, 9 languages</summary>
|
| 478 |
+
|
| 479 |
+
| language | Dewpoint | mmBERT half alone | best other model | which | gap |
|
| 480 |
+
|---|---|---|---|---|---|
|
| 481 |
+
| Arabic (`ar`) | 0.6538 | 0.6229 | 0.5586 | fullstop-large | +0.095 |
|
| 482 |
+
| Chinese (`zh`) | 0.6551 | 0.6546 | 0.5502 | xlm-r truecase | +0.105 |
|
| 483 |
+
| Dutch (`nl`) | 0.7686 | 0.7587 | 0.7304 | fullstop-sonar | +0.038 |
|
| 484 |
+
| English (`en`) | 0.8033 | 0.7935 | 0.7430 | fullstop-large | +0.060 |
|
| 485 |
+
| French (`fr`) | 0.7956 | 0.7841 | 0.7581 | fullstop-large | +0.038 |
|
| 486 |
+
| German (`de`) | 0.8496 | 0.8402 | 0.8338 | fullstop-large | +0.016 |
|
| 487 |
+
| Italian (`it`) | 0.7145 | 0.6886 | 0.6838 | fullstop-large | +0.031 |
|
| 488 |
+
| Korean (`ko`) | 0.8010 | 0.7825 | 0.6553 | xlm-r truecase | +0.146 |
|
| 489 |
+
| Romanian (`ro`) | 0.7618 | 0.7274 | 0.6860 | fullstop-large | +0.076 |
|
| 490 |
+
|
| 491 |
+
</details>
|
| 492 |
+
|
| 493 |
+
<details>
|
| 494 |
+
<summary>WIT3 extended set, 57 language tracks</summary>
|
| 495 |
+
|
| 496 |
+
| language | Dewpoint | mmBERT half alone | best other model | which | gap |
|
| 497 |
+
|---|---|---|---|---|---|
|
| 498 |
+
| Albanian (`sq`) | 0.7152 | 0.6828 | 0.6036 | fullstop-large | +0.112 |
|
| 499 |
+
| Arabic (`ar`) | 0.6538 | 0.6225 | 0.5585 | fullstop-large | +0.095 |
|
| 500 |
+
| Armenian (`hy`) | 0.7902 | 0.7709 | 0.4924 | kredor | +0.298 |
|
| 501 |
+
| Azerbaijani (`az`) | 0.7286 | 0.7105 | 0.5832 | fullstop-sonar | +0.145 |
|
| 502 |
+
| Bengali (`bn`) | 0.6940 | 0.6497 | 0.5584 | fullstop-large | +0.136 |
|
| 503 |
+
| Bosnian (`bs`) | 0.7753 | 0.7540 | 0.6324 | fullstop-large | +0.143 |
|
| 504 |
+
| Bulgarian (`bg`) | 0.8089 | 0.7906 | 0.7491 | kredor | +0.060 |
|
| 505 |
+
| Burmese (`my`) | 0.4427 | 0.3569 | 0.3553 | xlm-r truecase | +0.087 |
|
| 506 |
+
| Catalan (`ca`) | 0.7189 | 0.6880 | 0.6339 | fullstop-large | +0.085 |
|
| 507 |
+
| Chinese (`zh`) | 0.6543 | 0.6349 | 0.4509 | xlm-r truecase | +0.203 |
|
| 508 |
+
| Chinese (Simplified) (`zh-cn`) | 0.6486 | 0.6483 | 0.5439 | xlm-r truecase | +0.105 |
|
| 509 |
+
| Chinese (Traditional) (`zh-tw`) | 0.5941 | 0.5960 | 0.5136 | xlm-r truecase | +0.081 |
|
| 510 |
+
| Croatian (`hr`) | 0.7712 | 0.7540 | 0.6942 | fullstop-large | +0.077 |
|
| 511 |
+
| Czech (`cs`) | 0.8120 | 0.7930 | 0.7461 | kredor | +0.066 |
|
| 512 |
+
| Danish (`da`) | 0.7661 | 0.7498 | 0.6975 | fullstop-large | +0.069 |
|
| 513 |
+
| Dutch (`nl`) | 0.8171 | 0.8060 | 0.7819 | fullstop-sonar | +0.035 |
|
| 514 |
+
| English (`en`) | 0.8036 | 0.7937 | 0.7431 | fullstop-large | +0.061 |
|
| 515 |
+
| Esperanto (`eo`) | 0.7663 | 0.7297 | 0.6385 | fullstop-sonar | +0.128 |
|
| 516 |
+
| Estonian (`et`) | 0.8146 | 0.7884 | 0.7178 | fullstop-large | +0.097 |
|
| 517 |
+
| Finnish (`fi`) | 0.8207 | 0.7996 | 0.7283 | kredor | +0.092 |
|
| 518 |
+
| French (`fr`) | 0.7961 | 0.7842 | 0.7588 | fullstop-large | +0.037 |
|
| 519 |
+
| French (Canada) (`fr-ca`) | 0.7778 | 0.7604 | 0.7445 | fullstop-sonar | +0.033 |
|
| 520 |
+
| Georgian (`ka`) | 0.7142 | 0.6583 | 0.6179 | fullstop-large | +0.096 |
|
| 521 |
+
| German (`de`) | 0.8472 | 0.8392 | 0.8302 | fullstop-large | +0.017 |
|
| 522 |
+
| Greek (`el`) | 0.7331 | 0.7071 | 0.6951 | fullstop-large | +0.038 |
|
| 523 |
+
| Hebrew (`he`) | 0.7398 | 0.7030 | 0.6506 | fullstop-large | +0.089 |
|
| 524 |
+
| Hindi (`hi`) | 0.5855 | 0.5600 | 0.4925 | fullstop-large | +0.093 |
|
| 525 |
+
| Hungarian (`hu`) | 0.7903 | 0.7637 | 0.6771 | fullstop-large | +0.113 |
|
| 526 |
+
| Indonesian (`id`) | 0.7350 | 0.7174 | 0.6401 | fullstop-large | +0.095 |
|
| 527 |
+
| Italian (`it`) | 0.7412 | 0.7211 | 0.7148 | fullstop-large | +0.026 |
|
| 528 |
+
| Kazakh (`kk`) | 0.7657 | 0.7556 | 0.6508 | xlm-r truecase | +0.115 |
|
| 529 |
+
| Korean (`ko`) | 0.8008 | 0.7825 | 0.6564 | xlm-r truecase | +0.144 |
|
| 530 |
+
| Kurdish (`ku`) | 0.1114 | 0.2843 | 0.0298 | xlm-r truecase | +0.082 |
|
| 531 |
+
| Latvian (`lv`) | 0.8174 | 0.7928 | 0.7117 | kredor | +0.106 |
|
| 532 |
+
| Lithuanian (`lt`) | 0.7935 | 0.7725 | 0.6945 | kredor | +0.099 |
|
| 533 |
+
| Macedonian (`mk`) | 0.7479 | 0.7190 | 0.6378 | fullstop-large | +0.110 |
|
| 534 |
+
| Malay (`ms`) | 0.7748 | 0.7426 | 0.6387 | fullstop-large | +0.136 |
|
| 535 |
+
| Marathi (`mr`) | 0.6624 | 0.6120 | 0.5224 | fullstop-large | +0.140 |
|
| 536 |
+
| Mongolian (`mn`) | 0.7962 | 0.7447 | 0.6349 | xlm-r truecase | +0.161 |
|
| 537 |
+
| Norwegian (`nb`) | 0.7704 | 0.7531 | 0.7399 | fullstop-large | +0.031 |
|
| 538 |
+
| Persian (`fa`) | 0.6895 | 0.6700 | 0.5903 | fullstop-large | +0.099 |
|
| 539 |
+
| Polish (`pl`) | 0.8181 | 0.7999 | 0.7696 | kredor | +0.049 |
|
| 540 |
+
| Portuguese (`pt`) | 0.7725 | 0.7555 | 0.7095 | kredor | +0.063 |
|
| 541 |
+
| Portuguese (Brazil) (`pt-br`) | 0.7550 | 0.7354 | 0.6701 | kredor | +0.085 |
|
| 542 |
+
| Romanian (`ro`) | 0.7623 | 0.7320 | 0.6975 | fullstop-large | +0.065 |
|
| 543 |
+
| Russian (`ru`) | 0.8354 | 0.8218 | 0.7312 | kredor | +0.104 |
|
| 544 |
+
| Serbian (`sr`) | 0.7783 | 0.7588 | 0.6984 | fullstop-large | +0.080 |
|
| 545 |
+
| Slovak (`sk`) | 0.8050 | 0.7787 | 0.7440 | kredor | +0.061 |
|
| 546 |
+
| Slovenian (`sl`) | 0.8386 | 0.8211 | 0.8100 | kredor | +0.029 |
|
| 547 |
+
| Spanish (`es`) | 0.7582 | 0.7463 | 0.7070 | fullstop-large | +0.051 |
|
| 548 |
+
| Swahili (`sw`) | 0.6403 | 0.5944 | 0.5180 | fullstop-sonar | +0.122 |
|
| 549 |
+
| Swedish (`sv`) | 0.7822 | 0.7611 | 0.7432 | fullstop-large | +0.039 |
|
| 550 |
+
| Tamil (`ta`) | 0.5091 | 0.4862 | 0.4771 | xlm-r truecase | +0.032 |
|
| 551 |
+
| Turkish (`tr`) | 0.7290 | 0.7082 | 0.5883 | xlm-r truecase | +0.141 |
|
| 552 |
+
| Ukrainian (`uk`) | 0.8149 | 0.7951 | 0.7241 | kredor | +0.091 |
|
| 553 |
+
| Urdu (`ur`) | 0.5309 | 0.5186 | 0.5042 | fullstop-large | +0.027 |
|
| 554 |
+
| Vietnamese (`vi`) | 0.7105 | 0.6830 | 0.5800 | fullstop-sonar | +0.131 |
|
| 555 |
+
|
| 556 |
+
</details>
|
| 557 |
+
|
| 558 |
+
<details>
|
| 559 |
+
<summary>ASR condition, 33 languages</summary>
|
| 560 |
+
|
| 561 |
+
| language | Dewpoint | mmBERT half alone | best other model | which | gap |
|
| 562 |
+
|---|---|---|---|---|---|
|
| 563 |
+
| Albanian (`sq`) | 0.3580 | 0.4005 | 0.3830 | xlm-r truecase | −0.025 |
|
| 564 |
+
| Arabic (`ar`) | 0.5309 | 0.5020 | 0.4840 | fullstop-large | +0.047 |
|
| 565 |
+
| Bengali (`bn`) | 0.0730 | 0.1404 | 0.1170 | fullstop-sonar | −0.044 |
|
| 566 |
+
| Bulgarian (`bg`) | 0.7247 | 0.6928 | 0.6776 | kredor | +0.047 |
|
| 567 |
+
| Catalan (`ca`) | 0.7112 | 0.6698 | 0.6025 | fullstop-sonar | +0.109 |
|
| 568 |
+
| Chinese (`zh`) | 0.7528 | 0.7620 | 0.7312 | xlm-r truecase | +0.022 |
|
| 569 |
+
| Dutch (`nl`) | 0.6765 | 0.6661 | 0.6115 | kredor | +0.065 |
|
| 570 |
+
| English (`en`) | 0.7837 | 0.7869 | 0.9382 | raw Whisper | −0.155 |
|
| 571 |
+
| Finnish (`fi`) | 0.7233 | 0.7053 | 0.6593 | kredor | +0.064 |
|
| 572 |
+
| French (`fr`) | 0.7702 | 0.7705 | 0.8240 | raw Whisper | −0.054 |
|
| 573 |
+
| German (`de`) | 0.8329 | 0.8204 | 0.8140 | fullstop-large | +0.019 |
|
| 574 |
+
| Greek (`el`) | 0.6437 | 0.5979 | 0.5943 | kredor | +0.049 |
|
| 575 |
+
| Hebrew (`he`) | 0.6332 | 0.5870 | 0.5265 | fullstop-sonar | +0.107 |
|
| 576 |
+
| Hindi (`hi`) | 0.6238 | 0.5805 | 0.5509 | fullstop-sonar | +0.073 |
|
| 577 |
+
| Hungarian (`hu`) | 0.7210 | 0.7041 | 0.6368 | fullstop-large | +0.084 |
|
| 578 |
+
| Indonesian (`id`) | 0.6723 | 0.6582 | 0.6249 | fullstop-large | +0.047 |
|
| 579 |
+
| Italian (`it`) | 0.7025 | 0.6750 | 0.7745 | raw Whisper | −0.072 |
|
| 580 |
+
| Kazakh (`kk`) | 0.4953 | 0.4933 | 0.3990 | raw Whisper | +0.096 |
|
| 581 |
+
| Latvian (`lv`) | 0.7218 | 0.7039 | 0.6108 | kredor | +0.111 |
|
| 582 |
+
| Malay (`ms`) | 0.6743 | 0.6516 | 0.5907 | fullstop-large | +0.084 |
|
| 583 |
+
| Marathi (`mr`) | 0.2584 | 0.2685 | 0.2000 | xlm-r truecase | +0.058 |
|
| 584 |
+
| Persian (`fa`) | 0.5209 | 0.4990 | 0.4512 | xlm-r truecase | +0.070 |
|
| 585 |
+
| Polish (`pl`) | 0.7608 | 0.7462 | 0.7409 | kredor | +0.020 |
|
| 586 |
+
| Portuguese (`pt`) | 0.7616 | 0.7535 | 0.8522 | raw Whisper | −0.091 |
|
| 587 |
+
| Romanian (`ro`) | 0.6462 | 0.6148 | 0.5553 | xlm-r truecase | +0.091 |
|
| 588 |
+
| Russian (`ru`) | 0.7627 | 0.7647 | 0.6927 | kredor | +0.070 |
|
| 589 |
+
| Spanish (`es`) | 0.7866 | 0.7844 | 0.8386 | raw Whisper | −0.052 |
|
| 590 |
+
| Swahili (`sw`) | 0.4165 | 0.3931 | 0.3596 | kredor | +0.057 |
|
| 591 |
+
| Swedish (`sv`) | 0.6646 | 0.6514 | 0.6435 | fullstop-large | +0.021 |
|
| 592 |
+
| Tamil (`ta`) | 0.4052 | 0.3825 | 0.2485 | fullstop-sonar | +0.157 |
|
| 593 |
+
| Turkish (`tr`) | 0.6670 | 0.6613 | 0.5448 | xlm-r truecase | +0.122 |
|
| 594 |
+
| Ukrainian (`uk`) | 0.7310 | 0.6955 | 0.6545 | xlm-r truecase | +0.076 |
|
| 595 |
+
| Vietnamese (`vi`) | 0.6895 | 0.6684 | 0.5873 | xlm-r truecase | +0.102 |
|
| 596 |
+
|
| 597 |
+
</details>
|
| 598 |
+
|
| 599 |
+
All per-language, per-class scores for every system are in
|
| 600 |
+
[`results/scores.json`](results/scores.json).
|
| 601 |
+
|
| 602 |
+
---
|
| 603 |
+
|
| 604 |
+
## Training
|
| 605 |
+
|
| 606 |
+
### Text: 79 languages, no TED
|
| 607 |
+
|
| 608 |
+
| source | licence | role |
|
| 609 |
+
|---|---|---|
|
| 610 |
+
| [Wikipedia](https://huggingface.co/datasets/wikimedia/wikipedia) | CC-BY-SA-4.0 | volume, and most of the proper nouns truecasing needs |
|
| 611 |
+
| [Europarl](https://huggingface.co/datasets/Helsinki-NLP/europarl) | no known restrictions | spoken-style parliamentary prose |
|
| 612 |
+
| [Tatoeba](https://tatoeba.org/en/downloads) | CC-BY-2.0 FR | questions: 10–25% of its sentences are questions |
|
| 613 |
+
| [HPLT 2.0 cleaned](https://huggingface.co/datasets/HPLT/HPLT2.0_cleaned) | CC0-1.0 | fills six languages Wikipedia leaves thin |
|
| 614 |
+
| [VoxPopuli transcripts](https://github.com/facebookresearch/voxpopuli) | CC0-1.0 | verbatim spoken transcripts, 16 languages |
|
| 615 |
+
| [Aya dataset](https://huggingface.co/datasets/CohereLabs/aya_dataset) | Apache-2.0 | question-dense human prose for low-resource languages |
|
| 616 |
+
|
| 617 |
+
The corpus is temperature-sampled so that the largest languages don't swamp the rest.
|
| 618 |
+
Labels come free: strip the punctuation and case from clean prose, and what was
|
| 619 |
+
stripped is exactly what the model must predict. **Synthetic ASR noise** is applied to
|
| 620 |
+
about two thirds of training examples: filler words, dropped short words, character
|
| 621 |
+
substitutions, merged and split words, numbers written the way they're spoken, and
|
| 622 |
+
truncated utterances.
|
| 623 |
+
|
| 624 |
+
### Speech: round-trip ASR data
|
| 625 |
+
|
| 626 |
+
A slice of the training text was spoken aloud by TTS, transcribed by Whisper
|
| 627 |
+
large-v3-turbo, and aligned back to the original. That gives inputs with genuine
|
| 628 |
+
recognition errors but ground-truth labels. The corpus has 0.96M unique words across 23 languages, voiced by three permissively licensed engines. Oversampled to match the per-language dose of the original recipe, it makes up 6.8% of the final fine-tuning stage.
|
| 629 |
+
|
| 630 |
+
| engine | licence | languages (utterances kept) |
|
| 631 |
+
|---|---|---|
|
| 632 |
+
| [Kokoro-82M](https://huggingface.co/hexgrad/Kokoro-82M) | Apache-2.0 | en (5,998), es (5,077), fr (3,996), hi (3,879), it (3,996), pt (3,996), zh (2,992) |
|
| 633 |
+
| [Piper voices](https://huggingface.co/rhasspy/piper-voices) | CC0 or CC-BY-4.0, per voice | bg (2,180), de (2,198), el (2,124), fa (1,057), fi (1,196), hu (1,184), kk (244), lv (1,067), nl (2,168), pl (2,189), ro (1,194), ru (1,177), sq (662), sv (2,183), uk (1,150) |
|
| 634 |
+
| [Chatterbox Multilingual](https://huggingface.co/ResembleAI/chatterbox) | MIT | ar (606) |
|
| 635 |
+
|
| 636 |
+
Every Piper voice's licence was read from its model card by code, and any voice whose
|
| 637 |
+
card said non-commercial, share-alike, "see URL" or nothing at all was refused.
|
| 638 |
+
The voice that produced a language's ASR *test* audio is never used for its training
|
| 639 |
+
audio. French is the exception, because Kokoro has only one French voice. No audio was
|
| 640 |
+
kept or is distributed.
|
| 641 |
+
|
| 642 |
+
### Procedure
|
| 643 |
+
|
| 644 |
+
Each member was trained in stages. Every stage starts from the weights the previous
|
| 645 |
+
stage ended with, and each later stage makes smaller, more targeted changes:
|
| 646 |
+
|
| 647 |
+
1. **Learn the task.** The encoder starts from its public pretrained weights, knowing
|
| 648 |
+
language but nothing about punctuation, and learns to predict marks and capitals on
|
| 649 |
+
a large clean-text corpus.
|
| 650 |
+
2. **Rebalance toward speech.** Training continues on a resample of the corpus that
|
| 651 |
+
gives more weight to spoken-style sources (parliamentary transcripts, Tatoeba, Aya)
|
| 652 |
+
and mixes short passages in with long ones, closer to what ASR produces.
|
| 653 |
+
3. **Harden against recognition errors.** A short final stage on a 55M-word mix that
|
| 654 |
+
includes the round-trip ASR data above, at a low learning rate, so the model learns
|
| 655 |
+
to punctuate text containing Whisper's mistakes without forgetting the rest.
|
| 656 |
+
|
| 657 |
+
| member | stage | starts from | data | steps | peak learning rate |
|
| 658 |
+
|---|---|---|---|---|---|
|
| 659 |
+
| mmBERT-base | learn the task | `jhu-clsp/mmBERT-base` | 1.02B-word corpus, long passages | 34,000 | 4e-5 |
|
| 660 |
+
| mmBERT-base | rebalance | previous stage | 735M-word resample | 13,000 | 1.8e-5 |
|
| 661 |
+
| mmBERT-base | rebalance, continued | previous stage | 735M-word resample | 3,500 | 8e-6 |
|
| 662 |
+
| mmBERT-base | harden | previous stage | 55M-word mix with round-trip data | 4,000 | 9e-6 |
|
| 663 |
+
| XLM-R-large | learn + rebalance | `FacebookAI/xlm-roberta-large` | 735M-word resample | 17,000 | 8e-6 |
|
| 664 |
+
| XLM-R-large | harden | previous stage | 55M-word mix with round-trip data | 4,000 | 3e-6 |
|
| 665 |
+
|
| 666 |
+
How to read the table:
|
| 667 |
+
- **Steps.** A step is one update of the weights, computed from a batch of 64 text
|
| 668 |
+
windows of up to 512 subword tokens each, roughly 30,000 tokens. The first mmBERT
|
| 669 |
+
stage, for example, saw about a billion tokens.
|
| 670 |
+
- **Peak learning rate.** This is the largest step size in that stage's schedule. It
|
| 671 |
+
falls from stage to stage because later stages refine rather than relearn.
|
| 672 |
+
- **XLM-R-large has fewer stages** because it went straight to the rebalanced
|
| 673 |
+
corpus. It was first trained as a controlled comparison against mmBERT-base, and
|
| 674 |
+
later became the second member.
|
| 675 |
+
|
| 676 |
+
Settings shared by all stages:
|
| 677 |
+
- AdamW, with a learning rate that warms up and then decays along a cosine curve
|
| 678 |
+
- a higher learning rate for the two heads than for the encoder
|
| 679 |
+
- class-weighted cross-entropy on both heads, so the rare marks are not drowned out by
|
| 680 |
+
the many words that take none
|
| 681 |
+
- label smoothing 0.02
|
| 682 |
+
- for XLM-R-large only, a frozen embedding matrix
|
| 683 |
+
|
| 684 |
+
Everything was trained on a single RTX 5080 with 16 GB.
|
| 685 |
+
|
| 686 |
+
### What did not work
|
| 687 |
+
|
| 688 |
+
| attempt | result |
|
| 689 |
+
|---|---|
|
| 690 |
+
| supervised contrastive loss (reported +1.9 to +3.2 F1 on this task) | +0.001 / +0.001 / −0.002; the rarest class got worse |
|
| 691 |
+
| joint punctuation–case decoding | cut head contradictions from 9.0% to 0.27% but lowered F1 at every setting |
|
| 692 |
+
| mDeBERTa-v3-base as a third encoder | lost on all three sets |
|
| 693 |
+
| distilling the ensemble into one mmBERT-base | in-domain validation +0.015, every held-out set flat |
|
| 694 |
+
| choosing case *given* the decided punctuation | lowered case F1 at every coupling strength |
|
| 695 |
+
|
| 696 |
+
The distillation result is why Dewpoint is an ensemble. Its robustness comes from the
|
| 697 |
+
two members disagreeing on each input. A single model trained to mimic their average
|
| 698 |
+
has no disagreement left to exploit. Two of these experiments were pre-registered:
|
| 699 |
+
their success criteria were written down before the run.
|
| 700 |
+
|
| 701 |
+
---
|
| 702 |
+
|
| 703 |
+
## Limitations
|
| 704 |
+
|
| 705 |
+
- **Two forward passes.** The ensemble has 2.8× the parameters of its mmBERT half, for +0.013 to +0.020 macro-F1. If latency matters more than that margin, use `members=["mmbert-base"]`.
|
| 706 |
+
- **Text only.** Raw Whisper still wins in English, Spanish, French, Italian, Portuguese under ASR conditions, most likely because it hears pauses and intonation. Adding a tagger after ASR only helps if your ASR doesn't already punctuate.
|
| 707 |
+
- **English is not state of the art against English-only systems.** On English tst2011, Dewpoint scores 0.800 macro-F1 (COMMA 0.715, PERIOD 0.823, QUESTION 0.860) without having seen any TED text. The strongest published English-only systems report about 0.85 overall F1 on IWSLT2011; they were trained in-domain on TED and scored on a differently preprocessed version of the set.
|
| 708 |
+
- **Weak languages.** Kurdish (0.11), Burmese (0.44), Tamil (0.51), Urdu (0.53) are still low in absolute terms, even where Dewpoint leads the other models. In Kurdish (0.11 against 0.28) the ensemble is below its own mmBERT half, so averaging in probability space does not help everywhere.
|
| 709 |
+
- **Japanese and Thai are unmeasured.** Their TED references are nearly unpunctuated, so no trustworthy number exists. The model is trained on both.
|
| 710 |
+
- **Very short inputs.** With only a few words there is little context, and the case head can guess a sentence boundary that the punctuation head didn't place (`hello how are you` → `Hello, How are You?`). On full passages it is rare: 0.4% of words on the official test set. Pass whole utterances, not fragments.
|
| 711 |
+
- **Caseless-script loanwords** are never capitalised (see truecasing above).
|
| 712 |
+
- **EXCLAM is weak**: 0.11 F1 on the official set and 0.07 on WIT3, and exclamation marks are rare there too.
|
| 713 |
+
- **Only five marks.** Colons, semicolons, dashes, quotes and parentheses are not restored, and French typographic spacing before `?` and `!` is not added.
|
| 714 |
+
- **Domain.** The benchmark is TED, which is prepared monologue. Conversational, overlapping, multi-speaker speech is harder, and isn't measured here.
|
| 715 |
+
|
| 716 |
+
---
|
| 717 |
+
|
| 718 |
+
## Licence and provenance
|
| 719 |
+
|
| 720 |
+
**Weights and code: MIT.** Both base encoders are MIT-licensed. Every TTS voice used for
|
| 721 |
+
training audio is MIT, Apache-2.0, CC0 or CC-BY-4.0.
|
| 722 |
+
|
| 723 |
+
A few things are worth knowing before commercial use:
|
| 724 |
+
|
| 725 |
+
- **Wikipedia is CC-BY-SA-4.0.** Whether share-alike terms reach model weights trained
|
| 726 |
+
on such text is legally unsettled. Many encoders trained on Wikipedia, BERT among
|
| 727 |
+
them, are released under permissive licences, and Dewpoint follows that practice.
|
| 728 |
+
- **Tatoeba is CC-BY-2.0 FR.** Sentences © Tatoeba contributors, https://tatoeba.org.
|
| 729 |
+
- **Some Piper voices are CC-BY-4.0**, which asks for attribution to the voice
|
| 730 |
+
datasets. They are listed in [`results/rt2_voices.json`](results/rt2_voices.json).
|
| 731 |
+
- **Evaluation data.** IWSLT/WIT3 TED transcripts are CC-BY-NC-ND-4.0. They were used
|
| 732 |
+
for measurement only and are not included here.
|
| 733 |
+
|
| 734 |
+
This is a description, not legal advice.
|
| 735 |
+
|
| 736 |
+
---
|
| 737 |
+
|
| 738 |
+
## Files
|
| 739 |
+
|
| 740 |
+
| path | contents |
|
| 741 |
+
|---|---|
|
| 742 |
+
| `dewpoint.py` | self-contained inference: `Punctuator` (torch or ONNX backend), streaming, command line |
|
| 743 |
+
| `onnx/` | both members as fp32 ONNX graphs |
|
| 744 |
+
| `serve.py` | a standard-library HTTP server |
|
| 745 |
+
| `handler.py` | the Hugging Face Inference Endpoints handler |
|
| 746 |
+
| `requirements.txt`, `requirements-onnx.txt` | dependencies for each backend |
|
| 747 |
+
| `ensemble_config.json` | member list, weights, the ensemble's calibrated biases |
|
| 748 |
+
| `mmbert-base/` | weights (safetensors), encoder config, tokenizer, its own calibration |
|
| 749 |
+
| `xlm-roberta-large/` | the same, for the XLM-R-large member |
|
| 750 |
+
| `gazetteer.json` | mixed-case surface forms, per language |
|
| 751 |
+
| `results/` | per-language scores for every system, and round-trip voice provenance |
|
| 752 |
+
| `assets/` | the images on this page |
|
| 753 |
+
|
| 754 |
+
The inference file reproduces the published official-set score exactly, per language,
|
| 755 |
+
against the evaluation harness it was measured with.
|
| 756 |
+
|
| 757 |
+
## Citation
|
| 758 |
+
|
| 759 |
+
```bibtex
|
| 760 |
+
@misc{dewpoint2026,
|
| 761 |
+
title = {Dewpoint: multilingual punctuation restoration and truecasing for speech},
|
| 762 |
+
author = {valkayuh},
|
| 763 |
+
year = {2026},
|
| 764 |
+
url = {https://huggingface.co/valkayuh/dewpoint}
|
| 765 |
+
}
|
| 766 |
+
```
|
| 767 |
+
|
| 768 |
+
Please also cite the encoders Dewpoint builds on:
|
| 769 |
+
[mmBERT](https://huggingface.co/jhu-clsp/mmBERT-base) and
|
| 770 |
+
[XLM-RoBERTa](https://arxiv.org/abs/1911.02116).
|
assets/asr_condition_gap.jpg
ADDED
|
Git LFS Details
|
assets/banner.jpg
ADDED
|
Git LFS Details
|
assets/benchmarks.jpg
ADDED
|
Git LFS Details
|
assets/method_vs_architecture.jpg
ADDED
|
Git LFS Details
|
assets/per_class.jpg
ADDED
|
Git LFS Details
|
assets/per_language_wit3.jpg
ADDED
|
Git LFS Details
|
assets/size_vs_score.jpg
ADDED
|
Git LFS Details
|
dewpoint.py
ADDED
|
@@ -0,0 +1,596 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Dewpoint: multilingual punctuation restoration and truecasing.
|
| 2 |
+
|
| 3 |
+
Self-contained inference for the released checkpoints, in one file, with two
|
| 4 |
+
interchangeable backends:
|
| 5 |
+
|
| 6 |
+
from dewpoint import Punctuator
|
| 7 |
+
p = Punctuator.from_pretrained("valkayuh/dewpoint")
|
| 8 |
+
p.restore("so i said meet at three thirty tuesday what do you think", lang="en")
|
| 9 |
+
# -> 'So I said meet at three thirty Tuesday. What do you think?'
|
| 10 |
+
|
| 11 |
+
backend="torch" needs torch + transformers + safetensors, and uses a GPU if present.
|
| 12 |
+
backend="onnx" needs only onnxruntime + tokenizers + numpy (no torch at all),
|
| 13 |
+
and downloads the ONNX graphs instead of the safetensors.
|
| 14 |
+
backend="auto" (the default) uses torch when it is installed, otherwise ONNX.
|
| 15 |
+
|
| 16 |
+
The model is an ensemble of two dual-head token taggers (mmBERT-base and
|
| 17 |
+
XLM-RoBERTa-large). Each member reads the same word list and returns one
|
| 18 |
+
posterior per word; the posteriors are averaged in probability space, a
|
| 19 |
+
per-class decision bias fitted on IWSLT dev2010 is applied, and the argmax is
|
| 20 |
+
taken. Pass ``members=["mmbert-base"]`` for the single 307M-parameter model,
|
| 21 |
+
which uses its own calibration.
|
| 22 |
+
|
| 23 |
+
It is a tagger, not a generator: it never adds, drops, reorders or rewrites a
|
| 24 |
+
word. It only decides, per word, which mark follows it (none / , / . / ? / !)
|
| 25 |
+
and how it is cased (lower / Capitalised / UPPER).
|
| 26 |
+
"""
|
| 27 |
+
from __future__ import annotations
|
| 28 |
+
|
| 29 |
+
import json
|
| 30 |
+
import os
|
| 31 |
+
import re
|
| 32 |
+
import unicodedata
|
| 33 |
+
from typing import Iterable, Sequence
|
| 34 |
+
|
| 35 |
+
import numpy as np
|
| 36 |
+
|
| 37 |
+
try: # torch is optional: the ONNX backend needs none
|
| 38 |
+
import torch
|
| 39 |
+
import torch.nn as nn
|
| 40 |
+
except ImportError: # pragma: no cover
|
| 41 |
+
torch = None
|
| 42 |
+
nn = None
|
| 43 |
+
|
| 44 |
+
__all__ = ["Punctuator", "StreamingPunctuator"]
|
| 45 |
+
|
| 46 |
+
PUNCT_LABELS = ["O", "COMMA", "PERIOD", "QUESTION", "EXCLAM"]
|
| 47 |
+
CASE_LABELS = ["LOWER", "CAP", "UPPER"]
|
| 48 |
+
_SENT_END = {"PERIOD", "QUESTION", "EXCLAM"}
|
| 49 |
+
|
| 50 |
+
# ------------------------------------------------------------ language rules
|
| 51 |
+
# Scripts with no case distinction: the case head is masked to LOWER.
|
| 52 |
+
CASELESS_LANGS = {
|
| 53 |
+
"zh", "zh-cn", "zh-tw", "ja", "ko", "ar", "he", "fa", "ur", "th", "hi", "bn",
|
| 54 |
+
"ta", "te", "ml", "kn", "mr", "gu", "pa", "ne", "si", "my", "km", "lo", "am",
|
| 55 |
+
"ti", "dv", "bo", "dz", "ps", "sd", "ug", "yi", "yue", "wuu", "as", "or",
|
| 56 |
+
"arq", "arz", "ary", "acm", "apc", "ckb", "ka", "he-il", "sa", "ks", "bho",
|
| 57 |
+
}
|
| 58 |
+
# Written without spaces between words: one grapheme cluster is one token.
|
| 59 |
+
NO_SPACE_LANGS = {"zh", "zh-cn", "zh-tw", "ja", "th", "my", "km", "lo", "bo",
|
| 60 |
+
"dz", "yue", "wuu"}
|
| 61 |
+
|
| 62 |
+
_SURFACE = {
|
| 63 |
+
"ascii": {"O": "", "COMMA": ",", "PERIOD": ".", "QUESTION": "?", "EXCLAM": "!"},
|
| 64 |
+
"cjk": {"O": "", "COMMA": ",", "PERIOD": "。", "QUESTION": "?", "EXCLAM": "!"},
|
| 65 |
+
"ja": {"O": "", "COMMA": "、", "PERIOD": "。", "QUESTION": "?", "EXCLAM": "!"},
|
| 66 |
+
"arab": {"O": "", "COMMA": "،", "PERIOD": ".", "QUESTION": "؟", "EXCLAM": "!"},
|
| 67 |
+
"deva": {"O": "", "COMMA": ",", "PERIOD": "।", "QUESTION": "?", "EXCLAM": "!"},
|
| 68 |
+
# Greek writes its question mark with the ASCII semicolon.
|
| 69 |
+
"el": {"O": "", "COMMA": ",", "PERIOD": ".", "QUESTION": ";", "EXCLAM": "!"},
|
| 70 |
+
"hy": {"O": "", "COMMA": ",", "PERIOD": "։", "QUESTION": "՞", "EXCLAM": "՜"},
|
| 71 |
+
}
|
| 72 |
+
_SURFACE_BY_LANG = {}
|
| 73 |
+
for _l in ("zh", "zh-cn", "zh-tw", "yue", "wuu"):
|
| 74 |
+
_SURFACE_BY_LANG[_l] = _SURFACE["cjk"]
|
| 75 |
+
for _l in ("ar", "fa", "ur", "ps", "arq", "arz", "ary", "ckb", "sd", "ug"):
|
| 76 |
+
_SURFACE_BY_LANG[_l] = _SURFACE["arab"]
|
| 77 |
+
for _l in ("hi", "bn", "mr", "ne", "pa", "as", "or", "sa", "bho"):
|
| 78 |
+
_SURFACE_BY_LANG[_l] = _SURFACE["deva"]
|
| 79 |
+
_SURFACE_BY_LANG.update(el=_SURFACE["el"], hy=_SURFACE["hy"], ja=_SURFACE["ja"])
|
| 80 |
+
|
| 81 |
+
# Characters stripped from the edges of an input token before tagging, so text
|
| 82 |
+
# that already carries some punctuation is handled the same as bare ASR output.
|
| 83 |
+
_MARKS = set(
|
| 84 |
+
".。.۔।॥။։។…⋯።,,،٫၊、᠂᠈፣??؟;՞⸮፧!!՜::;؛·-–—―−፡"
|
| 85 |
+
"\"'‘’“”„‚«»()[]{}〈〉《》「」『』()‹›‟″′¡¿⸘‛*_`•⁃・"'´‐‑"
|
| 86 |
+
)
|
| 87 |
+
_LATIN_RUN = re.compile(r"[A-Za-z0-9À-ɏ]+(?:[.'’-][A-Za-z0-9]+)*")
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def base_lang(lang: str) -> str:
|
| 91 |
+
return lang.split("-")[0].lower()
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def is_caseless(lang: str) -> bool:
|
| 95 |
+
return lang.lower() in CASELESS_LANGS or base_lang(lang) in CASELESS_LANGS
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
def is_no_space(lang: str) -> bool:
|
| 99 |
+
return lang.lower() in NO_SPACE_LANGS or base_lang(lang) in NO_SPACE_LANGS
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def surface_for(lang: str) -> dict:
|
| 103 |
+
return _SURFACE_BY_LANG.get(base_lang(lang), _SURFACE["ascii"])
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def apply_case(word: str, case: str) -> str:
|
| 107 |
+
if case == "UPPER":
|
| 108 |
+
return word.upper()
|
| 109 |
+
if case == "CAP":
|
| 110 |
+
return word[:1].upper() + word[1:]
|
| 111 |
+
return word
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
def split_words(text: str, lang: str) -> list[str]:
|
| 115 |
+
"""Raw text -> the lowercased, mark-free word list the model was trained on."""
|
| 116 |
+
text = unicodedata.normalize("NFC", text)
|
| 117 |
+
if is_no_space(lang):
|
| 118 |
+
return _split_charwise(text)
|
| 119 |
+
out = []
|
| 120 |
+
for raw in text.split():
|
| 121 |
+
chars = [i for i, ch in enumerate(raw) if ch not in _MARKS]
|
| 122 |
+
if not chars:
|
| 123 |
+
continue
|
| 124 |
+
core = raw[chars[0]:chars[-1] + 1]
|
| 125 |
+
if any(ch.isalnum() for ch in core):
|
| 126 |
+
out.append(core.lower())
|
| 127 |
+
return out
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def _split_charwise(text: str) -> list[str]:
|
| 131 |
+
"""CJK / Thai / Khmer: grapheme clusters, with Latin runs kept whole."""
|
| 132 |
+
words, i, n = [], 0, len(text)
|
| 133 |
+
while i < n:
|
| 134 |
+
ch = text[i]
|
| 135 |
+
if ch.isspace() or ch in _MARKS:
|
| 136 |
+
i += 1
|
| 137 |
+
continue
|
| 138 |
+
m = _LATIN_RUN.match(text, i)
|
| 139 |
+
if m and m.group():
|
| 140 |
+
words.append(m.group().lower())
|
| 141 |
+
i = m.end()
|
| 142 |
+
continue
|
| 143 |
+
if ch.isalnum() or unicodedata.category(ch) in ("Mn", "Mc", "Me"):
|
| 144 |
+
j = i + 1
|
| 145 |
+
while j < n and unicodedata.category(text[j]) in ("Mn", "Mc", "Me"):
|
| 146 |
+
j += 1
|
| 147 |
+
words.append(text[i:j].lower())
|
| 148 |
+
i = j
|
| 149 |
+
continue
|
| 150 |
+
i += 1
|
| 151 |
+
return words
|
| 152 |
+
|
| 153 |
+
|
| 154 |
+
# ------------------------------------------------------------------- model
|
| 155 |
+
if torch is not None:
|
| 156 |
+
class PunctCaseModel(nn.Module):
|
| 157 |
+
"""Encoder -> residual shared trunk -> punctuation head and case head."""
|
| 158 |
+
|
| 159 |
+
def __init__(self, config, n_punct=5, n_case=3):
|
| 160 |
+
super().__init__()
|
| 161 |
+
from transformers import AutoModel
|
| 162 |
+
self.encoder = AutoModel.from_config(config)
|
| 163 |
+
h = config.hidden_size
|
| 164 |
+
self.drop = nn.Dropout(0.1)
|
| 165 |
+
self.trunk = nn.Sequential(nn.Linear(h, h), nn.GELU(),
|
| 166 |
+
nn.LayerNorm(h, eps=1e-5))
|
| 167 |
+
self.head_punct = nn.Linear(h, n_punct)
|
| 168 |
+
self.head_case = nn.Linear(h, n_case)
|
| 169 |
+
|
| 170 |
+
def forward(self, input_ids, attention_mask=None):
|
| 171 |
+
x = self.encoder(input_ids=input_ids,
|
| 172 |
+
attention_mask=attention_mask).last_hidden_state
|
| 173 |
+
x = self.drop(x)
|
| 174 |
+
x = x + self.trunk(x)
|
| 175 |
+
return self.head_punct(x), self.head_case(x)
|
| 176 |
+
|
| 177 |
+
__all__.append("PunctCaseModel")
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
def plan_windows(sub_counts, max_sub=508, overlap_sub=256):
|
| 181 |
+
"""Overlapping windows under a subword budget; only each window's centre
|
| 182 |
+
commits. Returns (win_start, win_end, commit_start, commit_end) in words."""
|
| 183 |
+
n = len(sub_counts)
|
| 184 |
+
if n == 0:
|
| 185 |
+
return []
|
| 186 |
+
overlap_sub = min(overlap_sub, max_sub // 2)
|
| 187 |
+
prefix = [0] * (n + 1)
|
| 188 |
+
for i, c in enumerate(sub_counts):
|
| 189 |
+
prefix[i + 1] = prefix[i] + max(1, c)
|
| 190 |
+
|
| 191 |
+
def end_for(start):
|
| 192 |
+
lo, hi = start + 1, n
|
| 193 |
+
budget = prefix[start] + max_sub
|
| 194 |
+
while lo < hi:
|
| 195 |
+
mid = (lo + hi + 1) // 2
|
| 196 |
+
if prefix[mid] <= budget:
|
| 197 |
+
lo = mid
|
| 198 |
+
else:
|
| 199 |
+
hi = mid - 1
|
| 200 |
+
return max(lo, start + 1)
|
| 201 |
+
|
| 202 |
+
wins, start = [], 0
|
| 203 |
+
while True:
|
| 204 |
+
end = end_for(start)
|
| 205 |
+
wins.append([start, end])
|
| 206 |
+
if end >= n:
|
| 207 |
+
break
|
| 208 |
+
target = prefix[end] - overlap_sub
|
| 209 |
+
nxt = end
|
| 210 |
+
while nxt > start + 1 and prefix[nxt] > target:
|
| 211 |
+
nxt -= 1
|
| 212 |
+
start = max(nxt, start + 1)
|
| 213 |
+
out = []
|
| 214 |
+
for i, (s, e) in enumerate(wins):
|
| 215 |
+
cs = s if i == 0 else (wins[i - 1][1] + s) // 2
|
| 216 |
+
ce = e if i == len(wins) - 1 else (e + wins[i + 1][0] + 1) // 2
|
| 217 |
+
cs = max(cs, s)
|
| 218 |
+
out.append((s, e, cs, min(max(ce, cs), e)))
|
| 219 |
+
return out
|
| 220 |
+
|
| 221 |
+
|
| 222 |
+
class _MemberBase:
|
| 223 |
+
"""Windowing, first-subword gathering and caseless masking, shared by both
|
| 224 |
+
backends so that they cannot drift apart. Subclasses supply `_counts`
|
| 225 |
+
(subwords per word), `_encode` (a padded batch plus word ids) and
|
| 226 |
+
`_forward` (softmaxed posteriors as numpy)."""
|
| 227 |
+
|
| 228 |
+
def _load_meta(self, path):
|
| 229 |
+
with open(os.path.join(path, "vpunct_config.json"), encoding="utf-8") as f:
|
| 230 |
+
self.meta = json.load(f)
|
| 231 |
+
self.max_len = int(self.meta.get("max_len", 512))
|
| 232 |
+
|
| 233 |
+
def posteriors(self, words, lang):
|
| 234 |
+
n = len(words)
|
| 235 |
+
pp = np.zeros((n, len(PUNCT_LABELS)), dtype=np.float32)
|
| 236 |
+
pp[:, 0] = 1.0
|
| 237 |
+
cp = np.zeros((n, len(CASE_LABELS)), dtype=np.float32)
|
| 238 |
+
cp[:, 0] = 1.0
|
| 239 |
+
if n == 0:
|
| 240 |
+
return pp, cp
|
| 241 |
+
wins = plan_windows(self._counts(list(words)), max_sub=self.max_len - 4,
|
| 242 |
+
overlap_sub=256)
|
| 243 |
+
for i in range(0, len(wins), self.batch_size):
|
| 244 |
+
chunk = wins[i:i + self.batch_size]
|
| 245 |
+
ids, mask, wids = self._encode([list(words[s:e]) for s, e, _, _ in chunk])
|
| 246 |
+
p, c = self._forward(ids, mask)
|
| 247 |
+
for b, (s, e, cs, ce) in enumerate(chunk):
|
| 248 |
+
loc_p = np.zeros((e - s, len(PUNCT_LABELS)), dtype=np.float32)
|
| 249 |
+
loc_p[:, 0] = 1.0
|
| 250 |
+
loc_c = np.zeros((e - s, len(CASE_LABELS)), dtype=np.float32)
|
| 251 |
+
loc_c[:, 0] = 1.0
|
| 252 |
+
seen = set()
|
| 253 |
+
for pos, w in enumerate(wids[b]):
|
| 254 |
+
if w is None or w in seen or pos >= p.shape[1]:
|
| 255 |
+
continue
|
| 256 |
+
seen.add(w) # label lives on a word's first subword
|
| 257 |
+
loc_p[w] = p[b, pos]
|
| 258 |
+
loc_c[w] = c[b, pos]
|
| 259 |
+
pp[cs:ce] = loc_p[cs - s:ce - s]
|
| 260 |
+
cp[cs:ce] = loc_c[cs - s:ce - s]
|
| 261 |
+
if is_caseless(lang):
|
| 262 |
+
cp[:] = 0.0
|
| 263 |
+
cp[:, 0] = 1.0
|
| 264 |
+
return pp, cp
|
| 265 |
+
|
| 266 |
+
|
| 267 |
+
def _softmax(x):
|
| 268 |
+
x = x - x.max(-1, keepdims=True)
|
| 269 |
+
e = np.exp(x)
|
| 270 |
+
return e / e.sum(-1, keepdims=True)
|
| 271 |
+
|
| 272 |
+
|
| 273 |
+
class _TorchMember(_MemberBase):
|
| 274 |
+
"""One tagger run by torch, from its safetensors."""
|
| 275 |
+
|
| 276 |
+
def __init__(self, path, device, dtype, batch_size=64):
|
| 277 |
+
from safetensors.torch import load_file
|
| 278 |
+
from transformers import AutoConfig, AutoTokenizer
|
| 279 |
+
self._load_meta(path)
|
| 280 |
+
cfg = AutoConfig.from_pretrained(path)
|
| 281 |
+
model = PunctCaseModel(cfg, self.meta["n_punct"], self.meta["n_case"])
|
| 282 |
+
model.load_state_dict(load_file(os.path.join(path, "model.safetensors")))
|
| 283 |
+
self.model = model.to(device=device, dtype=dtype).eval()
|
| 284 |
+
try:
|
| 285 |
+
self.model.encoder.config._attn_implementation = "sdpa"
|
| 286 |
+
except Exception:
|
| 287 |
+
pass
|
| 288 |
+
self.tok = AutoTokenizer.from_pretrained(path)
|
| 289 |
+
self.device = device
|
| 290 |
+
self.batch_size = batch_size
|
| 291 |
+
|
| 292 |
+
def _counts(self, words):
|
| 293 |
+
return [len(e) for e in self.tok(words, add_special_tokens=False)["input_ids"]]
|
| 294 |
+
|
| 295 |
+
def _encode(self, batch):
|
| 296 |
+
enc = self.tok(batch, is_split_into_words=True, truncation=True,
|
| 297 |
+
max_length=self.max_len, padding=True, return_tensors="pt")
|
| 298 |
+
return enc["input_ids"], enc["attention_mask"], \
|
| 299 |
+
[enc.word_ids(b) for b in range(len(batch))]
|
| 300 |
+
|
| 301 |
+
def _forward(self, ids, mask):
|
| 302 |
+
with torch.no_grad():
|
| 303 |
+
lp, lc = self.model(ids.to(self.device), mask.to(self.device))
|
| 304 |
+
return (lp.float().softmax(-1).cpu().numpy(),
|
| 305 |
+
lc.float().softmax(-1).cpu().numpy())
|
| 306 |
+
|
| 307 |
+
|
| 308 |
+
class _OnnxMember(_MemberBase):
|
| 309 |
+
"""One tagger run by onnxruntime, tokenised by the `tokenizers` library.
|
| 310 |
+
No torch and no transformers anywhere on this path."""
|
| 311 |
+
|
| 312 |
+
def __init__(self, path, onnx_dir, providers=None, batch_size=8, threads=None):
|
| 313 |
+
import onnxruntime as ort
|
| 314 |
+
from tokenizers import Tokenizer
|
| 315 |
+
self._load_meta(path)
|
| 316 |
+
self.tok = Tokenizer.from_file(os.path.join(path, "tokenizer.json"))
|
| 317 |
+
self.tok.no_padding()
|
| 318 |
+
# truncate the way transformers does: before the special tokens are
|
| 319 |
+
# added, so an over-long window still ends in its closing token
|
| 320 |
+
self.tok.enable_truncation(max_length=self.max_len)
|
| 321 |
+
with open(os.path.join(path, "tokenizer_config.json"), encoding="utf-8") as f:
|
| 322 |
+
tcfg = json.load(f)
|
| 323 |
+
self.pad_id = self.tok.token_to_id(tcfg.get("pad_token", "<pad>")) or 0
|
| 324 |
+
so = ort.SessionOptions()
|
| 325 |
+
if threads:
|
| 326 |
+
so.intra_op_num_threads = threads
|
| 327 |
+
if providers is None:
|
| 328 |
+
avail = ort.get_available_providers()
|
| 329 |
+
providers = [p for p in ("CUDAExecutionProvider", "CoreMLExecutionProvider",
|
| 330 |
+
"DmlExecutionProvider") if p in avail]
|
| 331 |
+
providers.append("CPUExecutionProvider")
|
| 332 |
+
self.sess = ort.InferenceSession(os.path.join(onnx_dir, "model.onnx"), so,
|
| 333 |
+
providers=providers)
|
| 334 |
+
self.batch_size = batch_size
|
| 335 |
+
|
| 336 |
+
def _counts(self, words):
|
| 337 |
+
return [len(e.ids) for e in
|
| 338 |
+
self.tok.encode_batch(words, add_special_tokens=False)]
|
| 339 |
+
|
| 340 |
+
def _encode(self, batch):
|
| 341 |
+
encs = self.tok.encode_batch(batch, is_pretokenized=True)
|
| 342 |
+
L = min(self.max_len, max(len(e.ids) for e in encs))
|
| 343 |
+
ids = np.full((len(encs), L), self.pad_id, dtype=np.int64)
|
| 344 |
+
mask = np.zeros((len(encs), L), dtype=np.int64)
|
| 345 |
+
wids = []
|
| 346 |
+
for b, e in enumerate(encs):
|
| 347 |
+
n = min(L, len(e.ids))
|
| 348 |
+
ids[b, :n] = e.ids[:n]
|
| 349 |
+
mask[b, :n] = 1
|
| 350 |
+
wids.append(list(e.word_ids[:n]))
|
| 351 |
+
return ids, mask, wids
|
| 352 |
+
|
| 353 |
+
def _forward(self, ids, mask):
|
| 354 |
+
lp, lc = self.sess.run(None, {"input_ids": ids, "attention_mask": mask})
|
| 355 |
+
return _softmax(lp.astype(np.float32)), _softmax(lc.astype(np.float32))
|
| 356 |
+
|
| 357 |
+
|
| 358 |
+
# -------------------------------------------------------------- public api
|
| 359 |
+
class Punctuator:
|
| 360 |
+
"""Restore punctuation and case in unpunctuated, lowercased text."""
|
| 361 |
+
|
| 362 |
+
MEMBERS = ("mmbert-base", "xlm-roberta-large")
|
| 363 |
+
|
| 364 |
+
def __init__(self, path, members=None, backend="auto", device=None, dtype=None,
|
| 365 |
+
use_gazetteer=True, batch_size=None, providers=None, threads=None):
|
| 366 |
+
members = list(members or self.MEMBERS)
|
| 367 |
+
self.backend = self._pick_backend(path, members, backend)
|
| 368 |
+
if self.backend == "torch":
|
| 369 |
+
if device is None:
|
| 370 |
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 371 |
+
if dtype is None:
|
| 372 |
+
dtype = torch.bfloat16 if str(device).startswith("cuda") else torch.float32
|
| 373 |
+
self.members = [_TorchMember(os.path.join(path, m), device, dtype,
|
| 374 |
+
batch_size or 64) for m in members]
|
| 375 |
+
else:
|
| 376 |
+
self.members = [_OnnxMember(os.path.join(path, m),
|
| 377 |
+
os.path.join(path, "onnx", m), providers,
|
| 378 |
+
batch_size or 8, threads) for m in members]
|
| 379 |
+
if len(members) == len(self.MEMBERS):
|
| 380 |
+
# The ensemble is calibrated as its own system: each member's bias
|
| 381 |
+
# describes its own posterior, not the average of two.
|
| 382 |
+
with open(os.path.join(path, "ensemble_config.json"), encoding="utf-8") as f:
|
| 383 |
+
ens = json.load(f)
|
| 384 |
+
self.weights = ens["weights"]
|
| 385 |
+
self.bias = ens["punct_bias"]
|
| 386 |
+
self.bias_per_lang = ens["punct_bias_per_lang"]
|
| 387 |
+
else:
|
| 388 |
+
self.weights = [1.0 / len(members)] * len(members)
|
| 389 |
+
meta = self.members[0].meta if len(members) == 1 else {}
|
| 390 |
+
self.bias = meta.get("punct_bias")
|
| 391 |
+
self.bias_per_lang = meta.get("punct_bias_per_lang") or {}
|
| 392 |
+
gz = os.path.join(path, "gazetteer.json")
|
| 393 |
+
self.gazetteer = {}
|
| 394 |
+
if use_gazetteer and os.path.exists(gz):
|
| 395 |
+
with open(gz, encoding="utf-8") as f:
|
| 396 |
+
self.gazetteer = json.load(f)
|
| 397 |
+
|
| 398 |
+
@staticmethod
|
| 399 |
+
def _pick_backend(path, members, backend):
|
| 400 |
+
if backend == "auto":
|
| 401 |
+
has_st = all(os.path.exists(os.path.join(path, m, "model.safetensors"))
|
| 402 |
+
for m in members)
|
| 403 |
+
backend = "torch" if (torch is not None and has_st) else "onnx"
|
| 404 |
+
if backend == "torch" and torch is None:
|
| 405 |
+
raise ImportError("backend='torch' needs torch; install it, or use "
|
| 406 |
+
"backend='onnx' (onnxruntime + tokenizers only)")
|
| 407 |
+
if backend not in ("torch", "onnx"):
|
| 408 |
+
raise ValueError("backend must be 'auto', 'torch' or 'onnx'")
|
| 409 |
+
return backend
|
| 410 |
+
|
| 411 |
+
@classmethod
|
| 412 |
+
def from_pretrained(cls, repo_or_path="valkayuh/dewpoint", **kw):
|
| 413 |
+
"""A local directory, or a Hugging Face repo id to download. Only the
|
| 414 |
+
files the chosen backend needs are fetched: the ONNX backend never
|
| 415 |
+
downloads the safetensors, and the torch backend never downloads ONNX."""
|
| 416 |
+
if os.path.isdir(repo_or_path):
|
| 417 |
+
return cls(repo_or_path, **kw)
|
| 418 |
+
from huggingface_hub import snapshot_download
|
| 419 |
+
members = kw.get("members") or cls.MEMBERS
|
| 420 |
+
backend = kw.get("backend", "auto")
|
| 421 |
+
if backend == "auto":
|
| 422 |
+
backend = "torch" if torch is not None else "onnx"
|
| 423 |
+
kw["backend"] = backend
|
| 424 |
+
allow = ["dewpoint.py", "ensemble_config.json", "gazetteer.json"]
|
| 425 |
+
for m in members:
|
| 426 |
+
allow += [f"{m}/vpunct_config.json", f"{m}/tokenizer.json",
|
| 427 |
+
f"{m}/tokenizer_config.json"]
|
| 428 |
+
allow += ([f"{m}/config.json", f"{m}/model.safetensors"]
|
| 429 |
+
if backend == "torch" else [f"onnx/{m}/*"])
|
| 430 |
+
return cls(snapshot_download(repo_or_path, allow_patterns=allow), **kw)
|
| 431 |
+
|
| 432 |
+
# ------------------------------------------------------------ core
|
| 433 |
+
def _bias_for(self, lang):
|
| 434 |
+
b = None
|
| 435 |
+
if self.bias_per_lang:
|
| 436 |
+
b = self.bias_per_lang.get(lang) or self.bias_per_lang.get(base_lang(lang))
|
| 437 |
+
return b if b is not None else self.bias
|
| 438 |
+
|
| 439 |
+
def predict(self, words: Sequence[str], lang: str = "en") -> dict:
|
| 440 |
+
"""Per-word labels and averaged posteriors for an already-split word list.
|
| 441 |
+
|
| 442 |
+
``words`` should be lowercased and free of punctuation, as ASR output is.
|
| 443 |
+
"""
|
| 444 |
+
words = list(words)
|
| 445 |
+
acc_p = acc_c = None
|
| 446 |
+
for w, m in zip(self.weights, self.members):
|
| 447 |
+
p, c = m.posteriors(words, lang)
|
| 448 |
+
acc_p = w * p if acc_p is None else acc_p + w * p
|
| 449 |
+
acc_c = w * c if acc_c is None else acc_c + w * c
|
| 450 |
+
lp = np.log(np.clip(acc_p, 1e-9, None))
|
| 451 |
+
b = self._bias_for(lang)
|
| 452 |
+
if b is not None:
|
| 453 |
+
lp = lp + np.asarray(b, dtype=np.float32)[None, :]
|
| 454 |
+
punct = [PUNCT_LABELS[i] for i in lp.argmax(1)] if len(words) else []
|
| 455 |
+
case = [CASE_LABELS[i] for i in acc_c.argmax(1)] if len(words) else []
|
| 456 |
+
return {"words": words, "punct": punct, "case": case,
|
| 457 |
+
"punct_probs": acc_p, "case_probs": acc_c, "punct_scores": lp}
|
| 458 |
+
|
| 459 |
+
@staticmethod
|
| 460 |
+
def _close(punct, scores):
|
| 461 |
+
"""A complete text ends on a sentence mark: if the last word got none,
|
| 462 |
+
take the likeliest of . ? ! for it. Applied to finished text only --
|
| 463 |
+
predict() is left exactly as benchmarked."""
|
| 464 |
+
if punct and punct[-1] not in _SENT_END:
|
| 465 |
+
ends = [PUNCT_LABELS.index(x) for x in ("PERIOD", "QUESTION", "EXCLAM")]
|
| 466 |
+
punct[-1] = PUNCT_LABELS[max(ends, key=lambda i: scores[-1][i])]
|
| 467 |
+
return punct
|
| 468 |
+
|
| 469 |
+
def _postprocess(self, words, punct, case, lang, is_start=True, prev_punct=None):
|
| 470 |
+
"""Gazetteer surface forms (iPhone, McDonald) and a capital after every
|
| 471 |
+
sentence-final mark."""
|
| 472 |
+
case = list(case)
|
| 473 |
+
caseless = is_caseless(lang)
|
| 474 |
+
gz = self.gazetteer.get(base_lang(lang), {})
|
| 475 |
+
out = []
|
| 476 |
+
for i, w in enumerate(words):
|
| 477 |
+
s = gz.get(w)
|
| 478 |
+
if s and not caseless:
|
| 479 |
+
out.append(s)
|
| 480 |
+
case[i] = "AS_IS"
|
| 481 |
+
else:
|
| 482 |
+
out.append(w)
|
| 483 |
+
if not caseless and words:
|
| 484 |
+
lead = prev_punct in _SENT_END if prev_punct is not None else False
|
| 485 |
+
if case[0] == "LOWER" and (is_start or lead):
|
| 486 |
+
case[0] = "CAP"
|
| 487 |
+
for i in range(1, len(words)):
|
| 488 |
+
if punct[i - 1] in _SENT_END and case[i] == "LOWER":
|
| 489 |
+
case[i] = "CAP"
|
| 490 |
+
return out, list(punct), case
|
| 491 |
+
|
| 492 |
+
def _render(self, words, punct, case, lang):
|
| 493 |
+
surf = surface_for(lang)
|
| 494 |
+
sep = "" if is_no_space(lang) else " "
|
| 495 |
+
return sep.join((w if c == "AS_IS" else apply_case(w, c)) + surf.get(p, "")
|
| 496 |
+
for w, p, c in zip(words, punct, case))
|
| 497 |
+
|
| 498 |
+
def restore(self, text: str, lang: str = "en", close: bool = True) -> str:
|
| 499 |
+
"""Unpunctuated text in, punctuated and truecased text out.
|
| 500 |
+
|
| 501 |
+
``close`` ends the text on a sentence mark; turn it off when ``text``
|
| 502 |
+
is a fragment that continues elsewhere.
|
| 503 |
+
"""
|
| 504 |
+
words = split_words(text, lang)
|
| 505 |
+
if not words:
|
| 506 |
+
return text
|
| 507 |
+
r = self.predict(words, lang)
|
| 508 |
+
punct = self._close(r["punct"], r["punct_scores"]) if close else r["punct"]
|
| 509 |
+
w, p, c = self._postprocess(words, punct, r["case"], lang)
|
| 510 |
+
return self._render(w, p, c, lang)
|
| 511 |
+
|
| 512 |
+
def restore_batch(self, texts: Iterable[str], lang: str = "en") -> list[str]:
|
| 513 |
+
return [self.restore(t, lang) for t in texts]
|
| 514 |
+
|
| 515 |
+
def stream(self, lang="en", lag=6, context=180, stable_n=2):
|
| 516 |
+
return StreamingPunctuator(self, lang, lag, context, stable_n)
|
| 517 |
+
|
| 518 |
+
|
| 519 |
+
class StreamingPunctuator:
|
| 520 |
+
"""Commit-with-lag for live transcripts.
|
| 521 |
+
|
| 522 |
+
Later words change earlier decisions -- "what do you think" only becomes a
|
| 523 |
+
question at its last word -- so a word is released only once it has
|
| 524 |
+
``lag`` words of right context and the same label for ``stable_n``
|
| 525 |
+
consecutive updates. Call ``push(words)`` as ASR emits, ``finish()`` at
|
| 526 |
+
the end; each returns the newly committed text.
|
| 527 |
+
"""
|
| 528 |
+
|
| 529 |
+
def __init__(self, punctuator, lang="en", lag=6, context=180, stable_n=2):
|
| 530 |
+
self.p, self.lang = punctuator, lang
|
| 531 |
+
self.lag, self.context, self.stable_n = lag, context, stable_n
|
| 532 |
+
self.words, self.emitted = [], 0
|
| 533 |
+
self._last, self._streak = {}, {}
|
| 534 |
+
|
| 535 |
+
def push(self, new_words) -> str:
|
| 536 |
+
if isinstance(new_words, str):
|
| 537 |
+
new_words = split_words(new_words, self.lang)
|
| 538 |
+
self.words.extend(w.lower() for w in new_words)
|
| 539 |
+
return self._commit(final=False)
|
| 540 |
+
|
| 541 |
+
def finish(self) -> str:
|
| 542 |
+
return self._commit(final=True)
|
| 543 |
+
|
| 544 |
+
def _commit(self, final):
|
| 545 |
+
if not self.words:
|
| 546 |
+
return ""
|
| 547 |
+
off = max(0, len(self.words) - self.context)
|
| 548 |
+
r = self.p.predict(self.words[off:], self.lang)
|
| 549 |
+
punct, case = r["punct"], r["case"]
|
| 550 |
+
if final:
|
| 551 |
+
punct = self.p._close(punct, r["punct_scores"])
|
| 552 |
+
limit = len(self.words) if final else len(self.words) - self.lag
|
| 553 |
+
done, i = [], self.emitted
|
| 554 |
+
while i < limit:
|
| 555 |
+
j = i - off
|
| 556 |
+
if j < 0:
|
| 557 |
+
i += 1
|
| 558 |
+
continue
|
| 559 |
+
if not final:
|
| 560 |
+
lab = (punct[j], case[j])
|
| 561 |
+
if self._last.get(i) == lab:
|
| 562 |
+
self._streak[i] = self._streak.get(i, 1) + 1
|
| 563 |
+
else:
|
| 564 |
+
self._last[i], self._streak[i] = lab, 1
|
| 565 |
+
if self._streak[i] < self.stable_n:
|
| 566 |
+
break
|
| 567 |
+
done.append(i)
|
| 568 |
+
i += 1
|
| 569 |
+
if not done:
|
| 570 |
+
return ""
|
| 571 |
+
s, e = done[0], done[-1] + 1
|
| 572 |
+
prev = punct[s - off - 1] if s - off - 1 >= 0 else None
|
| 573 |
+
w, p, c = self.p._postprocess(self.words[s:e], punct[s - off:e - off],
|
| 574 |
+
case[s - off:e - off], self.lang,
|
| 575 |
+
is_start=(s == 0), prev_punct=prev)
|
| 576 |
+
self.emitted = e
|
| 577 |
+
out = self.p._render(w, p, c, self.lang)
|
| 578 |
+
return out if s == 0 or is_no_space(self.lang) else " " + out
|
| 579 |
+
|
| 580 |
+
|
| 581 |
+
if __name__ == "__main__":
|
| 582 |
+
import argparse
|
| 583 |
+
import sys
|
| 584 |
+
ap = argparse.ArgumentParser(description="Restore punctuation and case.")
|
| 585 |
+
ap.add_argument("text", nargs="*", help="text to restore (default: stdin)")
|
| 586 |
+
ap.add_argument("--lang", default="en", help="ISO 639-1 code, e.g. en, de, zh")
|
| 587 |
+
ap.add_argument("--model", default=os.path.dirname(os.path.abspath(__file__)))
|
| 588 |
+
ap.add_argument("--single", action="store_true",
|
| 589 |
+
help="use only the mmBERT-base member (307M, faster)")
|
| 590 |
+
ap.add_argument("--backend", default="auto", choices=["auto", "torch", "onnx"])
|
| 591 |
+
a = ap.parse_args()
|
| 592 |
+
p = Punctuator.from_pretrained(a.model, backend=a.backend,
|
| 593 |
+
members=["mmbert-base"] if a.single else None)
|
| 594 |
+
src = [" ".join(a.text)] if a.text else sys.stdin.read().splitlines()
|
| 595 |
+
for line in src:
|
| 596 |
+
print(p.restore(line, a.lang))
|
ensemble_config.json
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"members": [
|
| 3 |
+
"mmbert-base",
|
| 4 |
+
"xlm-roberta-large"
|
| 5 |
+
],
|
| 6 |
+
"weights": [
|
| 7 |
+
0.5,
|
| 8 |
+
0.5
|
| 9 |
+
],
|
| 10 |
+
"combine": "arithmetic mean of per-word softmax posteriors",
|
| 11 |
+
"punct_labels": [
|
| 12 |
+
"O",
|
| 13 |
+
"COMMA",
|
| 14 |
+
"PERIOD",
|
| 15 |
+
"QUESTION",
|
| 16 |
+
"EXCLAM"
|
| 17 |
+
],
|
| 18 |
+
"case_labels": [
|
| 19 |
+
"LOWER",
|
| 20 |
+
"CAP",
|
| 21 |
+
"UPPER"
|
| 22 |
+
],
|
| 23 |
+
"punct_bias": [
|
| 24 |
+
0.0,
|
| 25 |
+
-0.25,
|
| 26 |
+
0.375,
|
| 27 |
+
-1.0,
|
| 28 |
+
-3.0
|
| 29 |
+
],
|
| 30 |
+
"punct_bias_per_lang": {
|
| 31 |
+
"ar": [
|
| 32 |
+
0.0,
|
| 33 |
+
-0.1875,
|
| 34 |
+
0.46875,
|
| 35 |
+
-0.9375,
|
| 36 |
+
-3.0
|
| 37 |
+
],
|
| 38 |
+
"de": [
|
| 39 |
+
0.0,
|
| 40 |
+
-0.15625,
|
| 41 |
+
0.5625,
|
| 42 |
+
-0.75,
|
| 43 |
+
-2.625
|
| 44 |
+
],
|
| 45 |
+
"en": [
|
| 46 |
+
0.0,
|
| 47 |
+
-0.25,
|
| 48 |
+
0.3125,
|
| 49 |
+
-0.9375,
|
| 50 |
+
-2.84375
|
| 51 |
+
],
|
| 52 |
+
"fr": [
|
| 53 |
+
0.0,
|
| 54 |
+
-0.28125,
|
| 55 |
+
0.40625,
|
| 56 |
+
-0.96875,
|
| 57 |
+
-2.9375
|
| 58 |
+
],
|
| 59 |
+
"it": [
|
| 60 |
+
0.0,
|
| 61 |
+
-0.3125,
|
| 62 |
+
0.75,
|
| 63 |
+
-0.75,
|
| 64 |
+
-3.0
|
| 65 |
+
],
|
| 66 |
+
"ko": [
|
| 67 |
+
0.0,
|
| 68 |
+
-0.34375,
|
| 69 |
+
0.25,
|
| 70 |
+
-0.90625,
|
| 71 |
+
-3.0
|
| 72 |
+
],
|
| 73 |
+
"nl": [
|
| 74 |
+
0.0,
|
| 75 |
+
-0.1875,
|
| 76 |
+
0.5,
|
| 77 |
+
-1.0,
|
| 78 |
+
-2.9375
|
| 79 |
+
],
|
| 80 |
+
"ro": [
|
| 81 |
+
0.0,
|
| 82 |
+
-0.40625,
|
| 83 |
+
0.25,
|
| 84 |
+
-1.125,
|
| 85 |
+
-2.96875
|
| 86 |
+
],
|
| 87 |
+
"zh": [
|
| 88 |
+
0.0,
|
| 89 |
+
-0.28125,
|
| 90 |
+
0.34375,
|
| 91 |
+
-1.21875,
|
| 92 |
+
-3.0
|
| 93 |
+
]
|
| 94 |
+
},
|
| 95 |
+
"calibration": {
|
| 96 |
+
"fitted_on": "IWSLT dev2010 (held out of every test table)",
|
| 97 |
+
"dev_macro_f1_argmax": 0.7191,
|
| 98 |
+
"dev_macro_f1_calibrated": 0.7472
|
| 99 |
+
},
|
| 100 |
+
"window": {
|
| 101 |
+
"max_len": 512,
|
| 102 |
+
"overlap_subwords": 256
|
| 103 |
+
},
|
| 104 |
+
"params": {
|
| 105 |
+
"mmbert-base": {
|
| 106 |
+
"params": 307537928,
|
| 107 |
+
"embedding_params": 196608768,
|
| 108 |
+
"dtype": "torch.float32"
|
| 109 |
+
},
|
| 110 |
+
"xlm-roberta-large": {
|
| 111 |
+
"params": 560950280,
|
| 112 |
+
"embedding_params": 256531456,
|
| 113 |
+
"dtype": "torch.float32"
|
| 114 |
+
}
|
| 115 |
+
}
|
| 116 |
+
}
|
gazetteer.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
handler.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Hugging Face Inference Endpoints handler.
|
| 2 |
+
|
| 3 |
+
Deploy this repository as an Inference Endpoint and it serves Dewpoint with no
|
| 4 |
+
further code. Request body:
|
| 5 |
+
|
| 6 |
+
{"inputs": "so i said meet at three thirty tuesday what do you think",
|
| 7 |
+
"parameters": {"lang": "en"}}
|
| 8 |
+
|
| 9 |
+
`inputs` may also be a list of strings. Optional parameters:
|
| 10 |
+
lang ISO 639-1 code (default "en")
|
| 11 |
+
single true to run only the mmBERT-base member
|
| 12 |
+
labels true to also return per-word punctuation and case labels
|
| 13 |
+
|
| 14 |
+
Response: [{"text": "..."}] per input (plus "words", "punct", "case" if
|
| 15 |
+
labels=true).
|
| 16 |
+
"""
|
| 17 |
+
import os
|
| 18 |
+
import sys
|
| 19 |
+
|
| 20 |
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 21 |
+
from dewpoint import Punctuator, split_words # noqa: E402
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class EndpointHandler:
|
| 25 |
+
def __init__(self, path=""):
|
| 26 |
+
path = path or os.path.dirname(os.path.abspath(__file__))
|
| 27 |
+
self.full = Punctuator(path)
|
| 28 |
+
self._single = None
|
| 29 |
+
self.path = path
|
| 30 |
+
|
| 31 |
+
def _model(self, single):
|
| 32 |
+
if not single:
|
| 33 |
+
return self.full
|
| 34 |
+
if self._single is None:
|
| 35 |
+
self._single = Punctuator(self.path, members=["mmbert-base"])
|
| 36 |
+
return self._single
|
| 37 |
+
|
| 38 |
+
def __call__(self, data):
|
| 39 |
+
inputs = data.get("inputs", data)
|
| 40 |
+
params = data.get("parameters") or {}
|
| 41 |
+
lang = params.get("lang", "en")
|
| 42 |
+
p = self._model(bool(params.get("single", False)))
|
| 43 |
+
texts = [inputs] if isinstance(inputs, str) else list(inputs)
|
| 44 |
+
out = []
|
| 45 |
+
for t in texts:
|
| 46 |
+
item = {"text": p.restore(t, lang)}
|
| 47 |
+
if params.get("labels"):
|
| 48 |
+
r = p.predict(split_words(t, lang), lang)
|
| 49 |
+
item.update(words=r["words"], punct=r["punct"], case=r["case"])
|
| 50 |
+
out.append(item)
|
| 51 |
+
return out
|
mmbert-base/config.json
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"architectures": [
|
| 3 |
+
"ModernBertForMaskedLM"
|
| 4 |
+
],
|
| 5 |
+
"attention_bias": false,
|
| 6 |
+
"attention_dropout": 0.0,
|
| 7 |
+
"bos_token_id": 2,
|
| 8 |
+
"classifier_activation": "gelu",
|
| 9 |
+
"classifier_bias": false,
|
| 10 |
+
"classifier_dropout": 0.0,
|
| 11 |
+
"classifier_pooling": "mean",
|
| 12 |
+
"cls_token_id": 1,
|
| 13 |
+
"decoder_bias": true,
|
| 14 |
+
"deterministic_flash_attn": false,
|
| 15 |
+
"dtype": "bfloat16",
|
| 16 |
+
"embedding_dropout": 0.0,
|
| 17 |
+
"eos_token_id": 1,
|
| 18 |
+
"global_attn_every_n_layers": 3,
|
| 19 |
+
"gradient_checkpointing": false,
|
| 20 |
+
"hidden_activation": "gelu",
|
| 21 |
+
"hidden_size": 768,
|
| 22 |
+
"initializer_cutoff_factor": 2.0,
|
| 23 |
+
"initializer_range": 0.02,
|
| 24 |
+
"intermediate_size": 1152,
|
| 25 |
+
"layer_norm_eps": 1e-05,
|
| 26 |
+
"layer_types": [
|
| 27 |
+
"full_attention",
|
| 28 |
+
"sliding_attention",
|
| 29 |
+
"sliding_attention",
|
| 30 |
+
"full_attention",
|
| 31 |
+
"sliding_attention",
|
| 32 |
+
"sliding_attention",
|
| 33 |
+
"full_attention",
|
| 34 |
+
"sliding_attention",
|
| 35 |
+
"sliding_attention",
|
| 36 |
+
"full_attention",
|
| 37 |
+
"sliding_attention",
|
| 38 |
+
"sliding_attention",
|
| 39 |
+
"full_attention",
|
| 40 |
+
"sliding_attention",
|
| 41 |
+
"sliding_attention",
|
| 42 |
+
"full_attention",
|
| 43 |
+
"sliding_attention",
|
| 44 |
+
"sliding_attention",
|
| 45 |
+
"full_attention",
|
| 46 |
+
"sliding_attention",
|
| 47 |
+
"sliding_attention",
|
| 48 |
+
"full_attention"
|
| 49 |
+
],
|
| 50 |
+
"local_attention": 128,
|
| 51 |
+
"mask_token_id": 4,
|
| 52 |
+
"max_position_embeddings": 8192,
|
| 53 |
+
"mlp_bias": false,
|
| 54 |
+
"mlp_dropout": 0.0,
|
| 55 |
+
"model_type": "modernbert",
|
| 56 |
+
"norm_bias": false,
|
| 57 |
+
"norm_eps": 1e-05,
|
| 58 |
+
"num_attention_heads": 12,
|
| 59 |
+
"num_hidden_layers": 22,
|
| 60 |
+
"pad_token_id": 0,
|
| 61 |
+
"position_embedding_type": "sans_pos",
|
| 62 |
+
"rope_parameters": {
|
| 63 |
+
"full_attention": {
|
| 64 |
+
"rope_theta": 160000,
|
| 65 |
+
"rope_type": "default"
|
| 66 |
+
},
|
| 67 |
+
"sliding_attention": {
|
| 68 |
+
"rope_theta": 160000,
|
| 69 |
+
"rope_type": "default"
|
| 70 |
+
}
|
| 71 |
+
},
|
| 72 |
+
"sep_token_id": 1,
|
| 73 |
+
"sparse_pred_ignore_index": -100,
|
| 74 |
+
"sparse_prediction": false,
|
| 75 |
+
"tie_word_embeddings": true,
|
| 76 |
+
"transformers_version": "5.17.0",
|
| 77 |
+
"vocab_size": 256000
|
| 78 |
+
}
|
mmbert-base/model.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d2448124ab577870de8388cd6819587a52c311d5fb6b7ef816e25e3ac97b858d
|
| 3 |
+
size 615090800
|
mmbert-base/tokenizer.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:609d8f4c067cd3950f88594c5a802616cea245823836ef5848ee4fc40aab5b6f
|
| 3 |
+
size 34363188
|
mmbert-base/tokenizer_config.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"backend": "tokenizers",
|
| 3 |
+
"bos_token": "<bos>",
|
| 4 |
+
"clean_up_tokenization_spaces": false,
|
| 5 |
+
"cls_token": "<bos>",
|
| 6 |
+
"eos_token": "<eos>",
|
| 7 |
+
"extra_special_tokens": [
|
| 8 |
+
"<start_of_turn>",
|
| 9 |
+
"<end_of_turn>"
|
| 10 |
+
],
|
| 11 |
+
"is_local": false,
|
| 12 |
+
"local_files_only": false,
|
| 13 |
+
"mask_token": "<mask>",
|
| 14 |
+
"model_input_names": [
|
| 15 |
+
"input_ids",
|
| 16 |
+
"attention_mask"
|
| 17 |
+
],
|
| 18 |
+
"model_max_length": 8192,
|
| 19 |
+
"pad_token": "<pad>",
|
| 20 |
+
"padding_side": "right",
|
| 21 |
+
"sep_token": "<eos>",
|
| 22 |
+
"spaces_between_special_tokens": false,
|
| 23 |
+
"tokenizer_class": "TokenizersBackend",
|
| 24 |
+
"unk_token": "<unk>"
|
| 25 |
+
}
|
mmbert-base/vpunct_config.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"base_id": "jhu-clsp/mmBERT-base",
|
| 3 |
+
"n_punct": 5,
|
| 4 |
+
"n_case": 3,
|
| 5 |
+
"max_len": 512,
|
| 6 |
+
"punct_labels": [
|
| 7 |
+
"O",
|
| 8 |
+
"COMMA",
|
| 9 |
+
"PERIOD",
|
| 10 |
+
"QUESTION",
|
| 11 |
+
"EXCLAM"
|
| 12 |
+
],
|
| 13 |
+
"case_labels": [
|
| 14 |
+
"LOWER",
|
| 15 |
+
"CAP",
|
| 16 |
+
"UPPER"
|
| 17 |
+
],
|
| 18 |
+
"punct_bias": [
|
| 19 |
+
0.0,
|
| 20 |
+
-0.625,
|
| 21 |
+
-0.125,
|
| 22 |
+
-1.375,
|
| 23 |
+
-2.875
|
| 24 |
+
],
|
| 25 |
+
"punct_bias_per_lang": {},
|
| 26 |
+
"calibration_strategy": "pooled"
|
| 27 |
+
}
|
requirements.txt
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# torch backend (GPU or CPU): pip install -r requirements.txt
|
| 2 |
+
torch>=2.4
|
| 3 |
+
transformers>=5.0
|
| 4 |
+
safetensors>=0.4
|
| 5 |
+
huggingface_hub>=0.24
|
| 6 |
+
numpy>=1.26
|
| 7 |
+
# Measured with torch 2.11.0, transformers 5.17.0, tokenizers 0.23.2, numpy 2.5.3
|
results/rt2_voices.json
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"kokoro": {
|
| 3 |
+
"repo": "hexgrad/Kokoro-82M",
|
| 4 |
+
"licence": "Apache-2.0"
|
| 5 |
+
},
|
| 6 |
+
"chatterbox": {
|
| 7 |
+
"repo": "ResembleAI/chatterbox",
|
| 8 |
+
"licence": "MIT"
|
| 9 |
+
},
|
| 10 |
+
"piper": {
|
| 11 |
+
"admitted": {
|
| 12 |
+
"bg/bg_BG/dimitar/medium/bg_BG-dimitar-medium": [
|
| 13 |
+
"* License: CC0"
|
| 14 |
+
],
|
| 15 |
+
"de/de_DE/kerstin/low/de_DE-kerstin-low": [
|
| 16 |
+
"* License: CC0"
|
| 17 |
+
],
|
| 18 |
+
"de/de_DE/mls/medium/de_DE-mls-medium": [
|
| 19 |
+
"* License: CC-BY 4.0"
|
| 20 |
+
],
|
| 21 |
+
"de/de_DE/thorsten/high/de_DE-thorsten-high": [
|
| 22 |
+
"* License: CC0"
|
| 23 |
+
],
|
| 24 |
+
"de/de_DE/thorsten/low/de_DE-thorsten-low": [
|
| 25 |
+
"* License: CC0"
|
| 26 |
+
],
|
| 27 |
+
"de/de_DE/thorsten/medium/de_DE-thorsten-medium": [
|
| 28 |
+
"* License: CC0"
|
| 29 |
+
],
|
| 30 |
+
"de/de_DE/thorsten_emotional/medium/de_DE-thorsten_emotional-medium": [
|
| 31 |
+
"* License: CC0"
|
| 32 |
+
],
|
| 33 |
+
"el/el_GR/rapunzelina/low/el_GR-rapunzelina-low": [
|
| 34 |
+
"* License: CC0"
|
| 35 |
+
],
|
| 36 |
+
"el/el_GR/rapunzelina/medium/el_GR-rapunzelina-medium": [
|
| 37 |
+
"* License: CC0"
|
| 38 |
+
],
|
| 39 |
+
"hu/hu_HU/anna/medium/hu_HU-anna-medium": [
|
| 40 |
+
"* License: CC0"
|
| 41 |
+
],
|
| 42 |
+
"hu/hu_HU/berta/medium/hu_HU-berta-medium": [
|
| 43 |
+
"* License: CC0"
|
| 44 |
+
],
|
| 45 |
+
"hu/hu_HU/imre/medium/hu_HU-imre-medium": [
|
| 46 |
+
"* License: CC0"
|
| 47 |
+
],
|
| 48 |
+
"ro/ro_RO/mihai/medium/ro_RO-mihai-medium": [
|
| 49 |
+
"* License: CC0"
|
| 50 |
+
],
|
| 51 |
+
"fa/fa_IR/amir/medium/fa_IR-amir-medium": [
|
| 52 |
+
"* License: CC0"
|
| 53 |
+
],
|
| 54 |
+
"fa/fa_IR/ganji/medium/fa_IR-ganji-medium": [
|
| 55 |
+
"* License: CC0"
|
| 56 |
+
],
|
| 57 |
+
"fa/fa_IR/ganji_adabi/medium/fa_IR-ganji_adabi-medium": [
|
| 58 |
+
"* License: CC0"
|
| 59 |
+
],
|
| 60 |
+
"fa/fa_IR/reza_ibrahim/medium/fa_IR-reza_ibrahim-medium": [
|
| 61 |
+
"* License: CC0"
|
| 62 |
+
],
|
| 63 |
+
"fi/fi_FI/harri/low/fi_FI-harri-low": [
|
| 64 |
+
"* License: CC0"
|
| 65 |
+
],
|
| 66 |
+
"fi/fi_FI/harri/medium/fi_FI-harri-medium": [
|
| 67 |
+
"* License: CC0"
|
| 68 |
+
],
|
| 69 |
+
"uk/uk_UA/mykyta/high/uk_UA-mykyta-high": [
|
| 70 |
+
"* License: Apache 2.0"
|
| 71 |
+
],
|
| 72 |
+
"uk/uk_UA/oleksa/high/uk_UA-oleksa-high": [
|
| 73 |
+
"* License: Apache 2.0"
|
| 74 |
+
],
|
| 75 |
+
"uk/uk_UA/tetiana/high/uk_UA-tetiana-high": [
|
| 76 |
+
"* License: Apache 2.0"
|
| 77 |
+
],
|
| 78 |
+
"uk/uk_UA/ukrainian_tts/medium/uk_UA-ukrainian_tts-medium": [
|
| 79 |
+
"* License: CC0"
|
| 80 |
+
],
|
| 81 |
+
"lv/lv_LV/aivars/medium/lv_LV-aivars-medium": [
|
| 82 |
+
"* License: CC0 1.0 Universal"
|
| 83 |
+
],
|
| 84 |
+
"nl/nl_BE/nathalie/medium/nl_BE-nathalie-medium": [
|
| 85 |
+
"* License: CC0"
|
| 86 |
+
],
|
| 87 |
+
"nl/nl_BE/rdh/medium/nl_BE-rdh-medium": [
|
| 88 |
+
"* License: CC0 1.0 Universal"
|
| 89 |
+
],
|
| 90 |
+
"nl/nl_NL/alex/medium/nl_NL-alex-medium": [
|
| 91 |
+
"* License: CC0"
|
| 92 |
+
],
|
| 93 |
+
"nl/nl_NL/mls/medium/nl_NL-mls-medium": [
|
| 94 |
+
"* License: CC-BY 4.0"
|
| 95 |
+
],
|
| 96 |
+
"nl/nl_NL/mls_5809/low/nl_NL-mls_5809-low": [
|
| 97 |
+
"* License: CC BY 4.0"
|
| 98 |
+
],
|
| 99 |
+
"nl/nl_NL/mls_7432/low/nl_NL-mls_7432-low": [
|
| 100 |
+
"* License: CC BY 4.0"
|
| 101 |
+
],
|
| 102 |
+
"nl/nl_NL/pim/medium/nl_NL-pim-medium": [
|
| 103 |
+
"* License: CC0"
|
| 104 |
+
],
|
| 105 |
+
"nl/nl_NL/ronnie/medium/nl_NL-ronnie-medium": [
|
| 106 |
+
"* License: CC0"
|
| 107 |
+
],
|
| 108 |
+
"pl/pl_PL/bass/high/pl_PL-bass-high": [
|
| 109 |
+
"## License",
|
| 110 |
+
"[Apache 2.0](https://www.apache.org/licenses/LICENSE-2.0)"
|
| 111 |
+
],
|
| 112 |
+
"pl/pl_PL/darkman/medium/pl_PL-darkman-medium": [
|
| 113 |
+
"* License: CC0"
|
| 114 |
+
],
|
| 115 |
+
"pl/pl_PL/gosia/medium/pl_PL-gosia-medium": [
|
| 116 |
+
"* License: CC0"
|
| 117 |
+
],
|
| 118 |
+
"pl/pl_PL/mc_speech/medium/pl_PL-mc_speech-medium": [
|
| 119 |
+
"* License: CC0"
|
| 120 |
+
],
|
| 121 |
+
"pl/pl_PL/mls_6892/low/pl_PL-mls_6892-low": [
|
| 122 |
+
"* License: CC BY 4.0"
|
| 123 |
+
],
|
| 124 |
+
"ru/ru_RU/denis/medium/ru_RU-denis-medium": [
|
| 125 |
+
"* License: CC0"
|
| 126 |
+
],
|
| 127 |
+
"ru/ru_RU/dmitri/medium/ru_RU-dmitri-medium": [
|
| 128 |
+
"* License: CC0"
|
| 129 |
+
],
|
| 130 |
+
"sq/sq_AL/edon/medium/sq_AL-edon-medium": [
|
| 131 |
+
"* License: CC0"
|
| 132 |
+
],
|
| 133 |
+
"sv/sv_SE/alma/medium/sv_SE-alma-medium": [
|
| 134 |
+
"## License",
|
| 135 |
+
"The model weights are released under CC BY 4.0, following the license of the NST training data."
|
| 136 |
+
],
|
| 137 |
+
"sv/sv_SE/nst/medium/sv_SE-nst-medium": [
|
| 138 |
+
"* License: CC0"
|
| 139 |
+
]
|
| 140 |
+
},
|
| 141 |
+
"refused": {
|
| 142 |
+
"de/de_DE/karlsson/low/de_DE-karlsson-low": [
|
| 143 |
+
"* License: See URL"
|
| 144 |
+
],
|
| 145 |
+
"de/de_DE/pavoque/low/de_DE-pavoque-low": [
|
| 146 |
+
"* License: http://creativecommons.org/licenses/by-nc-sa/4.0/"
|
| 147 |
+
],
|
| 148 |
+
"de/de_DE/ramona/low/de_DE-ramona-low": [
|
| 149 |
+
"* License: See URL"
|
| 150 |
+
],
|
| 151 |
+
"el/el_GR/joy/medium/el_GR-joy-medium": [
|
| 152 |
+
"* License: CC BY-NC 4.0"
|
| 153 |
+
],
|
| 154 |
+
"fa/fa_IR/gyro/medium/fa_IR-gyro-medium": [
|
| 155 |
+
"* License: See URL"
|
| 156 |
+
],
|
| 157 |
+
"ru/ru_RU/irina/medium/ru_RU-irina-medium": [
|
| 158 |
+
"* License: Unknown"
|
| 159 |
+
],
|
| 160 |
+
"ru/ru_RU/ruslan/medium/ru_RU-ruslan-medium": [
|
| 161 |
+
"* License: https://creativecommons.org/licenses/by-nc-sa/4.0/"
|
| 162 |
+
],
|
| 163 |
+
"sv/sv_SE/lisa/medium/sv_SE-lisa-medium": []
|
| 164 |
+
}
|
| 165 |
+
}
|
| 166 |
+
}
|
results/scores.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
serve.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""A small local HTTP server for Dewpoint, using only the standard library.
|
| 2 |
+
|
| 3 |
+
python serve.py # ensemble, port 8000
|
| 4 |
+
python serve.py --single --port 9000 # the 307M member only
|
| 5 |
+
python serve.py --backend onnx # no torch needed
|
| 6 |
+
|
| 7 |
+
curl -s localhost:8000/restore -H "content-type: application/json" \\
|
| 8 |
+
-d '{"text": "what time is it in tokyo right now", "lang": "en"}'
|
| 9 |
+
-> {"text": "What time is it in Tokyo right now?"}
|
| 10 |
+
|
| 11 |
+
POST /restore {"text": str | [str], "lang": "en", "labels": false}
|
| 12 |
+
GET /health {"ok": true, "backend": ..., "members": [...]}
|
| 13 |
+
|
| 14 |
+
It serves one request at a time, which is right for a single model on one
|
| 15 |
+
machine; put it behind a process manager for anything larger.
|
| 16 |
+
"""
|
| 17 |
+
import argparse
|
| 18 |
+
import json
|
| 19 |
+
import os
|
| 20 |
+
import sys
|
| 21 |
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
| 22 |
+
|
| 23 |
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
| 24 |
+
from dewpoint import Punctuator, split_words # noqa: E402
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def make_handler(p):
|
| 28 |
+
class H(BaseHTTPRequestHandler):
|
| 29 |
+
def _send(self, code, obj):
|
| 30 |
+
body = json.dumps(obj, ensure_ascii=False).encode("utf-8")
|
| 31 |
+
self.send_response(code)
|
| 32 |
+
self.send_header("content-type", "application/json; charset=utf-8")
|
| 33 |
+
self.send_header("content-length", str(len(body)))
|
| 34 |
+
self.end_headers()
|
| 35 |
+
self.wfile.write(body)
|
| 36 |
+
|
| 37 |
+
def do_GET(self):
|
| 38 |
+
if self.path.rstrip("/") == "/health":
|
| 39 |
+
self._send(200, {"ok": True, "backend": p.backend,
|
| 40 |
+
"members": len(p.members)})
|
| 41 |
+
else:
|
| 42 |
+
self._send(404, {"error": "try POST /restore or GET /health"})
|
| 43 |
+
|
| 44 |
+
def do_POST(self):
|
| 45 |
+
if self.path.rstrip("/") != "/restore":
|
| 46 |
+
return self._send(404, {"error": "POST /restore"})
|
| 47 |
+
try:
|
| 48 |
+
n = int(self.headers.get("content-length", 0))
|
| 49 |
+
req = json.loads(self.rfile.read(n) or b"{}")
|
| 50 |
+
text, lang = req.get("text", ""), req.get("lang", "en")
|
| 51 |
+
texts = [text] if isinstance(text, str) else list(text)
|
| 52 |
+
out = []
|
| 53 |
+
for t in texts:
|
| 54 |
+
item = {"text": p.restore(t, lang)}
|
| 55 |
+
if req.get("labels"):
|
| 56 |
+
r = p.predict(split_words(t, lang), lang)
|
| 57 |
+
item.update(words=r["words"], punct=r["punct"], case=r["case"])
|
| 58 |
+
out.append(item)
|
| 59 |
+
self._send(200, out[0] if isinstance(text, str) else out)
|
| 60 |
+
except Exception as e: # report, don't crash the server
|
| 61 |
+
self._send(400, {"error": f"{type(e).__name__}: {e}"})
|
| 62 |
+
|
| 63 |
+
def log_message(self, fmt, *args):
|
| 64 |
+
pass
|
| 65 |
+
|
| 66 |
+
return H
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def main():
|
| 70 |
+
ap = argparse.ArgumentParser(description="Serve Dewpoint over HTTP.")
|
| 71 |
+
ap.add_argument("--host", default="127.0.0.1")
|
| 72 |
+
ap.add_argument("--port", type=int, default=8000)
|
| 73 |
+
ap.add_argument("--model", default=os.path.dirname(os.path.abspath(__file__)))
|
| 74 |
+
ap.add_argument("--single", action="store_true")
|
| 75 |
+
ap.add_argument("--backend", default="auto", choices=["auto", "torch", "onnx"])
|
| 76 |
+
a = ap.parse_args()
|
| 77 |
+
p = Punctuator.from_pretrained(a.model, backend=a.backend,
|
| 78 |
+
members=["mmbert-base"] if a.single else None)
|
| 79 |
+
print(f"Dewpoint ({p.backend}, {len(p.members)} member(s)) on http://{a.host}:{a.port}")
|
| 80 |
+
HTTPServer((a.host, a.port), make_handler(p)).serve_forever()
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
if __name__ == "__main__":
|
| 84 |
+
main()
|
xlm-roberta-large/config.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"add_cross_attention": false,
|
| 3 |
+
"architectures": [
|
| 4 |
+
"XLMRobertaForMaskedLM"
|
| 5 |
+
],
|
| 6 |
+
"attention_probs_dropout_prob": 0.1,
|
| 7 |
+
"bos_token_id": 0,
|
| 8 |
+
"classifier_dropout": null,
|
| 9 |
+
"dtype": "bfloat16",
|
| 10 |
+
"eos_token_id": 2,
|
| 11 |
+
"hidden_act": "gelu",
|
| 12 |
+
"hidden_dropout_prob": 0.1,
|
| 13 |
+
"hidden_size": 1024,
|
| 14 |
+
"initializer_range": 0.02,
|
| 15 |
+
"intermediate_size": 4096,
|
| 16 |
+
"is_decoder": false,
|
| 17 |
+
"layer_norm_eps": 1e-05,
|
| 18 |
+
"max_position_embeddings": 514,
|
| 19 |
+
"model_type": "xlm-roberta",
|
| 20 |
+
"num_attention_heads": 16,
|
| 21 |
+
"num_hidden_layers": 24,
|
| 22 |
+
"output_past": true,
|
| 23 |
+
"pad_token_id": 1,
|
| 24 |
+
"position_embedding_type": "absolute",
|
| 25 |
+
"tie_word_embeddings": true,
|
| 26 |
+
"transformers_version": "5.17.0",
|
| 27 |
+
"type_vocab_size": 1,
|
| 28 |
+
"use_cache": true,
|
| 29 |
+
"vocab_size": 250002
|
| 30 |
+
}
|
xlm-roberta-large/model.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:11e801af31f95a8bef83789559b6baf8f4aa868cdbb09f5db2f826c408d41680
|
| 3 |
+
size 1121949608
|
xlm-roberta-large/tokenizer.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:acbd420e2269cdc1ef45332d3d5c418be4aef6b8cb5a0b7ccae0893485307153
|
| 3 |
+
size 17098086
|
xlm-roberta-large/tokenizer_config.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"add_prefix_space": true,
|
| 3 |
+
"backend": "tokenizers",
|
| 4 |
+
"bos_token": "<s>",
|
| 5 |
+
"cls_token": "<s>",
|
| 6 |
+
"eos_token": "</s>",
|
| 7 |
+
"is_local": false,
|
| 8 |
+
"local_files_only": false,
|
| 9 |
+
"mask_token": "<mask>",
|
| 10 |
+
"model_max_length": 512,
|
| 11 |
+
"pad_token": "<pad>",
|
| 12 |
+
"sep_token": "</s>",
|
| 13 |
+
"tokenizer_class": "XLMRobertaTokenizer",
|
| 14 |
+
"unk_token": "<unk>"
|
| 15 |
+
}
|
xlm-roberta-large/vpunct_config.json
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"base_id": "FacebookAI/xlm-roberta-large",
|
| 3 |
+
"n_punct": 5,
|
| 4 |
+
"n_case": 3,
|
| 5 |
+
"max_len": 512,
|
| 6 |
+
"punct_labels": [
|
| 7 |
+
"O",
|
| 8 |
+
"COMMA",
|
| 9 |
+
"PERIOD",
|
| 10 |
+
"QUESTION",
|
| 11 |
+
"EXCLAM"
|
| 12 |
+
],
|
| 13 |
+
"case_labels": [
|
| 14 |
+
"LOWER",
|
| 15 |
+
"CAP",
|
| 16 |
+
"UPPER"
|
| 17 |
+
],
|
| 18 |
+
"punct_bias": [
|
| 19 |
+
0.0,
|
| 20 |
+
-0.25,
|
| 21 |
+
0.5,
|
| 22 |
+
-1.0,
|
| 23 |
+
-3.0
|
| 24 |
+
],
|
| 25 |
+
"punct_bias_per_lang": {
|
| 26 |
+
"ar": [
|
| 27 |
+
0.0,
|
| 28 |
+
-0.09375,
|
| 29 |
+
0.5625,
|
| 30 |
+
-1.03125,
|
| 31 |
+
-3.0
|
| 32 |
+
],
|
| 33 |
+
"de": [
|
| 34 |
+
0.0,
|
| 35 |
+
-0.46875,
|
| 36 |
+
0.5,
|
| 37 |
+
-1.03125,
|
| 38 |
+
-2.96875
|
| 39 |
+
],
|
| 40 |
+
"en": [
|
| 41 |
+
0.0,
|
| 42 |
+
-0.1875,
|
| 43 |
+
0.46875,
|
| 44 |
+
-0.75,
|
| 45 |
+
-3.0
|
| 46 |
+
],
|
| 47 |
+
"fr": [
|
| 48 |
+
0.0,
|
| 49 |
+
-0.28125,
|
| 50 |
+
0.53125,
|
| 51 |
+
-1.03125,
|
| 52 |
+
-2.6875
|
| 53 |
+
],
|
| 54 |
+
"it": [
|
| 55 |
+
0.0,
|
| 56 |
+
-0.28125,
|
| 57 |
+
0.59375,
|
| 58 |
+
-0.90625,
|
| 59 |
+
-2.9375
|
| 60 |
+
],
|
| 61 |
+
"ko": [
|
| 62 |
+
0.0,
|
| 63 |
+
-0.28125,
|
| 64 |
+
0.6875,
|
| 65 |
+
-0.875,
|
| 66 |
+
-3.0
|
| 67 |
+
],
|
| 68 |
+
"nl": [
|
| 69 |
+
0.0,
|
| 70 |
+
-0.28125,
|
| 71 |
+
0.6875,
|
| 72 |
+
-0.875,
|
| 73 |
+
-2.875
|
| 74 |
+
],
|
| 75 |
+
"ro": [
|
| 76 |
+
0.0,
|
| 77 |
+
-0.40625,
|
| 78 |
+
0.6875,
|
| 79 |
+
-0.875,
|
| 80 |
+
-2.9375
|
| 81 |
+
],
|
| 82 |
+
"zh": [
|
| 83 |
+
0.0,
|
| 84 |
+
-0.28125,
|
| 85 |
+
0.5,
|
| 86 |
+
-0.90625,
|
| 87 |
+
-2.4375
|
| 88 |
+
]
|
| 89 |
+
},
|
| 90 |
+
"calibration_strategy": "shrink0.25"
|
| 91 |
+
}
|