Files
talk/internal/config/config.go
T

154 lines
5.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package config
import (
"errors"
"fmt"
"os"
"path/filepath"
"strconv"
"github.com/BurntSushi/toml"
)
// Erlaubte Werte für audio.sink (Teil des öffentlichen Vertrags; Spec:
// „Audio-Sink wählbar inkl. null").
const (
SinkOto = "oto"
SinkNull = "null"
)
// DefaultServerPort ist der Default-Port des Daemon-HTTP-Servers auf
// 127.0.0.1 (Task-04-Brief: „Port Default 7800").
const DefaultServerPort = 7800
// Config ist der effektiv geladene Konfigurations-Stand von talk. Task 02
// führt audio.sink; Task 03 ergänzt den Provider-Pin, Provider-Endpunkte
// und das Voice-Mapping; Task 04 den Daemon-Port (server.port, TALK_PORT);
// spätere Tasks ergänzen weitere Felder (queue …), ohne die Auflösungs-
// reihenfolge zu ändern.
type Config struct {
Audio Audio `toml:"audio"`
// Server steuert den Daemon-HTTP-Server (talkd) auf 127.0.0.1.
Server Server `toml:"server"`
// Provider pinnt die Kette auf genau einen Provider (z. B.
// `provider = "elevenlabs"`); leer = komplette Default-Kette
// (elevenlabs → openai → edge → osbuiltin). Das CLI-Flag --provider
// schlägt diesen Wert. Gültige Namen prüft die Provider-Schicht.
Provider string `toml:"provider"`
// Providers enthält per Provider-Einstellungen (Endpunkt, Modell) —
// TOML `[providers.elevenlabs] endpoint = "…"`. Key `providers`
// (Mehrzahl), damit der Pin ( singular `provider`) und die
// Einstellungs-Tabelle in TOML nebeneinander existieren können.
Providers map[string]ProviderSettings `toml:"providers"`
// Voice ist das Voice-Mapping `voice.<provider>.<lang> = id` — z. B.
// `[voice.elevenlabs] de = "21m00Tcm4TlvDq8ikWAM"`. Schlägt die
// eingebauten neutralen Defaults; das CLI-Flag --voice schlägt alles.
Voice map[string]map[string]string `toml:"voice"`
}
// Audio steuert die Audio-Ausgabe.
type Audio struct {
// Sink wählt die Wiedergabe: "oto" (embedded Playback, Default) oder
// "null" (verwirft — CI, Server, Tests).
Sink string `toml:"sink"`
}
// Server sind die Netz-Einstellungen des Daemons (nur Loopback; Binding
// auf 127.0.0.1 macht cmd/talkd, die Config liefert nur den Port).
type Server struct {
// Port des Daemon-HTTP-Servers auf 127.0.0.1 — config
// `[server] port = 7800`, Env-Override TALK_PORT, Default 7800.
Port int `toml:"port"`
}
// ProviderSettings sind die per Provider konfigurierbaren Endpunkte und
// Modelle (TOML `[providers.<name>]`). Leer = eingebaute Defaults; die
// Env-Overrides TALK_ELEVENLABS_ENDPOINT / TALK_OPENAI_ENDPOINT /
// TALK_EDGE_ENDPOINT (ausgewertet in der Provider-Schicht) schlagen die
// Datei — Keys laufen ausschließlich über TALK_ELEVENLABS_KEY bzw.
// TALK_OPENAI_KEY und werden nie in die Datei geschrieben.
type ProviderSettings struct {
Endpoint string `toml:"endpoint"`
Model string `toml:"model"` // nur openai (Default "tts-1")
}
// Defaults liefert die eingebaute Default-Konfiguration (unterste Schicht
// der Auflösung).
func Defaults() Config {
return Config{
Audio: Audio{Sink: SinkOto},
Server: Server{Port: DefaultServerPort},
}
}
// Options steuert Load. Alle Felder sind injizierbar, damit die Unit-Tests
// ohne echte HOME-/Env-Umgebung und ohne echte Dateien auskommen.
type Options struct {
// File überschreibt den platform-nativen Pfad der config.toml (Tests
// injizieren hier eine Temp-Datei). Leer: Dir() + "config.toml".
File string
// Getenv liefert Umgebungsvariablen (Default: os.Getenv). Über TALK_*
// laufen die Env-Overrides sowie die Pfadauflösung (HOME,
// XDG_CONFIG_HOME, APPDATA).
Getenv func(string) string
}
// Load löst die Konfiguration schichtweise auf: Defaults → config.toml
// (falls vorhanden) → Env-Overrides (TALK_AUDIO_SINK schlägt die Datei).
// Env- und Datei-Validierungsfehler werden als klarer Fehler gemeldet, nie
// still ignoriert. Fehlt das Config-Verzeichnis (z. B. keine HOME-Variable),
// läuft talk mit Defaults + Env weiter — die Datei ist optional, kaputte
// Umgebungen dürfen talk nicht unbrauchbar machen.
func Load(opts Options) (Config, error) {
if opts.Getenv == nil {
opts.Getenv = os.Getenv
}
cfg := Defaults()
path := opts.File
if path == "" {
dir, err := Dir(opts.Getenv)
if err != nil {
// Kein auffindbares Config-Verzeichnis: optionale Datei entfällt.
return applyEnv(cfg, opts.Getenv)
}
path = filepath.Join(dir, fileName)
}
if _, err := toml.DecodeFile(path, &cfg); err != nil {
if !errors.Is(err, os.ErrNotExist) {
return cfg, fmt.Errorf("config %s: %w", path, err)
}
}
return applyEnv(cfg, opts.Getenv)
}
// applyEnv wendet die Env-Overrides an und validiert das Ergebnis.
func applyEnv(cfg Config, getenv func(string) string) (Config, error) {
if v := getenv("TALK_AUDIO_SINK"); v != "" {
cfg.Audio.Sink = v
}
if v := getenv("TALK_PROVIDER"); v != "" {
cfg.Provider = v
}
if v := getenv("TALK_PORT"); v != "" {
port, err := strconv.Atoi(v)
if err != nil || port < 1 || port > 65535 {
return cfg, fmt.Errorf("TALK_PORT: ungültiger Wert %q (1–65535)", v)
}
cfg.Server.Port = port
}
switch cfg.Audio.Sink {
case SinkOto, SinkNull:
return cfg, nil
default:
return cfg, fmt.Errorf("audio.sink: ungültiger Wert %q (erlaubt: %q, %q)",
cfg.Audio.Sink, SinkOto, SinkNull)
}
}