From ae9f3b680e760150b4f3a2c6a9be4a25c0297f16 Mon Sep 17 00:00:00 2001 From: m3ta-chiron Date: Sun, 18 Oct 2026 15:46:20 +0200 Subject: [PATCH] fix(audio): NullSink-Verwurf-Zeile im CLI-Pfad sichtbar machen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- cmd/talk/say.go | 2 +- cmd/talk/say_test.go | 7 ++++++- internal/audio/audio.go | 12 +++++++----- internal/audio/audio_test.go | 17 +++++++++++++++-- internal/langdetect/detect.go | 2 +- internal/langdetect/detect_test.go | 1 + 6 files changed, 31 insertions(+), 10 deletions(-) diff --git a/cmd/talk/say.go b/cmd/talk/say.go index d2fc1ab..d0f262b 100644 --- a/cmd/talk/say.go +++ b/cmd/talk/say.go @@ -106,7 +106,7 @@ func runSay(args []string, opts *sayOptions, stdin io.Reader, stderr io.Writer) return exitFailure } - sink, err := audio.NewSink(cfg.Audio.Sink) + sink, err := audio.NewSink(cfg.Audio.Sink, stderr) if err != nil { fmt.Fprintf(stderr, "talk: %v\n", err) return exitFailure diff --git a/cmd/talk/say_test.go b/cmd/talk/say_test.go index a661101..141c3bb 100644 --- a/cmd/talk/say_test.go +++ b/cmd/talk/say_test.go @@ -52,7 +52,9 @@ cat "`+wav+`"`) // TestSayArgumentMitNullSink: `talk "Hallo Welt"` läuft mit // TALK_AUDIO_SINK=null tonlos durch (Abnahmekriterium) — Synthese läuft -// (fake espeak wurde gerufen), die Wiedergabe wird verworfen. +// (fake espeak wurde gerufen), und die Wiedergabe wird mit sichtbarer +// Verwurf-Zeile auf stderr verworfen (extern beobachtbares Verhalten des +// null-Sinks auf dem CLI-Pfad). func TestSayArgumentMitNullSink(t *testing.T) { seen := withFakeEspeak(t) setGetenv(t, map[string]string{"TALK_AUDIO_SINK": "null"}) @@ -65,6 +67,9 @@ func TestSayArgumentMitNullSink(t *testing.T) { if _, err := os.Stat(seen); err != nil { t.Errorf("Synthese lief nicht (fake espeak nicht gerufen): %v", err) } + if !strings.Contains(errBuf.String(), "verworfen (null sink)") { + t.Errorf("stderr = %q, will Verwurf-Zeile des null-Sinks (Synthese war erfolgreich)", errBuf.String()) + } } // TestSaySubcommandWieDefault: `talk say "Text"` verhält sich wie der diff --git a/internal/audio/audio.go b/internal/audio/audio.go index aed92d7..d773bbd 100644 --- a/internal/audio/audio.go +++ b/internal/audio/audio.go @@ -24,21 +24,23 @@ type Sink interface { } // NewSink liefert den Sink zum konfigurierten Namen ("oto" oder "null"). -// Unbekannte Namen sind ein klarer Fehler. -func NewSink(name string) (Sink, error) { +// log ist ein optionaler Log-Output — die CLI übergibt ihr stderr, über den +// der null-Sink seine Verwurf-Zeile schreibt (nil = still verwerfen); der +// oto-Sink braucht kein Log. Unbekannte Namen sind ein klarer Fehler. +func NewSink(name string, log io.Writer) (Sink, error) { switch name { case NameOto: return OtoSink{}, nil case NameNull: - return NullSink{}, nil + return NullSink{W: log}, nil default: return nil, fmt.Errorf("unbekannter audio sink %q (erlaubt: %q, %q)", name, NameOto, NameNull) } } // NullSink verwirft jede Ansage ohne Ton — für CI, Server und Tests. Wurde -// ein W gesetzt, loggt Play eine Zeile darüber (der CLI-Pfad hängt stderr -// an, damit „läuft ohne Ton durch" auch sichtbar bleibt). +// ein W gesetzt (NewSink reicht dafür den Log-Output der CLI durch), loggt +// Play eine Zeile darüber, damit „läuft ohne Ton durch" sichtbar bleibt. type NullSink struct { W io.Writer // optionaler Log-Output; nil = still verwerfen } diff --git a/internal/audio/audio_test.go b/internal/audio/audio_test.go index 7a6281f..aeb178a 100644 --- a/internal/audio/audio_test.go +++ b/internal/audio/audio_test.go @@ -115,7 +115,7 @@ func TestDecodeWAVFehler(t *testing.T) { func TestNewSink(t *testing.T) { for _, name := range []string{NameOto, NameNull} { - s, err := NewSink(name) + s, err := NewSink(name, nil) if err != nil { t.Fatalf("NewSink(%q): %v", name, err) } @@ -123,9 +123,22 @@ func TestNewSink(t *testing.T) { t.Errorf("NewSink(%q).Name() = %q", name, s.Name()) } } - if _, err := NewSink("mpv"); err == nil { + if _, err := NewSink("mpv", nil); err == nil { t.Error("NewSink(\"mpv\") soll fehlschlagen") } + t.Run("null-Sink loggt über den Factory-Writer", func(t *testing.T) { + var buf bytes.Buffer + s, err := NewSink(NameNull, &buf) + if err != nil { + t.Fatalf("NewSink(%q): %v", NameNull, err) + } + if err := s.Play(context.Background(), []byte("audio")); err != nil { + t.Fatalf("Play: %v", err) + } + if !strings.Contains(buf.String(), "verworfen (null sink)") { + t.Errorf("Log = %q, will Verwurf-Zeile über den Factory-Writer", buf.String()) + } + }) } func TestNullSinkVerwirftUndLoggt(t *testing.T) { diff --git a/internal/langdetect/detect.go b/internal/langdetect/detect.go index 976ddf1..cc7b644 100644 --- a/internal/langdetect/detect.go +++ b/internal/langdetect/detect.go @@ -68,7 +68,7 @@ var englishWords = map[string]struct{}{ "finished": {}, "started": {}, "completed": {}, "error": {}, "warning": {}, "thanks": {}, "you": {}, "your": {}, "we": {}, "it": {}, "this": {}, "that": {}, "these": {}, - "those": {}, "was": {}, "were": {}, "are": {}, "be": {}, + "those": {}, "were": {}, "are": {}, "be": {}, "been": {}, "has": {}, "have": {}, "had": {}, "can": {}, "will": {}, "would": {}, "should": {}, "could": {}, "just": {}, "only": {}, "still": {}, "already": {}, "now": {}, "today": {}, diff --git a/internal/langdetect/detect_test.go b/internal/langdetect/detect_test.go index 8ad16a4..e7b8273 100644 --- a/internal/langdetect/detect_test.go +++ b/internal/langdetect/detect_test.go @@ -15,6 +15,7 @@ func TestDetect(t *testing.T) { {"nur whitespace", " \n\t ", "de"}, {"nur interpunktion", "!!! ??? ...", "de"}, {"deutsch satz", "Der Build wurde erfolgreich abgeschlossen und das Deployment ist fertig.", "de"}, + {"deutsch frage mit was", "Was macht er?", "de"}, {"deutsch umlaute", "Größe früh Ärzteübung — schön daß es klappt", "de"}, {"ß alleine", "weiß", "de"}, {"englisch satz", "The build has finished and the deployment is done.", "en"},