emilstabil commited on
Commit
7bd8ac9
·
verified ·
1 Parent(s): bbdb023

Document standalone ONNX PnC quickstart

Browse files
Files changed (1) hide show
  1. README.md +92 -18
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 ekko import Punctuator
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
- pnc = Punctuator(model_dir, max_length=128, overlap_words=12)
55
- print(pnc("hej mit navn er emil hvordan går det i dag"))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 "ekko-stt[pnc] @ git+https://github.com/Rye-A1/ekko-danish-stt.git" python pnc_demo.py
 
63
  ```
64
 
65
  The first run downloads the pinned int8 model; later runs use the Hugging Face
66
- cache. `Punctuator` also accepts longer text and keeps the input words in order.
67
- For longer transcripts, use **`max_length=128, overlap_words=12`**, as shown
68
- above. These are the current `Punctuator` defaults. `max_length` counts
69
- tokenizer subwords (including special tokens); `overlap_words` is a context
70
- margin measured in original words. Pass a continuous transcript when possible
71
- so the model can use context across ASR chunks. If you run the ONNX model
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;