package provider import ( "context" "encoding/json" "fmt" "net/http" "strings" ) // OpenAI-TTS-Konstanten (öffentlicher Vertrag wie bei ElevenLabs). const ( // DefaultEndpointOpenAI ist der produktive API-Endpunkt. DefaultEndpointOpenAI = "https://api.openai.com" // DefaultModelOpenAI: Entscheidungsnotiz aus dem Task-Brief — der // Modell-String ist konfigurierbar ([providers.openai] model), Default // "tts-1"; "gpt-4o-mini-tts" ist ein gültiger Alternativwert. DefaultModelOpenAI = "tts-1" EnvOpenAIKey = "TALK_OPENAI_KEY" EnvOpenAIEndpoint = "TALK_OPENAI_ENDPOINT" ) // OpenAI ist die zweite Stufe der Kette (für Nutzer mit vorhandenem // OpenAI-Key). Synthesize führt POST /v1/audio/speech aus — // Bearer-Key, Body {model, voice, input, response_format: "mp3"} — und // liefert die Antwort-Bytes unverändert zurück. Rückgabeformat: MP3. type OpenAI struct { // Endpoint ist die Basis-URL (ohne Pfad); leer = Default beim Bau. Endpoint string // APIKey ist der OpenAI-Key (aus TALK_OPENAI_KEY). APIKey string // Model ist der TTS-Modell-String; leer = DefaultModelOpenAI. Model string // Voices ist das config-Mapping voice.openai. = id; leer = // neutrale Repo-Defaults (alloy, siehe voices.go). Voices map[string]string // Client ist der HTTP-Client; nil = geteilter Client mit Timeout. Client *http.Client pinned string // --voice-Pin (VoicePinner) } // NewOpenAI baut den Provider aus der Ketten-Konfiguration: Endpunkt aus // TALK_OPENAI_ENDPOINT > [providers.openai].endpoint > Default; Modell aus // [providers.openai].model; Key ausschließlich aus 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 implementiert Provider. func (*OpenAI) Name() string { return "openai" } // PinVoice implementiert VoicePinner (CLI-Flag --voice). func (p *OpenAI) PinVoice(id string) { p.pinned = id } // Available implementiert Provider: Key und Endpunkt müssen gesetzt sein // (ohne Netz-Call, siehe ElevenLabs.Available). func (p *OpenAI) Available(ctx context.Context) bool { return p.APIKey != "" && p.Endpoint != "" } // openAIRequest ist der Anfrage-Body. type openAIRequest struct { Model string `json:"model"` Voice string `json:"voice"` Input string `json:"input"` ResponseFormat string `json:"response_format"` } // Synthesize implementiert Provider. Rückgabeformat: MP3. func (p *OpenAI) Synthesize(ctx context.Context, text, lang string) ([]byte, error) { if text == "" { return nil, fmt.Errorf("openai: leerer Text") } voice := resolveVoice("openai", lang, p.pinned, p.Voices) if voice == "" { return nil, fmt.Errorf("openai: keine Stimme für Sprache %q konfiguriert", 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: Anfrage-Body bauen: %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) }