sttlab-tech 936f1b6402 serialize ASR across tracks onto one shared worker, cut 1.7B peak memory ~13GB->~7GB
qwen_asr has no quantized-weights option, so the only lever available to
reduce the 1.7B model's memory footprint is avoiding concurrent instances.
Previously each track (system, mic) ran its own transcribeTrack goroutine
with independent qwen_asr subprocesses, so simultaneous speech on both
tracks meant two ~6.9GiB model instances alive at once - over half the
24GB target machine's memory. Merge both tracks onto a single worker
(asrSegs channel, dispatch() routes both tracks onto it) so invocations
are strictly serialized; per-track order is preserved since VAD's output
is already chronological. Verified via pgrep -x qwen_asr polling during a
real dual-track capture: never more than 1 concurrent process.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-10 11:30:23 +02:00

transcriptor-ai

Local, no-cloud meeting transcription for macOS. Captures system audio and microphone as two separate tracks, transcribes both live, and serves a live-updating transcript over HTTP (SSE). Everything runs on-device — no external API calls at runtime.

How it works

audiotee (system audio + mic, 2 tracks)
  → VAD (speech segmentation, per track)
  → qwen-asr (speech-to-text, per track)
  → live transcript, served over SSE

Two tracks (system audio vs microphone) act as a cheap "me vs them" diarization: whatever you hear vs whatever you say, without needing a real speaker-diarization model.

Prerequisites

  • macOS (Apple Silicon)
  • Go 1.26+ (brew install go)
  • audiotee — built and signed with a stable identity so its audio permissions survive rebuilds. Follow that repo's own README/CONTEXT.md first; you should end up with a working binary, e.g. at ~/bin/audiotee.
  • qwen-asr — vendored as a git submodule (third_party/qwen-asr, pinned to a specific commit), built automatically by scripts/build-dist.sh below. You still need to download a model yourself (not part of the build — see "Models"), since that's a multi-GB download, not something to fetch on every build:
    git submodule update --init third_party/qwen-asr   # if you haven't run build-dist.sh yet
    cd third_party/qwen-asr && ./download_model.sh --model large   # or --model small
    

llama.cpp (brew install llama.cpp) will be needed once post-processing (glossary correction, LLM reread) is implemented, but isn't used yet — no need to install it today.

Build

git clone --recurse-submodules <this repo>   # or: git submodule update --init
scripts/build-dist.sh

Builds qwen_asr (from the submodule) and transcriptor-ai into dist/. Also build transcript-tail directly, it's not part of dist/ (that's for what transcriptor-ui, the future native app, embeds — see CLAUDE.md):

go build -o transcript-tail ./cmd/transcript-tail

Usage

Start the transcriber:

./dist/transcriptor-ai \
  -asr-binary ./dist/qwen_asr \
  -asr-model-dir ./third_party/qwen-asr/qwen3-asr-1.7b \
  -transcript-file ~/Desktop/meeting-transcript.txt

In another terminal, follow the live transcript:

./transcript-tail

Output looks like:

[mic ] Bonjour, ceci est un test.
[system…] Hello, this is a partial hypothesis that may still be revised

(no marker = a closed/final segment; = a partial hypothesis, ASR-in-progress)

Stop with Ctrl+C — shutdown is graceful (in-flight transcriptions finish before exiting).

Flags

transcriptor-ai:

Flag Default Description
-audiotee-binary PATH, then ~/bin/audiotee Path to the audiotee binary
-asr-binary (required) Path to the qwen_asr binary
-asr-model-dir (required) Path to a qwen-asr model directory
-prompt (empty) Glossary biasing text passed to qwen-asr (e.g. "Preserve spelling: Kubernetes, PostgreSQL")
-http-addr :8420 Address to serve the live transcript SSE endpoint on
-transcript-file (empty) Path to append the live transcript to as plain text, flushed to disk after every line. Not written if empty.
-capture-system true Transcribe the system audio track. Note: audiotee's system tap always runs regardless of this flag — disabling it only stops transcribing that track, it doesn't skip the underlying capture (audiotee has no flag for that).
-capture-mic true Capture and transcribe the microphone track. Unlike -capture-system, disabling this actually skips mic capture entirely (no --capture-mic passed to audiotee, no microphone permission requested).

At least one of -capture-system/-capture-mic must stay enabled.

transcript-tail:

Flag Default Description
-url http://localhost:8420/events transcriptor-ai SSE endpoint to follow

Consuming the transcript programmatically

The SSE endpoint (http://localhost:8420/events by default) streams one JSON object per transcript segment:

{"track": "system", "text": "...", "is_final": false, "timestamp": "2026-08-07T14:32:01.123Z"}

Any SSE client works, e.g. curl -N http://localhost:8420/events, or a browser's EventSource.

Status

Live transcription (capture → VAD → ASR → SSE) works end to end, tested with real speech (English + French, code-switching) through an actual microphone. Not yet implemented: post-processing (glossary fuzzy-matching, LLM reread) and real-time content analysis — see CLAUDE.md for what's deliberately deferred and why.

  • audiotee — the system/mic audio capture CLI this project depends on.
  • antirez/qwen-asr — the speech-to-text engine used.
S
Description
No description provided
Readme 67 KiB
Languages
Go 97.6%
Shell 2.4%