feat(config): Provider-Pin, Provider-Endpunkte und Voice-Mapping
config.toml bekommt provider = "…" (Pin, Env-Override TALK_PROVIDER), [providers.<name>] endpoint/model (Endpoints fake-bar für Tests; Keys bleiben ausschließlich Env) und voice.<provider>.<lang> = id. TOML-Key ist bewusst providers (Mehrzahl), damit Pin ( Singular) und Einstellungs- Tabelle koexistieren können. Env-Overrides der Endpunkte liest die Provider-Schicht (TALK_ELEVENLABS_ENDPOINT etc.) — das config-Paket bleibt frei von Provider-Namen.
This commit is contained in:
1 parent
7a843ae441
commit
88eddc63d2
2 files changed
+118
-2
No files matched your search
@@ -17,10 +17,28 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Config ist der effektiv geladene Konfigurations-Stand von talk. Task 02
|
// Config ist der effektiv geladene Konfigurations-Stand von talk. Task 02
|
||||||
// führt nur audio.sink; spätere Tasks ergänzen Felder (Provider-Endpunkte,
|
// führt audio.sink; Task 03 ergänzt den Provider-Pin, Provider-Endpunkte
|
||||||
// Voice-Mapping, server, queue …), ohne die Auflösungsreihenfolge zu ändern.
|
// und das Voice-Mapping; spätere Tasks ergänzen weitere Felder (server,
|
||||||
|
// queue …), ohne die Auflösungsreihenfolge zu ändern.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Audio Audio `toml:"audio"`
|
Audio Audio `toml:"audio"`
|
||||||
|
|
||||||
|
// 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.
|
// Audio steuert die Audio-Ausgabe.
|
||||||
@@ -30,6 +48,17 @@ type Audio struct {
|
|||||||
Sink string `toml:"sink"`
|
Sink string `toml:"sink"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
// Defaults liefert die eingebaute Default-Konfiguration (unterste Schicht
|
||||||
// der Auflösung).
|
// der Auflösung).
|
||||||
func Defaults() Config {
|
func Defaults() Config {
|
||||||
@@ -84,6 +113,9 @@ func applyEnv(cfg Config, getenv func(string) string) (Config, error) {
|
|||||||
if v := getenv("TALK_AUDIO_SINK"); v != "" {
|
if v := getenv("TALK_AUDIO_SINK"); v != "" {
|
||||||
cfg.Audio.Sink = v
|
cfg.Audio.Sink = v
|
||||||
}
|
}
|
||||||
|
if v := getenv("TALK_PROVIDER"); v != "" {
|
||||||
|
cfg.Provider = v
|
||||||
|
}
|
||||||
switch cfg.Audio.Sink {
|
switch cfg.Audio.Sink {
|
||||||
case SinkOto, SinkNull:
|
case SinkOto, SinkNull:
|
||||||
return cfg, nil
|
return cfg, nil
|
||||||
|
|||||||
@@ -204,3 +204,87 @@ func TestLoadMerge(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestLoadProviderKette deckt die Task-03-Felder ab: Provider-Pin per
|
||||||
|
// Datei/Env, Provider-Endpunkte/-Modelle und das Voice-Mapping.
|
||||||
|
func TestLoadProviderKette(t *testing.T) {
|
||||||
|
writeTOML := func(t *testing.T, content string) string {
|
||||||
|
t.Helper()
|
||||||
|
path := filepath.Join(t.TempDir(), "config.toml")
|
||||||
|
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
toml := `provider = "openai"
|
||||||
|
|
||||||
|
[providers.elevenlabs]
|
||||||
|
endpoint = "https://eleven.example.com"
|
||||||
|
|
||||||
|
[providers.openai]
|
||||||
|
endpoint = "https://openai.example.com"
|
||||||
|
model = "gpt-4o-mini-tts"
|
||||||
|
|
||||||
|
[providers.edge]
|
||||||
|
endpoint = "wss://edge.example.com/synth"
|
||||||
|
|
||||||
|
[voice.elevenlabs]
|
||||||
|
de = "MeineStimme"
|
||||||
|
en = "MyVoice"
|
||||||
|
|
||||||
|
[voice.edge]
|
||||||
|
de = "de-DE-KatjaNeural"
|
||||||
|
`
|
||||||
|
|
||||||
|
t.Run("TOML liefert Pin, Endpunkte und Voice-Mapping", func(t *testing.T) {
|
||||||
|
cfg, err := Load(Options{File: writeTOML(t, toml), Getenv: env(nil)})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Load: %v", err)
|
||||||
|
}
|
||||||
|
if cfg.Provider != "openai" {
|
||||||
|
t.Errorf("Provider = %q, want %q", cfg.Provider, "openai")
|
||||||
|
}
|
||||||
|
if got := cfg.Providers["elevenlabs"].Endpoint; got != "https://eleven.example.com" {
|
||||||
|
t.Errorf("Providers[elevenlabs].Endpoint = %q", got)
|
||||||
|
}
|
||||||
|
if got := cfg.Providers["openai"].Model; got != "gpt-4o-mini-tts" {
|
||||||
|
t.Errorf("Providers[openai].Model = %q", got)
|
||||||
|
}
|
||||||
|
if got := cfg.Providers["edge"].Endpoint; got != "wss://edge.example.com/synth" {
|
||||||
|
t.Errorf("Providers[edge].Endpoint = %q", got)
|
||||||
|
}
|
||||||
|
if got := cfg.Voice["elevenlabs"]["de"]; got != "MeineStimme" {
|
||||||
|
t.Errorf("Voice[elevenlabs][de] = %q", got)
|
||||||
|
}
|
||||||
|
if got := cfg.Voice["elevenlabs"]["en"]; got != "MyVoice" {
|
||||||
|
t.Errorf("Voice[elevenlabs][en] = %q", got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("TALK_PROVIDER schlägt Datei-Pin", func(t *testing.T) {
|
||||||
|
cfg, err := Load(Options{
|
||||||
|
File: writeTOML(t, toml),
|
||||||
|
Getenv: env(map[string]string{"TALK_PROVIDER": "edge"}),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Load: %v", err)
|
||||||
|
}
|
||||||
|
if cfg.Provider != "edge" {
|
||||||
|
t.Errorf("Provider = %q, want Env-Override %q", cfg.Provider, "edge")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ohne alles: keine Pins, keine Endpunkte", func(t *testing.T) {
|
||||||
|
cfg, err := Load(Options{
|
||||||
|
File: filepath.Join(t.TempDir(), "gibt-es-nicht.toml"),
|
||||||
|
Getenv: env(nil),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Load: %v", err)
|
||||||
|
}
|
||||||
|
if cfg.Provider != "" || cfg.Providers != nil || cfg.Voice != nil {
|
||||||
|
t.Errorf("Provider-Konfiguration = %+v, will leer", cfg)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
Reference in new issue
Block a user