2026-07-16 12:12:01 +03:00
// Package config defines Captioneer's command-line configuration.
package config
import (
"errors"
"flag"
2026-07-16 18:09:09 +03:00
"strings"
2026-07-16 12:12:01 +03:00
"time"
)
2026-07-16 18:00:11 +03:00
const (
DefaultModelsDir = "models"
2026-07-16 18:09:09 +03:00
DefaultOverlayFontFamily = "Sans"
2026-07-16 18:00:11 +03:00
MinimumOverlayFinalLifetime = 3 * time . Second
2026-07-17 01:47:37 +03:00
DefaultModelTimeout = 30 * time . Second
DefaultModelShutdownTimeout = 2 * time . Second
2026-07-16 18:00:11 +03:00
)
2026-07-16 12:12:01 +03:00
type Mode string
const (
ModeFixed Mode = "fixed"
ModeVAD Mode = "vad"
)
type Settings struct {
2026-07-17 01:47:37 +03:00
Mode Mode
ChunkDuration time . Duration
ModelsDir string
Threads int
PreviewThresholdDB float64
ModelAutoRestart bool
ModelTimeout time . Duration
ModelShutdownTimeout time . Duration
2026-07-16 12:12:01 +03:00
}
func Parse () Settings {
var settings Settings
mode := ""
flag . StringVar ( & mode , "mode" , "" , "caption mode: vad or fixed (required)" )
flag . DurationVar ( & settings . ChunkDuration , "chunk-duration" , time . Second , "fixed chunk size or VAD preview refresh interval" )
flag . StringVar ( & settings . ModelsDir , "models-dir" , DefaultModelsDir , "directory containing downloaded Sherpa models" )
flag . IntVar ( & settings . Threads , "threads" , 2 , "CPU threads used by recognition and VAD" )
flag . Float64Var ( & settings . PreviewThresholdDB , "preview-threshold-dbfs" , - 45 , "RMS dBFS threshold used to begin VAD previews" )
2026-07-17 01:47:37 +03:00
flag . BoolVar ( & settings . ModelAutoRestart , "model-auto-restart" , true , "restart the isolated model worker after a crash or timeout" )
flag . DurationVar ( & settings . ModelTimeout , "model-timeout" , DefaultModelTimeout , "maximum model startup or processing time; zero disables hang detection" )
flag . DurationVar ( & settings . ModelShutdownTimeout , "model-shutdown-timeout" , DefaultModelShutdownTimeout , "time allowed for model flush and cleanup before force-killing the worker" )
2026-07-16 12:12:01 +03:00
flag . Parse ()
settings . Mode = Mode ( mode )
return settings
}
2026-07-16 17:24:50 +03:00
type OutputMode string
const (
OutputTerminal OutputMode = "terminal"
OutputOverlay OutputMode = "overlay"
OutputBoth OutputMode = "both"
)
type OverlayBackend string
const (
BackendAuto OverlayBackend = "auto"
BackendWayland OverlayBackend = "wayland"
BackendX11 OverlayBackend = "x11"
)
type OverlaySettings struct {
Backend OverlayBackend
Opacity float64
FontSize int
2026-07-16 18:09:09 +03:00
FontFamily string
2026-07-16 17:24:50 +03:00
BottomMargin int
MaxWidth int
Monitor string
FinalTimeout time . Duration
ClickThrough bool
}
type DesktopSettings struct {
Settings
Output OutputMode
Overlay OverlaySettings
}
func ParseDesktop () DesktopSettings {
flags := flag . CommandLine
var desktop DesktopSettings
var mode string
var output string
var backend string
flags . StringVar ( & mode , "mode" , "" , "caption mode: vad or fixed (required)" )
flags . DurationVar ( & desktop . ChunkDuration , "chunk-duration" , time . Second , "fixed chunk size or VAD preview refresh interval" )
flags . StringVar ( & desktop . ModelsDir , "models-dir" , DefaultModelsDir , "directory containing downloaded Sherpa models" )
flags . IntVar ( & desktop . Threads , "threads" , 2 , "CPU threads used by recognition and VAD" )
flags . Float64Var ( & desktop . PreviewThresholdDB , "preview-threshold-dbfs" , - 45 , "RMS dBFS threshold used to begin VAD previews" )
2026-07-17 01:47:37 +03:00
flags . BoolVar ( & desktop . ModelAutoRestart , "model-auto-restart" , true , "restart the isolated model worker after a crash or timeout" )
flags . DurationVar ( & desktop . ModelTimeout , "model-timeout" , DefaultModelTimeout , "maximum model startup or processing time; zero disables hang detection" )
flags . DurationVar ( & desktop . ModelShutdownTimeout , "model-shutdown-timeout" , DefaultModelShutdownTimeout , "time allowed for model flush and cleanup before force-killing the worker" )
2026-07-16 17:24:50 +03:00
flags . StringVar ( & output , "output" , "" , "caption output: terminal, overlay, or both (required)" )
flags . StringVar ( & backend , "overlay-backend" , "auto" , "display backend: auto, wayland, or x11" )
flags . Float64Var ( & desktop . Overlay . Opacity , "overlay-opacity" , 0.90 , "caption background opacity from 0 to 1" )
flags . IntVar ( & desktop . Overlay . FontSize , "overlay-font-size" , 28 , "caption font size in logical pixels" )
2026-07-16 18:09:09 +03:00
flags . StringVar ( & desktop . Overlay . FontFamily , "overlay-font-family" , DefaultOverlayFontFamily , "caption font family" )
2026-07-16 17:24:50 +03:00
flags . IntVar ( & desktop . Overlay . BottomMargin , "overlay-bottom-margin" , 100 , "distance from the monitor bottom in logical pixels" )
2026-07-16 17:38:42 +03:00
flags . IntVar ( & desktop . Overlay . MaxWidth , "overlay-max-width" , 1200 , "fixed caption width in logical pixels before the monitor safety cap" )
2026-07-16 17:24:50 +03:00
flags . StringVar ( & desktop . Overlay . Monitor , "overlay-monitor" , "auto" , "monitor selection: auto, numeric index, or connector name" )
2026-07-16 18:00:11 +03:00
flags . DurationVar ( & desktop . Overlay . FinalTimeout , "overlay-final-timeout" , MinimumOverlayFinalLifetime , "completed-caption lifetime; zero keeps lines until the six-line cap removes them" )
2026-07-16 17:24:50 +03:00
flags . BoolVar ( & desktop . Overlay . ClickThrough , "overlay-click-through" , true , "pass pointer input through the caption window" )
flag . Parse ()
desktop . Mode = Mode ( mode )
desktop . Output = OutputMode ( output )
desktop . Overlay . Backend = OverlayBackend ( backend )
return desktop
}
2026-07-16 12:12:01 +03:00
func ( s Settings ) Validate () error {
if s . Mode != ModeFixed && s . Mode != ModeVAD {
return errors . New ( "--mode is required and must be either fixed or vad" )
}
if s . ChunkDuration <= 0 {
return errors . New ( "--chunk-duration must be positive" )
}
if s . Threads <= 0 {
return errors . New ( "--threads must be positive" )
}
2026-07-17 01:47:37 +03:00
if s . ModelTimeout < 0 {
return errors . New ( "--model-timeout cannot be negative" )
}
if s . ModelShutdownTimeout <= 0 {
return errors . New ( "--model-shutdown-timeout must be positive" )
}
2026-07-16 12:12:01 +03:00
return nil
}
2026-07-16 17:24:50 +03:00
func ( s DesktopSettings ) Validate () error {
if err := s . Settings . Validate (); err != nil {
return err
}
if s . Output != OutputTerminal && s . Output != OutputOverlay && s . Output != OutputBoth {
return errors . New ( "--output is required and must be terminal, overlay, or both" )
}
if s . Overlay . Backend != BackendAuto && s . Overlay . Backend != BackendWayland && s . Overlay . Backend != BackendX11 {
return errors . New ( "--overlay-backend must be auto, wayland, or x11" )
}
if s . Overlay . Opacity < 0 || s . Overlay . Opacity > 1 {
return errors . New ( "--overlay-opacity must be between 0 and 1" )
}
if s . Overlay . FontSize <= 0 {
return errors . New ( "--overlay-font-size must be positive" )
}
2026-07-16 18:09:09 +03:00
if strings . TrimSpace ( s . Overlay . FontFamily ) == "" {
return errors . New ( "--overlay-font-family cannot be empty" )
}
2026-07-16 17:24:50 +03:00
if s . Overlay . BottomMargin < 0 {
return errors . New ( "--overlay-bottom-margin cannot be negative" )
}
if s . Overlay . MaxWidth <= 0 {
return errors . New ( "--overlay-max-width must be positive" )
}
if s . Overlay . FinalTimeout < 0 {
return errors . New ( "--overlay-final-timeout cannot be negative" )
}
2026-07-16 18:00:11 +03:00
if s . Overlay . FinalTimeout > 0 && s . Overlay . FinalTimeout < MinimumOverlayFinalLifetime {
return errors . New ( "--overlay-final-timeout must be zero or at least 3s" )
}
2026-07-16 17:24:50 +03:00
if s . Overlay . Monitor == "" {
return errors . New ( "--overlay-monitor cannot be empty" )
}
return nil
}