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