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..-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 }