2026-09-09 15:57:39 +08:00
2026-09-09 15:57:39 +08:00

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 buildx produces 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-onnx C API (v1.13.7) no longer exposes FSMN-VAD — its VadModelConfig only supports silero_vad and ten_vad (verified in the shipped c-api.h and 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 /models in 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 pending tasks, transcribes them, and records the result. Tasks left processing at crash time are reset to pending on next start (so they re-run — crash recovery).
  • Retry: on failure, the task is requeued with exponential backoff (5s, 10s, 20s, …, capped at 60s), up to ASR_MAX_RETRIES attempts; the worker continues to other tasks meanwhile. After exhausting retries the task is failed.
  • Continue / retry (manual): POST /api/tasks/{id}/skip abandons a failed task (terminal skipped); POST /api/tasks/{id}/retry requeues 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
S
Description
No description provided
Readme
139 KiB
Languages
Go 63%
HTML 31.6%
Dockerfile 3.1%
Makefile 2.3%