package provider import ( "context" "encoding/json" "fmt" "net/http" "strings" ) // OpenAI TTS constants (public contract as with ElevenLabs). const ( // DefaultEndpointOpenAI is the production API endpoint. DefaultEndpointOpenAI = "https://api.openai.com" // DefaultModelOpenAI: decision note. The model string is configurable // ([providers.openai] model), default "tts-1"; "gpt-4o-mini-tts" is a // valid alternative. DefaultModelOpenAI = "tts-1" EnvOpenAIKey = "TALK_OPENAI_KEY" EnvOpenAIEndpoint = "TALK_OPENAI_ENDPOINT" ) // OpenAI is the second tier of the chain (for users with an existing OpenAI // key). Synthesize performs POST /v1/audio/speech with a bearer // key and body {model, voice, input, response_format: "mp3"} and returns // the response bytes unchanged. Return format: MP3. type OpenAI struct { // Endpoint is the base URL (without path); empty = default at // construction. Endpoint string // APIKey is the OpenAI key (from TALK_OPENAI_KEY). APIKey string // Model is the TTS model string; empty = DefaultModelOpenAI. Model string // Voices is the config mapping voice.openai. = id; empty = // neutral repo defaults (alloy, see voices.go). Voices map[string]string // Client is the HTTP client; nil = shared client with timeout. Client *http.Client pinned string // --voice pin (VoicePinner) } // NewOpenAI builds the provider from the chain configuration: endpoint from // TALK_OPENAI_ENDPOINT > [providers.openai].endpoint > default; model from // [providers.openai].model; key exclusively from TALK_OPENAI_KEY. func NewOpenAI(cfg ChainConfig) *OpenAI { return &OpenAI{ Endpoint: endpointFrom(cfg, "openai", EnvOpenAIEndpoint, DefaultEndpointOpenAI), APIKey: cfg.Getenv(EnvOpenAIKey), Model: cfg.Model, Voices: cfg.Voices["openai"], } } // Name implements Provider. func (*OpenAI) Name() string { return "openai" } // PinVoice implements VoicePinner (CLI flag --voice). func (p *OpenAI) PinVoice(id string) { p.pinned = id } // Available implements Provider: key and endpoint must be set (without a // network call, see ElevenLabs.Available). func (p *OpenAI) Available(ctx context.Context) bool { return p.APIKey != "" && p.Endpoint != "" } // openAIRequest is the request body. type openAIRequest struct { Model string `json:"model"` Voice string `json:"voice"` Input string `json:"input"` ResponseFormat string `json:"response_format"` } // Synthesize implements Provider. Return format: MP3. func (p *OpenAI) Synthesize(ctx context.Context, text, lang string) ([]byte, error) { if text == "" { return nil, fmt.Errorf("openai: empty text") } voice := resolveVoice("openai", lang, p.pinned, p.Voices) if voice == "" { return nil, fmt.Errorf("openai: no voice configured for language %q", lang) } model := p.Model if model == "" { model = DefaultModelOpenAI } body, err := json.Marshal(openAIRequest{ Model: model, Voice: voice, Input: text, ResponseFormat: "mp3", }) if err != nil { return nil, fmt.Errorf("openai: building request body: %w", err) } url := strings.TrimRight(p.Endpoint, "/") + "/v1/audio/speech" return postForBytes(ctx, p.Client, "openai", url, map[string]string{"Authorization": "Bearer " + p.APIKey}, body) }