hayamimi README

The project's README from github.com/oboroge0/hayamimi. As one markdown file: README.md.

hayamimi (早耳)

tests license release

Real-time, multilingual speech-to-text on CPU only. Live subtitles, a browser dashboard, speaker labels, and on-the-fly translation -- no GPU, no cloud API, under 2GB RAM.

日本語版 README は README.ja.md にあります。

"早耳" (hayamimi) is Japanese for "quick ear" -- someone who picks up on things fast. That's the design goal: partial subtitles appear while you're still talking, and a finalized line lands roughly 100ms after you stop.

Why

Most CPU-only real-time transcription setups fall back to a single general-purpose model (Whisper) and accept its accuracy ceiling. hayamimi instead routes each utterance to whichever specialist model is best for its language, all running as quantized (INT8) ONNX models via sherpa-onnx -- no PyTorch, no CUDA.

On real broadcast Japanese audio (see docs/results/scorecard.md), that routing gets 3.8% CER (remeasured 2026-09-01 with the head-dropout fix and CJK number normalization in the pipeline), vs whisper-large-v3-turbo's 13.8% on the same clips, while running at 10-50x realtime on a 6-core desktop CPU. A full comparison against Whisper variants, cloud STT APIs, and other local models -- including the languages where hayamimi loses -- is in docs/results/comparison.md.

Features

Feature What it does
5-route language catalog ja/zh/ko/yue/en+24 EU languages each go to a dedicated best-in-class model; everything else (~1600 languages) falls back to Meta's Omnilingual ASR
Partial subtitles in-progress draft text updates every ~0.5s while you're still speaking
Fast finals a finalized line typically lands ~100ms after you stop talking (ja; see docs/design/goals.md for other languages)
Two-pass refinement after 2s of silence, recent utterances are batch re-decoded for a higher-accuracy "clean" transcript (ja real-broadcast CER 15.5% -> 12.0%)
Speaker labels --speakers tags each utterance S1/S2/... live via CAM++ nearest-centroid matching, then the refine pass re-diarizes each group with pyannote segmentation-3.0 and remaps clusters onto the same S{n} labels (mean DER 25.7% -> 13.9% on 5 AMI meetings, see Limitations)
Translation --translate en,zh,ko,es,... translates Japanese lines live (en via FuguMT; any other M2M-100 target code is accepted if the model's vocabulary supports it -- zh/ko/es have measured quality, see docs/design/translate_m2m.md)
Hotwords / user dictionary --hotwords biases decoding toward proper nouns (currently has no effect on the ja tier -- see Limitations); --replace does post-hoc find/replace and works everywhere
CJK number normalization conservative kanji-numeral -> arabic-digit conversion (ja/zh/yue only): magnitude numbers, 点-decimals, and digit-string years/codes; idioms/proper nouns are left alone by default (scripts/itn_cjk.py)
Runtime dictionary APIs RoutedASR.set_replacements()/set_itn_overrides() swap the find/replace and ITN exclude/force dictionaries mid-session; with --serve, GET/POST /replacements and /itn_overrides do the same over HTTP
Structured events + runtime config every stage of the pipeline (finals, translations, model loads, warnings, session summaries...) publishes to an EventHub an embedding app can listen to directly; --serve additionally exposes GET/POST /config and POST /reset for changing language/translation/VAD settings and resetting a session without restarting the process -- see "Embedding: runtime control and structured events" below
OBS overlay + dashboard --serve starts a local HTTP server with a browser-source overlay and a live dashboard
Network audio input --input ws accepts mic audio over a WebSocket (phone, ESP32/stackchan) and feeds it through the same pipeline, including --serve's dashboard/overlay
Memory-bounded LRU model eviction keeps resident models under a configurable cap (default: <2GB total)
CPU-only every model runs as quantized ONNX via sherpa-onnx; no GPU or PyTorch required

Demo UI

--serve starts a local server exposing three views:

dashboard

🎬 Watch the demo video — real 4-language audio (ja/en/ko/zh) transcribed live, replayed frame-accurately from a captured session.

Network audio input

--input ws runs a WebSocket ingest endpoint instead of reading the local microphone, so a phone or a stackchan-class ESP32 board can stream mic audio over the LAN and get it transcribed through hayamimi's normal pipeline:

.venv/Scripts/python scripts/realtime_transcribe.py --input ws --serve
# -> ws://<host>:8766/ingest accepts audio; http://localhost:8833/dashboard shows the results

Protocol: connect to /ingest, send one JSON text frame ({"sr": 16000, "format": "pcm_s16le", "channels": 1}), then stream raw pcm_s16le audio as binary frames. The server resamples non-16kHz audio and replies with the same partial/final/translation/refine JSON events the dashboard's SSE stream carries, so a client can show its own subtitles too. Only one audio-producing client is accepted at a time; scripts/ws_mic_client.py is a dependency-free reference client (streams a wav file at real-time pace) that doubles as a template for a phone/ESP32 implementation.

--input ws binds 127.0.0.1 by default, so the endpoint is localhost-only until you opt in with --ws-host 0.0.0.0 -- do that only on a network you trust, since /ingest has no authentication. The bound address is printed to stderr on startup either way.

Embedding in another app

scripts/realtime_transcribe.py's pieces (RoutedASR, build_vad, run_stream) are importable, not just CLI-only, and the session they build can be reconfigured and observed while it runs: hub.add_listener(callback) delivers structured JSON events (partial, final, refine, model_load, warning, ...) in-process with no HTTP server, and --serve adds GET/POST /config and POST /reset over the same session. The full guide -- the event table, the /config keys, and what POST /reset does -- is in docs/guide/embedding.md. For Flutter/Dart, see mobile/hayamimi_core/README.md.

Requirements

Python 3.10+ and ffmpeg on PATH. Developed and tested on Windows 11; macOS/Linux are expected to work (all runtimes are cross-platform) but are not yet CI-tested end to end — reports welcome.

Quickstart

python -m venv .venv

# Windows
.venv\Scripts\pip install -r requirements.txt
.venv\Scripts\python scripts\download_models.py

# macOS / Linux
.venv/bin/pip install -r requirements.txt
.venv/bin/python scripts/download_models.py

# Real-time transcription from your microphone
.venv/Scripts/python scripts/realtime_transcribe.py     # Windows
.venv/bin/python scripts/realtime_transcribe.py          # macOS/Linux

# With the dashboard + OBS overlay
.venv/Scripts/python scripts/realtime_transcribe.py --serve
# -> open http://localhost:8833/dashboard in a browser

scripts/download_models.py pulls ~3.1GB of pretrained models into models/ (git-ignored). Pass --minimal for a ~1.1GB ja/en-only install (ReazonSpeech, whisper-tiny, Silero VAD, Japanese punctuation). See THIRD_PARTY_NOTICES.md for what each model's license commits you to.

CLI reference

All flags are on scripts/realtime_transcribe.py:

Flag Default Description
--wav PATH mic input simulate streaming from a 16kHz mono WAV file instead of the microphone
--no-realtime off with --wav, don't sleep between chunks (fast batch processing)
--input {mic,wav,ws} mic, or wav if --wav is given audio source; ws accepts audio over the network (see below)
--ws-host HOST 127.0.0.1 bind host for --input ws's /ingest endpoint; pass 0.0.0.0 to accept LAN clients
--ws-port PORT 8766 port for --input ws's /ingest endpoint
--threads N 4 inference threads per model
--no-partial off disable in-progress draft subtitles
--min-silence SEC 0.35 silence duration that ends an utterance; lower = snappier finals, more splits
--max-speech SEC 12.0 force-finalize an utterance after this many seconds of continuous speech
--max-resident N 3 max non-tier0 models kept resident (LRU eviction); <=0 = unlimited
--serve [PORT] off, 8833 serve the dashboard + OBS overlay at http://localhost:PORT
--no-refine off disable the second-pass re-decode of utterance groups
--transcript PATH none append refined transcript lines to this file
--hotwords PATH none hotword list (one per line) to bias decoding toward proper nouns -- currently has no effect on the ja tier (ReazonSpeech's byte-level BPE tokens.txt can't encode them; a startup warning tells you how many failed). Use --replace for ja proper nouns instead
--replace PATH none user dictionary: wrong=right per line, applied to all output
--mode {single,balanced,fast} balanced language-switching preset. balanced switches only when both language detectors agree (see docs/eval/lid.md); single pins the language given by --lang and never switches; fast switches on every detection, matching pre-v0.2.0 behavior. Individual flags below override the preset
--lang-switch-guard SEC 2.0 treat a new-language detection shorter than this as noise: it can never count toward confirming a switch (see --lid-switch-confirm) and it suppresses the omnilingual fallback on an empty decode (0 disables)
--lid-switch-confirm N 2 consecutive new-language detections (each >= --lang-switch-guard long) required before the session actually switches language; raise for stickier single-language sessions
--speakers off label utterances with speaker ids (S1, S2, ...); the refine pass re-diarizes each group with pyannote segmentation-3.0
--speaker-remap-threshold T 0.35 cosine-similarity threshold for mapping the refine pass's local diarization clusters onto the session's global S{n} labels (the live fast path keeps its own 0.45 threshold)
--translate [LANGS] off, en translate Japanese lines to these comma-separated languages. en uses the dedicated FuguMT module; any other M2M-100 target code (zh, ko, es, fr, ...) is accepted if the model's vocabulary supports it -- unvalidated targets (anything outside zh/ko/es) print a quality-not-measured note, see docs/design/translate_m2m.md

Architecture

                          ┌─────────────┐
  mic / wav ───────────▶ │  Silero VAD │  0.35s end-of-speech + 0.8s preroll
                          └──────┬──────┘
                                 │ speech segment
                                 ▼
                   ┌───────────────────────────┐
                   │  whisper-tiny spoken-LID   │  runs on first ~4s while
                   │  (+ char-set arbitration)  │  the segment is still coming in
                   └─────────────┬─────────────┘
                                 │ language tag
                 ┌───────────────┼────────────────┬─────────────┬──────────────┐
                 ▼               ▼                ▼             ▼              ▼
             ┌───────┐      ┌─────────┐      ┌──────────┐  ┌─────────┐   ┌──────────┐
             │  ja   │      │   zh    │      │  ko/yue  │  │ en + 24 │   │  ~1600   │
             │ Reazon│      │Paraformer│      │SenseVoice│  │EU langs │   │  other   │
             │Speech │      │   -zh   │      │  small   │  │Parakeet │   │Omnilingual│
             │Zipform│      │         │      │          │  │TDT v3   │   │  ASR     │
             └───┬───┘      └────┬────┘      └────┬─────┘  └────┬────┘   └────┬─────┘
                 └───────────────┴────────────────┴─────────────┴─────────────┘
                                                │
                     partial (every ~0.5s)      │      final (~0.1s after end-of-speech)
                     ◀───────────────────────────┴───────────────────────▶
                                                │
                          ┌─────────────────────┼─────────────────────┐
                          ▼                     ▼                     ▼
                 ┌────────────────┐   ┌──────────────────┐   ┌────────────────┐
                 │ ja punctuation  │   │ speaker labeling  │   │  translation    │
                 │ (BERT restore)  │   │ (CAM++, --speakers)│   │ (FuguMT/M2M-100)│
                 └────────────────┘   └──────────────────┘   └────────────────┘
                                                │
                     2s silence: batch re-decode recent utterances (two-pass refine)
                                                │
                                                ▼
                              dashboard / OBS overlay / transcript file

Models are lazy-loaded on first use; an LRU cache evicts the least-recently-used non-Japanese models (--max-resident) so memory stays bounded no matter how many languages a session wanders through.

Measured performance

End-to-end (LID -> routing -> decode -> ja punctuation), real speech, no preroll/two-pass (single clips). en uses WER, all others use CER (yue t2s-normalized). Full methodology in docs/results/scorecard.md.

Language Clips LID accuracy Route Mean error Mean RTF
ja 15 15/15 ReazonSpeech 3.8% 0.090
en 15 15/15 Parakeet v3 2.3% 0.102
zh 12 12/12 Paraformer-zh 6.6%* 0.084
ko 12 12/12 SenseVoice 8.1% 0.060
yue 12 12/12 SenseVoice 6.1% 0.043

RTF (real-time factor) well under 0.2 across every route means each route runs 9-16x faster than realtime on CPU alone -- see docs/design/goals.md for the full target table and docs/results/benchmarks.md for the complete iteration log (30+ measured changes, latency/memory/accuracy tradeoffs and why each one was made or rejected).

Headline numbers from that log:

Limitations (honest list)

hayamimi (oboroge0/hayamimi)

Searching Database…

What is this?hayamimi is a real-time, multilingual speech-to-text tool that runs entirely on CPU, giving you live subtitles, a browser dashboard, speaker labels, and on-the-fly translation without a GPU or cloud API.
Is this a command-line tool, or does it run as a web service?It is a Python CLI that you run with flags, and it can optionally start a local HTTP server (--serve) that exposes a browser-source overlay, a live dashboard, and a small REST-style API for config and events.
How do I start a transcription session?You launch the CLI with flags for the features you want, such as --serve for the dashboard, --speakers for speaker labels, --translate for live translation, and --hotwords or --replace for vocabulary fixes.
Does it handle someone switching languages mid-sentence?No. The router picks one language per utterance, so a sentence that mixes two languages within itself gets the minority-language portion mangled or dropped; switching between full sentences works fine.
Who is this built for?Developers who want accurate live captions, translation, or speaker labels for streams, meetings, or broadcasts without GPU hardware or sending audio to a cloud service.
Does any audio or text leave my machine?No, the whole pipeline (speech recognition, translation, diarization) runs locally on CPU with no cloud API involved.
Which languages get a dedicated accuracy-tuned model instead of a generic fallback?Japanese, Chinese, Korean, Cantonese, English, and 24 EU languages each route to a dedicated best-in-class model (e.g. ReazonSpeech for ja, Parakeet for en); roughly 1600 other languages fall back to Meta's Omnilingual ASR.
How does its accuracy compare to Whisper?On real Japanese broadcast audio it measures 3.8% CER versus 13.8% for whisper-large-v3-turbo on the same clips, while running 10-50x realtime on a 6-core desktop CPU.
Can it tell different speakers apart?Yes, --speakers tags utterances live with CAM++ speaker matching, then a refine pass re-diarizes with pyannote segmentation-3.0, cutting mean diarization error from 25.7% to 13.9% in testing; overlapping speakers are not separated.
Can it translate the transcript as it goes?Yes, --translate turns on live translation of Japanese lines; English uses a dedicated FuguMT model, while other languages use M2M-100 with measured quality only verified for zh/ko/es.
Creator
oboroge0
Size
4.6 MB
Stars
446
Age
1mo
Commits · 90 Days