package config import ( "encoding/json" "errors" "os" "path/filepath" "strconv" "strings" ) type Config struct { Hotkey string `json:"hotkey"` FfmpegInput string `json:"ffmpeg_input"` WhisperModel string `json:"whisper_model"` WhisperLang string `json:"whisper_lang"` WhisperDevice string `json:"whisper_device"` WhisperExtraArgs string `json:"whisper_extra_args"` RecordTimeoutSec int `json:"record_timeout_sec"` WhisperTimeoutSec int `json:"whisper_timeout_sec"` SegmentSec int `json:"segment_sec"` Streaming bool `json:"streaming"` InjectionBackend string `json:"injection_backend"` AIEnabled bool `json:"ai_enabled"` AIProvider string `json:"ai_provider"` AIModel string `json:"ai_model"` AITemperature float64 `json:"ai_temperature"` AISystemPromptFile string `json:"ai_system_prompt_file"` AIBaseURL string `json:"ai_base_url"` AIAPIKey string `json:"ai_api_key"` AITimeoutSec int `json:"ai_timeout_sec"` } func DefaultPath() string { home, _ := os.UserHomeDir() return filepath.Join(home, ".config", "lel", "config.json") } func Defaults() Config { return Config{ Hotkey: "Cmd+m", FfmpegInput: "pulse:default", WhisperModel: "base", WhisperLang: "en", WhisperDevice: "cpu", WhisperExtraArgs: "", RecordTimeoutSec: 120, WhisperTimeoutSec: 300, SegmentSec: 5, Streaming: false, InjectionBackend: "clipboard", AIEnabled: false, AIProvider: "ollama", AIModel: "llama3.2:3b", AITemperature: 0.0, AISystemPromptFile: "", AIBaseURL: "http://localhost:11434", AIAPIKey: "", AITimeoutSec: 20, } } func Load(path string) (Config, error) { cfg := Defaults() if path == "" { path = DefaultPath() } if _, err := os.Stat(path); err == nil { data, err := os.ReadFile(path) if err != nil { return cfg, err } if err := json.Unmarshal(data, &cfg); err != nil { return cfg, err } } applyEnv(&cfg) if strings.TrimSpace(cfg.Hotkey) == "" { return cfg, errors.New("hotkey cannot be empty") } if cfg.RecordTimeoutSec <= 0 { return cfg, errors.New("record_timeout_sec must be > 0") } if cfg.WhisperTimeoutSec <= 0 { return cfg, errors.New("whisper_timeout_sec must be > 0") } return cfg, nil } func applyEnv(cfg *Config) { if v := os.Getenv("WHISPER_MODEL"); v != "" { cfg.WhisperModel = v } if v := os.Getenv("WHISPER_LANG"); v != "" { cfg.WhisperLang = v } if v := os.Getenv("WHISPER_DEVICE"); v != "" { cfg.WhisperDevice = v } if v := os.Getenv("WHISPER_EXTRA_ARGS"); v != "" { cfg.WhisperExtraArgs = v } if v := os.Getenv("WHISPER_FFMPEG_IN"); v != "" { cfg.FfmpegInput = v } if v := os.Getenv("WHISPER_STREAM"); v != "" { cfg.Streaming = parseBool(v) } if v := os.Getenv("WHISPER_SEGMENT_SEC"); v != "" { if n, err := strconv.Atoi(v); err == nil { cfg.SegmentSec = n } } if v := os.Getenv("WHISPER_TIMEOUT_SEC"); v != "" { if n, err := strconv.Atoi(v); err == nil { cfg.WhisperTimeoutSec = n } } if v := os.Getenv("LEL_RECORD_TIMEOUT_SEC"); v != "" { if n, err := strconv.Atoi(v); err == nil { cfg.RecordTimeoutSec = n } } if v := os.Getenv("LEL_HOTKEY"); v != "" { cfg.Hotkey = v } if v := os.Getenv("LEL_INJECTION_BACKEND"); v != "" { cfg.InjectionBackend = v } if v := os.Getenv("LEL_AI_ENABLED"); v != "" { cfg.AIEnabled = parseBool(v) } if v := os.Getenv("LEL_AI_PROVIDER"); v != "" { cfg.AIProvider = v } if v := os.Getenv("LEL_AI_MODEL"); v != "" { cfg.AIModel = v } if v := os.Getenv("LEL_AI_TEMPERATURE"); v != "" { if n, err := strconv.ParseFloat(v, 64); err == nil { cfg.AITemperature = n } } if v := os.Getenv("LEL_AI_SYSTEM_PROMPT_FILE"); v != "" { cfg.AISystemPromptFile = v } if v := os.Getenv("LEL_AI_BASE_URL"); v != "" { cfg.AIBaseURL = v } if v := os.Getenv("LEL_AI_API_KEY"); v != "" { cfg.AIAPIKey = v } if v := os.Getenv("LEL_AI_TIMEOUT_SEC"); v != "" { if n, err := strconv.Atoi(v); err == nil { cfg.AITimeoutSec = n } } } func parseBool(v string) bool { v = strings.ToLower(strings.TrimSpace(v)) return v == "1" || v == "true" || v == "yes" || v == "on" }