feat(provider): Cloud-Provider und Vier-Stufen-Fallback-Kette

- elevenlabs: POST /v1/text-to-speech/{voice}, Header xi-api-key, Body mit
  model_id eleven_multilingual_v2 und voice_settings 0.5/0.75, MP3-Bytes.
- openai: POST /v1/audio/speech, Bearer-Key, Body {model, voice, input,
  response_format:mp3}; Modell konfigurierbar (Default tts-1).
- edge: Microsofts Read-Aloud-WSS-Protokoll nach edge-tts-Referenz,
  inklusive aktuellem DRM Sec-MS-GEC v2 (CS64 + verschleierte SHA-256-
  Variante, wörtlich portiert und gegen Referenzvektoren getestet) und
  MUID-Cookie; bei 403 Uhrenkorrektur aus dem Date-Header + ein Retry.
  Klare Protokoll-geaendert-Fehler statt stiller Fehlfunktion.
- chain: geordnete Kette (elevenlabs -> openai -> edge -> osbuiltin), Pin
  per --provider/config, Available ohne Netz-Calls, Fallback mit
  Protokoll je Stufe (nie still). Chain implementiert Provider UND
  DirectSpeaker: Speak orchestriert Bytes -> Player bzw. Direktansage,
  verwirft am null-Player tonlos — CLI und Daemon (Task 04) nutzen
  dieselbe Kette unverändert.
- voices: neutrale Repo-Defaults je Provider+Sprache (Rachel/alloy/
  Katja/Aria); Rangfolge --voice > config-Mapping > Default. KXxZ… bleibt
  bewusst Privat-Default der eigenen config.toml (Spec: nicht
  Repo-Default).

Tests: Cloud-Provider gegen httptest-Fakes (200/500/401, Request-Vertrag
wörtlich), Edge über Interface + Fehlerpfad-Mocks + Loopback-WSS-Fake
(kein echtes Netz) + als testing.Short überspringbare Integration-
Markierung (TALK_TEST_REAL_EDGE=1, am Dev-Host einmalig ausgeführt:
17136 Bytes echtes Audio). Kette: erster 500 -> zweiter getroffen -> Bytes;
Pin, alle-Stufen-tot, Direktansage mit/ohne null-Player.
This commit is contained in:
m3ta-chiron committed 2026-10-18 16:34:43 +02:00
1 parent 88eddc63d2
commit 91cd0ec8da
15 files changed
+2434 -27

No files matched your search

+14 -4
View File
@@ -48,7 +48,7 @@ func TestOSBuiltinName(t *testing.T) {
func TestNewRegistry(t *testing.T) {
for _, name := range []string{"", "osbuiltin"} {
p, err := New(name)
p, err := New(name, ChainConfig{})
if err != nil {
t.Fatalf("New(%q): %v", name, err)
}
@@ -56,9 +56,19 @@ func TestNewRegistry(t *testing.T) {
t.Errorf("New(%q).Name() = %q", name, p.Name())
}
}
if _, err := New("elevenlabs"); err == nil {
t.Error("New(\"elevenlabs\") soll in diesem Stand fehlschlagen (Task 03 ergänzt)")
} else if !strings.Contains(err.Error(), "osbuiltin") {
// Task 03: die Cloud-Provider stehen in der Registry.
for _, name := range []string{"elevenlabs", "openai", "edge"} {
p, err := New(name, ChainConfig{})
if err != nil {
t.Fatalf("New(%q): %v", name, err)
}
if p.Name() != name {
t.Errorf("New(%q).Name() = %q", name, p.Name())
}
}
if _, err := New("gibtsnicht", ChainConfig{}); err == nil {
t.Error("New(\"gibtsnicht\") soll fehlschlagen")
} else if !strings.Contains(err.Error(), "osbuiltin") || !strings.Contains(err.Error(), "elevenlabs") {
t.Errorf("New-Fehler soll verfügbare Provider nennen: %v", err)
}
}