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
+40
-44
@@ -6,16 +6,15 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Zwei einmalig erzeugte, echte MP3-Fixtures (ffmpeg am Dev-Host, in den
|
||||
// Test eingebettet — Tests bleiben zur Laufzeit frei von Werkzeug-Abhängig-
|
||||
// keiten). Beide entsprechen dem Lieferformat der Cloud-Provider aus
|
||||
// Task 03:
|
||||
// Two real MP3 fixtures generated once (ffmpeg on the dev host, embedded in
|
||||
// the test so tests stay free of tool dependencies at runtime). Both match
|
||||
// the delivery format of the cloud providers:
|
||||
//
|
||||
// fixtureMP3Mono: 0,15 s Stille, 24 kHz mono 48 kbit/s — exakt das
|
||||
// Edge-TTS-Ausgabeformat (audio-24khz-48kbitrate-mono-
|
||||
// mp3); übt den Mono→Stereo-Upsample-Pfad des Dekoders.
|
||||
// fixtureMP3Stereo: 0,15 s 440-Hz-Sinus, 44,1 kHz stereo 64 kbit/s — das
|
||||
// typische ElevenLabs/OpenAI-Format.
|
||||
// fixtureMP3Mono: 0.15 s silence, 24 kHz mono 48 kbit/s; exactly the
|
||||
// Edge TTS output format (audio-24khz-48kbitrate-mono-
|
||||
// mp3); exercises the decoder's mono-to-stereo path.
|
||||
// fixtureMP3Stereo: 0.15 s 440 Hz sine, 44.1 kHz stereo 64 kbit/s; the
|
||||
// typical ElevenLabs/OpenAI format.
|
||||
const fixtureMP3MonoB64 = "SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjYwLjE2LjEwMQAAAAAAAAAAAAAA//OEwAAAAAAAAAAAAEluZm8AAAAPAAAACQAABdAAOTk5OTk5OTk5OTlSUlJSUlJSUlJSUmtra2tra2tra2trhISEhISEhISEhIScnJycnJycnJycnLW1tbW1tbW1tbW1zs7Ozs7Ozs7Ozs7n5+fn5+fn5+fn5///////////////AAAAAExhdmM2MC4zMQAAAAAAAAAAAAAAACQD8AAAAAAAAAXQx638BQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//NkxAAAAANIAAAAAExBTUVVVVVMQU1FNC4wVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//NkxHwAAANIAAAAAFVVVVVVVVVMQU1FNC4wVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//NkxHwAAANIAAAAAFVVVVVVVVVMQU1FNC4wVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//NkxHwAAANIAAAAAFVVVVVVVVVMQU1FNC4wVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//NkxHwAAANIAAAAAFVVVVVVVVVMQU1FNC4wVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//NkxHwAAANIAAAAAFVVVVVVVVVMQU1FNC4wVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//NkxHwAAANIAAAAAFVVVVVVVVVMQU1FNC4wVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//NkxHwAAANIAAAAAFVVVVVVVVVMQU1FNC4wVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV//NkxHwAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV"
|
||||
|
||||
const fixtureMP3StereoB64 = "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"
|
||||
@@ -24,14 +23,14 @@ func fixture(t *testing.T, b64 string) []byte {
|
||||
t.Helper()
|
||||
data, err := base64.StdEncoding.DecodeString(b64)
|
||||
if err != nil {
|
||||
t.Fatalf("Fixture dekodieren: %v", err)
|
||||
t.Fatalf("decoding fixture: %v", err)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
// TestDecodeMP3Mono: das Edge-Format (24 kHz mono) dekodiert zu 16-Bit-PCM
|
||||
// stereo — der Dekoder spiegelt Mono auf zwei Kanäle, Samples sind 4 Bytes
|
||||
// (2 Kanäle × 2 Bytes) aligned.
|
||||
// TestDecodeMP3Mono: the Edge format (24 kHz mono) decodes to 16-bit PCM
|
||||
// stereo; the decoder mirrors mono onto two channels, samples are aligned
|
||||
// to 4 bytes (2 channels x 2 bytes).
|
||||
func TestDecodeMP3Mono(t *testing.T) {
|
||||
f, pcm, err := decodeMP3(fixture(t, fixtureMP3MonoB64))
|
||||
if err != nil {
|
||||
@@ -41,60 +40,57 @@ func TestDecodeMP3Mono(t *testing.T) {
|
||||
t.Errorf("SampleRate = %d, want 24000", f.SampleRate)
|
||||
}
|
||||
if f.Channels != 2 || f.BitsPerCoded != 16 || f.Float {
|
||||
t.Errorf("Format = %+v, want 2 Kanäle/16 Bit/int", f)
|
||||
t.Errorf("format = %+v, want 2 channels/16 bit/int", f)
|
||||
}
|
||||
if len(pcm) == 0 || len(pcm)%4 != 0 {
|
||||
t.Errorf("PCM-Länge = %d, want > 0 und durch 4 teilbar (16-Bit-Samples stereo)", len(pcm))
|
||||
t.Errorf("PCM length = %d, want > 0 and divisible by 4 (16-bit stereo samples)", len(pcm))
|
||||
}
|
||||
}
|
||||
|
||||
// TestDecodeMP3Stereo: ElevenLabs/OpenAI-typisches Format (44,1 kHz stereo).
|
||||
// TestDecodeMP3Stereo: the ElevenLabs/OpenAI-typical format (44.1 kHz stereo).
|
||||
func TestDecodeMP3Stereo(t *testing.T) {
|
||||
f, pcm, err := decodeMP3(fixture(t, fixtureMP3StereoB64))
|
||||
if err != nil {
|
||||
t.Fatalf("decodeMP3 (stereo): %v", err)
|
||||
}
|
||||
if f.SampleRate != 44100 || f.Channels != 2 || f.BitsPerCoded != 16 {
|
||||
t.Errorf("Format = %+v, want 44100 Hz stereo 16 Bit", f)
|
||||
t.Errorf("format = %+v, want 44100 Hz stereo 16 bit", f)
|
||||
}
|
||||
if len(pcm) == 0 || len(pcm)%4 != 0 {
|
||||
t.Errorf("PCM-Länge = %d, want > 0 und durch 4 teilbar", len(pcm))
|
||||
t.Errorf("PCM length = %d, want > 0 and divisible by 4", len(pcm))
|
||||
}
|
||||
}
|
||||
|
||||
// TestDecodeMP3Fehler: Daten, die nach MP3 aussehen, aber keine Frames
|
||||
// enthalten, sind ein klarer mp3-Fehler (nicht der WAV-Fehlerpfad).
|
||||
func TestDecodeMP3Fehler(t *testing.T) {
|
||||
cases := []struct{ name, want string }{
|
||||
{"Müll mit Frame-Sync", "mp3:"},
|
||||
{"ID3-Tag ohne Frames", "mp3:"},
|
||||
// TestDecodeMP3Errors: data that looks like MP3 but contains no frames is a
|
||||
// clear mp3 error (not the WAV error path).
|
||||
func TestDecodeMP3Errors(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
data []byte
|
||||
want string
|
||||
}{
|
||||
{"garbage with frame sync", []byte{0xFF, 0xFB, 'n', 'o', 't', ' ', 'm', 'p', '3'}, "mp3:"},
|
||||
{"ID3 tag without frames", []byte("ID3\x04\x00\x00\x00\x00\x00\x00"), "mp3:"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var data []byte
|
||||
switch tc.name {
|
||||
case "Müll mit Frame-Sync":
|
||||
data = []byte{0xFF, 0xFB, 'k', 'e', 'i', 'n', ' ', 'm', 'p', '3'}
|
||||
case "ID3-Tag ohne Frames":
|
||||
data = []byte("ID3\x04\x00\x00\x00\x00\x00\x00")
|
||||
}
|
||||
_, _, err := decodeMP3(data)
|
||||
_, _, err := decodeMP3(tc.data)
|
||||
if err == nil || !strings.Contains(err.Error(), tc.want) {
|
||||
t.Errorf("decodeMP3-Fehler = %v, will Meldung mit %q", err, tc.want)
|
||||
t.Errorf("decodeMP3 error = %v, want message containing %q", err, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestLooksLikeMP3 pinnt die Format-Erkennung.
|
||||
// TestLooksLikeMP3 pins the format detection.
|
||||
func TestLooksLikeMP3(t *testing.T) {
|
||||
yes := [][]byte{
|
||||
fixture(t, fixtureMP3StereoB64), // ID3-Tag
|
||||
{0xFF, 0xFB, 0x00, 0x00}, // roher Frame-Sync
|
||||
fixture(t, fixtureMP3StereoB64), // ID3 tag
|
||||
{0xFF, 0xFB, 0x00, 0x00}, // raw frame sync
|
||||
}
|
||||
no := [][]byte{
|
||||
[]byte("RIFF....WAVE"),
|
||||
[]byte("kein audio"),
|
||||
[]byte("not audio"),
|
||||
{0xFF},
|
||||
{},
|
||||
}
|
||||
@@ -110,18 +106,18 @@ func TestLooksLikeMP3(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestDecodeAudioWeiche: RIFF geht an den WAV-Parser, MP3 an den MP3-
|
||||
// Dekoder, alles andere fällt mit dem WAV-Fehler durch.
|
||||
func TestDecodeAudioWeiche(t *testing.T) {
|
||||
// TestDecodeAudioSwitch: RIFF goes to the WAV parser, MP3 to the MP3
|
||||
// decoder, everything else falls through with the WAV error.
|
||||
func TestDecodeAudioSwitch(t *testing.T) {
|
||||
f, _, err := decodeAudio(buildWAV(1, 1, 22050, 16, []byte{1, 2, 3, 4}))
|
||||
if err != nil || f.SampleRate != 22050 {
|
||||
t.Errorf("decodeAudio(WAV) = %+v, %v — soll den WAV-Pfad nehmen", f, err)
|
||||
t.Errorf("decodeAudio(WAV) = %+v, %v; should take the WAV path", f, err)
|
||||
}
|
||||
f, _, err = decodeAudio(fixture(t, fixtureMP3MonoB64))
|
||||
if err != nil || f.SampleRate != 24000 {
|
||||
t.Errorf("decodeAudio(MP3) = %+v, %v — soll den MP3-Pfad nehmen", f, err)
|
||||
t.Errorf("decodeAudio(MP3) = %+v, %v; should take the MP3 path", f, err)
|
||||
}
|
||||
if _, _, err = decodeAudio([]byte("kein audio")); err == nil || !strings.Contains(err.Error(), "wav:") {
|
||||
t.Errorf("decodeAudio(Müll) = %v, will WAV-Fehler", err)
|
||||
if _, _, err = decodeAudio([]byte("not audio")); err == nil || !strings.Contains(err.Error(), "wav:") {
|
||||
t.Errorf("decodeAudio(garbage) = %v, want WAV error", err)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user