Files
talk/internal/provider/catalog.go
T
m3ta-chironandClaude Fable 5.1 df9a9b30cf feat(cli): doctor, test, voices, setup — Einrichtung und Diagnose ohne TUI
Usability-Schicht für Nutzer, die kein Terminal-Handbuch lesen wollen:

- `talk doctor`: eine Zeile je Prüfpunkt mit klarem Satz, was Sache ist und
  was zu tun wäre — Config (kaputt → ✘, die übrigen Prüfungen laufen mit
  Defaults weiter), Daemon (läuft / startet beim nächsten Sprechbefehl /
  fremder Dienst auf dem Port → ✘ mit Port-Hinweis), Audio (oto-Kontext
  wirklich initialisieren; null → ⚠), jede Provider-Stufe (Key-Env-Name,
  Endpunkt, Default-Stimme; OS-Stimme nennt den gefundenen Mechanismus),
  Mute (an → ⚠ mit unmute-Hinweis), Gesetze inkl. gerade aktiver Ruhezeit.
  --probe macht echte Synthese-Proben, --json liefert die Liste. Exit 0
  ohne ✘. Windows-Konsolen bekommen [OK]/[WARN]/[FAIL].
- `talk test`: Testsatz direkt über den Sprech-Kern (nicht über den
  Daemon), nennt die Stufe, die wirklich gesprochen hat, Synthese- und
  Wiedergabezeit; Mute/Ruhezeit gelten bewusst nicht, Mute wird genannt.
- `talk voices`: kuratierter Katalog mit Namen (ElevenLabs-Premade, OpenAI,
  Edge-Neural de/en-Locales) als Tabelle, „*“ = effektive Default-Stimme
  (Config vor Repo-Default), Config-IDs außerhalb des Katalogs als eigene
  Zeile, Filter --provider/--lang, --json.
- `talk setup`: wenige Fragen statt Bubble Tea — Keys nur als Hinweis
  (ohne Key läuft die Edge-Stimme), Stimme je Sprache aus dem Katalog mit
  Probehören, Ruhezeit; schreibt die config.toml zusammengeführt mit
  bestehenden Werten (.bak), spricht einen Testsatz. q bricht ab, bevor
  etwas geschrieben wird; ohne Terminal keine Fragen, nur die wirksame
  Konfiguration.

Unterbau: provider.Catalog/VoiceName/DefaultVoice, Chain.SpeakOutcome
(Stufe, Bytes, Zeiten) durch speak.Engine bis in Daemon-Log und test,
audio.Check (oto-Initialisierung ohne Ton), config.Path,
OSBuiltin.Describe. README: Abschnitt „Erste Schritte“.

Tests: voices-Zeilen (Default-Markierung, Config-Override, Filter, JSON),
doctor (ohne Daemon, Mute/Key via JSON, fremder Dienst → Exit 1, kaputte
Config → ✘ aber vollständiger Bericht), test (fake espeak), setup
(stdin-Antworten → config.toml-Inhalt, Merge mit .bak, Abbruch schreibt
nichts, Pipe stellt keine Fragen), Prozessgrenze: doctor/--probe/voices/
test gegen echten Daemon.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-24 10:08:44 +02:00

91 lines
3.4 KiB
Go

package provider
import "sort"
// Voice ist ein Eintrag des Stimmen-Katalogs: eine bekannte Stimme eines
// Providers für eine Sprache, mit lesbarem Namen. Der Katalog ist kuratiert
// (die gängigen Standardstimmen), nicht vollständig — jede Voice-ID des
// Providers funktioniert auch ohne Katalog-Eintrag (--voice, config).
type Voice struct {
Provider string
Lang string // de | en
ID string
Name string
}
// catalog sind die bekannten Stimmen je Provider. ElevenLabs: öffentliche
// Premade-Stimmen (multilingual, daher für de UND en gelistet). OpenAI: die
// Stimmen der TTS-API (sprachunabhängig). Edge: Neural-Stimmen der
// jeweiligen Locale. osbuiltin kennt keine IDs (das OS wählt).
var catalog = []Voice{
{"elevenlabs", "de", "21m00Tcm4TlvDq8ikWAM", "Rachel"},
{"elevenlabs", "de", "pNInz6obpgDQGcFmaJgB", "Adam"},
{"elevenlabs", "de", "EXAVITQu4vr4xnSDxMaL", "Bella"},
{"elevenlabs", "de", "ErXwobaYiN019PkySvjV", "Antoni"},
{"elevenlabs", "de", "TxGEqnHWrfWFTfGW9XjX", "Josh"},
{"elevenlabs", "en", "21m00Tcm4TlvDq8ikWAM", "Rachel"},
{"elevenlabs", "en", "pNInz6obpgDQGcFmaJgB", "Adam"},
{"elevenlabs", "en", "EXAVITQu4vr4xnSDxMaL", "Bella"},
{"elevenlabs", "en", "ErXwobaYiN019PkySvjV", "Antoni"},
{"elevenlabs", "en", "TxGEqnHWrfWFTfGW9XjX", "Josh"},
{"openai", "de", "alloy", "Alloy"},
{"openai", "de", "echo", "Echo"},
{"openai", "de", "fable", "Fable"},
{"openai", "de", "onyx", "Onyx"},
{"openai", "de", "nova", "Nova"},
{"openai", "de", "shimmer", "Shimmer"},
{"openai", "en", "alloy", "Alloy"},
{"openai", "en", "echo", "Echo"},
{"openai", "en", "fable", "Fable"},
{"openai", "en", "onyx", "Onyx"},
{"openai", "en", "nova", "Nova"},
{"openai", "en", "shimmer", "Shimmer"},
{"edge", "de", "de-DE-KatjaNeural", "Katja"},
{"edge", "de", "de-DE-ConradNeural", "Conrad"},
{"edge", "de", "de-DE-AmalaNeural", "Amala"},
{"edge", "de", "de-DE-KillianNeural", "Killian"},
{"edge", "de", "de-AT-IngridNeural", "Ingrid (AT)"},
{"edge", "de", "de-CH-LeniNeural", "Leni (CH)"},
{"edge", "en", "en-US-AriaNeural", "Aria"},
{"edge", "en", "en-US-GuyNeural", "Guy"},
{"edge", "en", "en-US-JennyNeural", "Jenny"},
{"edge", "en", "en-US-ChristopherNeural", "Christopher"},
{"edge", "en", "en-GB-SoniaNeural", "Sonia (GB)"},
{"edge", "en", "en-GB-RyanNeural", "Ryan (GB)"},
}
// Catalog liefert den Stimmen-Katalog in stabiler Reihenfolge
// (Ketten-Reihenfolge der Provider, dann de vor en, dann Katalog-Reihe).
func Catalog() []Voice {
out := make([]Voice, len(catalog))
copy(out, catalog)
rank := map[string]int{}
for i, name := range DefaultOrder {
rank[name] = i
}
sort.SliceStable(out, func(i, j int) bool {
if rank[out[i].Provider] != rank[out[j].Provider] {
return rank[out[i].Provider] < rank[out[j].Provider]
}
return out[i].Lang < out[j].Lang
})
return out
}
// VoiceName liefert den Katalog-Namen einer Voice-ID; leer, wenn unbekannt.
func VoiceName(provider, id string) string {
for _, v := range catalog {
if v.Provider == provider && v.ID == id {
return v.Name
}
}
return ""
}
// DefaultVoice liefert die effektive Default-Stimme eines Providers für
// eine Sprache: config-Mapping voice.<provider>.<lang> vor dem Repo-Default
// (ohne --voice-Pin). Leer bei osbuiltin (das OS wählt).
func DefaultVoice(provider, lang string, cfgVoices map[string]map[string]string) string {
return resolveVoice(provider, lang, "", cfgVoices[provider])
}