package config import ( "os" "time" ) // Config holds all runtime configuration, loaded from environment variables. // See .env.example for the full list and defaults. type Config struct { Port string BaseURL string DatabasePath string ImageDir string // on-disk store for editor image uploads // LLM LLMBackend string // "vllm" | "ollama" LLMEndpoint string LLMModel string // checkpoint model (small, fast) LLMChatModel string // Ask Petal model; falls back to LLMModel if empty LLMTimeout time.Duration // TTS (read-aloud). Off unless TTSEndpoint is set — when empty, the /api/tts // route isn't mounted and the frontend falls back to the browser's Web Speech // API. Endpoint points at a local Piper HTTP server. TTSEndpoint string // Piper instance serving the English voice TTSEndpointZH string // Piper instance serving the Chinese voice; empty = zh falls back to Web Speech TTSVoiceEN string // Piper voice id for English (e.g. en_US-amy-medium) TTSVoiceZH string // Piper voice id for Chinese (e.g. zh_CN-huayan-medium) TTSCacheDir string // on-disk store for synthesized clips (content-addressed) TTSTimeout time.Duration TTSFormat string // mp3 | opus | wav — mp3/opus transcode Piper's WAV via ffmpeg // Auth (deferred — not wired in the local-dev build, kept for later) AuthentikURL string AuthentikClientID string AuthentikClientSecret string SessionSecret string } // Load reads configuration from the environment, applying sane local-dev defaults. func Load() *Config { return &Config{ Port: env("PORT", "8080"), BaseURL: env("BASE_URL", "http://localhost:8080"), DatabasePath: env("DATABASE_PATH", "./data/petal.db"), ImageDir: env("IMAGE_DIR", "./data/images"), LLMBackend: env("LLM_BACKEND", "vllm"), LLMEndpoint: env("LLM_ENDPOINT", "http://localhost:8000"), LLMModel: env("LLM_MODEL", ""), LLMChatModel: env("LLM_CHAT_MODEL", ""), LLMTimeout: envDuration("LLM_TIMEOUT", 30*time.Second), TTSEndpoint: env("TTS_ENDPOINT", ""), TTSEndpointZH: env("TTS_ENDPOINT_ZH", ""), TTSVoiceEN: env("TTS_VOICE_EN", "en_US-amy-medium"), TTSVoiceZH: env("TTS_VOICE_ZH", "zh_CN-huayan-medium"), TTSCacheDir: env("TTS_CACHE_DIR", "./data/tts"), TTSTimeout: envDuration("TTS_TIMEOUT", 15*time.Second), TTSFormat: env("TTS_AUDIO_FORMAT", "mp3"), AuthentikURL: env("AUTHENTIK_URL", ""), AuthentikClientID: env("AUTHENTIK_CLIENT_ID", ""), AuthentikClientSecret: env("AUTHENTIK_CLIENT_SECRET", ""), SessionSecret: env("SESSION_SECRET", "dev-insecure-secret-change-me"), } } func env(key, fallback string) string { if v := os.Getenv(key); v != "" { return v } return fallback } func envDuration(key string, fallback time.Duration) time.Duration { if v := os.Getenv(key); v != "" { if d, err := time.ParseDuration(v); err == nil { return d } } return fallback }