9.6 KiB
record-asr
A self-contained Go HTTP transcription service built on sherpa-onnx Go bindings, running SenseVoiceSmall (int8) for recognition and a VAD for segmentation.
- Runs on linux/amd64 and linux/arm64.
- Models download at build time into the Docker image (see Docker) — or, for the standalone binary, on first run.
- All runtime knobs are environment variables; an access password can
be set with
ASR_PASSWORD. - Uploaded tasks are persisted to disk (
data/tasks.json+data/uploads/) so they survive restarts, and can be deleted. - On error a task auto-retries with exponential backoff, up to
ASR_MAX_RETRIES; the worker then continues to the next task. You can also manually retry or skip (continue) any task via the API. - Ships a
Dockerfile+docker-compose.yml;docker buildxproduces a multi-arch image with no cross-compiler needed. - Bundles an embedded web console (
GET /) — drag-drop upload, live task queue, transcript view with per-segment timestamps, and retry/skip/delete — no separate frontend to deploy.
Note on the VAD. The original request asked for FSMN-VAD. The current
sherpa-onnxC API (v1.13.7) no longer exposes FSMN-VAD — itsVadModelConfigonly supportssilero_vadandten_vad(verified in the shippedc-api.hand across older tags). After confirming this with you, this project uses silero-vad as the maintained replacement. The rest of the spec (SenseVoiceSmall int8, Go, both arches, password, task save/delete, retry/continue) is implemented exactly as requested.
Build
Requires Go ≥ 1.22 and a C compiler (cgo is used by the sherpa-onnx bindings).
make build # portable native build -> dist/<arch>/{record-asr, lib/*.so}
The build bakes an $ORIGIN/lib rpath into the binary and copies the
sherpa-onnx / onnxruntime .so files next to it, so dist/<arch>/ is a
self-contained bundle — copy the folder anywhere and run it.
Both architectures
make build-amd64 # linux/amd64 (native toolchain)
make build-arm64 # linux/arm64 (cross-compile)
make dist # build both + tarballs in dist/
build-arm64 cross-compiles from amd64 and needs a C cross-compiler:
sudo apt-get install gcc-aarch64-linux-gnu # Debian/Ubuntu
The arm64 .so files are already present in the sherpa-onnx-go-linux module,
so no extra download is needed; only the cross-compiler is required. (This
environment has no aarch64-linux-gnu-gcc/qemu, so only the amd64 build was
runtime-tested here; the arm64 path uses identical build tags selecting the
arm64 .so.)
Docker
The multi-stage Dockerfile builds the binary, downloads the models at build
time, and bakes both into the image — so a container starts instantly and
needs no network at runtime.
docker build -t record-asr .
# Restricted-network build args (optional):
# GOPROXY Go module proxy (default proxy.golang.org)
# APT_MIRROR swap deb.debian.org for a faster mirror (e.g. mirrors.tuna.tsinghua.edu.cn)
docker build --build-arg GOPROXY=https://goproxy.cn,direct \
--build-arg APT_MIRROR=mirrors.tuna.tsinghua.edu.cn \
-t record-asr .
The image bundles ffmpeg (so .m4a/.aac/.opus/… work out of the box)
and the models (downloaded at build time).
Run a single container:
docker run -d -p 8080:8080 -e ASR_PASSWORD=changeme \
-v record-asr-data:/app/data record-asr
Or with docker compose (sets sensible defaults; only the password is
required):
ASR_PASSWORD=changeme docker compose up -d
The image is multi-arch. Publish both platforms at once with buildx —
each platform builds natively, so no cross-compiler is required (unlike
make build-arm64):
docker buildx build --platform linux/amd64,linux/arm64 \
-t record-asr:latest --push .
Models (~230 MB SenseVoice + silero VAD) live at
/modelsin the image. Only/app/data(tasks + uploads) is a volume; models are not on the volume, so they're not re-downloaded on restart. To upgrade a model, rebuild.
Run
# Set a password (recommended); pick a data dir; launch.
ASR_PASSWORD=changeme ASR_DATA_DIR=./data ./dist/$(go env GOARCH)/record-asr
On first start it downloads the models into <ASR_MODEL_DIR>/ (~230 MB
SenseVoice + ~2 MB silero VAD); subsequent starts load them from disk.
Environment variables
| Variable | Default | Description |
|---|---|---|
ASR_PASSWORD |
empty | Shared secret required on /api/*. If unset, the API is open and a warning is logged. |
ASR_ADDR |
:8080 |
Listen address. |
ASR_DATA_DIR |
./data |
Root for tasks.json and uploads/. |
ASR_MODEL_DIR |
<DataDir>/models |
Where models live / are downloaded. |
ASR_NUM_THREADS |
min(4, NumCPU) |
ONNX compute threads. |
ASR_LANGUAGE |
auto |
SenseVoice hint: auto/zh/en/ja/ko/yue. |
ASR_USE_ITN |
1 |
Inverse text normalization (numbers, punctuation). |
ASR_MAX_RETRIES |
3 |
Auto-retry attempts before a task is marked failed. |
ASR_MAX_UPLOAD_MB |
512 |
Per-upload size cap. |
ASR_WORKERS |
1 |
Concurrent transcription workers. |
ASR_VAD_THRESHOLD |
0.5 |
Silero speech probability threshold. |
ASR_VAD_MIN_SPEECH |
0.5 |
Min speech segment (s). |
ASR_VAD_MIN_SILENCE |
0.5 |
Min silence to split (s). |
ASR_VAD_MAX_SPEECH |
20 |
Max segment length (s); keeps SenseVoice within its window. |
ASR_VAD_WINDOW |
512 |
VAD window (samples @16k). |
ASR_VAD_BUFFER_SECS |
60 |
VAD internal buffer (s). |
ASR_DOWNLOAD_TIMEOUT |
1800 |
Model download timeout (s). |
ASR_FFMPEG |
ffmpeg |
ffmpeg binary used to decode m4a/aac/opus/wma (beep handles wav/mp3/flac/ogg). Only needed for those formats. |
ASR_DECODE_TIMEOUT |
1800 |
Per-file decode timeout (beep + ffmpeg fallback) (s). |
Web UI
Open http://<host>:<port>/ in a browser. The page itself is public; enter the
ASR_PASSWORD value in the top-bar field (stored in localStorage, sent as
X-Api-Key on every API call). The console is a single embedded SPA (no
separate frontend to deploy) and covers the whole API:
- Upload — drag & drop or click; audio (
.wav/.mp3/.flac/.ogg/.m4a/.aac/.opus/…) and video (.mp4/.mkv/.mov/.avi/.ts/…— the audio track is auto-extracted via ffmpeg). - Task list — auto-refreshing; status dots (pending/processing/completed/ failed/skipped), attempts, timestamps.
- Detail — transcript (full text + per-segment timestamps), detected language/emotion, duration, and Retry / Skip / Delete actions.
- Copy — one-click copy for the full transcript, each segment's text, and each segment's timestamp (时间轴).
- Theme — light (default) / dark toggle in the top bar; choice persisted.
- A live health dot in the top bar reflects model readiness.
HTTP API
Auth is required on /api/*: send the password as X-Api-Key, as
Authorization: Bearer <password>, or as ?token=<password>. /, /healthz
are public.
| Method | Path | Description |
|---|---|---|
GET |
/ |
Web console (embedded SPA). |
GET |
/healthz |
Liveness + model readiness. |
POST |
/api/tasks |
Upload audio (multipart: file=). → 202 + task. |
GET |
/api/tasks |
List tasks (newest-first). |
GET |
/api/tasks/{id} |
Task detail (includes result when done). |
DELETE |
/api/tasks/{id} |
Delete task + its audio. → 204. |
POST |
/api/tasks/{id}/retry |
Requeue a failed/skipped task. → 202. |
POST |
/api/tasks/{id}/skip |
Abandon a failed task (queue moves on). → 202. |
GET |
/api/tasks/{id}/transcript |
Just the transcript (when completed). |
Supported audio: .wav, .mp3, .flac, .ogg (pure-Go, no external tool)
plus .m4a, .aac, .opus, .wma, .aiff, … and video .mp4, .mkv,
.mov, .avi, .ts, … — all decoded via ffmpeg (which drops the video
stream and extracts the audio track). The Docker image ships ffmpeg; the
standalone binary needs ffmpeg on PATH (or ASR_FFMPEG).
Examples
# Upload
curl -H "X-Api-Key: changeme" -F "[email protected]" http://localhost:8080/api/tasks
# Poll
curl -H "X-Api-Key: changeme" http://localhost:8080/api/tasks/<id>
# Transcript
curl -H "X-Api-Key: changeme" http://localhost:8080/api/tasks/<id>/transcript
Task lifecycle & error handling
States: pending → processing → completed | failed | skipped.
- A background worker claims
pendingtasks, transcribes them, and records the result. Tasks leftprocessingat crash time are reset topendingon next start (so they re-run — crash recovery). - Retry: on failure, the task is requeued with exponential backoff
(
5s, 10s, 20s, …, capped at60s), up toASR_MAX_RETRIESattempts; the worker continues to other tasks meanwhile. After exhausting retries the task isfailed. - Continue / retry (manual):
POST /api/tasks/{id}/skipabandons a failed task (terminalskipped);POST /api/tasks/{id}/retryrequeues it (pending, attempts reset).
Project layout
main.go entrypoint + signal handling
internal/
config/ env-based config
models/ auto-download + load recognizer/VAD
audio/ wav/mp3/flac/ogg decode -> 16k mono float32
transcribe/ VAD segmentation + SenseVoice recognition
store/ task persistence (tasks.json + uploads/)
server/ HTTP API, auth middleware, worker loop
Makefile portable builds for amd64 + arm64