add full source: transcribe pipeline, HTTP API, OpenAI endpoint, web UI, Docker
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user