// Package config defines Captioneer's command-line configuration. package config import ( "errors" "flag" "strings" "time" ) const ( DefaultModelsDir = "models" DefaultOverlayFontFamily = "Sans" MinimumOverlayFinalLifetime = 3 * time.Second ) type Mode string const ( ModeFixed Mode = "fixed" ModeVAD Mode = "vad" ) type Settings struct { Mode Mode ChunkDuration time.Duration ModelsDir string Threads int PreviewThresholdDB float64 } 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") flag.Parse() settings.Mode = Mode(mode) return settings } 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 FontFamily string 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") 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") flags.StringVar(&desktop.Overlay.FontFamily, "overlay-font-family", DefaultOverlayFontFamily, "caption font family") flags.IntVar(&desktop.Overlay.BottomMargin, "overlay-bottom-margin", 100, "distance from the monitor bottom in logical pixels") flags.IntVar(&desktop.Overlay.MaxWidth, "overlay-max-width", 1200, "fixed caption width in logical pixels before the monitor safety cap") flags.StringVar(&desktop.Overlay.Monitor, "overlay-monitor", "auto", "monitor selection: auto, numeric index, or connector name") flags.DurationVar(&desktop.Overlay.FinalTimeout, "overlay-final-timeout", MinimumOverlayFinalLifetime, "completed-caption lifetime; zero keeps lines until the six-line cap removes them") 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 } 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") } return nil } 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") } if strings.TrimSpace(s.Overlay.FontFamily) == "" { return errors.New("--overlay-font-family cannot be empty") } 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") } if s.Overlay.FinalTimeout > 0 && s.Overlay.FinalTimeout < MinimumOverlayFinalLifetime { return errors.New("--overlay-final-timeout must be zero or at least 3s") } if s.Overlay.Monitor == "" { return errors.New("--overlay-monitor cannot be empty") } return nil }