2026-10-18 15:39:01 +02:00
|
|
|
package provider
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
|
|
|
|
"sort"
|
|
|
|
|
"strings"
|
|
|
|
|
)
|
|
|
|
|
|
2026-10-18 16:34:43 +02:00
|
|
|
// 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.
|
2026-10-18 15:39:01 +02:00
|
|
|
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
|
2026-10-18 16:34:43 +02:00
|
|
|
// Implementierung dokumentiert: osbuiltin liefert RIFF/WAVE (PCM), die
|
|
|
|
|
// Cloud-Provider elevenlabs, openai und edge liefern MP3 — beides wird vom
|
|
|
|
|
// audio-Paket (oto-Sink) verstanden.
|
2026-10-18 15:39:01 +02:00
|
|
|
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-
|
2026-10-18 16:34:43 +02:00
|
|
|
// wirkungen sein; false heißt: Kette soll überspringen werden.
|
2026-10-18 15:39:01 +02:00
|
|
|
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).
|
|
|
|
|
//
|
2026-10-18 16:34:43 +02:00
|
|
|
// 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.
|
2026-10-18 15:39:01 +02:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-18 16:34:43 +02:00
|
|
|
// 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{} },
|
2026-10-18 15:39:01 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-18 16:34:43 +02:00
|
|
|
// 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) {
|
2026-10-18 15:39:01 +02:00
|
|
|
if name == "" {
|
|
|
|
|
name = DefaultName
|
|
|
|
|
}
|
|
|
|
|
ctor, ok := registry[name]
|
|
|
|
|
if !ok {
|
2026-10-18 16:34:43 +02:00
|
|
|
return nil, fmt.Errorf("unbekannter oder noch nicht implementierter Provider %q (verfügbar: %s)",
|
2026-10-18 15:39:01 +02:00
|
|
|
name, strings.Join(Names(), ", "))
|
|
|
|
|
}
|
2026-10-18 16:34:43 +02:00
|
|
|
if cfg.Getenv == nil {
|
|
|
|
|
cfg.Getenv = func(string) string { return "" }
|
|
|
|
|
}
|
|
|
|
|
return ctor(cfg), nil
|
2026-10-18 15:39:01 +02:00
|
|
|
}
|