File size: 9,125 Bytes
cf2e148
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2ee561c
8d36e37
 
 
 
 
 
 
 
 
1749c93
8d36e37
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2ee561c
 
cf2e148
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
---
license: other
license_name: s1-mini-license
license_link: LICENSE
library_name: coreai
pipeline_tag: text-generation
base_model: superwhisper/s1-mini
base_model_relation: quantized
language: [en]
tags: [core-ai, coreaikit, asr, speech-to-text, text-normalization,
       inverse-text-normalization, punctuation, truecasing, dictation,
       post-processing, on-device, apple, qwen3]
---

# S1-mini by Superwhisper β€” Core AI

[**S1-mini**](https://huggingface.co/superwhisper/s1-mini) by **Superwhisper** converted to
Apple **Core AI**, running fully on-device on iPhone and Mac.

S1-mini is a 0.6B **text normalizer for speech-to-text output**. Give it a raw ASR
transcript and it returns clean written text: fillers removed, false starts and
self-corrections resolved to whatever the speaker landed on, punctuation and capitalization
applied, and spoken numbers, dates, times, currency and email addresses rendered in written
form. It is not a chat model β€” it does one job, steered by a control line at the top of the
input.

That makes it the piece an on-device dictation stack is usually missing. Core AI already has
ASR (Parakeet, Nemotron-3.5-ASR-Streaming, Whisper, Qwen3-ASR); this is the post-processor
that turns a raw transcript into text a person would actually send, with nothing leaving the
device.

> **Naming.** The upstream license adds a term to Apache-2.0: any use, distribution or
> product integration must keep identifying this model as **"S1-mini"** by **"Superwhisper"**,
> with that exact capitalization, whatever the surrounding product is called. See
> [`LICENSE`](LICENSE).

<!-- gen-cards:use-it begin id=s1-mini (managed by scripts/gen-cards β€” edit cards.json / QuickStart.swift, not this block) -->
## Use it

⚑ **One line** β€” this model is the default behind the kit's task op
(`import CoreAIOps`; no session, no model plumbing, downloads on first use):

```swift
let clean = try await CoreAI.tidyTranscript(rawTranscript)
```

Every op, one shape β€” [Cookbook](https://github.com/john-rocky/coreai-kit/blob/main/docs/COOKBOOK.md).

▢️ **Run it (source)** β€” the [Tidy runner](https://github.com/john-rocky/coreai-kit/tree/main/Examples/Tidy)
(GUI + CLI, the three control axes as pickers):

```bash
git clone https://github.com/john-rocky/coreai-kit
open coreai-kit/Examples/Tidy/Tidy.xcodeproj
# β†’ Run, then pick "S1-mini by Superwhisper" in the model picker

# agents / headless (macOS):
cd coreai-kit/Examples/Tidy
swift run tidy-cli --model s1-mini --text "so um i need to like send the the report by uh friday no wait make that thursday"
```

πŸ’» **Build with it** β€” complete; the glue is kit API, copy-paste runs:

```swift
import CoreAIKit

let tidier = try await KitTextNormalizer(catalog: "s1-mini")
// Long input is cut at word boundaries into ~450-token chunks and the rewrites stitched:
// on iPhone the engine caps prompt + generated at 1024 tokens, so a whole meeting
// transcript passed in one call would stop mid-sentence.
let result = try await tidier.normalize(transcript)
// result: the transcript as written text β€” English only; filler-only input returns ""
```

The take-home is [`Examples/Tidy/Sources/QuickStart.swift`](https://github.com/john-rocky/coreai-kit/blob/main/Examples/Tidy/Sources/QuickStart.swift)
β€” this exact code as one typed function, no UI; both the runner's GUI and its CLI call it.
Cleaning transcripts repeatedly? Keep the `KitTextNormalizer` loaded and call
`normalize(_:)` per transcript β€” the 796 MB load is what you are avoiding.

**Integration checklist**

- SPM: `https://github.com/john-rocky/coreai-kit` β†’ product **CoreAIKit**
- Info.plist: none needed
- Entitlements: none needed
- First run downloads the model β€” 0.8 GB (Mac) / 0.8 GB (iPhone) β€” then it loads from the
  local cache (Application Support; progress via the `downloadProgress` callback)
- Measure in Release β€” Debug is ~3Γ— slower on per-token host work
<!-- gen-cards:use-it end -->

## Contents

`gpu-pipelined/s1_mini_decode_int8lin/` β€” decode bundle for the Core AI pipelined engine,
**759 MB**. Body int8 per-block-32; the head is tied to the embedding and stays fp16 (see
*Quantization* below). Runs unchanged on macOS and iOS β€” no AOT compile needed.

## Measured

| | decode | prefill | numerics |
|---|---:|---:|---|
| **M4 Max** (Mac Studio, macOS 27.0) | **268.4 tok/s** | **4161 tok/s** | 16/16 token-exact vs the fp32 HF oracle |
| **iPhone 17 Pro** (A19 Pro, Release) | **62.4 tok/s** | **69.0 tok/s** | 276/276 + 27/27 token-exact vs the Mac engine |

`device == Mac == fp32 HF`. Load 0.2–1.0 s on device. The iPhone numbers are cold and
reproduced across two runs; **under sustained load expect about half** (34.9 prefill / 30.5
decode after back-to-back 1024-token generations, restored by seven idle minutes β€” thermal,
not a regression). A dictation post-processor runs repeatedly, so plan against the sustained
number.

### Task quality

The conversion gate above is a free-run continuation, which on a single-task model measures
the base language prior and very little of the task. So this port also gates the model in its
own input format, across the card's three control axes, against the released weights run
through `transformers`: **13/14**, the one miss being punctuation
(`$23,450 and` for `$23,450, and`).

## ⚠️ iPhone ceiling: prompt + generated must stay under 1024 tokens

Measured: a 611-token transcript whose rewrite runs 603 tokens produced **413 tokens on
device, every one token-identical to the Mac**, then stopped at absolute position exactly
**1024**. Truncation, not corruption.

This is shipped engine behaviour β€” `CoreAIPipelinedEngine` caps iOS growing-KV capacity at
1024 (`1024 - processed - prompt.count`) and throws `contextLengthExceeded` when a prompt
leaves no budget, guarding the iOS compiler's miscompilation of growing-KV specializations at
seq β‰₯ 2048. `1024 βˆ’ 611 = 413`, exactly the measurement.

**Chunk input to roughly ≀450–500 tokens** so prompt + rewrite clears the cap. macOS has no
such cap.

## Prompt format β€” `enable_thinking=False` is mandatory

The system prompt and the control line are part of the trained input format. Leave thinking
on and the model emits an empty `<think>` block and stops: every call returns the empty
string, which reads like a working pipeline producing nothing.

```
<|im_start|>system
You are a text normalizer for speech-to-text transcripts. The input begins with a control line specifying the styling, structure, and context settings; clean the transcript to match those settings and output only the cleaned text.<|im_end|>
<|im_start|>user
[Styling: semi-formal] [Structure: prose] [Context: general]
<raw transcript><|im_end|>
<|im_start|>assistant
<think>

</think>

```

`Styling` ∈ `casual` / `semi-casual` / `semi-formal` / `formal` ·
`Structure` ∈ `prose` / `lists` · `Context` ∈ `general` / `email`.
All three axes are independent and every combination was trained.

Example, `[Styling: semi-formal] [Structure: prose] [Context: general]`:

| in | out |
|---|---|
| `so um i need to like send the the report by uh friday no wait make that thursday` | `So I need to send the report by Thursday.` |
| `the invoice came to twenty three thousand four hundred and fifty dollars and it's due on march third twenty twenty six` | `The invoice came to $23,450 and it's due on March 3, 2026.` |
| `um` | *(empty string)* |

## Quantization

- **Body int8**, per-block-32 `symmetric_with_clipping`; norms, RoPE, SDPA and the embedding
  stay full precision.
- **No head quantization, on purpose.** The head is *tied* to the 151936Γ—1024 embedding, and
  the eager quantizer skips shared params β€” so quantizing it means untying it first, which
  *adds* a tensor rather than shrinking one: tied fp16 embed+head is 311 MB, while fp16 embed
  + int8 untied head is 311 + 156 = 467 MB. Untying is a pure loss at every bit width.
- **int4 is a measured no-go and is not published here.** It is 549 MB, decodes faster, and
  passes the *same* 16/16 fp32 oracle gate β€” and it corrupts digits: `$23,450` β†’ `$2,345`,
  `107` β†’ `177`, and it drops "tomorrow" from a time normalization. For a model whose job
  includes inverse text normalization of money and counts, that closes it. Only the
  task-format gate sees this; the continuation gate is blind to it.

## Reproduce

Exporter, gates, card and port notes live in the
[Core AI model zoo](https://github.com/john-rocky/coreai-model-zoo):
[`models/s1-mini/`](https://github.com/john-rocky/coreai-model-zoo/tree/main/models/s1-mini),
[`conversion/export_s1_mini_decode_pipelined.py`](https://github.com/john-rocky/coreai-model-zoo/blob/main/conversion/export_s1_mini_decode_pipelined.py),
[`knowledge/s1-mini-port.md`](https://github.com/john-rocky/coreai-model-zoo/blob/main/knowledge/s1-mini-port.md).

```bash
python3 conversion/zoo_convert.py show s1-mini
python3 conversion/zoo_convert.py run  s1-mini
```

## Credits

Model: **S1-mini** by **Superwhisper** ([superwhisper.com](https://superwhisper.com)).
Core AI conversion: the Core AI model zoo.