feat(provider): Provider-Vertrag und osbuiltin für OS-eingebautes TTS

Interface Name/Available/Synthesize (Contract für die Kette aus Task 03)
plus dokumentierte optionale Erweiterung DirectSpeaker für Plattformen,
deren OS-TTS keine Audio-Bytes liefert: spd-say (speech-dispatcher) kann
nach --help-Verifikation nur selbst abspielen — dort spricht
SpeakDirect blockierend (spd-say -w -l <lang>). Synthesize liefert
RIFF/WAVE: Linux espeak/espeak-ng (--stdout), macOS say (temporäre
WAV-Datei, LEI16@22050), Windows SAPI (PowerShell, -EncodedCommand).
Tests mit Fake-Prozessen über PATH-Manipulation; echte spd-say-Ausgabe
nur hinter TALK_TEST_REAL_SPD=1.
This commit is contained in:
m3ta-chiron committed 2026-10-18 15:39:01 +02:00
1 parent 36ad1dfa4e
commit 5f450ea015
4 files changed
+518 -5

No files matched your search

+87
View File
@@ -0,0 +1,87 @@
package provider
import (
"context"
"errors"
"fmt"
"sort"
"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).
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: RIFF/WAVE mit PCM — von allen
// Sinks der Aufgabe abspielbar; Cloud-Provider ab Task 03: mp3).
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.
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).
//
// Task 03+ muss diese Erweiterung beachten: In der Kette ist SpeakDirect
// die letzte Stufe, wenn kein Provider Bytes liefern kann.
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
}
// 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{} },
}
// 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; "" wählt den Default.
// Unbekannte (oder in diesem Stand noch nicht implementierte) Namen sind
// ein klarer Fehler.
func New(name string) (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)",
name, strings.Join(Names(), ", "))
}
return ctor(), nil
}