# talk > **Status: WIP** — aktive Entwicklung (talk 2.0). Binde Spezifikation: > [`.scratch/talk-2-0/spec.md`](.scratch/talk-2-0/spec.md) · > Implementierungsplan: [`.scratch/talk-2-0/plan.md`](.scratch/talk-2-0/plan.md) **talk** macht Skripte und KI-Agenten hörbar: Ein Aufruf — `talk "Build fertig"` — spricht eine kurze Nachricht über eine TTS-Provider-Kette (ElevenLabs → OpenAI → Edge-TTS → OS-Stimme), serialisiert durch den Daemon `talkd`. Cross-platform (Windows/macOS/Linux, amd64/arm64), statische Binaries, keine Runtime-Abhängigkeiten. ## Befehle | Befehl | Zweck | | --- | --- | | `talk "Text"` | Ansage — Abkürzung für `talk say`, non-blocking | | `talk say "Text"` | Ansage (Text als Argument oder von stdin) | | `talk wrap -- ` | Befehl ausführen, Erfolg/Misserfolg + Dauer ansagen | | `talk mute` / `talk unmute` | Ansagen dauerhaft stummschalten / Stummschaltung aufheben | | `talk status` | Daemon-Status (Queue, Mute, Provider, Version) | | `talk cancel` | laufende und geplante Ansagen abbrechen | | `talk voices` | verfügbare Stimmen anzeigen | | `talk test` | Test-Ansage ausgeben | | `talk doctor` | Selbstdiagnose des Setups | | `talk setup` | geführte Ersteinrichtung (Wizard) | Say-Flags (gelten auch für `talk "Text"`): `--wait`, `--now`, `--priority`, `--voice`, `--lang`, `--provider`. **Hinweis:** In diesem Stand sind implementiert: `say` (und `talk "Text"`) mit der kompletten Provider-Kette und Daemon-Anbindung (non-blocking über `talkd`, siehe unten), `mute`, `unmute`, `status`, `cancel`. Die übrigen Befehle sind Stubs und melden `not implemented yet` (Exit-Code ≠ 0). ## Daemon talkd (Task 04) `talkd` besitzt die Ansage-Queue und spricht seriell ab — genau eine Ansage gleichzeitig, Reihenfolge `now` > `high` > `normal` > `low` (FIFO je Level). Der HTTP-Server lauscht nur auf `127.0.0.1`; Port: Default `7800`, config `[server] port`, Env `TALK_PORT` (schlagen den Default). | Endpunkt | Wirkung | | --- | --- | | `POST /speak` | `{text, priority?, provider?, voice?, lang?, now?}` → `202 {"id":"…"}`; bei Mute: `202 {"id":"","muted":true}` (Ansage verworfen — dokumentiertes Verhalten) | | `GET /status` | `{"version", "queue", "queue_ids", "current", "muted", "provider"}` | | `POST /cancel` | bricht die laufende Ansage ab + leert die Queue → `200 {"cancelled":n}` | | `POST /mute` / `POST /unmute` | persistenter Mute-State → `200 {"muted":bool}` | - **CLI-Verhalten:** `talk say` (und `talk "Text"`) erkennt den Daemon via `GET /status`. Läuft er: enqueue + sofortige Rückkehr (non-blocking, stdout erhält die Item-ID); `--wait` pollt bis zur Fertigstellung; `--now` springt vor die Warteschlange; bei Mute sagt die CLI `muted`. `talk mute`/`unmute`/`status`/`cancel` sind Daemon-Aufrufe mit plain Output. - **Mute ist persistent** (Datei `mute` im Config-Dir, Inhalt `true`/ `false`) und überlebt Daemon-Restarts. `talk mute` ohne laufenden Daemon schreibt die Datei direkt — der Daemon liest sie bei jeder Ansage neu, der Zustand greift also auch offline. - **Degraded Mode:** Antwortet kein Daemon, spielt `talk` lokal und sequenziell weiter — serialisiert über die Lock-Datei `talk.lock` im Config-Dir (Kollision = warten; verwaisste Locks gelten nach 60 s als verfallen). Mute wird auch hier respektiert. Nichts bricht still. - Rate-Limit und Ruhezeiten folgen in Task 05; die Rate-Limit-Struktur existiert bereits (`internal/queue`), ist in diesem Stand aber bewusst noch nicht angebunden (Default: unbegrenzt). ### Provider-Kette (Task 03) Ohne Pin läuft die Vier-Stufen-Fallback-Kette **ElevenLabs → OpenAI TTS → Edge-TTS → OS built-in**: Bei fehlendem Key, HTTP-Fehler, Timeout oder Netz-Problem fällt die Kette automatisch auf die nächste Stufe; jeder Fallback wird auf stderr protokolliert (nie still). Pinnen per `--provider=` oder `provider = ""` in der config.toml. | Stufe | Key (Env) | Default-Endpunkt | Audio | | --- | --- | --- | --- | | `elevenlabs` | `TALK_ELEVENLABS_KEY` | `https://api.elevenlabs.io` | mp3 | | `openai` | `TALK_OPENAI_KEY` | `https://api.openai.com` | mp3 | | `edge` | — (kostenlos) | `wss://speech.platform.bing.com/...` | mp3 | | `osbuiltin` | — | lokale OS-TTS | WAV/PCM | Keys laufen ausschließlich über Env-Variablen (nie in die config.toml). Endpunkte sind konfigurierbar — Datei `[providers.] endpoint = "…"` bzw. Env `TALK_ELEVENLABS_ENDPOINT` / `TALK_OPENAI_ENDPOINT` / `TALK_EDGE_ENDPOINT` (Env schlägt Datei; so zeigen Tests/CI die Kette auf Fake-Server). Das OpenAI-Modell ist konfigurierbar (`[providers.openai] model`, Default `tts-1`). Stimmen: neutralen Repo-Defaults je Provider+Sprache (ElevenLabs „Rachel", OpenAI „alloy", Edge `de-DE-KatjaNeural`/`en-US-AriaNeural`) kann man pro Sprache überordnen — config.toml: ```toml [voice.elevenlabs] de = "21m00Tcm4TlvDq8ikWAM" # eigene Stimme je Sprache en = "21m00Tcm4TlvDq8ikWAM" [voice.edge] de = "de-DE-KatjaNeural" ``` Rangfolge: `--voice`-Flag > `voice..` > Repo-Default. MP3 wird von der Binärdatei selbst dekodiert (reiner Go-Dekoder, kein ffmpeg, CGO-frei). ### OS-TTS auf Linux (Dev-Abhängigkeit espeak) Der OS-Provider `osbuiltin` nutzt auf Linux zwei Wege: - **Audio-Bytes** (Wiedergabe über den eingebauten oto-Sink): benötigt `espeak` (oder `espeak-ng`). Auf NixOS als Dev-Abhängigkeit: `nix shell nixpkgs#espeak-ng`. - **Direktansage** ohne espeak: `spd-say` (speech-dispatcher) — spielt selbst über den Daemon ab, kann aber nach `spd-say --help` keine Bytes in Dateien/Pipes liefern. `talk` nutzt diesen Weg automatisch, wenn kein espeak vorhanden ist. `TALK_AUDIO_SINK=null` (oder `audio.sink = "null"` in der config.toml) schaltet jede Tongabe ab — für CI und Server. ## Entwicklung ```console go build ./... # bauen go test ./... # Tests go vet ./... # statische Analyse gofmt -l . # Formatierung prüfen (leerer Output = ok) ``` CI: `.gitea/workflows/test.yml` (Gitea Actions) läuft vet, gofmt-Check, Tests und Build bei jedem Push und Pull Request. ## Lizenz MIT — siehe [LICENSE](LICENSE).