diff --git a/README.md b/README.md index ab930dd..38f1087 100644 --- a/README.md +++ b/README.md @@ -28,11 +28,42 @@ keine Runtime-Abhängigkeiten. Say-Flags (gelten auch für `talk "Text"`): `--wait`, `--now`, `--priority`, `--voice`, `--lang`, `--provider`. -**Hinweis:** In diesem Stand ist `say` (und der Default-Aufruf -`talk "Text"`) implementiert — inklusive der kompletten Provider-Kette -(ElevenLabs → OpenAI → Edge-TTS → OS-Stimme, blockierend; -Daemon-Anbindung folgt). Alle anderen Befehle sind Stubs und melden -`not implemented yet` (Exit-Code ≠ 0). +**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) diff --git a/cmd/talk/control.go b/cmd/talk/control.go index 354c2f6..e1113ec 100644 --- a/cmd/talk/control.go +++ b/cmd/talk/control.go @@ -43,7 +43,10 @@ func setMute(muted bool, stdout, stderr io.Writer) int { return exitOK } - fmt.Fprintf(stderr, "talk: Daemon nicht erreichbar (%v) — Mute-Datei wird direkt geschrieben\n", err) + // Daemon-Aufruf fehlgeschlagen (meist: nicht erreichbar): Datei direkt + // schreiben — der Daemon (und der Degraded Mode) liest sie bei jeder + // Ansage neu. + fmt.Fprintf(stderr, "talk: Daemon-Aufruf fehlgeschlagen (%v) — Mute-Datei wird direkt geschrieben\n", err) dir, err := config.Dir(getenv) if err != nil { fmt.Fprintf(stderr, "talk: %v\n", err)