S1-mini by Superwhisper

S1-mini banner

Website Discord v1

A 0.6B-parameter text normalizer for speech-to-text output. It takes a raw ASR transcript and rewrites it as clean written text: fillers removed, false starts and self-corrections resolved to the value the speaker landed on, punctuation and capitalization applied, and spoken numbers, dates, times, currency and email addresses rendered in written form.

On a held-out set of 7,519 English cases it reaches 94.8% token accuracy, and the quantized build is a 462 MiB file that runs comfortably on a laptop CPU.

This is release v1, and it covers English only. S1-mini is not a chat model and will not follow general instructions; it does one job, and you steer it with a control line at the top of the input.

Fine-tuned from Qwen/Qwen3-0.6B. If you want to run it in llama.cpp, Ollama, LM Studio, or anything else built on llama.cpp, grab the GGUF builds from superwhisper/s1-mini-GGUF. You can use it in your own dictation app too, just check the license first.

Releases are tagged, so you can pin one: from_pretrained("superwhisper/s1-mini", revision="v1").

Model overview

Type Causal language model, fine-tuned for a single transformation task
Base model Qwen/Qwen3-0.6B
Parameters 596M total (0.44B non-embedding), embeddings tied
Layers 28
Attention heads 16 for Q, 8 for KV (GQA)
Precision BF16
Recommended input Up to ~1,000 tokens; chunk longer transcripts
Language English
License Apache 2.0 + naming clause (LICENSE)

The Hub sidebar reports 0.8B parameters for this repo. config.json sets tie_word_embeddings, but model.safetensors still stores lm_head.weight as a materialized copy of the input embedding, so the 155.6M-parameter embedding is counted twice: 751.6M tensor elements against 596.0M unique parameters. The layout is inherited from Qwen/Qwen3-0.6B, which reports 0.8B on the Hub for the same reason. The table above counts unique parameters.

Input. The model expects the system prompt, then a control line, a newline, and one raw ASR transcript, which will usually arrive lowercase and unpunctuated. That is the shape it was trained on.

Output. It returns the cleaned transcript as plain text and nothing else, with no preamble and no explanation. Under Structure: lists the output may contain Markdown bullets, and under Context: email it may contain blank lines separating a greeting, body and sign-off. When the input is nothing but filler or noise, the correct output is an empty string, and that is what you get.

Quickstart

Qwen3 support landed in transformers 4.51.0; with anything older you will get KeyError: 'qwen3'.

pip install "transformers>=4.51.0" torch
from transformers import AutoModelForCausalLM, AutoTokenizer

MODEL = "superwhisper/s1-mini"

tok = AutoTokenizer.from_pretrained(MODEL)
model = AutoModelForCausalLM.from_pretrained(MODEL, torch_dtype="auto")

# Required. Use this exact system prompt.
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."
)

def normalize(transcript, styling="semi-formal", structure="prose", context="general"):
    control = f"[Styling: {styling}] [Structure: {structure}] [Context: {context}]"
    messages = [
        {"role": "system", "content": SYSTEM},
        {"role": "user", "content": f"{control}\n{transcript}"},
    ]
    text = tok.apply_chat_template(
        messages,
        tokenize=False,
        add_generation_prompt=True,
        enable_thinking=False,      # required, see below
    )
    inputs = tok(text, return_tensors="pt").to(model.device)
    out = model.generate(**inputs, max_new_tokens=1024, do_sample=False)
    return tok.decode(out[0][inputs.input_ids.shape[1]:], skip_special_tokens=True)


raw = "so um i need to like send the the report by uh friday no wait make that thursday"
print(normalize(raw))
# I need to send the report by Thursday.

torch_dtype="auto" picks up the BF16 in config.json. At 0.6B this runs comfortably on CPU; add device_map="auto" to place it on a GPU automatically (requires accelerate).

The control line

Every input starts with a control line, then a newline, then the transcript:

[Styling: <value>] [Structure: <value>] [Context: <value>]
<raw transcript>
Axis Values What it does
Styling casual, semi-casual, semi-formal, formal Sets the register: how much capitalization, apostrophe and contraction cleanup to apply.
Structure prose, lists Whether the model may break enumerable content into a bulleted list. It needs at least three items, and anything that isn't really a list stays as prose.
Context general, email Destination conventions. email turns on greeting-line and sign-off-block layout.

The three axes are independent and every combination was trained.

The system prompt and the control line are part of the input format the model was trained on. Skip either one, change the system prompt's wording, or send values outside the trained sets, and the model can hallucinate or produce garbled output. Always send both, exactly as shown.

Styling

The register decides how much of the speaker's voice survives into the written text.

Value Behavior
casual Everything lowercase, apostrophes stripped, colloquialisms kept, final period usually omitted.
semi-casual Keeps the speaker's phrasing. I and its contractions are capitalized, sentence starts stay lowercase, final period usually omitted.
semi-formal Standard written English: full capitalization and punctuation, contractions kept, colloquialisms smoothed (gonna becomes going to). A good default.
formal Like semi-formal, with contractions expanded (I am, cannot).

Here is the same input under all four registers:

Input: hmm im gonna be late theres a cute dog outside i cant just walk past him

Styling Output
casual hmm im gonna be late. theres a cute dog outside. i cant just walk past him
semi-casual hmm, I'm gonna be late. there's a cute dog outside. I can't just walk past him
semi-formal I'm going to be late. There's a cute dog outside. I can't just walk past him.
formal I am going to be late. There is a cute dog outside. I cannot just walk past him.

Filled pauses like um and uh are removed in every register. Apostrophes are decided by the register rather than the raw transcript, so the input can arrive as im or I'm and the output comes out the same either way.

Structure

prose keeps everything in sentences and paragraphs. lists permits the model to break enumerable content into Markdown bullets, and it is deliberately conservative about it: it wants at least three items, and content that is not clearly an enumeration stays as prose. Here is the same input under both values:

Input: so for the trip we need to pack sunscreen and then also a first aid kit and um chargers for everything

Structure: prose

So for the trip, we need to pack sunscreen and then also a first aid kit and chargers for everything.

Structure: lists

So for the trip, we need to pack:
- Sunscreen
- A first aid kit
- Chargers for everything

Context

general produces flowing text, while email reshapes the transcript into email layout, with a greeting line, the body and a sign-off block separated by blank lines. Here is the same input under both values:

Input: hey sarah just wanted to follow up on the proposal can you send the numbers by end of week thanks john

Context: general

Hey Sarah, just wanted to follow up on the proposal. Can you send the numbers by end of week? Thanks, John.

Context: email

Hey Sarah,

Just wanted to follow up on the proposal. Can you send the numbers by end of week?

Thanks,
John

Examples

All of these were measured on the BF16 weights in this repo with greedy decoding, under [Styling: semi-formal] [Structure: prose] [Context: general].

Input Output
so um i need to like send the the report by uh friday no wait make that thursday I need to send the report by Thursday.
i think the answer is forty two no sorry forty three I think the answer is 43.
let's meet at half past two tomorrow uh actually make it three fifteen p m Let's meet at 3:15pm tomorrow.
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.
send it to support at superwhisper dot com Send it to support@superwhisper.com.
um (empty string)

Set enable_thinking=False

The chat template comes from Qwen3 unchanged, and Qwen3 turns on thinking mode by default. S1-mini was trained with thinking off and has no reasoning traces in its training data.

If you leave the flag out you will usually get no usable output at all: the model emits an empty <think> block and stops. This is the single most common way to get a blank result from this model.

The flag makes the template emit an empty think block before the assistant turn. That is the exact prefix the model saw during training. The template ships as chat_template.jinja in this repo, so apply_chat_template picks it up with no extra configuration. If you build prompts by hand instead, the literal string is:

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

Written out, the assistant prefix is <|im_start|>assistant\n<think>\n\n</think>\n\n, with two newlines inside the think block and two more after it.

Deployment

vLLM. Serve normally, then disable thinking per request:

vllm serve superwhisper/s1-mini
curl http://localhost:8000/v1/chat/completions -H "Content-Type: application/json" -d '{
  "model": "superwhisper/s1-mini",
  "messages": [
    {"role": "system", "content": "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."},
    {"role": "user", "content": "[Styling: semi-formal] [Structure: prose] [Context: general]\nso um send the report by uh friday"}
  ],
  "temperature": 0,
  "chat_template_kwargs": {"enable_thinking": false}
}'

SGLang. The same request shape works here, since chat_template_kwargs is supported on the OpenAI-compatible endpoint.

python -m sglang.launch_server --model-path superwhisper/s1-mini

llama.cpp. Use the GGUF builds. The template must be applied with thinking disabled, which means passing --jinja together with --chat-template-kwargs. Don't substitute --reasoning-budget 0, which suppresses the think block a different way and degrades the output:

llama-server -hf superwhisper/s1-mini-GGUF:Q4_K_M --jinja --chat-template-kwargs '{"enable_thinking":false}' --temp 0

Ollama and LM Studio. Both work from the GGUF builds, but both inherit Qwen3's thinking-on-by-default template. Make sure the assistant turn begins with the empty think block shown above, or you will get blank output.

Best practices

  1. Send the system prompt and the control line, exactly as documented. They are the only steering mechanism, and the model was never trained without them.
  2. Decode greedily. generation_config.json already ships do_sample: false, and for good reason: normalization is a deterministic transformation, and sampling only adds variance. If you override the config, use temperature 0.
  3. Size max_new_tokens to the input. Output length closely tracks input length; 1.3 × input_tokens + 32 is a safe ceiling, and much cheaper than leaving it at 1024.
  4. Chunk long transcripts at sentence boundaries. The model is built for dictation-length input; keep single passes under roughly 1,000 tokens.
  5. Expect an empty string sometimes. Filler-only input returns nothing, and your pipeline should treat that as a valid result rather than a failure.

Evaluation

Evaluated on a held-out English test set of 7,519 cases covering real ASR output and synthetic stress sets for numbers, self-corrections, lists, email and adversarial inputs: 94.8% token accuracy, measured greedy on the Q4_K_M GGUF build. The BF16 weights here should do at least as well.

Using S1-mini in your own app

S1-mini is Apache 2.0 plus a naming clause, the same base license it inherits from Qwen3-0.6B, so it can be embedded in open-source and commercial software alike: dictation apps, meeting-notes tools, live captioning, voice-driven editors, or any pipeline that has to turn raw ASR output into text a person will read.

It is a post-processing stage rather than a standalone system:

audio ──▶ ASR (Whisper, Parakeet, …) ──▶ S1-mini ──▶ clean text

The ASR's raw transcript becomes the transcript line, your app's settings choose the three control-line values, and the model returns text ready to display. At 0.6B it is small enough to ship on-device, and the GGUF builds exist for exactly that.

Nothing about the model is Superwhisper-specific. The two things to get right in any integration are the input format documented above and the thinking flag; nearly every integration bug traces back to one of those.

Read the LICENSE before you ship. Apache 2.0 is permissive but not obligation-free: you must retain the license text and attribution notices, and state significant changes if you redistribute a modified version. It also carries one additional term: the model must keep its name, "S1-mini" by "Superwhisper", with that exact capitalization, wherever it's used. If you are bundling S1-mini into a commercial dictation app or redistributing the weights yourself, confirm the terms cover your case rather than assuming they do.

License

S1-mini is released under Apache 2.0, which it inherits from Qwen3-0.6B, plus one additional term: wherever it's used, it must keep its name, "S1-mini" by "Superwhisper", with that exact capitalization. See LICENSE.

Citation

@misc{s1mini2026,
  title  = {S1-mini: a small text normalizer for speech-to-text output},
  author = {Superwhisper},
  year   = {2026},
  url    = {https://huggingface.co/superwhisper/s1-mini}
}

Built on Qwen3:

@misc{qwen3technicalreport,
  title         = {Qwen3 Technical Report},
  author        = {Qwen Team},
  year          = {2025},
  eprint        = {2505.09388},
  archivePrefix = {arXiv},
  primaryClass  = {cs.CL},
  url           = {https://arxiv.org/abs/2505.09388}
}
Downloads last month
-
Safetensors
Model size
0.8B params
Tensor type
BF16
·
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support

Model tree for superwhisper/s1-mini

Finetuned
Qwen/Qwen3-0.6B
Finetuned
(1183)
this model
Quantizations
3 models

Spaces using superwhisper/s1-mini 2

Paper for superwhisper/s1-mini