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 // DictPath is DreamDict's built dict.db, read-only, sitting beside // petal.db. It is what gives Petal French, European Portuguese and Spanish // word lookups; without it only the embedded English/Chinese datasets // exist, which is exactly how a laptop checkout runs. A missing file is // therefore not an error — see lexicon.OpenDreamDict. DictPath string // 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) // TTSPath is the path Piper serves synthesis on. Piper moved it from "/" to // "/synthesize" in 1.6.0 with an unchanged request body, so this is a // version knob, not a feature: millenia's older server keeps the default, // the containerised sidecars set "/synthesize". TTSPath string 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. OIDC against Authentik. Login is enabled only when the issuer, the // client id and the secret are all present; with any of them missing Petal // falls back to the single hardcoded local user, which is what local // development wants and what every deployment did before Phase 16. AuthentikURL string // issuer URL of the Petal provider in Authentik AuthentikClientID string AuthentikClientSecret string // AllowedSubs gates who may sign in, as a comma-separated list of OIDC // subject ids and/or email addresses. Empty means everyone Authentik // authenticates — right for a single-household instance, wrong the moment // the IdP serves an audience wider than Petal's. AllowedSubs string } // AuthEnabled reports whether real logins are configured. When false, Petal // resolves every request to the local user. func (c *Config) AuthEnabled() bool { return c.AuthentikURL != "" && c.AuthentikClientID != "" && c.AuthentikClientSecret != "" } // 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"), DictPath: env("DICT_PATH", "./data/dict.db"), 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"), TTSPath: env("TTS_PATH", "/"), 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", ""), AllowedSubs: env("PETAL_ALLOWED_SUBS", ""), } } 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 }