Files
talk/.superpowers/sdd/plan/review-c8f3902..fa4f193.diff
T

663 lines
25 KiB
Diff
Raw Normal View History

# 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>` | 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 <Befehl> --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: "-- <Befehl> [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 <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 <Befehl> [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.<provider>.<lang>
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