refactor: switch the whole project to English

talk is going to be published. Code comments, CLI output, error messages,
daemon log lines, MCP descriptions and results, test names and assertions,
README, spec, skill, build scripts and flake are now English. Only the
spoken announcement texts stay bilingual: the wrap result and the test
sentence are German or English depending on the detected language.

Language fallback is now configurable: `lang = "en" | "de"` (env
TALK_LANG, default en) is used when detection cannot tell (numbers, single
words, ties) and for talk's own announcements. langdetect.Detect takes the
fallback as an argument instead of hard-coding German. `talk setup` asks for
it; `talk doctor` and `talk status` show it.

Vocabulary pinned across packages so the process-boundary tests observe
stable strings: chain log lines ("chain: tier X skipped (not available)",
"falling back to the next tier", "spoken via X"), queue lines ("discarded:
quiet hours ... active", "replaced by toast", "max_age"), CLI notes
("speaking directly", "daemon started at", "no daemon", "nothing to cancel"),
status block with aligned English labels, MCP messages, origin guard 403.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
m3ta-chironandClaude Fable 5.1 committed 2026-10-24 11:15:35 +02:00
1 parent 4d30bd95ad
commit 85fa64cfe6
95 files changed
+4707 -4854

No files matched your search

+29 -28
View File
@@ -8,47 +8,48 @@ import (
"strings"
)
// OpenAI-TTS-Konstanten (öffentlicher Vertrag wie bei ElevenLabs).
// OpenAI TTS constants (public contract as with ElevenLabs).
const (
// DefaultEndpointOpenAI ist der produktive API-Endpunkt.
// DefaultEndpointOpenAI is the production API endpoint.
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: 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 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.
// OpenAI is the second tier of the chain (for users with an existing OpenAI
// key). Synthesize performs POST <endpoint>/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 ist die Basis-URL (ohne Pfad); leer = Default beim Bau.
// Endpoint is the base URL (without path); empty = default at
// construction.
Endpoint string
// APIKey ist der OpenAI-Key (aus TALK_OPENAI_KEY).
// APIKey is the OpenAI key (from TALK_OPENAI_KEY).
APIKey string
// Model ist der TTS-Modell-String; leer = DefaultModelOpenAI.
// Model is the TTS model string; empty = DefaultModelOpenAI.
Model string
// Voices ist das config-Mapping voice.openai.<lang> = id; leer =
// neutrale Repo-Defaults (alloy, siehe voices.go).
// Voices is the config mapping voice.openai.<lang> = id; empty =
// neutral repo defaults (alloy, see voices.go).
Voices map[string]string
// Client ist der HTTP-Client; nil = geteilter Client mit Timeout.
// Client is the HTTP client; nil = shared client with timeout.
Client *http.Client
pinned string // --voice-Pin (VoicePinner)
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.
// 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),
@@ -58,19 +59,19 @@ func NewOpenAI(cfg ChainConfig) *OpenAI {
}
}
// Name implementiert Provider.
// Name implements Provider.
func (*OpenAI) Name() string { return "openai" }
// PinVoice implementiert VoicePinner (CLI-Flag --voice).
// PinVoice implements 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).
// 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 ist der Anfrage-Body.
// openAIRequest is the request body.
type openAIRequest struct {
Model string `json:"model"`
Voice string `json:"voice"`
@@ -78,14 +79,14 @@ type openAIRequest struct {
ResponseFormat string `json:"response_format"`
}
// Synthesize implementiert Provider. Rückgabeformat: MP3.
// 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: leerer Text")
return nil, fmt.Errorf("openai: empty text")
}
voice := resolveVoice("openai", lang, p.pinned, p.Voices)
if voice == "" {
return nil, fmt.Errorf("openai: keine Stimme für Sprache %q konfiguriert", lang)
return nil, fmt.Errorf("openai: no voice configured for language %q", lang)
}
model := p.Model
if model == "" {
@@ -98,7 +99,7 @@ func (p *OpenAI) Synthesize(ctx context.Context, text, lang string) ([]byte, err
ResponseFormat: "mp3",
})
if err != nil {
return nil, fmt.Errorf("openai: Anfrage-Body bauen: %w", err)
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,