Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 11 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -334,14 +334,19 @@ chunk_%03d.wav`) or use a CPU image, which is bounded by system RAM instead.

### Environment Variables

Every command-line flag also reads from an environment variable: take the flag
name, uppercase it and replace dashes with underscores, then prefix it with
`PARAKEET_`. So `-log-level` maps to `PARAKEET_LOG_LEVEL`, `-ffmpeg-timeout` to
`PARAKEET_FFMPEG_TIMEOUT`, and so on. An explicit flag always overrides its env
var (precedence: **CLI flag > env var > default**); an invalid env value is
ignored with a warning and the default is kept.

A few variables have no flag equivalent:

| Variable | Description | Default |
| ------------------ | ------------------------------------------- | --------------------- |
| `ONNXRUNTIME_LIB` | Path to libonnxruntime.so | Auto-detected |
| `PARAKEET_API_KEY` | API key for `/v1/*` endpoint authentication | Empty (auth disabled) |
| `PARAKEET_GPU` | Execution provider when `-gpu` is unset: `cpu`/`cuda` | `cpu` |
| `PARAKEET_GPU_DEVICE` | GPU device index when `-gpu-device` is unset | `0` |

An explicit `-gpu`/`-gpu-device` flag always overrides the corresponding environment variable.
| `ONNXRUNTIME_LIB` | Path to libonnxruntime.so | Auto-detected |
| `PARAKEET_API_KEY` | API key for `/v1/*` endpoint authentication | Empty (auth disabled) |

### Model Files

Expand Down
65 changes: 39 additions & 26 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,16 @@ import (
"log/slog"
"os"
"os/signal"
"strconv"
"strings"
"syscall"
"time"

"parakeet/internal/server"
)

// envPrefix namespaces every environment variable derived from a command-line flag.
const envPrefix = "PARAKEET_"

func main() {
cfg := server.Config{}

Expand All @@ -28,10 +30,14 @@ func main() {
flag.BoolVar(&cfg.FFmpegEnabled, "ffmpeg", true, "Enable ffmpeg fallback for non-WAV audio (requires ffmpeg in PATH)")
flag.StringVar(&cfg.FFmpegPath, "ffmpeg-path", "", "Path to the ffmpeg binary (default: resolved from PATH)")
flag.DurationVar(&cfg.FFmpegTimeout, "ffmpeg-timeout", 60*time.Second, "Maximum wall-clock time for a single ffmpeg conversion")
flag.StringVar(&cfg.GPUProvider, "gpu", envOr("PARAKEET_GPU", "cpu"), "Execution provider: cpu or cuda (env: PARAKEET_GPU)")
flag.IntVar(&cfg.GPUDeviceID, "gpu-device", envInt("PARAKEET_GPU_DEVICE", 0), "GPU device index for cuda (env: PARAKEET_GPU_DEVICE)")
flag.StringVar(&cfg.GPUProvider, "gpu", "cpu", "Execution provider: cpu or cuda")
flag.IntVar(&cfg.GPUDeviceID, "gpu-device", 0, "GPU device index for cuda")
flag.Parse()

// Any flag not set on the command line falls back to its matching env var,
// e.g. --log-level -> PARAKEET_LOG_LEVEL. Precedence: CLI flag > env var > default.
applyEnvDefaults(flag.CommandLine)

setupLogger(cfg.LogFormat, cfg.LogLevel)

srv, err := server.New(cfg)
Expand Down Expand Up @@ -73,29 +79,36 @@ func main() {
slog.Info("server stopped")
}

// envOr returns the value of environment variable key, or fallback if unset.
// Used to source a flag default from the environment so an explicit flag always
// overrides it (flag-over-env precedence) without any extra resolution logic.
func envOr(key, fallback string) string {
if v := os.Getenv(key); v != "" {
return v
}
return fallback
}

// envInt is envOr for integer-valued variables. A non-integer value is treated
// as unset (after a warning) so a typo never silently selects the wrong device.
func envInt(key string, fallback int) int {
v := os.Getenv(key)
if v == "" {
return fallback
}
n, err := strconv.Atoi(v)
if err != nil {
slog.Warn("ignoring invalid integer environment variable", "var", key, "value", v)
return fallback
}
return n
// applyEnvDefaults sources any flag not passed explicitly on the command line from
// its matching environment variable, mapping the flag name to upper snake case with
// the PARAKEET_ prefix (e.g. --log-level -> PARAKEET_LOG_LEVEL). This gives every
// flag an env var for free, so new flags need no extra wiring. Precedence stays
// CLI flag > env var > flag default: flags set on the CLI are skipped, and the
// value is parsed through the flag's own type so an invalid value is rejected
// (with a warning) instead of silently corrupting the config.
func applyEnvDefaults(fs *flag.FlagSet) {
// Flags set explicitly on the CLI win and must not be overridden by env.
setOnCLI := make(map[string]bool)
fs.Visit(func(f *flag.Flag) { setOnCLI[f.Name] = true })

fs.VisitAll(func(f *flag.Flag) {
if setOnCLI[f.Name] {
return
}
key := envPrefix + strings.ToUpper(strings.ReplaceAll(f.Name, "-", "_"))
val, ok := os.LookupEnv(key)
if !ok {
return
}
// Snapshot the current value: flag.Value.Set clobbers numeric flags to zero
// even when parsing fails, so restore it on error to keep the default.
prev := f.Value.String()
if err := f.Value.Set(val); err != nil {
slog.Warn("ignoring invalid environment variable",
"var", key, "value", val, "error", err)
_ = f.Value.Set(prev)
}
})
}

func setupLogger(format, level string) {
Expand Down
109 changes: 109 additions & 0 deletions main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// SPDX-FileCopyrightText: 2026 Alby Hernández <hola@achetronic.com>
// SPDX-License-Identifier: Apache-2.0

package main

import (
"flag"
"testing"
"time"
)

// newTestFlags builds an isolated FlagSet mirroring the real flags so tests never
// touch the global flag.CommandLine.
func newTestFlags() (*flag.FlagSet, *struct {
port int
level string
ffmpeg bool
timeout time.Duration
}) {
fs := flag.NewFlagSet("test", flag.ContinueOnError)
vals := &struct {
port int
level string
ffmpeg bool
timeout time.Duration
}{}
fs.IntVar(&vals.port, "port", 5092, "")
fs.StringVar(&vals.level, "log-level", "info", "")
fs.BoolVar(&vals.ffmpeg, "ffmpeg", true, "")
fs.DurationVar(&vals.timeout, "ffmpeg-timeout", 60*time.Second, "")
return fs, vals
}

func TestApplyEnvDefaults(t *testing.T) {
t.Run("env value fills a flag left at its default", func(t *testing.T) {
t.Setenv("PARAKEET_PORT", "8080")
fs, vals := newTestFlags()
if err := fs.Parse(nil); err != nil {
t.Fatalf("parse: %v", err)
}
applyEnvDefaults(fs)
if vals.port != 8080 {
t.Fatalf("port = %d, want 8080 (from env)", vals.port)
}
})

t.Run("explicit CLI flag beats the env var", func(t *testing.T) {
t.Setenv("PARAKEET_PORT", "8080")
fs, vals := newTestFlags()
if err := fs.Parse([]string{"-port", "9090"}); err != nil {
t.Fatalf("parse: %v", err)
}
applyEnvDefaults(fs)
if vals.port != 9090 {
t.Fatalf("port = %d, want 9090 (CLI overrides env)", vals.port)
}
})

t.Run("no env keeps the flag default", func(t *testing.T) {
fs, vals := newTestFlags()
if err := fs.Parse(nil); err != nil {
t.Fatalf("parse: %v", err)
}
applyEnvDefaults(fs)
if vals.port != 5092 {
t.Fatalf("port = %d, want 5092 (default)", vals.port)
}
})

t.Run("dashed flag name maps to upper snake case env var", func(t *testing.T) {
t.Setenv("PARAKEET_LOG_LEVEL", "debug")
t.Setenv("PARAKEET_FFMPEG_TIMEOUT", "30s")
fs, vals := newTestFlags()
if err := fs.Parse(nil); err != nil {
t.Fatalf("parse: %v", err)
}
applyEnvDefaults(fs)
if vals.level != "debug" {
t.Fatalf("log-level = %q, want %q", vals.level, "debug")
}
if vals.timeout != 30*time.Second {
t.Fatalf("ffmpeg-timeout = %s, want 30s", vals.timeout)
}
})

t.Run("typed flag parses env value through its own type", func(t *testing.T) {
t.Setenv("PARAKEET_FFMPEG", "false")
fs, vals := newTestFlags()
if err := fs.Parse(nil); err != nil {
t.Fatalf("parse: %v", err)
}
applyEnvDefaults(fs)
if vals.ffmpeg {
t.Fatal("ffmpeg = true, want false (from env)")
}
})

t.Run("invalid env value is ignored and the default survives", func(t *testing.T) {
t.Setenv("PARAKEET_PORT", "not-a-number")
fs, vals := newTestFlags()
if err := fs.Parse(nil); err != nil {
t.Fatalf("parse: %v", err)
}
applyEnvDefaults(fs)
if vals.port != 5092 {
t.Fatalf("port = %d, want 5092 (invalid env ignored)", vals.port)
}
})
}
Loading