chore(sdd): handoff snapshot — ledger, briefs, reports, review packages + HANDOFF.md for cross-machine continuation
This commit is contained in:
1 parent
acfba19fc2
commit
b934571b97
25 files changed
+17590
No files matched your search
@@ -0,0 +1,663 @@
|
||||
# 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
|
||||
|
||||
Reference in new issue
Block a user