- 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.
108 lines
4.3 KiB
Go
108 lines
4.3 KiB
Go
package provider
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// 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
|
|
// beziehbaren Audio-Bytes liefern kann — konkret Linux mit speech-dispatcher
|
|
// (spd-say) ohne espeak: spd-say kann Audio nur selbst über den Daemon
|
|
// abspielen, nicht in einen Prozess schreiben. Aufrufer sollen auf
|
|
// DirectSpeaker ausweichen (siehe dort).
|
|
var ErrNoByteOutput = errors.New(
|
|
"diese Plattform kann keine Audio-Bytes liefern (Linux ohne espeak; spd-say spielt nur direkt)")
|
|
|
|
// Provider ist der Vertrag aller TTS-Backends von talk.
|
|
//
|
|
// Synthesize liefert dekodierbare Audio-Bytes; das Format ist von der
|
|
// 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 überspringen werden.
|
|
Available(ctx context.Context) bool
|
|
|
|
// Synthesize erzeugt Audio-Bytes für text in der Sprache lang.
|
|
Synthesize(ctx context.Context, text, lang string) ([]byte, error)
|
|
}
|
|
|
|
// DirectSpeaker ist eine dokumentierte optionale Erweiterung über das
|
|
// Provider-Interface hinaus (kein Bestandteil des Basisvertrags): Provider,
|
|
// die Text ohne Umweg über Synthesize-Bytes selbst hörbar ausgeben können.
|
|
// Notwendig auf Linux-Systemen, deren OS-TTS (speech-dispatcher) keine
|
|
// Audio-Bytes an Prozesse liefern kann — dort spielt osbuiltin.SpeakDirect
|
|
// über spd-say selbst ab (blockierend bis zum Ende der Ansage).
|
|
//
|
|
// 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
|
|
// wird).
|
|
SpeakDirect(ctx context.Context, text, lang string) error
|
|
}
|
|
|
|
// 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).
|
|
func Names() []string {
|
|
names := make([]string, 0, len(registry))
|
|
for name := range registry {
|
|
names = append(names, name)
|
|
}
|
|
sort.Strings(names)
|
|
return names
|
|
}
|
|
|
|
// 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 (verfügbar: %s)",
|
|
name, strings.Join(Names(), ", "))
|
|
}
|
|
if cfg.Getenv == nil {
|
|
cfg.Getenv = func(string) string { return "" }
|
|
}
|
|
return ctor(cfg), nil
|
|
}
|