| --- |
| 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. |
| |