Files
talk/internal/provider/provider.go
T

107 lines
4.3 KiB
Go
Raw Normal View History

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
}