// 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 "/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 }