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:
1 parent
88eddc63d2
commit
91cd0ec8da
15 files changed
+2434
-27
No files matched your search
@@ -8,9 +8,9 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// DefaultName ist der Provider, den die Kette nutzt, wenn kein --provider
|
||||
// gepinnt ist. Task 02: osbuiltin; Task 03 ersetzt den Default durch die
|
||||
// Fallback-Kette (ElevenLabs → OpenAI → Edge → osbuiltin).
|
||||
// DefaultName ist der Provider, den New("") liefert (Einzelkonstruktion,
|
||||
// z. B. für doctor). Die Default-Ansage-Kette ab Task 03 steht in
|
||||
// DefaultOrder (chain.go): elevenlabs → openai → edge → osbuiltin.
|
||||
const DefaultName = "osbuiltin"
|
||||
|
||||
// ErrNoByteOutput meldet, dass die Plattform keine über Synthesize
|
||||
@@ -24,15 +24,16 @@ var ErrNoByteOutput = errors.New(
|
||||
// Provider ist der Vertrag aller TTS-Backends von talk.
|
||||
//
|
||||
// Synthesize liefert dekodierbare Audio-Bytes; das Format ist von der
|
||||
// Implementierung dokumentiert (osbuiltin: RIFF/WAVE mit PCM — von allen
|
||||
// Sinks der Aufgabe abspielbar; Cloud-Provider ab Task 03: mp3).
|
||||
// Implementierung dokumentiert: osbuiltin liefert RIFF/WAVE (PCM), die
|
||||
// Cloud-Provider elevenlabs, openai und edge liefern MP3 — beides wird vom
|
||||
// audio-Paket (oto-Sink) verstanden.
|
||||
type Provider interface {
|
||||
// Name liefert den eindeutigen Providernamen (CLI --provider, Config).
|
||||
Name() string
|
||||
|
||||
// Available meldet, ob der Provider auf diesem System überhaupt nutzbar
|
||||
// ist (z. B. Binary oder Key vorhanden). Muss billig und ohne Neben-
|
||||
// wirkungen sein; false heißt: Kette soll übersprungen werden.
|
||||
// wirkungen sein; false heißt: Kette soll überspringen werden.
|
||||
Available(ctx context.Context) bool
|
||||
|
||||
// Synthesize erzeugt Audio-Bytes für text in der Sprache lang.
|
||||
@@ -46,8 +47,9 @@ type Provider interface {
|
||||
// Audio-Bytes an Prozesse liefern kann — dort spielt osbuiltin.SpeakDirect
|
||||
// über spd-say selbst ab (blockierend bis zum Ende der Ansage).
|
||||
//
|
||||
// Task 03+ muss diese Erweiterung beachten: In der Kette ist SpeakDirect
|
||||
// die letzte Stufe, wenn kein Provider Bytes liefern kann.
|
||||
// In der Kette (chain.go) ist SpeakDirect die letzte Stufe, wenn kein
|
||||
// Provider Bytes liefern kann; Chain implementiert DirectSpeaker selbst und
|
||||
// reicht an den Stufen-Provider durch.
|
||||
type DirectSpeaker interface {
|
||||
// SpeakDirect gibt text in der Sprache lang direkt über das OS-TTS aus
|
||||
// und blockiert bis zum Ende der Wiedergabe (oder bis ctx abgebrochen
|
||||
@@ -55,10 +57,25 @@ type DirectSpeaker interface {
|
||||
SpeakDirect(ctx context.Context, text, lang string) error
|
||||
}
|
||||
|
||||
// registry bildet Providernamen auf Konstruktoren; Task 03 ergänzt hier
|
||||
// elevenlabs, openai und edge.
|
||||
var registry = map[string]func() Provider{
|
||||
"osbuiltin": func() Provider { return OSBuiltin{} },
|
||||
// VoicePinner ist eine dokumentierte optionale Erweiterung über das
|
||||
// Provider-Interface hinaus: Provider, die eine explizite Voice-ID pinnen
|
||||
// können (das CLI-Flag --voice). Die gepinnte Stimme schlägt das
|
||||
// voice.<provider>.<lang>-Mapping der config.toml und die eingebauten
|
||||
// neutralen Defaults. Die Kette ruft PinVoice beim Bau auf alle Stufen auf,
|
||||
// die die Erweiterung implementieren (Stufen ohne Voice-Begriff, z. B.
|
||||
// osbuiltin, loggen einen Hinweis statt still zu ignorieren).
|
||||
type VoicePinner interface {
|
||||
PinVoice(id string)
|
||||
}
|
||||
|
||||
// registry bildet Providernamen auf Konstruktoren; NewChain und New bauen
|
||||
// darüber. Die Konstruktoren erhalten die ChainConfig (Endpunkte, Voices,
|
||||
// Keys via Getenv), damit Kette und Daemon (Task 04) dieselbe Quelle haben.
|
||||
var registry = map[string]func(ChainConfig) Provider{
|
||||
"elevenlabs": func(cfg ChainConfig) Provider { return NewElevenLabs(cfg) },
|
||||
"openai": func(cfg ChainConfig) Provider { return NewOpenAI(cfg) },
|
||||
"edge": func(cfg ChainConfig) Provider { return NewEdge(cfg) },
|
||||
"osbuiltin": func(ChainConfig) Provider { return OSBuiltin{} },
|
||||
}
|
||||
|
||||
// Names liefert die registrierten Providernamen (sortiert).
|
||||
@@ -71,17 +88,20 @@ func Names() []string {
|
||||
return names
|
||||
}
|
||||
|
||||
// New liefert den Provider mit dem angegebenen Namen; "" wählt den Default.
|
||||
// Unbekannte (oder in diesem Stand noch nicht implementierte) Namen sind
|
||||
// ein klarer Fehler.
|
||||
func New(name string) (Provider, error) {
|
||||
// New liefert den Provider mit dem angegebenen Namen (Einzelkonstruktion);
|
||||
// "" wählt DefaultName. Für die Ansage-Kette (Fallback) NewChain verwenden.
|
||||
// Unbekannte Namen sind ein klarer Fehler.
|
||||
func New(name string, cfg ChainConfig) (Provider, error) {
|
||||
if name == "" {
|
||||
name = DefaultName
|
||||
}
|
||||
ctor, ok := registry[name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unbekannter oder noch nicht implementierter Provider %q (in diesem Stand verfügbar: %s)",
|
||||
return nil, fmt.Errorf("unbekannter oder noch nicht implementierter Provider %q (verfügbar: %s)",
|
||||
name, strings.Join(Names(), ", "))
|
||||
}
|
||||
return ctor(), nil
|
||||
if cfg.Getenv == nil {
|
||||
cfg.Getenv = func(string) string { return "" }
|
||||
}
|
||||
return ctor(cfg), nil
|
||||
}
|
||||
Reference in new issue
Block a user