Files
talk/internal/provider/provider.go
T

106 lines
4.1 KiB
Go
Raw Permalink Normal View History

package provider
import (
"context"
"errors"
"fmt"
"sort"
"strings"
)
// DefaultName is the provider New("") returns (single construction, e.g.
// for doctor). The default announcement chain lives in DefaultOrder
// (chain.go): elevenlabs → openai → edge → osbuiltin.
const DefaultName = "osbuiltin"
// ErrNoByteOutput reports that the platform cannot provide audio bytes via
// Synthesize; concretely Linux with speech-dispatcher (spd-say) but without
// espeak: spd-say can only play audio itself through the daemon, it cannot
// write it to a process. Callers should fall back to DirectSpeaker (see
// there).
var ErrNoByteOutput = errors.New(
"this platform cannot provide audio bytes (Linux without espeak; spd-say only speaks directly)")
// Provider is the contract of all talk TTS backends.
//
// Synthesize returns decodable audio bytes; the format is documented by the
// implementation: osbuiltin returns RIFF/WAVE (PCM), the cloud providers
// elevenlabs, openai and edge return MP3. Both are understood by the audio
// package (oto sink).
type Provider interface {
// Name returns the unique provider name (CLI --provider, config).
Name() string
// Available reports whether the provider is usable on this system at all
// (e.g. binary or key present). Must be cheap and side-effect free;
// false means: the chain should skip this tier.
Available(ctx context.Context) bool
// Synthesize produces audio bytes for text in language lang.
Synthesize(ctx context.Context, text, lang string) ([]byte, error)
}
// DirectSpeaker is a documented optional extension beyond the Provider
// interface (not part of the base contract): providers that can make text
// audible themselves without going through Synthesize bytes. Required on
// Linux systems whose OS TTS (speech-dispatcher) cannot hand audio bytes to
// processes; there osbuiltin.SpeakDirect plays via spd-say itself (blocking
// until the announcement has finished).
//
// In the chain (chain.go) SpeakDirect is the last tier when no provider can
// deliver bytes; Chain implements DirectSpeaker itself and forwards to the
// tier provider.
type DirectSpeaker interface {
// SpeakDirect speaks text in language lang directly through the OS TTS
// and blocks until playback has finished (or ctx is cancelled).
SpeakDirect(ctx context.Context, text, lang string) error
}
// VoicePinner is a documented optional extension beyond the Provider
// interface: providers that can pin an explicit voice ID (the CLI flag
// --voice). The pinned voice beats the voice.<provider>.<lang> mapping of
// config.toml and the built-in neutral defaults. The chain calls PinVoice
// at construction on all tiers that implement the extension (tiers without
// a notion of voice, e.g. osbuiltin, log a note instead of silently
// ignoring it).
type VoicePinner interface {
PinVoice(id string)
}
// registry maps provider names to constructors; NewChain and New build
// through it. The constructors receive the ChainConfig (endpoints, voices,
// keys via Getenv) so that the chain and the daemon share one source.
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 returns the registered provider names (sorted).
func Names() []string {
names := make([]string, 0, len(registry))
for name := range registry {
names = append(names, name)
}
sort.Strings(names)
return names
}
// New returns the provider with the given name (single construction); ""
// selects DefaultName. For the announcement chain (fallback) use NewChain.
// Unknown names are a clear error.
func New(name string, cfg ChainConfig) (Provider, error) {
if name == "" {
name = DefaultName
}
ctor, ok := registry[name]
if !ok {
return nil, fmt.Errorf("unknown provider %q (available: %s)",
name, strings.Join(Names(), ", "))
}
if cfg.Getenv == nil {
cfg.Getenv = func(string) string { return "" }
}
return ctor(cfg), nil
}