Commit Graph
42 Commits
Author SHA1 Message Date
m3ta-chiron 8909f37802 chore(sdd): evening handoff — task 7 review verdict, task 5 time-bomb defect documented as first action tomorrow 2026-10-18 21:10:01 +02:00
m3ta-chiron b934571b97 chore(sdd): handoff snapshot — ledger, briefs, reports, review packages + HANDOFF.md for cross-machine continuation 2026-10-18 21:03:24 +02:00
m3ta-chiron acfba19fc2 docs(readme): talk wrap dokumentieren — --Semantik, Label, Exit-Codes
Neuer Abschnitt zur Befehlsummwicklung: Aufrufform, Schutzwirkung des
ersten „--“, Label (wrap.label/TALK_WRAP_LABEL/Basisname) samt Sprach-
erkennung, Exit-Code-Politik (Befehls-Code gewinnt immer, 127 für nicht
startbare Befehle, 1 für Signaltode), geerbte say-Flags und der
Ansageweg über Daemon/Degraded Mode; Implementiert-Liste um wrap ergänzt.
2026-10-18 21:01:13 +02:00
m3ta-chiron 6877d64042 test: Prozessgrenzen-Tests für talk wrap an echter CLI + Daemon
talk läuft als echter Prozess gegen talkd mit Fake-Provider und sink null
(Harness aus Task 04, unverändert erweitert): Erfolg mit Dauer
(sleep-Ansage), Exit-Code-Durchreichung (sh -c 'exit 2' → 2 +
Fehler-Ansage wortgenau), --bar-Shielding-Regression (true --bar →
Exit 0 + Erfolg-Ansage, kein talk-Flag-Fehler), Label-Override per
TALK_WRAP_LABEL, non-blocking (wrap kehrt vor Ansage-Ende zurück,
Befehls-stdout bleibt frei von talk-Zeilen), --wait und Degraded Mode
ohne Daemon (Befehls-Code bleibt).
2026-10-18 21:01:13 +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 a3424c36e5 feat(config): Ansage-Label wrap.label mit Env-Override TALK_WRAP_LABEL
Config-Schlüssel [wrap] label für talk wrap (Task 07): ersetzt den
Basisnamen des Befehls in der Ansage („Deployment erfolgreich. Dauer: …“
statt „terraform erfolgreich. …“). Auflösung wie üblich Defaults →
config.toml → Env (TALK_WRAP_LABEL schlägt die Datei); Tisch-Tests für
alle drei Schichten.
2026-10-18 20:46:39 +02:00
m3ta-chiron 30bd535400 docs(readme): CORS-Konfiguration und MCP-Endpoint mit curl-Handshake dokumentiert 2026-10-18 20:06:11 +02:00
m3ta-chiron 150c374bdc test: Prozessgrenzen-Tests für CORS-Tür und MCP-Handshake
CORS: Preflight 204+Header von gelisteter Origin, kein ACAO für ungeliste/
leere Liste, Wildcard, nur-/speak, cors-Feld in /status. MCP: kompletter
Handshake als literal JSON-RPC über HTTP, tools/call enqueue sichtbar am
Fake-Provider, muted/rate_limit/quiet_hours als isError:false-Ergebnisse,
Fehlerpfade (-32700/-32600/-32601/-32602), GET /mcp 405.
2026-10-18 20:06:11 +02:00
m3ta-chiron a577597714 feat(api): MCP-Endpoint POST /mcp — schlankes eigenes JSON-RPC-Subset mit speak-Tool
Streamable HTTP nach MCP 2025-06-18-Semantik, handgewirkt ohne SDK:
initialize (Version-Echo), notifications/initialized (202 ohne Body), ping,
tools/list, tools/call. tools/call fährt denselben Aufnahme-Pfad wie
POST /speak (admitSpeak, inkl. Mute/Gesetz); muted/discarded sind bewusst
isError:false mit erklärendem Text, Validierungsfehler -32602. Zustandslos,
GET /mcp → 405 (keine server-initiierten Streams). /status: mcp:true.
2026-10-18 20:06:11 +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 cef6e6b9f2 feat(config): CORS-Erlaubnisliste server.allowed_origins (TALK_ALLOWED_ORIGINS)
Exakte Origins wie https://chat.az-gruppe.com; Wildcard "*" nur explizit.
Bewusster Default: leer = keine CORS-Header (Tür zu). Env schlägt Datei
(kommagetrennt; "off" leert), leere Einträge sind klare Fehler.
2026-10-18 20:06:05 +02:00
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
m3ta-chiron 5979d45710 test: Prozessgrenzen-Tests für Rate-Limit und Ruhezeiten
Kurzes Fenster (1 s): fünf Ansagen → genau eine spielt sofort, low
verfällt beim Eintritt (202 + CLI 'discarded: rate_limit'), normal parkt
(sichtbar in queue_ids/next_slot) und spielt frühestens zum Slot
(Timing-am-Fake gemessen). High-Burst an/aus; now bricht durch Rate-Limit
UND Ruhezeit; Ruhezeit-Fenster künstlich um die lokale Jetztzeit gelegt
→ discard (auch high) und toast (Protokoll-Zeile, kein Provider-Kontakt);
/status- und talk-status-Felder im Default. Harness-Basis: TALK_RATE_LIMIT=0s
— die Task-04-Semantik-Tests erwarten dichtes Durchspielen, Gesetz-Tests
überschreiben bewusst.
2026-10-18 17:40:51 +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 5385a4b87d feat(queue): Rate-Limit und Ruhezeiten als Gesetz im Worker-Loop
Das Anti-Spam-Gesetz wird an den Loop verwired (die Rate-Limiter-Struktur
aus Task 04): Jede gestartete Ansage bucht das Fenster — low über dem
Budget verfällt (beim Eintritt, Admission nennt den Grund für die
202-Antwort), normal parkt bis zum Slot (Slot-Wecker broadcastet die
Cond, geparkte bleiben in der Queue und in /status sichtbar), high darf
mit Burst-Recht verstoßen, now bricht bewusst aus Rate-Limit UND
Ruhezeit aus und bucht trotzdem ein neues Fenster. Ruhezeiten
(quiet.go: lokales Fenster HH:MM-HH:MM über Mitternacht) verwerfen still
oder ersetzen durch Toast — gilt auch für high. Rangordnung: mute (API)
> now > Ruhezeit > Rate-Limit. Enqueue liefert Admission statt nur ID;
LawState für /status. Lock-Ordnung Queue → RateLimiter bleibt gewahrt.
2026-10-18 17:40:41 +02:00
m3ta-chiron b20e6dd319 feat(config): Gesetz-Felder rate_limit, allow_high_burst, quiet_hours, quiet_mode
Queue-Section ([queue] rate_limit = "30s", allow_high_burst = true) und
Ruhezeiten (quiet_hours = "18:00-08:00" lokal über Mitternacht,
quiet_mode = discard|toast) mit Env-Overrides TALK_RATE_LIMIT,
TALK_ALLOW_HIGH_BURST, TALK_QUIET_HOURS ("off" = aus), TALK_QUIET_MODE.
Default: 1 Ansage / 30 s, Burst an, Ruhezeit aus (Opt-in wie Mute —
ein Uhrzeit-Gesetz als Default würde jeden Cron in den Abendstunden
stilllegen). Validierung gegen queue.ParseQuietHours als eine Quelle;
partielle [queue]-Tabellen behalten Defaults (tischgetestet).
2026-10-18 17:40:35 +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 1ae1f1262c test: Prozessgrenzen-Tests — talkd + talk als echte Prozesse gegen Fake-Provider und sink null
Primäre Seam laut Spec: talkd/talk gebuildet und als Prozesse gefahren,
ElevenLabs-Fake per httptest (Ankünfte + Antworten mit Zeiten), TALK_PORT
auf freie Ports (nie 7800), isoliertes Config-Dir. Getestet:
Serialisierung/Priorität/now-Sprung, cancel (Synthese-Abbruch + Zahl),
non-blocking + --wait, mute-Persistenz über Restart + offline geschriebene
Datei, Degraded Mode (Lock-Serialisierung, mute, Lock-Freigabe),
3 parallele talk-Aufrufe, HTTP-Validierung (400/405), Status-Vertrag.
2026-10-18 17:10:36 +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 39c3563195 feat(api): HTTP-Vertrag des Daemons — Endpunkte, Validierung und CLI-Client
- POST /speak (202 {id}; bei Mute 202 {muted:true}), GET /status,
  POST /cancel, POST /mute, POST /unmute (methodengebunden: 405/404)
- Request/Response-Typen als einzige Vertragsschicht für Daemon und CLI
- Client inkl. HTTPError: Netz-Fehler (Daemon tot) bleibt von HTTP-Fehlern
  unterscheidbar — Basis für den Degraded Mode
2026-10-18 17:10:26 +02:00
m3ta-chiron e492baddac feat(queue): serialisierte Ansage-Queue mit Prioritäten, now-Sprung, Mute-Store und Rate-Limit-Struktur
- Worker-Loop: eine Ansage gleichzeitig; now > high > normal > low, FIFO je Level
- Cancel bricht die laufende Ansage ab (ctx) und leert die Queue (Zahl als Rückgabe)
- Close verwirft wartende Items, bricht die laufende Ansage ab und wartet auf den Worker
- MuteStore: persistenter Mute-State als Datei mute im Config-Dir (atomar geschrieben)
- RateLimiter: Struktur + Semantik für Task 05, bewusst noch nicht angebunden
  (Task-04-Default unbegrenzt, dokumentierte Seam in rate.go)
2026-10-18 17:10:26 +02:00
m3ta-chiron 716f6b2fc9 feat(config): Daemon-Port server.port mit Env-Override TALK_PORT (Default 7800) 2026-10-18 17:10:26 +02:00
m3ta-chiron 3b44605234 fix(provider): Chain.Speak läuft im Direkt-Endgame nur einmal durch die Kette
Speak rief bei ErrNoByteOutput SpeakDirect auf, das einen eigenen
vollständigen walk machte: fehlgeschlagene Cloud-Stufen wurden zweimal
angefragt (doppelte POSTs auf bezahlten APIs), Fehlschlag-Zeilen
verdoppelten sich im Log, die Worst-Case-Latenz verdoppelte sich, und
Bytes aus einer transienten Erholung im zweiten Durchlauf hätten einen
Fehler statt Wiedergabe produziert.

Speak entscheidet jetzt aus EINEM walk-Ergebnis (Bytes → Player,
nur-Direkt-Stufe → SpeakDirect direkt daran, sonst gebündelter
Stufen-Fehler aus dem geteilten Helper walkResult.noTierErr).
Synthesize/SpeakDirect behalten ihre öffentlichen Verträge; null-Player
verwirft tonlos mit Exit 0 wie zuvor.

Test: TestChainDirektEndgameEinDurchlauf pinnt Cloud-Stufe genau 1×
angefragt, jede Fehlschlag-Zeile genau 1×, Direktansage kommt zustande.
2026-10-18 16:41:23 +02:00
m3ta-chiron 8351d40717 docs(readme): Provider-Kette, Keys/Endpunkte und Voice-Mapping dokumentieren 2026-10-18 16:34:54 +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 91cd0ec8da feat(provider): Cloud-Provider und Vier-Stufen-Fallback-Kette
- elevenlabs: POST /v1/text-to-speech/{voice}, Header xi-api-key, Body mit
  model_id eleven_multilingual_v2 und voice_settings 0.5/0.75, MP3-Bytes.
- openai: POST /v1/audio/speech, Bearer-Key, Body {model, voice, input,
  response_format:mp3}; Modell konfigurierbar (Default tts-1).
- edge: Microsofts Read-Aloud-WSS-Protokoll nach edge-tts-Referenz,
  inklusive aktuellem DRM Sec-MS-GEC v2 (CS64 + verschleierte SHA-256-
  Variante, wörtlich portiert und gegen Referenzvektoren getestet) und
  MUID-Cookie; bei 403 Uhrenkorrektur aus dem Date-Header + ein Retry.
  Klare Protokoll-geaendert-Fehler statt stiller Fehlfunktion.
- chain: geordnete Kette (elevenlabs -> openai -> edge -> osbuiltin), Pin
  per --provider/config, Available ohne Netz-Calls, Fallback mit
  Protokoll je Stufe (nie still). Chain implementiert Provider UND
  DirectSpeaker: Speak orchestriert Bytes -> Player bzw. Direktansage,
  verwirft am null-Player tonlos — CLI und Daemon (Task 04) nutzen
  dieselbe Kette unverändert.
- voices: neutrale Repo-Defaults je Provider+Sprache (Rachel/alloy/
  Katja/Aria); Rangfolge --voice > config-Mapping > Default. KXxZ… bleibt
  bewusst Privat-Default der eigenen config.toml (Spec: nicht
  Repo-Default).

Tests: Cloud-Provider gegen httptest-Fakes (200/500/401, Request-Vertrag
wörtlich), Edge über Interface + Fehlerpfad-Mocks + Loopback-WSS-Fake
(kein echtes Netz) + als testing.Short überspringbare Integration-
Markierung (TALK_TEST_REAL_EDGE=1, am Dev-Host einmalig ausgeführt:
17136 Bytes echtes Audio). Kette: erster 500 -> zweiter getroffen -> Bytes;
Pin, alle-Stufen-tot, Direktansage mit/ohne null-Player.
2026-10-18 16:34:43 +02:00
m3ta-chiron 88eddc63d2 feat(config): Provider-Pin, Provider-Endpunkte und Voice-Mapping
config.toml bekommt provider = "…" (Pin, Env-Override TALK_PROVIDER),
[providers.<name>] endpoint/model (Endpoints fake-bar für Tests; Keys
bleiben ausschließlich Env) und voice.<provider>.<lang> = id. TOML-Key ist
bewusst providers (Mehrzahl), damit Pin ( Singular) und Einstellungs-
Tabelle koexistieren können. Env-Overrides der Endpunkte liest die
Provider-Schicht (TALK_ELEVENLABS_ENDPOINT etc.) — das config-Paket
bleibt frei von Provider-Namen.
2026-10-18 16:34:24 +02:00
m3ta-chiron 7a843ae441 feat(audio): mp3 dekodieren für Cloud-Provider (reiner Go-Dekoder)
oto-Sink bekommt eine Format-Weiche: RIFF/WAVE-PCM (osbuiltin) und MP3
(ElevenLabs/OpenAI/Edge ab Task 03). Dekoder ist github.com/hajimehoshi/
go-mp3 v0.3.4 — reines Go ohne CGO, verifiziert per GOOS/GOARCH-Matrix mit
CGO_ENABLED=0 (alle 6 Targets bauen). Der Dekoder liefert immer 16-Bit-LE
stereo (Mono-Quellen wie Edge 24 kHz werden gespiegelt); die Sample-Rate
kommt aus dem ersten Frame. Fixtures sind echte, einmalig erzeugte MP3s
(mono 24 kHz 48 kbit/s = Edge-Format, stereo 44,1 kHz = Cloud-typisch),
als Base64 eingebettet — Tests bleiben werkzeugfrei.
2026-10-18 16:33:46 +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 8306c6b384 docs(readme): say-Pfad als implementiert markieren, espeak als Dev-Abhängigkeit dokumentieren 2026-10-18 15:39:49 +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 523e6f4bf2 feat(audio): Audio-Sinks oto und null mit eigener WAV-Dekodierung
Sink-Vertrag Play/Name; oto-Sink (github.com/ebitengine/oto/v3, auf
v3.5.0 gepinnt — aktuelle stabile Major-Version; purego statt CGO, damit
cross-compilierbar) spielt RIFF/WAVE-PCM blockierend, ein Context pro
PCM-Format gecacht, da oto nur einen Kontext pro Prozess erlaubt.
null-Sink verwirft (CI/Server/Tests) und loggt optional. WAV-Parser
(RIFF-Chunks, PCM 8/16 Bit, Float32) mit klarer Fehler für alles andere.
Echte Wiedergabe nur hinter TALK_TEST_REAL_AUDIO=1.
2026-10-18 15:39:09 +02:00
m3ta-chiron 5f450ea015 feat(provider): Provider-Vertrag und osbuiltin für OS-eingebautes TTS
Interface Name/Available/Synthesize (Contract für die Kette aus Task 03)
plus dokumentierte optionale Erweiterung DirectSpeaker für Plattformen,
deren OS-TTS keine Audio-Bytes liefert: spd-say (speech-dispatcher) kann
nach --help-Verifikation nur selbst abspielen — dort spricht
SpeakDirect blockierend (spd-say -w -l <lang>). Synthesize liefert
RIFF/WAVE: Linux espeak/espeak-ng (--stdout), macOS say (temporäre
WAV-Datei, LEI16@22050), Windows SAPI (PowerShell, -EncodedCommand).
Tests mit Fake-Prozessen über PATH-Manipulation; echte spd-say-Ausgabe
nur hinter TALK_TEST_REAL_SPD=1.
2026-10-18 15:39:01 +02:00
m3ta-chiron 36ad1dfa4e feat(config): geschichtete Konfigurationsauflösung mit injizierbaren Pfaden
Defaults → optionale config.toml → Env-Overrides (TALK_AUDIO_SINK);
Validierungsfehler sind klar, nie still. Plattform-Pfadlogik
(linux/darwin/windows, XDG_CONFIG_HOME, HOME, APPDATA) mit goos als
Parameter — alle Plattformen tischgesteuert vom Dev-Host testbar.
Erste externe Dependency: github.com/BurntSushi/toml v1.6.0 (MIT, pure Go).
2026-10-18 15:38:56 +02:00
m3ta-chiron de1ef64b8f feat(langdetect): deutsche/englische Spracherkennung als reine Funktion
Detect(text) → "de" | "en" (Default "de"): Common-Word-Listen plus
Umlaute/ß als Deutsch-Signale; Englisch gewinnt nur bei strikter Mehrheit.
Tischgesteuerte Tests für de/en/gemischt/leer.
2026-10-18 15:38:42 +02:00
m3ta-chiron fa4f193be2 ci: add gitea actions test workflow (vet, gofmt, test, build) 2026-10-18 15:14:18 +02:00
m3ta-chiron 33b97a50ea feat(cli): add subcommand dispatch, --version and stub commands 2026-10-18 15:14:18 +02:00
m3ta-chiron 24dfe4d46c chore: bootstrap go module, license, readme and package skeleton 2026-10-18 15:14:18 +02:00
m3ta-chiron c8f39024e1 docs: normalize task headings for brief extraction 2026-10-18 15:06:56 +02:00
m3ta-chiron 1d32f4dc71 chore: initial commit — spec + implementation plan for talk 2.0 2026-10-18 15:05:24 +02:00