Commit Graph
13 Commits
Author SHA1 Message Date
m3ta-chironandClaude Fable 5.1 97650ad0ec feat(queue): Gesetze max_text und max_age — gekürzte Langtexte, verfallene Stau-Ansagen
Zwei Löcher im Anti-Spam-Gesetz, die ein Prompt trivial nutzt: Ein Agent
kann 64 KB Text einreichen (ElevenLabs stellt die Rechnung, die Ansage
dauert Minuten), und geparkte normal-Ansagen spielen unbegrenzt spät —
„Build fertig" zweieinhalb Minuten nach dem Build ist keine Nachricht.

- queue.max_text (Default 400 Zeichen, Env TALK_MAX_TEXT): längere Texte
  werden beim Eintritt an einer Wortgrenze gekürzt und so gesprochen; die
  /speak-Antwort meldet truncated, die CLI warnt auf stderr, das MCP-
  Ergebnis sagt es im Text. Runen, nicht Bytes.
- queue.max_age (Default 2m, Env TALK_MAX_AGE): Items, die beim
  Fälligwerden länger gewartet haben, werden verworfen (discarded
  "expired", Protokoll nennt Wartezeit und Grenze). now:true ist
  ausgenommen. Rang: now > veraltet > Ruhezeit > Rate-Limit.
- /status trägt "limits": {max_text, max_age}; `talk status` zeigt sie.
- Negative Werte sind Config-Fehler; 0 = unbegrenzt.

Tests: truncateText-Tabelle (Wortgrenze, Satzzeichen, Runen), Enqueue-
Kürzung, decide-Vorrang inkl. now-Ausnahme, geparktes Item verfällt;
Config-Schichten; Prozessgrenze: Provider bekommt die gekürzte Fassung,
dritte von drei Stau-Ansagen wird nie gesprochen.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-24 09:54:40 +02:00
m3ta-chironandClaude Fable 5.1 bc26a73aeb refactor: ein Binary — talk startet den Daemon selbst, talkd und Degraded Mode entfallen
Zwei Binaries erzeugten den halben Komplexitätsballast: Degraded Mode mit
Lock-Datei und Stale-Lock-Race, „läuft talkd?", Autostart-Konfiguration je
OS, doppelte Chain-Konstruktion in cmd/talk und cmd/talkd — und der
Degraded Mode kannte weder Rate-Limit noch Ruhezeiten.

Jetzt:
- internal/speak: der eine Sprech-Kern (Config + Sink + Kette → Ansage);
  Daemon und Direkt-Pfad benutzen ihn, es gibt keine zweite
  Zusammensetzung der Kette mehr.
- internal/daemon: Run (der bisherige talkd-Kern), Spawn (detached per
  Setsid bzw. DETACHED_PROCESS, stdout/stderr → daemon.log), PID-Datei
  daemon.pid, Shutdown-Trigger.
- `talk daemon run|start|stop`: Vordergrund (systemd/launchd), expliziter
  Hintergrund-Start, Stop per POST /shutdown — stop wartet, bis HTTP weg
  und die PID-Datei gelöscht ist (Prozess wirklich beendet).
- Autostart: say/wrap starten den Daemon beim ersten Sprechbefehl selbst
  (daemon.autostart, Default an; Env TALK_AUTOSTART). Verliert ein
  paralleler Aufruf das Port-Rennen, erkennt er das am Kind-Ende und nutzt
  den Daemon des anderen — kein Timeout-Warten.
- Direkt-Pfad statt Degraded Mode: ohne erreichbaren oder startbaren
  Daemon spricht talk direkt (Mute respektiert, kein Lock, Hinweis +
  Grund + Log-Pfad auf stderr). Steuerbefehle starten den Daemon bewusst
  nicht; `talk status` zeigt „läuft nicht" (Exit 1).
- cmd/talkd und cmd/talk/degraded.go entfernt; README-Daemon-Abschnitt
  neu.

Tests: Harness baut nur noch talk und startet `talk daemon run`;
TALK_AUTOSTART=false als Grundausstattung aller Tests (kein Test
hinterlässt Daemons), autostart_test.go schaltet es bewusst an: Autostart
beim ersten say, Idempotenz, paralleler Start in EINE Queue, expliziter
start/stop, Port fremd belegt → Direkt-Pfad ohne Timeout. Cross-Compile
aller sechs Targets mit CGO_ENABLED=0 verifiziert.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-24 09:49:16 +02:00
m3ta-chiron 51cd5a686c feat(cli): talk wrap — Befehlsummwicklung mit Ansage und Code-Durchreichung
talk wrap -- <Befehl> [Argumente …] führt den Befehl aus (stdin/stdout/
stderr durchgereicht — stdout gehört allein dem Befehl, talk-Informations-
zeilen gehen auf stderr), misst die Wandzeit und sagt danach das Ergebnis
an: „<label> erfolgreich. Dauer: X.“ bzw. „<label> fehlgeschlagen,
Code N.“, menschlich formatiert (Stunden/Minuten/Sekunden, Einzahl
dekliniert) und lokalisiert de/en nach langdetect des Labels (--lang
überschreibt). Label: config wrap.label bzw. TALK_WRAP_LABEL oder der
Basisname des Befehls.

Exit-Code-Politik: wrap exitet immer mit dem Code des Befehls — scheitert
die Ansage selbst (Daemon lehnt ab, Kette leer), bleibt der Befehls-Code
mit Warning auf stderr (talk-Fehler ≠ Befehls-Fehler). Nicht startbare
Befehle zählen als 127 (Shell-Konvention), Signaltode portabel als 1
(syscall.WaitStatus wäre nicht frei cross-compilierbar; dokumentiert).
Benutzungsfehler von talk (kein Befehl, ungültige say-Flags) scheitern
vor dem Befehlsstart; Config-Fehler nach dem Lauf — Ansage ausgesetzt.

Ansageweg wie say: Daemon-Pfad (enqueue, non-blocking; --wait pollt),
ohne Daemon Degraded Mode; wrap erbt die say-Flags und reicht sie durch.
Dafür bekommen sayViaDaemon/sayDegraded einen info-Writer: say schreibt
Item-ID/muted/discarded weiter auf stdout, wrap auf stderr — Pipes des
Befehls bleiben sauber. Unit-Tests: Dauer-Formatierung (de/en), Ansage-
Texte, Label-Auflösung, In-Prozess-Durchreichung gegen Fake-Provider
(Erfolg/Fehlschlag/127/Label/Ansage-scheitert/stdout-Reinheit).
2026-10-18 21:01:07 +02:00
m3ta-chiron 671a97ca71 fix(cli): Erstes „--“ schirmt Positionsargumente vollständig ab
Der Interspersed-Loop von Task 03 nahm nach jedem Positionsargument das
Flag-Parsen wieder auf und hob damit die Stdlib-Terminator-Semantik von
„--“ auf: Argumente, die für einen NACHFOLGENDEN Befehl gemeint waren,
wurden als talk-Flags fehlinterpretiert (talk wrap -- foo --bar wäre an
--bar als unbekanntem Flag gescheitert; auch talk -- „Text --flag“ brach).
Jetzt trennt parseInterspersed argv am ersten allein stehenden „--“: Nur
der Teil davor wird interspersed geparst, der Rest ist bedingungslos
Positionsargument. Die Abnahme-Form „Flag nach dem Text“ ohne „--“
(talk "Text" --provider=x) bleibt unverändert — Regressionstests für
beide Formen in parse_test.go.
2026-10-18 20:46:39 +02:00
m3ta-chiron 156c20dbb3 feat(api): CORS-Tür für POST /speak — OPTIONS-Preflight und ACAO aus Config
OPTIONS /speak beantwortet Preflights mit 204 + Allow-Headers/Methods/Max-Age,
wenn die Origin in server.allowed_origins steht; ungeliste Origins und leere
Liste bekommen keine CORS-Header (Browser blockt). Nur /speak, kein anderer
Endpunkt. /status ergänzt additiv cors:[origins] (immer vorhanden, [] wenn zu).
2026-10-18 20:06:05 +02:00
m3ta-chiron b56c8abfae feat(api): /speak meldet Gesetz-Verwürfe, /status zeigt Gesetz-State
POST /speak antwortet bei Verwurf dokumentiert mit 202 {id:"",
discarded:"rate_limit"|"quiet_hours"} (+ toast:true im Toast-Modus)
— analog zum Mute-Muster, kein Fehler. GET /status ergänzt additiv
rate_limit {interval, allow_high_burst, next_slot} und quiet_hours
{window, active, mode, until} (RFC3339 bzw. null). talkd verwired die
Gesetze aus der Config und loggt sie beim Start. Die CLI meldet
'discarded: <grund>' plain (wie muted) und zeigt in talk status die
Zeilen rate-limit/ruhezeit mit Slot bzw. Fenster-Ende.
2026-10-18 17:40:46 +02:00
m3ta-chiron dc94f45d11 docs(readme): Daemon talkd, HTTP-Endpunkte, Mute-Persistenz und Degraded Mode dokumentieren 2026-10-18 17:11:14 +02:00
m3ta-chiron e1a3981d17 feat(cli): say über den Daemon, Steuer-Befehle als Daemon-Aufrufe, Degraded Mode
- say/Default erkennt den Daemon via GET /status: enqueue + sofort zurück
  (stdout = Item-ID), --wait pollt bis zur Fertigstellung, --now springt vor
- mute/unmute/status/cancel mit plain Output; mute/unmute schreiben ohne
  Daemon die Mute-Datei direkt (gleiche Quelle, wirkt sofort im Daemon)
- Degraded Mode: Lock-Datei talk.lock im Config-Dir (Kollision = warten,
  verwaist nach 60 s), sequenzielles lokales Abspielen, mute respektiert
- Handler-Signatur der Befehlstabelle erhält stdout (say/Steuer-Befehle
  geben Ergebnisse aus); --priority wird validiert
2026-10-18 17:10:36 +02:00
m3ta-chiron d09aff6874 feat(talkd): Daemon mit HTTP-API auf 127.0.0.1, Queue-Betrieb und Shutdown
- HTTP-Server nur auf 127.0.0.1 (Port aus config server.port/TALK_PORT),
  klarer Fehler wenn der Port belegt ist
- Ansagen laufen über die unveränderte Provider-Kette (pro Item gebaut,
  Pin/Voice je Anfrage); /status meldet den aktiven Provider
- SIGINT/SIGTERM: erst HTTP dicht, dann Queue schließen (laufende Ansage
  wird abgebrochen, Worker-Exit wird abgewartet)
- Mute ohne Config-Dir: dokumentierte Warnung statt Absturz
2026-10-18 17:10:36 +02:00
m3ta-chiron 28b4e558c3 feat(cli): say-Pfad über die Provider-Kette
runSay baut jetzt die Kette aus Config + Flags (Pin: --provider schlägt
config provider; --voice wird über VoicePinner durchgepinnt) und ruft
chain.Speak — Synthese mit Fallback, Wiedergabe über den Sink, Direkt-
ansage inklusive tonlosem Verwerfen am null-Sink steckt komplett in der
Kette (wiederverwendbar für den Daemon aus Task 04).

Zusätzlich: Flags dürfen jetzt überall stehen — Go-stdlib-flag stoppt
bei dem ersten Nicht-Flag-Argument, die Abnahme-Aufrufform ist aber
`talk "Test" --provider=elevenlabs`. parseInterspersed parst wiederholt
über den Rest; Positionsargumente (der Text) bleiben vollständig.

Tests: Cloud-Provider-End-to-End über die Env-Seam gegen httptest-Fakes,
--voice im Request-Pfad sichtbar, 500-Fallback bis osbuiltin auf CLI-
Ebene, Flag-nach-Text-Regression.
2026-10-18 16:34:54 +02:00
m3ta-chiron ae9f3b680e fix(audio): NullSink-Verwurf-Zeile im CLI-Pfad sichtbar machen
NewSink bekommt einen optionalen Log-Writer (die CLI übergibt stderr);
zuvor verwarf der null-Sink bei erfolgreicher Synthese stumm — die
sichtbare Zeile kam nur aus dem ErrNoByteOutput-Zweig. Sink-Interface
unverändert; Aufrufstellen ab Task 03 müssen den Writer übergeben.
TestSayArgumentMitNullSink beweist die Zeile jetzt extern auf dem
CLI-Pfad, TestNewSink deckt das Factory-Wiring ab; Live-Beweis mit
espeak-ng dokumentiert ("audio: N Bytes verworfen (null sink)", Exit 0).

Dazu (Minor): "was" aus englishWords entfernt — widersprach dem
Mehrdeutigkeits-Kommentar und dem Report; neuer TestFall
"Was macht er?" → de.
2026-10-18 15:46:20 +02:00
m3ta-chiron 8d42163aac feat(cli): say-Pfad end-to-end — erste Ansage über OS-TTS
Text aus Argument oder stdin → langdetect → Provider (Default osbuiltin;
--provider existiert, Kette folgt Task 03) → Synthesize → Sink.Play,
blockierend. Ohne byte-fähiges OS-TTS (Linux ohne espeak): Direktansage
über DirectSpeaker (spd-say); mit audio.sink=null wird die Ansage
stattdessen verworfen — CI/Server laufen tonlos mit Exit 0 durch.
run() bekommt stdin als Reader-Seam; an einem Terminal ohne Text gibt
es einen Benutzungsfehler statt blockierendem Warten. say-Hilfe zeigt
nicht mehr den Stub-Hinweis; alle anderen Befehle bleiben Stubs.
2026-10-18 15:39:13 +02:00
m3ta-chiron 33b97a50ea feat(cli): add subcommand dispatch, --version and stub commands 2026-10-18 15:14:18 +02:00