Files
record-asr/README.md
T
2026-09-09 15:57:39 +08:00

225 lines
9.6 KiB
Markdown

# record-asr
A self-contained **Go** HTTP transcription service built on
[sherpa-onnx](https://github.com/k2-fsa/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](#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).
```sh
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
```sh
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:
```sh
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.
```sh
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:
```sh
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):
```sh
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`):
```sh
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
```sh
# 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
```sh
# 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
```