File size: 8,028 Bytes
c50b158
 
 
d4cb542
5b90a6e
 
c50b158
 
 
13f8026
539e399
 
 
 
 
c50b158
313b5f6
13f8026
c50b158
7e2069c
c50b158
13f8026
 
c50b158
 
2c3f393
eb65df1
2c3f393
eb65df1
2c3f393
 
fde1765
2c3f393
 
eb65df1
fde1765
 
 
c50b158
28bdfae
0ad1fd7
de465d1
0ad1fd7
 
de465d1
0ad1fd7
 
df2c1c4
 
de465d1
 
28bdfae
 
 
1d05f98
d1372b2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fde1765
 
 
 
 
c50b158
7e2069c
c50b158
fde1765
 
 
 
 
c50b158
fde1765
 
7e2069c
fde1765
9b0372a
fde1765
 
7e2069c
fde1765
80d2347
 
bbdb023
 
 
 
 
 
7e2069c
fde1765
 
 
c50b158
5bb210a
 
a6fa157
 
 
70becac
5bb210a
fde1765
c50b158
fde1765
 
 
 
c50b158
fde1765
 
c50b158
fde1765
 
c50b158
fde1765
5b90a6e
7e2069c
 
 
 
 
 
 
fde1765
 
 
 
 
 
 
 
 
 
80d2347
 
 
 
fde1765
 
 
 
 
 
 
 
 
 
 
d4cb542
 
5b90a6e
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
---
language:
- da
license: cc-by-4.0
license_link: https://huggingface.co/RyeAI/ekko-pnc/blob/main/MODEL_LICENSE.md
thumbnail: https://huggingface.co/RyeAI/ekko-pnc/resolve/d3129544e4e0155093e192cac58a9e69a4dea1ff/cover.png
library_name: transformers
pipeline_tag: token-classification
base_model: jonfd/electra-small-nordic
base_model_relation: finetune
datasets:
- alexandrainst/nst-da
- alexandrainst/ftspeech
- CoRal-project/coral-v3
- wikimedia/wikipedia
tags:
- punctuation-restoration
- sentence-boundary-detection
- truecasing
- danish
- onnx
metrics:
- f1
---

![Ekko PnC v2 — Danish punctuation restoration](cover.png)

# Ekko PnC v2 — Danish punctuation restoration and truecasing

Ekko PnC v2 restores punctuation and capitalization in Danish speech transcripts.
It adds commas, full stops, question marks, and exclamation marks while
keeping the original words in the same order. This makes it suitable for
transcripts with word timestamps. The int8 ONNX model runs locally. Ekko v1 Tiny
is a separate speech recognizer; PnC v2 can process its output.

> **Input:** hej mit navn er emil hvordan går det i dag jeg har været på kontoret og bagefter skal jeg hjem
>
> **Output:** Hej, mit navn er Emil. Hvordan går det i dag? Jeg har været på kontoret, og bagefter skal jeg hjem.

## Quickstart

With [uv](https://docs.astral.sh/uv/) and Python 3.11+:

```bash
uvx --from 'ekko-stt[pnc]' ekko pnc "hej mit navn er emil hvordan går det i dag"
```

Expected output: `Hej, mit navn er Emil. Hvordan går det i dag?`

The command installs the [Ekko STT package](https://pypi.org/project/ekko-stt/).
The first PnC run downloads the pinned int8 model from Hugging Face; later runs use the cache.
For longer transcripts, the runtime uses 128-token windows with 12 words of overlap
and keeps every input word in order. Pass continuous text when possible so PnC can
use context across speech recognition chunks.

### Python with ONNX

Install `ekko-stt[pnc]` in your Python environment. This loads the int8 ONNX
export and applies the same windowing as the command above:

```python
from ekko import Punctuator
from ekko.artifacts import ensure_pnc_directory

pnc = Punctuator(ensure_pnc_directory())
print(pnc("hej mit navn er emil hvordan går det i dag"))
```

<details>
<summary>Python with Transformers (PyTorch)</summary>

Install `transformers` and `torch` to use the PyTorch weights directly. This
short-input example maps predictions from the first token of each word back to
the original words:

```python
import torch
from transformers import AutoModelForTokenClassification, AutoTokenizer

repo = "RyeAI/ekko-pnc"
tokenizer = AutoTokenizer.from_pretrained(repo)
model = AutoModelForTokenClassification.from_pretrained(repo).eval()
words = "hej mit navn er emil hvordan går det i dag".split()
inputs = tokenizer(words, is_split_into_words=True, return_tensors="pt")

with torch.inference_mode():
    predictions = model(**inputs).logits.argmax(dim=-1)[0].tolist()

output, seen, capitalize_next = [], set(), True
for word_id, prediction in zip(inputs.word_ids(), predictions):
    if word_id is None or word_id in seen:
        continue
    seen.add(word_id)
    label = model.config.id2label[prediction]
    mark = label.removesuffix("|U")
    word = words[word_id]
    if capitalize_next or label.endswith("|U"):
        word = word[:1].upper() + word[1:]
    output.append(word + ("" if mark == "O" else mark))
    capitalize_next = mark in {".", "?", "!"}

print(" ".join(output))
```

</details>

For longer transcripts, use the ONNX example above; it preserves all words
across overlapping windows.

Version 2 is tuned for **continuous speech** such as dictation and meetings.
The earlier version often missed sentence endings when given a long transcript;
it had mainly learned from short, isolated utterances. Version 2 was trained
on longer stretches of conversation and is substantially better at finding
sentence boundaries in that setting.

## Results

The main evaluation used 88 clips from 8 meetings (4,322 words), with
punctuation reviewed by a person. Both versions received the *same correct
words*, so these scores measure punctuation and casing rather than speech
recognition. The numbers below use the FP32 ONNX model.
F1 combines precision and recall; higher is better.

| Model | Sentence boundary F1 | Boundary recall | Boundary precision | Full stop F1 | Question F1 | Casing F1 |
|---|---:|---:|---:|---:|---:|---:|
| v1 | 0.402 | 0.264 | 0.849 | 0.350 | 0.333 | 0.523 |
| **v2** | **0.698** | **0.619** | 0.801 | **0.654** | **0.462** | **0.736** |

The v2 casing score includes a simple rule that capitalizes the word after a
predicted sentence ending. Without that rule, its casing F1 was 0.653.

On 55 held-out CoRal conversations, v2 improved sentence boundary F1 from
0.095 to 0.474. The punctuation reference for this set was machine-generated
and checked to preserve every word, so this result measures agreement with
that reference. This earlier comparison used the settings
`max_length=256, overlap_words=24`. In a later runtime comparison on the same
conversations, the same v2 int8 weights scored 0.471 with 256/24 and **0.599
with the recommended 128/12**.
The set has been used in earlier experiments, so this is guidance for runtime
configuration rather than an independent evaluation of new model weights.

Very short clips are a trade-off: on isolated 3–4 second utterances, boundary
F1 decreased from 0.865 (v1) to 0.818 (v2). Version 2 may add extra full stops
on such inputs.

## Speed

**1,000 Danish words in about 0.09 seconds** on an Apple M1 Pro CPU, after model
loading. The int8 ONNX benchmark includes tokenization, inference, and rendering;
it excludes download, speech recognition, and browser WASM. Speed varies by device
and input length.

## Files and use

| Format | Files | When to use |
|---|---|---|
| int8 ONNX | `pnc.int8.onnx`, `tokenizer.json`, `config.json` | Local and browser inference; about 22.4 MB for the model file |
| FP32 ONNX | `pnc.onnx`, `pnc.onnx.data`, `tokenizer.json`, `config.json` | Best measured quality when memory is less constrained |

On the meeting evaluation, int8 was behind FP32 by 0.006 boundary F1,
0.022 question F1, and 0.016 casing F1.

<details>
<summary>Exact revision and checksums</summary>

The int8 runtime pins revision
`d3129544e4e0155093e192cac58a9e69a4dea1ff`. SHA-256 checksums:

```text
pnc.int8.onnx  46bb979d409731872f10b6cb71298594595f16368d2aec2fcaf94d87b4266527
tokenizer.json a1e0d43ee307ad7a1ccc23e6e5398d877a68ca7f576067ac507625a0d50e9502
config.json    dc655c46f69eeb15dd43786ad58fb5862cc794b36cc9d2c426ab5bc4b01f23ca
```

The files in this repository also have checksums in `SHA256SUMS`. Version 1
remains available through the `v1` tag.

</details>

## Training and limitations

Ekko PnC builds on
[`jonfd/electra-small-nordic`](https://huggingface.co/jonfd/electra-small-nordic).
Its Danish training text included Wikipedia, NST, CoRal, and FT Speech.
Training combined human-punctuated source text with machine-generated
punctuation labels. The main meeting evaluation above uses human-reviewed
punctuation; the CoRal conversation evaluation uses machine-generated
punctuation references.

- Punctuation comes from text alone; the model cannot hear pauses or intonation.
  Question marks remain its weakest common punctuation class.
- It handles capitalization as an initial uppercase letter. It cannot create
  mixed-case words such as `iPhone` from lowercase input; existing capitals
  are preserved by the Ekko runtimes.
- The human-reviewed meeting set is small: 8 meetings and 324 reference full
  stops. Results on other speaking styles may differ.

## License and data terms

Ekko PnC v2 is released under CC BY 4.0. See
[`MODEL_LICENSE.md`](MODEL_LICENSE.md), [`NOTICE`](NOTICE), and
[`LICENSES/`](LICENSES/) for the terms and source notices. CoRal's use
restrictions also apply to this trained model. The FT Speech and NST notices
are included in the license bundle.