Files
talk/README.md
T
m3ta-chiron 24ceff3f48 docs(readme): Rate-Limit, Ruhezeiten und now-Ausbruch dokumentieren
Endpunkt-Tabelle (discarded-Feld, Gesetz-State in /status), neuer
Abschnitt zu Semantik (wer wartet, wer verfällt, was now darf),
Rangordnung mute > now > Ruhezeit > Rate-Limit, Config-/Env-Referenz
und die status-Zeilen der CLI.
2026-10-18 17:40:56 +02:00

178 lines
8.0 KiB
Markdown

# 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>` | 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 + 05)
`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}`; bei Gesetz-Verwurf: `202 {"id":"","discarded":"rate_limit"\|"quiet_hours"}` (+ `"toast":true` im Toast-Modus) — verworfen, dokumentiertes Verhalten |
| `GET /status` | `{"version", "queue", "queue_ids", "current", "muted", "provider", "rate_limit": {"interval", "allow_high_burst", "next_slot"}, "quiet_hours": {"window", "active", "mode", "until"}}` |
| `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`,
bei Gesetz-Verwurf `discarded: <grund>`. `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 + Ruhezeiten (Task 05)
Der Daemon erzwingt das Anti-Spam-Gesetz als Backstop unter allem
Prompt-Verhalten. **Rate-Limit:** Jede *gestartete* Ansage bucht ein
Zeitfenster — Default **1 Ansage / 30 s**. Bis zum Fenster-Ablauf gilt:
- `priority=low` wird **verworfen** (schon beim Eintritt, die 202-Antwort
nennt `discarded: "rate_limit"`); war das Fenster beim Eintritt noch
frei, entscheidet der Worker später.
- `normal` **parkt** in der Queue und spielt zum freien Slot (in `/status`
als `rate_limit.next_slot` sichtbar).
- `priority=high` darf mit `queue.allow_high_burst = true` (Default)
**bewusst verstoßen**; ohne Burst-Recht wartet high wie normal.
- `now:true` **umgeht auch das Rate-Limit** — der dokumentierte Ausbruch
für Dringendes (startet trotzdem ein neues Fenster).
**Ruhezeiten:** `quiet_hours = "18:00-08:00"` (lokale Zeit, überspannt
Mitternacht; leer/`off` = aus — bewusst kein Default-Fenster, Ruhezeiten
sind Opt-in wie Mute). In der Ruhezeit verfällt Audio, auch für high
(Burst ist ein Rate-Recht, kein Ruhe-Recht):
- `quiet_mode = "discard"` (Default): still verwerfen.
- `quiet_mode = "toast"`: Benachrichtigung statt Audio — derzeit eine
Protokoll-Zeile über den Notification-Pfad (OS-Toast folgt mit der
Notification-Anbindung).
- `now:true` **bricht auch die Ruhezeit** (bewusster Ausbruch).
**Rangordnung:** manuelles Mute gewinnt immer > `now` > Ruhezeit >
Rate-Limit. Konfiguration (config.toml, Env schlägt Datei):
```toml
[queue]
rate_limit = "30s" # Dauer; "0s" = unbegrenzt (TALK_RATE_LIMIT)
allow_high_burst = true # high darf verstoßen (TALK_ALLOW_HIGH_BURST)
quiet_hours = "18:00-08:00" # leer = aus; Env TALK_QUIET_HOURS ("off" = aus)
quiet_mode = "discard" # discard | toast (TALK_QUIET_MODE)
```
`talk status` zeigt den Gesetz-State: `rate-limit: 30s (high-burst
erlaubt), nächster slot …` und `ruhezeit: 18:00-08:00 (modus discard,
aktiv bis 08:00)`.
### 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=<name>` oder `provider = "<name>"` 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.<name>] 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.<provider>.<lang>` > 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).