# Review package: c8f3902..HEAD ## Commits fa4f193 ci: add gitea actions test workflow (vet, gofmt, test, build) 33b97a5 feat(cli): add subcommand dispatch, --version and stub commands 24dfe4d chore: bootstrap go module, license, readme and package skeleton ## Files changed .gitea/workflows/test.yml | 30 ++++++ .gitignore | 10 ++ LICENSE | 21 ++++ README.md | 49 +++++++++ cmd/talk/cli.go | 259 +++++++++++++++++++++++++++++++++++++++++++++ cmd/talk/cli_test.go | 146 +++++++++++++++++++++++++ cmd/talk/main.go | 21 ++++ cmd/talkd/main.go | 13 +++ go.mod | 3 + internal/api/doc.go | 4 + internal/audio/doc.go | 5 + internal/config/doc.go | 7 ++ internal/langdetect/doc.go | 4 + internal/notify/doc.go | 4 + internal/provider/doc.go | 6 ++ internal/queue/doc.go | 6 ++ 16 files changed, 588 insertions(+) ## Diff .gitea/workflows/test.yml --- YAML 1 name: test 2 3 on: 4 push: 5 pull_request: 6 7 jobs: 8 test: 9 runs-on: ubuntu-latest 10 steps: 11 - name: Checkout 12 uses: actions/checkout@v4 13 - name: Go einrichten 14 uses: actions/setup-go@v5 15 with: 16 go-version-file: go.mod 17 - name: go vet 18 run: go vet ./... 19 - name: gofmt-Check 20 run: | 21 unformatted=$(gofmt -l .) 22 if [ -n "$unformatted" ]; then 23 echo "Folgende Dateien sind nicht gofmt-formatiert:" 24 echo "$unformatted" 25 exit 1 26 fi 27 - name: go test 28 run: go test ./... 29 - name: go build 30 run: go build ./... 31 .gitignore --- Text 1 # Go-Binaries 2 /talk 3 /talkd 4 /talk.exe 5 /talkd.exe 6 *.test 7 *.out 8 9 # Release-Artefakte (GoReleaser, Task 11) 10 /dist/ 11 LICENSE --- Text 1 MIT License 2 3 Copyright (c) 2026 m3tam3re 4 5 Permission is hereby granted, free of charge, to any person obtaining a copy 6 of this software and associated documentation files (the "Software"), to deal 7 in the Software without restriction, including without limitation the rights 8 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 copies of the Software, and to permit persons to whom the Software is 10 furnished to do so, subject to the following conditions: 11 12 The above copyright notice and this permission notice shall be included in all 13 copies or substantial portions of the Software. 14 15 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 SOFTWARE. 22 README.md --- Text 1 # talk 2 3 > **Status: WIP** — aktive Entwicklung (talk 2.0). Binde Spezifikation: 4 > [`.scratch/talk-2-0/spec.md`](.scratch/talk-2-0/spec.md) · 5 > Implementierungsplan: [`.scratch/talk-2-0/plan.md`](.scratch/talk-2-0/plan.md) 6 7 **talk** macht Skripte und KI-Agenten hörbar: Ein Aufruf — `talk "Build fertig"` — 8 spricht eine kurze Nachricht über eine TTS-Provider-Kette 9 (ElevenLabs → OpenAI → Edge-TTS → OS-Stimme), serialisiert durch den Daemon 10 `talkd`. Cross-platform (Windows/macOS/Linux, amd64/arm64), statische Binaries, 11 keine Runtime-Abhängigkeiten. 12 13 ## Befehle 14 15 | Befehl | Zweck | 16 | --- | --- | 17 | `talk "Text"` | Ansage — Abkürzung für `talk say`, non-blocking | 18 | `talk say "Text"` | Ansage (Text als Argument oder von stdin) | 19 | `talk wrap -- ` | Befehl ausführen, Erfolg/Misserfolg + Dauer ansagen | 20 | `talk mute` / `talk unmute` | Ansagen dauerhaft stummschalten / Stummschaltung aufheben | 21 | `talk status` | Daemon-Status (Queue, Mute, Provider, Version) | 22 | `talk cancel` | laufende und geplante Ansagen abbrechen | 23 | `talk voices` | verfügbare Stimmen anzeigen | 24 | `talk test` | Test-Ansage ausgeben | 25 | `talk doctor` | Selbstdiagnose des Setups | 26 | `talk setup` | geführte Ersteinrichtung (Wizard) | 27 28 Say-Flags (gelten auch für `talk "Text"`): `--wait`, `--now`, `--priority`, 29 `--voice`, `--lang`, `--provider`. 30 31 **Hinweis:** Alle Befehle sind in diesem Stand Stubs und melden 32 `not implemented yet` (Exit-Code ≠ 0). Die Implementierung entsteht 33 Aufgabenweise gemäß Plan; `--version` und `--help` funktionieren. 34 35 ## Entwicklung 36 37 ```console 38 go build ./... # bauen 39 go test ./... # Tests 40 go vet ./... # statische Analyse 41 gofmt -l . # Formatierung prüfen (leerer Output = ok) 42 ``` 43 44 CI: `.gitea/workflows/test.yml` (Gitea Actions) läuft vet, gofmt-Check, Tests 45 und Build bei jedem Push und Pull Request. 46 47 ## Lizenz 48 49 MIT — siehe [LICENSE](LICENSE). 50 cmd/talk/cli.go --- Go 1 package main 2 3 import ( 4 "errors" 5 "flag" 6 "fmt" 7 "io" 8 ) 9 10 // Exit-Codes. 11 const ( 12 exitOK = 0 // Erfolg (auch --help, --version) 13 exitFailure = 1 // Befehl fehlgeschlagen oder noch nicht implementiert 14 exitUsage = 2 // Falsche Benutzung (unbekannte Flags, kein Befehl) 15 ) 16 17 // command ist ein Eintrag der Befehlstabelle. Die Tabelle ist bewusst 18 // datengetrieben: Spätere Tasks (z. B. die TUI-Befehle aus Task 09) ergänzen 19 // hier Einträge bzw. Register-Funktionen, ohne die Dispatch-Logik in run() 20 // anzufassen. 21 type command struct { 22 name string // Subcommand-Name (bindende CLI-Surface aus der Spec) 23 short string // Kurzbeschreibung für `talk --help` 24 args string // Positionsargumente-Hinweis für die Usage-Zeile 25 detail string // Beschreibung für `talk --help` 26 register func(fs *flag.FlagSet) // hängt die Flags des Befehls an den FlagSet 27 } 28 29 // commands ist die vollständige Befehlstabelle. Die Namen sind Teil der 30 // bindenden CLI-Surface (Spec: „CLI-Surface (exakt)“) — nicht umbenennen. 31 var commands = []command{ 32 { 33 name: "say", 34 short: "Text ansagen (Default-Befehl)", 35 args: `"Text"`, 36 detail: "Spricht den Text über die Provider-Kette aus. Ohne Text-Argument wird stdin gelesen. `talk \"Text\"` ist die Abkürzung für `talk say \"Text\"`.", 37 register: addSayFlags, 38 }, 39 { 40 name: "wrap", 41 short: "Befehl ausführen und Ergebnis ansagen", 42 args: "-- [Argumente …]", 43 detail: "Führt den Befehl aus (stdin/stdout/stderr werden durchgereicht) und spricht danach Erfolg bzw. Misserfolg und Dauer.", 44 }, 45 { 46 name: "mute", 47 short: "Ansagen dauerhaft stummschalten", 48 detail: "Aktiviert den persistenten Mute-State des Daemons (Datei im Config-Verzeichnis).", 49 }, 50 { 51 name: "unmute", 52 short: "Stummschaltung aufheben", 53 detail: "Hebt den persistenten Mute-State des Daemons wieder auf.", 54 }, 55 { 56 name: "status", 57 short: "Daemon-Status anzeigen", 58 detail: "Zeigt Queue, aktuelles Item, Mute-State, aktiven Provider und Daemon-Version.", 59 }, 60 { 61 name: "cancel", 62 short: "Laufende und geplante Ansagen abbrechen", 63 detail: "Bricht die laufende Ansage ab und leert die Queue des Daemons.", 64 }, 65 { 66 name: "voices", 67 short: "Verfügbare Stimmen anzeigen", 68 detail: "Listet die Stimmen aller Provider (Defaults + Config-Overrides) als Tabelle.", 69 }, 70 { 71 name: "test", 72 short: "Test-Ansage ausgeben", 73 detail: "Spricht eine feste Testphrase und zeigt den verwendeten Provider samt Latenz.", 74 }, 75 { 76 name: "doctor", 77 short: "Setup-Diagnose anzeigen", 78 detail: "Prüft Daemon, Audio-Sink, Provider-Keys und Config und berichtet zeilenweise mit ✔/✘/⚠.", 79 }, 80 { 81 name: "setup", 82 short: "Geführte Ersteinrichtung (Wizard)", 83 detail: "Interaktiver Wizard: Provider-Keys, Stimmen probehören, config.toml schreiben. Ohne TTY: plain.", 84 }, 85 } 86 87 // addSayFlags registriert die Flags des say-Befehls. Sie gelten auch für den 88 // Default-Pfad `talk "Text"` (gleiche Flag-Oberfläche, festgelegt in der 89 // Spec). Das Verhalten entsteht in Task 02 (direkter Pfad) bzw. Task 04 90 // (Daemon-Pfad). 91 func addSayFlags(fs *flag.FlagSet) { 92 fs.Bool("wait", false, "warten, bis die Ansage fertig abgespielt ist") 93 fs.Bool("now", false, "Ansage sofort ausgeben (springt die Warteschlange)") 94 fs.String("priority", "normal", "Priorität: low | normal | high") 95 fs.String("voice", "", "Stimme als provider-spezifische ID") 96 fs.String("lang", "", "Sprache: de | en (Default: automatische Erkennung)") 97 fs.String("provider", "", "Provider pinnen: elevenlabs | openai | edge | osbuiltin") 98 } 99 100 // run ist der Einstiegspunkt der CLI (Argumente ohne Programmnamen). Alle 101 // Ausgaben gehen auf die übergebenen Writer, damit run direkt und ohne 102 // Prozessstart testbar ist. Rückgabe ist der Exit-Code. 103 func run(argv []string, stdout, stderr io.Writer) int { 104 if len(argv) == 0 { 105 // Task 02 macht aus dem nackten Aufruf ggf. einen stdin-Leser 106 // (Text kommt dann über die Pipe); bis dahin: Benutzungsfehler. 107 writeUsage(stderr) 108 return exitUsage 109 } 110 111 if argv[0] == "help" { 112 if len(argv) > 1 { 113 if cmd, ok := lookupCommand(argv[1]); ok { 114 writeCommandHelp(cmd, stdout) 115 return exitOK 116 } 117 fmt.Fprintf(stderr, "talk: unbekannter Befehl %q\n", argv[1]) 118 return exitUsage 119 } 120 writeUsage(stdout) 121 return exitOK 122 } 123 124 if cmd, ok := lookupCommand(argv[0]); ok { 125 return runCommand(cmd, argv[1:], stdout, stderr) 126 } 127 128 // Kein bekannter Befehl: Default-Pfad `talk "Text"` (mappt auf say) 129 // bzw. Root-Flags (--version, --help). Führt aktuell auf den say-Stub. 130 return runDefault(argv, stdout, stderr) 131 } 132 133 func lookupCommand(name string) (command, bool) { 134 for _, cmd := range commands { 135 if cmd.name == name { 136 return cmd, true 137 } 138 } 139 return command{}, false 140 } 141 142 // newCommandFlagSet baut den FlagSet eines Befehls: eigene Flags plus ein 143 // explizites --help. Die Default-Usage wird stummgeschaltet, damit Hilfetext 144 // ausschließlich über writeCommandHelp (eine Quelle) ausgegeben wird. 145 func newCommandFlagSet(cmd command, out io.Writer) (*flag.FlagSet, *bool) { 146 fs := flag.NewFlagSet("talk "+cmd.name, flag.ContinueOnError) 147 fs.SetOutput(out) 148 fs.Usage = func() {} 149 if cmd.register != nil { 150 cmd.register(fs) 151 } 152 help := fs.Bool("help", false, "Hilfe zu diesem Befehl anzeigen") 153 return fs, help 154 } 155 156 // runCommand führt einen benannten Subcommand aus — in diesem Stand als Stub. 157 func runCommand(cmd command, args []string, stdout, stderr io.Writer) int { 158 fs, help := newCommandFlagSet(cmd, stderr) 159 if err := fs.Parse(args); err != nil { 160 if errors.Is(err, flag.ErrHelp) { // z. B. nacktes -h 161 writeCommandHelp(cmd, stdout) 162 return exitOK 163 } 164 // Die Fehlermeldung selbst hat der FlagSet bereits ausgegeben. 165 fmt.Fprintf(stderr, "Hinweis: `talk %s --help` zeigt die Verwendung.\n", cmd.name) 166 return exitUsage 167 } 168 if *help { 169 writeCommandHelp(cmd, stdout) 170 return exitOK 171 } 172 return notImplemented(cmd.name, stderr) 173 } 174 175 // runDefault behandelt Aufrufe ohne bekannten Subcommand: `talk "Text"`, 176 // `talk --version`, `talk --help` sowie say-Flags vor dem Text. 177 func runDefault(argv []string, stdout, stderr io.Writer) int { 178 fs := flag.NewFlagSet("talk", flag.ContinueOnError) 179 fs.SetOutput(stderr) 180 fs.Usage = func() {} 181 showVersion := fs.Bool("version", false, "Version anzeigen") 182 help := fs.Bool("help", false, "diese Hilfe anzeigen") 183 addSayFlags(fs) 184 185 if err := fs.Parse(argv); err != nil { 186 if errors.Is(err, flag.ErrHelp) { 187 writeUsage(stdout) 188 return exitOK 189 } 190 fmt.Fprintln(stderr, "Hinweis: `talk --help` zeigt die Verwendung.") 191 return exitUsage 192 } 193 if *showVersion { 194 fmt.Fprintln(stdout, versionString()) 195 return exitOK 196 } 197 if *help { 198 writeUsage(stdout) 199 return exitOK 200 } 201 return notImplemented("say", stderr) 202 } 203 204 // versionString rendert die Versionsausgabe: `talk version ` plus 205 // Commit-Hash in Klammern, falls per ldflags injiziert. 206 func versionString() string { 207 if commit == "" { 208 return fmt.Sprintf("talk version %s", version) 209 } 210 return fmt.Sprintf("talk version %s (%s)", version, commit) 211 } 212 213 // notImplemented meldet einen noch nicht implementierten Befehl auf stderr 214 // und liefert den dafür vorgesehenen Exit-Code (non-zero). 215 func notImplemented(name string, stderr io.Writer) int { 216 fmt.Fprintf(stderr, "talk %s is not implemented yet\n", name) 217 return exitFailure 218 } 219 220 // writeUsage schreibt die Gesamt-Hilfe; die Befehlsliste wird direkt aus der 221 // Befehlstabelle generiert, damit Hilfe und Dispatch nicht auseinanderlaufen. 222 func writeUsage(w io.Writer) { 223 fmt.Fprint(w, `talk — Sprach-Ausgaben für Skripte und KI-Agenten 224 225 Usage: 226 talk [Flags] "Text" Text ansagen (Abkürzung für: talk say) 227 talk [Flags] Befehl ausführen 228 talk help [Befehl] Hilfe anzeigen 229 230 Befehle: 231 `) 232 for _, cmd := range commands { 233 fmt.Fprintf(w, " %-8s %s\n", cmd.name, cmd.short) 234 } 235 fmt.Fprint(w, ` 236 Flags: 237 --version Version anzeigen 238 --help diese Hilfe anzeigen 239 240 Die Flags von say gelten auch für talk "Text"; Details: talk say --help 241 `) 242 } 243 244 // writeCommandHelp schreibt die Hilfe eines einzelnen Befehls inkl. Flags. 245 func writeCommandHelp(cmd command, w io.Writer) { 246 fmt.Fprintf(w, "Usage:\n talk %s", cmd.name) 247 if cmd.args != "" { 248 fmt.Fprintf(w, " %s", cmd.args) 249 } 250 fmt.Fprintln(w, " [Flags]") 251 fmt.Fprintln(w) 252 if cmd.detail != "" { 253 fmt.Fprintf(w, "%s\n\n", cmd.detail) 254 } 255 fmt.Fprintln(w, "Flags:") 256 fs, _ := newCommandFlagSet(cmd, w) 257 fs.PrintDefaults() 258 fmt.Fprintln(w, "\nStatus: noch nicht implementiert (WIP).") 259 } 260 cmd/talk/cli_test.go --- Go 1 package main 2 3 import ( 4 "bytes" 5 "strings" 6 "testing" 7 ) 8 9 // wantSubcommands ist die bindende CLI-Surface aus der Spec — bewusst als 10 // hartkodierte Liste getestet (nicht aus der Befehlstabelle abgeleitet), 11 // damit die Tests anschlagen, wenn Tabelle und Spec auseinanderlaufen. 12 var wantSubcommands = []string{ 13 "say", "wrap", "mute", "unmute", "status", "cancel", "voices", "test", "doctor", "setup", 14 } 15 16 func TestHelpListetAlleSubcommands(t *testing.T) { 17 var out, errBuf bytes.Buffer 18 code := run([]string{"--help"}, &out, &errBuf) 19 if code != exitOK { 20 t.Fatalf("--help: Exit-Code = %d, want %d (stderr: %q)", code, exitOK, errBuf.String()) 21 } 22 lines := strings.Split(out.String(), "\n") 23 for _, name := range wantSubcommands { 24 found := false 25 for _, ln := range lines { 26 if strings.HasPrefix(strings.TrimLeft(ln, " "), name+" ") { 27 found = true 28 break 29 } 30 } 31 if !found { 32 t.Errorf("--help listet Befehl %q nicht (Output: %q)", name, out.String()) 33 } 34 } 35 } 36 37 func TestVersionDefault(t *testing.T) { 38 var out, errBuf bytes.Buffer 39 code := run([]string{"--version"}, &out, &errBuf) 40 if code != exitOK { 41 t.Fatalf("--version: Exit-Code = %d, want %d", code, exitOK) 42 } 43 if got, want := out.String(), "talk version dev\n"; got != want { 44 t.Errorf("--version Output = %q, want %q", got, want) 45 } 46 } 47 48 func TestVersionStringMitCommit(t *testing.T) { 49 oldCommit := commit 50 commit = "c0ffee1" 51 defer func() { commit = oldCommit }() 52 53 if got, want := versionString(), "talk version dev (c0ffee1)"; got != want { 54 t.Errorf("versionString() = %q, want %q", got, want) 55 } 56 } 57 58 func TestSubcommandStubs(t *testing.T) { 59 for _, name := range wantSubcommands { 60 t.Run(name, func(t *testing.T) { 61 var out, errBuf bytes.Buffer 62 code := run([]string{name}, &out, &errBuf) 63 if code == exitOK { 64 t.Errorf("talk %s: Exit-Code 0, want non-zero für Stub", name) 65 } 66 if !strings.Contains(errBuf.String(), "not implemented yet") { 67 t.Errorf("talk %s: stderr = %q, will Hinweis auf nicht implementiert", name, errBuf.String()) 68 } 69 if !strings.Contains(errBuf.String(), name) { 70 t.Errorf("talk %s: stderr nennt den Befehlsnamen nicht: %q", name, errBuf.String()) 71 } 72 if out.Len() != 0 { 73 t.Errorf("talk %s: unerwartete stdout-Ausgabe %q", name, out.String()) 74 } 75 }) 76 } 77 } 78 79 func TestDefaultAufrufMapptAufSay(t *testing.T) { 80 var out, errBuf bytes.Buffer 81 code := run([]string{"Hallo Welt"}, &out, &errBuf) 82 if code != exitFailure { 83 t.Fatalf(`talk "Hallo Welt": Exit-Code = %d, want %d`, code, exitFailure) 84 } 85 if !strings.Contains(errBuf.String(), "talk say is not implemented yet") { 86 t.Errorf("Default-Aufruf mappt offenbar nicht auf say, stderr = %q", errBuf.String()) 87 } 88 } 89 90 func TestSayHelpZeigtFlags(t *testing.T) { 91 var out, errBuf bytes.Buffer 92 code := run([]string{"say", "--help"}, &out, &errBuf) 93 if code != exitOK { 94 t.Fatalf("talk say --help: Exit-Code = %d, want %d", code, exitOK) 95 } 96 for _, f := range []string{"-wait", "-now", "-priority", "-voice", "-lang", "-provider"} { 97 if !strings.Contains(out.String(), f) { 98 t.Errorf("talk say --help zeigt Flag %s nicht (Output: %q)", f, out.String()) 99 } 100 } 101 } 102 103 func TestHelpSubcommand(t *testing.T) { 104 t.Run("ohne Argument", func(t *testing.T) { 105 var out, errBuf bytes.Buffer 106 if code := run([]string{"help"}, &out, &errBuf); code != exitOK { 107 t.Fatalf("talk help: Exit-Code = %d, want %d", code, exitOK) 108 } 109 for _, name := range wantSubcommands { 110 if !strings.Contains(out.String(), name) { 111 t.Errorf("talk help listet %q nicht", name) 112 } 113 } 114 }) 115 t.Run("mit Befehl", func(t *testing.T) { 116 var out, errBuf bytes.Buffer 117 if code := run([]string{"help", "mute"}, &out, &errBuf); code != exitOK { 118 t.Fatalf("talk help mute: Exit-Code = %d, want %d", code, exitOK) 119 } 120 if !strings.Contains(out.String(), "talk mute") { 121 t.Errorf("talk help mute zeigt Usage von mute nicht: %q", out.String()) 122 } 123 }) 124 } 125 126 func TestOhneArgumenteZeigtUsage(t *testing.T) { 127 var out, errBuf bytes.Buffer 128 code := run(nil, &out, &errBuf) 129 if code != exitUsage { 130 t.Fatalf("talk ohne Argumente: Exit-Code = %d, want %d", code, exitUsage) 131 } 132 if !strings.Contains(errBuf.String(), "Usage") { 133 t.Errorf("talk ohne Argumente: Usage fehlt auf stderr: %q", errBuf.String()) 134 } 135 } 136 137 func TestUnbekanntesFlag(t *testing.T) { 138 var out, errBuf bytes.Buffer 139 code := run([]string{"--bogus"}, &out, &errBuf) 140 if code != exitUsage { 141 t.Fatalf("talk --bogus: Exit-Code = %d, want %d", code, exitUsage) 142 } 143 if !strings.Contains(errBuf.String(), "bogus") { 144 t.Errorf("talk --bogus: Fehlermeldung fehlt auf stderr: %q", errBuf.String()) 145 } 146 } 147 cmd/talk/main.go --- Go 1 // talk ist die CLI für Sprach-Ausgaben (talk 2.0). 2 // 3 // Dieser Stand (Task 01) liefert das Gerüst: Subcommand-Dispatch, --version 4 // und alle Befehle als Stubs. Die Funktionalität entsteht in den Tasks 02–09. 5 package main 6 7 import "os" 8 9 // Build-Informationen. Werden beim Release per -ldflags injiziert, z. B. 10 // 11 // go build -ldflags "-X main.version=1.0.0 -X main.commit=abc1234" ./cmd/talk 12 // 13 // Defaults: version "dev", commit leer (siehe auch .goreleaser.yaml, Task 11). 14 var ( 15 version = "dev" 16 commit = "" 17 ) 18 19 func main() { 20 os.Exit(run(os.Args[1:], os.Stdout, os.Stderr)) 21 } 22 cmd/talkd/main.go --- Go 1 // talkd ist der Daemon für talk 2.0 (Queue, Mute-State, HTTP-API, MCP). 2 // 3 // Dieser Stand (Task 01) ist ein Platzhalter: talkd startet, loggt eine Zeile 4 // und beendet sich mit Exit-Code 0. Die eigentliche Daemon-Implementierung 5 // (HTTP-Server auf 127.0.0.1, serialisierte Ansage-Queue, persistentes Mute) 6 // entsteht in Task 04. 7 package main 8 9 import "log" 10 11 func main() { 12 log.Println("talkd: Platzhalter — Daemon-Implementierung folgt in Task 04") 13 } 14 go.mod --- Text 1 module code.m3ta.dev/m3tam3re/talk 2 3 go 1.26.8 4 internal/api/doc.go --- Go 1 // Package api implementiert die HTTP-Oberfläche des Daemons auf 127.0.0.1: 2 // POST /speak, GET /status, POST /cancel, POST /mute, POST /unmute (Task 04), 3 // CORS für /speak und den MCP-Endpoint (Streamable HTTP, Task 06). 4 package api 5 internal/audio/doc.go --- Go 1 // Package audio implementiert die Audio-Ausgabe von talk über das 2 // Sink-Interface: Playback embedded via oto (PCM/WAV) und ein null-Sink, 3 // das verwirft (CI, Server, Tests). Auswahl per Config audio.sink. 4 // Implementierung folgt in Task 02. 5 package audio 6 internal/config/doc.go --- Go 1 // Package config implementiert die Config-Auflösung von talk: Defaults + 2 // optionale config.toml im platform-nativen Verzeichnis (~/.config/talk, 3 // ~/Library/Application Support/talk, %APPDATA%\talk) + Env-Overrides 4 // (TALK_*); Env schlägt Datei. Enthält Voice-Mapping voice.. 5 // und Provider-Endpunkte (Fake-bar für Tests). Implementierung folgt in 6 // Task 02. 7 package config 8 internal/langdetect/doc.go --- Go 1 // Package langdetect erkennt die Sprache eines Ansage-Texts als reine 2 // Funktion Detect(text) → "de" | "en" (Default "de"; Heuristik über 3 // Common-Word-Listen und Umlaute/ß). Implementierung folgt in Task 02. 4 package langdetect 5 internal/notify/doc.go --- Go 1 // Package notify implementiert Best-effort-Desktop-Notifications parallel 2 // zum Audio (beeep: notify-send / macOS Notification Center / Windows Toast), 3 // mit Abbrechen-Button, wo das OS ihn hergibt. Implementierung folgt. 4 package notify 5 internal/provider/doc.go --- Go 1 // Package provider implementiert die TTS-Provider-Kette von talk: 2 // ElevenLabs → OpenAI TTS → Edge-TTS → OS built-in (macOS say, Windows SAPI, 3 // Linux espeak/speech-dispatcher), überschreibbar per Config/Flag, mit 4 // Fallback bei Fehlern. Implementierung folgt in Task 02 (osbuiltin) und 5 // Task 03 (Cloud-Provider, Kette). 6 package provider 7 internal/queue/doc.go --- Go 1 // Package queue implementiert die Ansage-Queue des Daemons: serialisierte 2 // Abarbeitung (eine Ansage gleichzeitig), Prioritäten (high > normal > low, 3 // FIFO je Level), now-Items springen vor, Rate-Limit mit prioritätsbewusstem 4 // Verwerfen. Implementierung folgt in Task 04 (Queue) und Task 05 5 // (Rate-Limit, Ruhezeiten). 6 package queue 7