Files
record-asr/internal/config/config.go
T

198 lines
5.9 KiB
Go

// Package config loads all runtime configuration from environment variables.
//
// Every knob the transcriber exposes is controlled by an ASR_* environment
// variable so the same binary can be reconfigured without a rebuild.
package config
import (
"fmt"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"time"
)
// Config holds all runtime configuration derived from environment variables.
type Config struct {
// ASR_PASSWORD: shared secret required on every API request. If empty,
// auth is disabled and a loud warning is logged (intended for local use).
Password string
// ASR_ADDR: listen address (default ":8080").
Addr string
// ASR_DATA_DIR: root for persisted state (default "./data").
DataDir string
// ASR_MODEL_DIR: where models live / are auto-downloaded
// (default "<DataDir>/models").
ModelDir string
// ASR_NUM_THREADS: ONNX compute threads (default: min(4, NumCPU)).
NumThreads int
// ASR_LANGUAGE: SenseVoice language hint. "auto" detects per segment.
Language string
// ASR_USE_ITN: enable inverse text normalization (numbers/punctuation).
UseITN bool
// ASR_MAX_RETRIES: automatic retry attempts before a task is marked failed.
MaxRetries int
// ASR_MAX_UPLOAD_MB: per-upload size cap (default 512).
MaxUploadBytes int64
// ASR_WORKERS: concurrent transcription workers (default 1).
Workers int
// ASR_MAX_CONCURRENT: cap on simultaneously in-flight synchronous
// transcriptions on the OpenAI endpoint. Bounds onnxruntime thread
// contention under load. Defaults to ASR_NUM_THREADS. Excess requests
// wait for a slot (or get 503 if the client disconnects while waiting).
MaxConcurrent int
// VAD (silero) tuning.
VadThreshold float32
VadMinSpeechDuration float32
VadMinSilenceDuration float32
VadMaxSpeechDuration float32
VadWindowSize int
VadBufferSeconds float32
// ASR_DOWNLOAD_TIMEOUT: timeout for the initial model download.
DownloadTimeout time.Duration
// ASR_FFMPEG: path to the ffmpeg binary used to decode formats beep
// can't (m4a/aac/opus/wma …). Defaults to "ffmpeg" on PATH. Only
// required when such formats are actually uploaded.
FFmpegPath string
// ASR_DECODE_TIMEOUT: per-file decode timeout (beep + ffmpeg fallback).
DecodeTimeout time.Duration
}
// Load reads the environment and returns a validated Config.
func Load() (Config, error) {
c := Config{
Password: strings.TrimSpace(os.Getenv("ASR_PASSWORD")),
Addr: envStr("ASR_ADDR", ":8080"),
DataDir: envStr("ASR_DATA_DIR", "./data"),
NumThreads: envInt("ASR_NUM_THREADS", defaultThreads()),
Language: envStr("ASR_LANGUAGE", "auto"),
UseITN: envBool("ASR_USE_ITN", true),
MaxRetries: envInt("ASR_MAX_RETRIES", 3),
MaxUploadBytes: int64(envInt("ASR_MAX_UPLOAD_MB", 512)) * 1024 * 1024,
Workers: envInt("ASR_WORKERS", 1),
MaxConcurrent: envInt("ASR_MAX_CONCURRENT", 0),
VadThreshold: float32(envFloat("ASR_VAD_THRESHOLD", 0.5)),
VadMinSpeechDuration: float32(envFloat("ASR_VAD_MIN_SPEECH", 0.5)),
VadMinSilenceDuration: float32(envFloat("ASR_VAD_MIN_SILENCE", 0.5)),
VadMaxSpeechDuration: float32(envFloat("ASR_VAD_MAX_SPEECH", 20)),
VadWindowSize: envInt("ASR_VAD_WINDOW", 512),
VadBufferSeconds: float32(envFloat("ASR_VAD_BUFFER_SECS", 60)),
DownloadTimeout: time.Duration(envInt("ASR_DOWNLOAD_TIMEOUT", 1800)) * time.Second,
FFmpegPath: envStr("ASR_FFMPEG", "ffmpeg"),
DecodeTimeout: time.Duration(envInt("ASR_DECODE_TIMEOUT", 1800)) * time.Second,
}
c.ModelDir = envStr("ASR_MODEL_DIR", filepath.Join(c.DataDir, "models"))
if c.Workers < 1 {
c.Workers = 1
}
if c.MaxConcurrent < 1 {
c.MaxConcurrent = c.NumThreads
}
if c.NumThreads < 1 {
c.NumThreads = 1
}
if c.MaxRetries < 0 {
c.MaxRetries = 0
}
if c.VadMaxSpeechDuration < 1 {
c.VadMaxSpeechDuration = 1
}
if c.DecodeTimeout < time.Minute {
c.DecodeTimeout = 30 * time.Minute
}
if c.VadWindowSize <= 0 {
c.VadWindowSize = 512
}
if c.MaxUploadBytes < 1<<20 {
c.MaxUploadBytes = 512 << 20
}
if c.DownloadTimeout < time.Minute {
c.DownloadTimeout = 30 * time.Minute
}
// Absolutize data dir so relative file paths in persisted state survive
// regardless of the process working directory at request time.
if abs, err := filepath.Abs(c.DataDir); err == nil {
c.DataDir = abs
}
if abs, err := filepath.Abs(c.ModelDir); err == nil {
c.ModelDir = abs
}
return c, nil
}
// AuthEnabled reports whether API auth is enforced.
func (c Config) AuthEnabled() bool { return c.Password != "" }
func (c Config) String() string {
return fmt.Sprintf("addr=%s data=%s models=%s threads=%d lang=%s itn=%t workers=%d maxconcurrent=%d retries=%d vad{thr=%.2f max=%.0fs win=%d}",
c.Addr, c.DataDir, c.ModelDir, c.NumThreads, c.Language, c.UseITN, c.Workers,
c.MaxConcurrent, c.MaxRetries, c.VadThreshold, c.VadMaxSpeechDuration, c.VadWindowSize)
}
func defaultThreads() int {
n := runtime.NumCPU()
if n > 4 {
n = 4
}
if n < 1 {
n = 1
}
return n
}
func envStr(key, def string) string {
if v := strings.TrimSpace(os.Getenv(key)); v != "" {
return v
}
return def
}
func envInt(key string, def int) int {
v := strings.TrimSpace(os.Getenv(key))
if v == "" {
return def
}
n, err := strconv.Atoi(v)
if err != nil {
return def
}
return n
}
func envFloat(key string, def float64) float64 {
v := strings.TrimSpace(os.Getenv(key))
if v == "" {
return def
}
f, err := strconv.ParseFloat(v, 64)
if err != nil {
return def
}
return f
}
func envBool(key string, def bool) bool {
v := strings.TrimSpace(os.Getenv(key))
if v == "" {
return def
}
b, err := strconv.ParseBool(v)
if err != nil {
// Accept "0"/"1" and "yes"/"no" too.
switch strings.ToLower(v) {
case "yes", "on", "true", "1":
return true
case "no", "off", "false", "0":
return false
}
return def
}
return b
}