feat(provider): Cloud-Provider und Vier-Stufen-Fallback-Kette
- elevenlabs: POST /v1/text-to-speech/{voice}, Header xi-api-key, Body mit
model_id eleven_multilingual_v2 und voice_settings 0.5/0.75, MP3-Bytes.
- openai: POST /v1/audio/speech, Bearer-Key, Body {model, voice, input,
response_format:mp3}; Modell konfigurierbar (Default tts-1).
- edge: Microsofts Read-Aloud-WSS-Protokoll nach edge-tts-Referenz,
inklusive aktuellem DRM Sec-MS-GEC v2 (CS64 + verschleierte SHA-256-
Variante, wörtlich portiert und gegen Referenzvektoren getestet) und
MUID-Cookie; bei 403 Uhrenkorrektur aus dem Date-Header + ein Retry.
Klare Protokoll-geaendert-Fehler statt stiller Fehlfunktion.
- chain: geordnete Kette (elevenlabs -> openai -> edge -> osbuiltin), Pin
per --provider/config, Available ohne Netz-Calls, Fallback mit
Protokoll je Stufe (nie still). Chain implementiert Provider UND
DirectSpeaker: Speak orchestriert Bytes -> Player bzw. Direktansage,
verwirft am null-Player tonlos — CLI und Daemon (Task 04) nutzen
dieselbe Kette unverändert.
- voices: neutrale Repo-Defaults je Provider+Sprache (Rachel/alloy/
Katja/Aria); Rangfolge --voice > config-Mapping > Default. KXxZ… bleibt
bewusst Privat-Default der eigenen config.toml (Spec: nicht
Repo-Default).
Tests: Cloud-Provider gegen httptest-Fakes (200/500/401, Request-Vertrag
wörtlich), Edge über Interface + Fehlerpfad-Mocks + Loopback-WSS-Fake
(kein echtes Netz) + als testing.Short überspringbare Integration-
Markierung (TALK_TEST_REAL_EDGE=1, am Dev-Host einmalig ausgeführt:
17136 Bytes echtes Audio). Kette: erster 500 -> zweiter getroffen -> Bytes;
Pin, alle-Stufen-tot, Direktansage mit/ohne null-Player.
This commit is contained in:
1 parent
88eddc63d2
commit
91cd0ec8da
15 files changed
+2434
-27
No files matched your search
@@ -0,0 +1,112 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ElevenLabs-Konstanten (öffentlicher Vertrag: Env-Namen sind Teil der
|
||||
// Fleet-Doku, Endpunkt-Default Teil der Konfigurierbarkeit).
|
||||
const (
|
||||
// DefaultEndpointElevenLabs ist der produktive API-Endpunkt.
|
||||
DefaultEndpointElevenLabs = "https://api.elevenlabs.io"
|
||||
|
||||
// EnvElevenLabsKey liefert den API-Key (Spec: Keys ausschließlich Env,
|
||||
// nie config.toml).
|
||||
EnvElevenLabsKey = "TALK_ELEVENLABS_KEY"
|
||||
|
||||
// EnvElevenLabsEndpoint überschreibt den Endpunkt (Tests/Firmenserver;
|
||||
// schlägt die config.toml, siehe voices.go/chain.go-Doku).
|
||||
EnvElevenLabsEndpoint = "TALK_ELEVENLABS_ENDPOINT"
|
||||
|
||||
elevenLabsModel = "eleven_multilingual_v2" // multilingual v2, Task-Brief
|
||||
)
|
||||
|
||||
// ElevenLabs ist die Premium-Stufe der Kette. Synthesize führt POST
|
||||
// <endpoint>/v1/text-to-speech/<voiceID> aus — Header `xi-api-key`, Body
|
||||
// {text, model_id: "eleven_multilingual_v2", voice_settings:
|
||||
// {stability: 0.5, similarity_boost: 0.75}} — und liefert die Antwort-Bytes
|
||||
// unverändert zurück. Rückgabeformat: MP3 (dokumentierter Vertrag des
|
||||
// audio-Pakets).
|
||||
type ElevenLabs struct {
|
||||
// Endpoint ist die Basis-URL (ohne Pfad); leer wird beim Bau durch den
|
||||
// Default ersetzt.
|
||||
Endpoint string
|
||||
|
||||
// APIKey ist der ElevenLabs-Key (aus TALK_ELEVENLABS_KEY).
|
||||
APIKey string
|
||||
|
||||
// Voices ist das config-Mapping voice.elevenlabs.<lang> = id; leer =
|
||||
// neutrale Repo-Defaults (siehe voices.go).
|
||||
Voices map[string]string
|
||||
|
||||
// Client ist der HTTP-Client; nil = geteilter Client mit Timeout
|
||||
// (cloudTimeout).
|
||||
Client *http.Client
|
||||
|
||||
pinned string // --voice-Pin (VoicePinner)
|
||||
}
|
||||
|
||||
// NewElevenLabs baut den Provider aus der Ketten-Konfiguration: Endpunkt
|
||||
// aus TALK_ELEVENLABS_ENDPOINT > [providers.elevenlabs].endpoint > Default;
|
||||
// Key ausschließlich aus TALK_ELEVENLABS_KEY.
|
||||
func NewElevenLabs(cfg ChainConfig) *ElevenLabs {
|
||||
return &ElevenLabs{
|
||||
Endpoint: endpointFrom(cfg, "elevenlabs", EnvElevenLabsEndpoint, DefaultEndpointElevenLabs),
|
||||
APIKey: cfg.Getenv(EnvElevenLabsKey),
|
||||
Voices: cfg.Voices["elevenlabs"],
|
||||
}
|
||||
}
|
||||
|
||||
// Name implementiert Provider.
|
||||
func (*ElevenLabs) Name() string { return "elevenlabs" }
|
||||
|
||||
// PinVoice implementiert VoicePinner (CLI-Flag --voice).
|
||||
func (p *ElevenLabs) PinVoice(id string) { p.pinned = id }
|
||||
|
||||
// Available implementiert Provider: Key und Endpunkt müssen gesetzt sein
|
||||
// (bewusst ohne Netz-Call — Available muss billig bleiben; ob der Key
|
||||
// gültig ist, stellt Synthesize fest und die Kette fällt weiter).
|
||||
func (p *ElevenLabs) Available(ctx context.Context) bool {
|
||||
return p.APIKey != "" && p.Endpoint != ""
|
||||
}
|
||||
|
||||
// elevenLabsRequest ist der Antwort-Body (Task-Brief, wörtliche Werte).
|
||||
type elevenLabsRequest struct {
|
||||
Text string `json:"text"`
|
||||
ModelID string `json:"model_id"`
|
||||
VoiceSettings elevenLabsVoiceSettings `json:"voice_settings"`
|
||||
}
|
||||
|
||||
type elevenLabsVoiceSettings struct {
|
||||
Stability float64 `json:"stability"`
|
||||
SimilarityBoost float64 `json:"similarity_boost"`
|
||||
}
|
||||
|
||||
// Synthesize implementiert Provider. Rückgabeformat: MP3.
|
||||
func (p *ElevenLabs) Synthesize(ctx context.Context, text, lang string) ([]byte, error) {
|
||||
if text == "" {
|
||||
return nil, fmt.Errorf("elevenlabs: leerer Text")
|
||||
}
|
||||
voice := resolveVoice("elevenlabs", lang, p.pinned, p.Voices)
|
||||
if voice == "" {
|
||||
return nil, fmt.Errorf("elevenlabs: keine Stimme für Sprache %q konfiguriert", lang)
|
||||
}
|
||||
body, err := json.Marshal(elevenLabsRequest{
|
||||
Text: text,
|
||||
ModelID: elevenLabsModel,
|
||||
VoiceSettings: elevenLabsVoiceSettings{Stability: 0.5, SimilarityBoost: 0.75},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("elevenlabs: Anfrage-Body bauen: %w", err)
|
||||
}
|
||||
url := strings.TrimRight(p.Endpoint, "/") + "/v1/text-to-speech/" + voice
|
||||
return postForBytes(ctx, p.Client, "elevenlabs", url,
|
||||
map[string]string{
|
||||
"xi-api-key": p.APIKey,
|
||||
"Accept": "audio/mpeg",
|
||||
}, body)
|
||||
}
|
||||
Reference in new issue
Block a user