Files
talk/internal/provider/openai.go
T

106 lines
3.3 KiB
Go
Raw Normal View History

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 <endpoint>/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.<lang> = 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)
}