Token Classification
Transformers
ONNX
Safetensors
Danish
electra
punctuation-restoration
sentence-boundary-detection
truecasing
danish
Instructions to use RyeAI/ekko-pnc with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use RyeAI/ekko-pnc with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("token-classification", model="RyeAI/ekko-pnc")# pip install -U transformers accelerate # Load model directly from transformers import AutoTokenizer, AutoModelForTokenClassification tokenizer = AutoTokenizer.from_pretrained("RyeAI/ekko-pnc") model = AutoModelForTokenClassification.from_pretrained("RyeAI/ekko-pnc", device_map="auto") - Notebooks
- Google Colab
- Kaggle
Document standalone ONNX PnC quickstart
Browse files
README.md
CHANGED
|
@@ -42,38 +42,112 @@ is a separate speech recognizer; PnC v2 can process its output.
|
|
| 42 |
Save this as `pnc_demo.py` (Python 3.11+):
|
| 43 |
|
| 44 |
```python
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
from huggingface_hub import snapshot_download
|
| 46 |
-
from
|
| 47 |
|
| 48 |
-
model_dir = snapshot_download(
|
| 49 |
repo_id="RyeAI/ekko-pnc",
|
| 50 |
revision="d3129544e4e0155093e192cac58a9e69a4dea1ff",
|
| 51 |
allow_patterns=["pnc.int8.onnx", "tokenizer.json", "config.json"],
|
| 52 |
-
)
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
# Hej, mit navn er Emil. Hvordan går det i dag?
|
| 57 |
```
|
| 58 |
|
| 59 |
Run it with [uv](https://docs.astral.sh/uv/):
|
| 60 |
|
| 61 |
```bash
|
| 62 |
-
uv run --with
|
|
|
|
| 63 |
```
|
| 64 |
|
| 65 |
The first run downloads the pinned int8 model; later runs use the Hugging Face
|
| 66 |
-
cache.
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
directly, reproduce Ekko's overlapping windows and combine the word
|
| 73 |
-
predictions; a single long model call will not give the same result. These
|
| 74 |
-
settings change inference only; they do not change the v2 model weights. The
|
| 75 |
-
[reference implementation](https://github.com/Rye-A1/ekko-danish-stt/blob/main/src/ekko/pnc.py)
|
| 76 |
-
shows the window assembly.
|
| 77 |
|
| 78 |
Version 2 is tuned for **continuous speech** such as dictation and meetings.
|
| 79 |
The earlier version often missed sentence endings when given a long transcript;
|
|
|
|
| 42 |
Save this as `pnc_demo.py` (Python 3.11+):
|
| 43 |
|
| 44 |
```python
|
| 45 |
+
from bisect import bisect_right
|
| 46 |
+
import json
|
| 47 |
+
from pathlib import Path
|
| 48 |
+
|
| 49 |
+
import numpy as np
|
| 50 |
+
import onnxruntime as ort
|
| 51 |
from huggingface_hub import snapshot_download
|
| 52 |
+
from tokenizers import Tokenizer
|
| 53 |
|
| 54 |
+
model_dir = Path(snapshot_download(
|
| 55 |
repo_id="RyeAI/ekko-pnc",
|
| 56 |
revision="d3129544e4e0155093e192cac58a9e69a4dea1ff",
|
| 57 |
allow_patterns=["pnc.int8.onnx", "tokenizer.json", "config.json"],
|
| 58 |
+
))
|
| 59 |
+
tokenizer = Tokenizer.from_file(str(model_dir / "tokenizer.json"))
|
| 60 |
+
labels = json.loads((model_dir / "config.json").read_text())["id2label"]
|
| 61 |
+
session = ort.InferenceSession(str(model_dir / "pnc.int8.onnx"), providers=["CPUExecutionProvider"])
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def capitalize_initial(word):
|
| 65 |
+
for index, character in enumerate(word):
|
| 66 |
+
if character.isalpha():
|
| 67 |
+
return word[:index] + character.upper() + word[index + 1:]
|
| 68 |
+
return word
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def punctuate(text):
|
| 72 |
+
words = text.split()
|
| 73 |
+
if not words:
|
| 74 |
+
return ""
|
| 75 |
+
|
| 76 |
+
encoding = tokenizer.encode(words, is_pretokenized=True)
|
| 77 |
+
counts = [0] * len(words)
|
| 78 |
+
special_tokens = 0
|
| 79 |
+
for word_index in encoding.word_ids:
|
| 80 |
+
if word_index is None:
|
| 81 |
+
special_tokens += 1
|
| 82 |
+
else:
|
| 83 |
+
counts[word_index] += 1
|
| 84 |
+
prefix = [0]
|
| 85 |
+
for count in counts:
|
| 86 |
+
prefix.append(prefix[-1] + count)
|
| 87 |
+
|
| 88 |
+
def window_end(start):
|
| 89 |
+
budget = prefix[start] + 128 - special_tokens
|
| 90 |
+
return min(len(words), start + 128,
|
| 91 |
+
max(start + 1, bisect_right(prefix, budget) - 1))
|
| 92 |
+
|
| 93 |
+
output = []
|
| 94 |
+
next_word = 0
|
| 95 |
+
while next_word < len(words):
|
| 96 |
+
start = max(0, next_word - 12)
|
| 97 |
+
end = window_end(start)
|
| 98 |
+
if end <= next_word:
|
| 99 |
+
start, end = next_word, window_end(next_word)
|
| 100 |
+
window_words = words[start:end]
|
| 101 |
+
encoded = tokenizer.encode(window_words, is_pretokenized=True)
|
| 102 |
+
inputs = {
|
| 103 |
+
"input_ids": np.array([encoded.ids], dtype=np.int64),
|
| 104 |
+
"attention_mask": np.ones((1, len(encoded.ids)), dtype=np.int64),
|
| 105 |
+
}
|
| 106 |
+
predictions = session.run(None, inputs)[0][0].argmax(-1)
|
| 107 |
+
rendered = window_words.copy()
|
| 108 |
+
seen = set()
|
| 109 |
+
for token_index, word_index in enumerate(encoded.word_ids):
|
| 110 |
+
if word_index is None or word_index in seen:
|
| 111 |
+
continue
|
| 112 |
+
seen.add(word_index)
|
| 113 |
+
label = labels[str(int(predictions[token_index]))]
|
| 114 |
+
upper = label.endswith("|U")
|
| 115 |
+
mark = label[:-2] if upper else label
|
| 116 |
+
word = window_words[word_index]
|
| 117 |
+
rendered[word_index] = (word.capitalize() if upper else word) + ("" if mark == "O" else mark)
|
| 118 |
+
emit_end = end if end == len(words) else max(next_word + 1, end - 12)
|
| 119 |
+
output.extend(rendered[next_word - start:emit_end - start])
|
| 120 |
+
next_word = emit_end
|
| 121 |
+
|
| 122 |
+
if len(output) != len(words):
|
| 123 |
+
raise RuntimeError("PnC changed the number of words")
|
| 124 |
+
output[0] = capitalize_initial(output[0])
|
| 125 |
+
for index, word in enumerate(output[:-1]):
|
| 126 |
+
abbreviation = any(word[i - 1].isalpha() and word[i + 1].isalpha()
|
| 127 |
+
for i in range(1, len(word) - 1) if word[i] == ".")
|
| 128 |
+
if word.endswith((".", "!", "?")) and not abbreviation \
|
| 129 |
+
and not any(char.isupper() for char in output[index + 1]):
|
| 130 |
+
output[index + 1] = capitalize_initial(output[index + 1])
|
| 131 |
+
return " ".join(output)
|
| 132 |
+
|
| 133 |
+
print(punctuate("hej mit navn er emil hvordan går det i dag"))
|
| 134 |
# Hej, mit navn er Emil. Hvordan går det i dag?
|
| 135 |
```
|
| 136 |
|
| 137 |
Run it with [uv](https://docs.astral.sh/uv/):
|
| 138 |
|
| 139 |
```bash
|
| 140 |
+
uv run --no-project --with huggingface-hub --with onnxruntime==1.29.0 \
|
| 141 |
+
--with tokenizers==0.22.2 --with numpy python pnc_demo.py
|
| 142 |
```
|
| 143 |
|
| 144 |
The first run downloads the pinned int8 model; later runs use the Hugging Face
|
| 145 |
+
cache. The example uses only published packages and model files; it does not
|
| 146 |
+
install code from GitHub. It mirrors the Ekko runtime's **128-token windows
|
| 147 |
+
with 12 words of overlap**, so it also handles longer transcripts while keeping
|
| 148 |
+
every input word in order. Pass continuous text when possible so the model can
|
| 149 |
+
use context across ASR chunks. These settings affect inference, not the v2
|
| 150 |
+
model weights.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
|
| 152 |
Version 2 is tuned for **continuous speech** such as dictation and meetings.
|
| 153 |
The earlier version often missed sentence endings when given a long transcript;
|