refactor: switch the whole project to English
talk is going to be published. Code comments, CLI output, error messages,
daemon log lines, MCP descriptions and results, test names and assertions,
README, spec, skill, build scripts and flake are now English. Only the
spoken announcement texts stay bilingual: the wrap result and the test
sentence are German or English depending on the detected language.
Language fallback is now configurable: `lang = "en" | "de"` (env
TALK_LANG, default en) is used when detection cannot tell (numbers, single
words, ties) and for talk's own announcements. langdetect.Detect takes the
fallback as an argument instead of hard-coding German. `talk setup` asks for
it; `talk doctor` and `talk status` show it.
Vocabulary pinned across packages so the process-boundary tests observe
stable strings: chain log lines ("chain: tier X skipped (not available)",
"falling back to the next tier", "spoken via X"), queue lines ("discarded:
quiet hours ... active", "replaced by toast", "max_age"), CLI notes
("speaking directly", "daemon started at", "no daemon", "nothing to cancel"),
status block with aligned English labels, MCP messages, origin guard 403.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
4d30bd95ad
commit
85fa64cfe6
95 files changed
+4707
-4854
No files matched your search
@@ -10,9 +10,9 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// buildWAV baut nach RIFF-Spezifikation eine WAV-Datei (unabhängig vom
|
||||
// Parser implementiert, damit Parser und Test-Fixture nicht denselben
|
||||
// Fehler teilen).
|
||||
// buildWAV builds a WAV file per the RIFF specification (implemented
|
||||
// independently of the parser so parser and test fixture cannot share the
|
||||
// same bug).
|
||||
func buildWAV(audioFormat uint16, channels, rate, bits int, samples []byte) []byte {
|
||||
var buf bytes.Buffer
|
||||
buf.WriteString("RIFF")
|
||||
@@ -44,22 +44,22 @@ func TestDecodeWAV(t *testing.T) {
|
||||
t.Fatalf("decodeWAV: %v", err)
|
||||
}
|
||||
if f.SampleRate != 22050 || f.Channels != 1 || f.BitsPerCoded != 16 || f.Float {
|
||||
t.Errorf("Format = %+v", f)
|
||||
t.Errorf("format = %+v", f)
|
||||
}
|
||||
if string(pcm) != "\x01\x02\x03\x04" {
|
||||
t.Errorf("PCM = %x", pcm)
|
||||
}
|
||||
})
|
||||
t.Run("8-bit stereo mit Extra-Chunk", func(t *testing.T) {
|
||||
t.Run("8-bit stereo with extra chunk", func(t *testing.T) {
|
||||
wav := buildWAV(1, 2, 8000, 8, []byte{7, 8, 9})
|
||||
// LIST-Chunk (word-aligned, ungerade Länge) zwischen fmt und data einfügen
|
||||
// insert a LIST chunk (word-aligned, odd length) between fmt and data
|
||||
withList := append(wav[:36], append([]byte("LIST\x03\x00\x00\x00abc\x00"), wav[36:]...)...)
|
||||
f, pcm, err := decodeWAV(withList)
|
||||
if err != nil {
|
||||
t.Fatalf("decodeWAV: %v", err)
|
||||
}
|
||||
if f.BitsPerCoded != 8 || f.Channels != 2 {
|
||||
t.Errorf("Format = %+v", f)
|
||||
t.Errorf("format = %+v", f)
|
||||
}
|
||||
if string(pcm) != "\x07\x08\x09" {
|
||||
t.Errorf("PCM = %x", pcm)
|
||||
@@ -72,42 +72,42 @@ func TestDecodeWAV(t *testing.T) {
|
||||
t.Fatalf("decodeWAV: %v", err)
|
||||
}
|
||||
if !f.Float || f.BitsPerCoded != 32 {
|
||||
t.Errorf("Format = %+v, want Float32", f)
|
||||
t.Errorf("format = %+v, want Float32", f)
|
||||
}
|
||||
})
|
||||
t.Run("leerer data-Chunk ist erlaubt", func(t *testing.T) {
|
||||
t.Run("empty data chunk is allowed", func(t *testing.T) {
|
||||
wav := buildWAV(1, 1, 22050, 16, nil)
|
||||
_, pcm, err := decodeWAV(wav)
|
||||
if err != nil {
|
||||
t.Fatalf("decodeWAV: %v", err)
|
||||
}
|
||||
if len(pcm) != 0 {
|
||||
t.Errorf("PCM = %x, want leer", pcm)
|
||||
t.Errorf("PCM = %x, want empty", pcm)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestDecodeWAVFehler(t *testing.T) {
|
||||
func TestDecodeWAVErrors(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
data []byte
|
||||
want string
|
||||
}{
|
||||
{"zu kurz", []byte("RIFF"), "zu kurz"},
|
||||
{"kein riff", []byte("RIFX" + "\x00\x00\x00\x00" + "WAVE"), "RIFF"},
|
||||
{"24-bit pcm", buildWAV(1, 1, 44100, 24, []byte{1, 2, 3}), "nicht unterstütztes Format"},
|
||||
{"3 kanäle", buildWAV(1, 3, 44100, 16, []byte{1, 2, 3, 4, 5, 6}), "Kanäle"},
|
||||
{"ohne data", func() []byte {
|
||||
{"too short", []byte("RIFF"), "too short"},
|
||||
{"no riff", []byte("RIFX" + "\x00\x00\x00\x00" + "WAVE"), "RIFF"},
|
||||
{"24-bit pcm", buildWAV(1, 1, 44100, 24, []byte{1, 2, 3}), "unsupported format"},
|
||||
{"3 channels", buildWAV(1, 3, 44100, 16, []byte{1, 2, 3, 4, 5, 6}), "channels"},
|
||||
{"without data", func() []byte {
|
||||
wav := buildWAV(1, 1, 22050, 16, []byte{1, 2})
|
||||
return wav[:36] // nur fmt-Chunk behalten
|
||||
return wav[:36] // keep only the fmt chunk
|
||||
}(), "data"},
|
||||
{"kaputter chunk", append(buildWAV(1, 1, 22050, 16, nil), "data\xff\xff\xff\xff"...), "überragt"},
|
||||
{"broken chunk", append(buildWAV(1, 1, 22050, 16, nil), "data\xff\xff\xff\xff"...), "exceeds"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
_, _, err := decodeWAV(tc.data)
|
||||
if err == nil || !strings.Contains(err.Error(), tc.want) {
|
||||
t.Errorf("decodeWAV-Fehler = %v, will Meldung mit %q", err, tc.want)
|
||||
t.Errorf("decodeWAV error = %v, want message containing %q", err, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -124,9 +124,9 @@ func TestNewSink(t *testing.T) {
|
||||
}
|
||||
}
|
||||
if _, err := NewSink("mpv", nil); err == nil {
|
||||
t.Error("NewSink(\"mpv\") soll fehlschlagen")
|
||||
t.Error("NewSink(\"mpv\") should fail")
|
||||
}
|
||||
t.Run("null-Sink loggt über den Factory-Writer", func(t *testing.T) {
|
||||
t.Run("null sink logs via the factory writer", func(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
s, err := NewSink(NameNull, &buf)
|
||||
if err != nil {
|
||||
@@ -135,49 +135,49 @@ func TestNewSink(t *testing.T) {
|
||||
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())
|
||||
if !strings.Contains(buf.String(), "discarded (null sink)") {
|
||||
t.Errorf("log = %q, want discard line via the factory writer", buf.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestNullSinkVerwirftUndLoggt(t *testing.T) {
|
||||
func TestNullSinkDiscardsAndLogs(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
sink := NullSink{W: &buf}
|
||||
if err := sink.Play(context.Background(), []byte("audio")); err != nil {
|
||||
t.Fatalf("NullSink.Play: %v", err)
|
||||
}
|
||||
if !strings.Contains(buf.String(), "verworfen") {
|
||||
t.Errorf("NullSink-Log = %q, will Verwurf-Zeile", buf.String())
|
||||
if !strings.Contains(buf.String(), "discarded") {
|
||||
t.Errorf("NullSink log = %q, want discard line", buf.String())
|
||||
}
|
||||
if sink.Name() != NameNull {
|
||||
t.Errorf("Name() = %q", sink.Name())
|
||||
}
|
||||
}
|
||||
|
||||
// TestOtoSinkUngueltigeDaten: OtoSink.Play muss an der WAV-Dekodierung
|
||||
// scheitern, bevor irgend ein Audio-Gerät angefasst wird — das ist der
|
||||
// CI-/Server-sichere Fehlerpfad des oto-Sinks.
|
||||
func TestOtoSinkUngueltigeDaten(t *testing.T) {
|
||||
err := (OtoSink{}).Play(context.Background(), []byte("kein audio"))
|
||||
// TestOtoSinkInvalidData: OtoSink.Play must fail at WAV decoding before any
|
||||
// audio device is touched; this is the CI/server-safe error path of the oto
|
||||
// sink.
|
||||
func TestOtoSinkInvalidData(t *testing.T) {
|
||||
err := (OtoSink{}).Play(context.Background(), []byte("not audio"))
|
||||
if err == nil {
|
||||
t.Fatal("OtoSink.Play mit Müll: kein Fehler")
|
||||
t.Fatal("OtoSink.Play with garbage: no error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "wav:") {
|
||||
t.Errorf("Fehler soll aus dem WAV-Parser kommen: %v", err)
|
||||
t.Errorf("error should come from the WAV parser: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestOtoSinkEchteWiedergabe läuft nur mit TALK_TEST_REAL_AUDIO=1 (erzeugt
|
||||
// echten Ton); Standard-Testläufe bleiben stumm.
|
||||
func TestOtoSinkEchteWiedergabe(t *testing.T) {
|
||||
// TestOtoSinkRealPlayback runs only with TALK_TEST_REAL_AUDIO=1 (produces
|
||||
// real sound); default test runs stay silent.
|
||||
func TestOtoSinkRealPlayback(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Übersprungen (-short)")
|
||||
t.Skip("skipped (-short)")
|
||||
}
|
||||
if os.Getenv("TALK_TEST_REAL_AUDIO") != "1" {
|
||||
t.Skip("Übersprungen (hörbar) — mit TALK_TEST_REAL_AUDIO=1 ausführen")
|
||||
t.Skip("skipped (audible); run with TALK_TEST_REAL_AUDIO=1")
|
||||
}
|
||||
// 0,1 s 440-Hz-Sinus, 16 Bit mono 22050 Hz.
|
||||
// 0.1 s 440 Hz sine, 16 bit mono 22050 Hz.
|
||||
const rate = 22050
|
||||
samples := make([]byte, rate/10*2)
|
||||
for i := 0; i < rate/10; i++ {
|
||||
|
||||
Reference in new issue
Block a user