diff options
| author | Paul Buetow <paul@buetow.org> | 2026-04-01 20:29:50 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-04-01 20:29:50 +0300 |
| commit | 5ae64f1658fb8e1daa9400efab96f3fcee5d17c0 (patch) | |
| tree | 7cf31ca579cd010594431af0e37679c2a8a8da93 | |
| parent | 3c82415b3b9bca4b376d3044bb677438b17e491e (diff) | |
zg: add consumer coverage for shared voice lists
| -rw-r--r-- | internal/gui/generator.go | 8 | ||||
| -rw-r--r-- | internal/gui/generator_test.go | 87 | ||||
| -rw-r--r-- | internal/processor/processor.go | 4 | ||||
| -rw-r--r-- | internal/processor/processor_test.go | 72 |
4 files changed, 167 insertions, 4 deletions
diff --git a/internal/gui/generator.go b/internal/gui/generator.go index 3491db2..34e713e 100644 --- a/internal/gui/generator.go +++ b/internal/gui/generator.go @@ -27,6 +27,8 @@ var newNanoBananaImageClient = func(config *image.NanoBananaConfig) promptAwareI return image.NewNanoBananaClient(config) } +var newAudioProvider = audio.NewProvider + func randomVoiceAndSpeed(voices []string) (string, float64) { rng := rand.New(rand.NewSource(time.Now().UnixNano())) voice := voices[rng.Intn(len(voices))] @@ -80,7 +82,7 @@ func (a *Application) generateAudio(ctx context.Context, word string, cardDir st } // Create audio provider - provider, err := audio.NewProvider(&audioConfig) + provider, err := newAudioProvider(&audioConfig) if err != nil { return "", err } @@ -131,7 +133,7 @@ func (a *Application) generateAudioFront(ctx context.Context, word string, cardD audioConfig.OpenAISpeed = speed audioConfig.OutputDir = a.config.OutputDir - provider, err := audio.NewProvider(&audioConfig) + provider, err := newAudioProvider(&audioConfig) if err != nil { fmt.Printf("DEBUG (generateAudioFront): Failed to create audio provider: %v\n", err) return "", err @@ -172,7 +174,7 @@ func (a *Application) generateAudioBack(ctx context.Context, text string, cardDi audioConfig.OpenAISpeed = speed audioConfig.OutputDir = a.config.OutputDir - provider, err := audio.NewProvider(&audioConfig) + provider, err := newAudioProvider(&audioConfig) if err != nil { fmt.Printf("DEBUG (generateAudioBack): Failed to create audio provider: %v\n", err) return "", err diff --git a/internal/gui/generator_test.go b/internal/gui/generator_test.go index b17e5ac..d768560 100644 --- a/internal/gui/generator_test.go +++ b/internal/gui/generator_test.go @@ -8,6 +8,7 @@ import ( "strings" "testing" + "codeberg.org/snonux/totalrecall/internal/audio" "codeberg.org/snonux/totalrecall/internal/image" ) @@ -53,6 +54,27 @@ func (f *fakePromptAwareImageClient) SetPromptCallback(callback func(prompt stri f.promptCallback = callback } +type fakeAudioProvider struct { + generateCalls int + lastText string + lastOutputFile string +} + +func (f *fakeAudioProvider) GenerateAudio(_ context.Context, text, outputFile string) error { + f.generateCalls++ + f.lastText = text + f.lastOutputFile = outputFile + return nil +} + +func (f *fakeAudioProvider) Name() string { + return "fake-audio" +} + +func (f *fakeAudioProvider) IsAvailable() error { + return nil +} + func TestGenerateImagesWithPromptUsesNanoBananaProvider(t *testing.T) { originalNanoBananaClient := newNanoBananaImageClient originalOpenAIClient := newOpenAIImageClient @@ -127,3 +149,68 @@ func TestGenerateImagesWithPromptUsesNanoBananaProvider(t *testing.T) { t.Fatalf("outputPath = %q, want a PNG output file", outputPath) } } + +func TestGenerateAudioUsesSharedOpenAIVoices(t *testing.T) { + originalFactory := newAudioProvider + t.Cleanup(func() { + newAudioProvider = originalFactory + }) + + originalVoices := append([]string(nil), audio.OpenAIVoices...) + t.Cleanup(func() { + audio.OpenAIVoices = originalVoices + }) + + audio.OpenAIVoices = []string{"sentinel-gui-voice"} + + fakeProvider := &fakeAudioProvider{} + var capturedConfig *audio.Config + newAudioProvider = func(config *audio.Config) (audio.Provider, error) { + copyConfig := *config + capturedConfig = ©Config + return fakeProvider, nil + } + + tempDir := t.TempDir() + cardDir := filepath.Join(tempDir, "card") + if err := os.MkdirAll(cardDir, 0755); err != nil { + t.Fatalf("failed to create card dir: %v", err) + } + + app := &Application{ + config: &Config{ + OutputDir: tempDir, + AudioFormat: "mp3", + }, + audioConfig: &audio.Config{ + Provider: "openai", + OutputDir: tempDir, + OpenAIModel: "gpt-4o-mini-tts", + OpenAIInstruction: "Speak clearly.", + }, + } + + outputPath, err := app.generateAudio(context.Background(), "ябълка", cardDir) + if err != nil { + t.Fatalf("generateAudio() unexpected error: %v", err) + } + + if capturedConfig == nil { + t.Fatal("expected audio provider config to be captured") + } + if capturedConfig.OpenAIVoice != "sentinel-gui-voice" { + t.Fatalf("captured OpenAIVoice = %q, want %q", capturedConfig.OpenAIVoice, "sentinel-gui-voice") + } + if fakeProvider.generateCalls != 1 { + t.Fatalf("GenerateAudio() calls = %d, want %d", fakeProvider.generateCalls, 1) + } + if fakeProvider.lastText != "ябълка" { + t.Fatalf("GenerateAudio() text = %q, want %q", fakeProvider.lastText, "ябълка") + } + if fakeProvider.lastOutputFile != outputPath { + t.Fatalf("GenerateAudio() output file = %q, want %q", fakeProvider.lastOutputFile, outputPath) + } + if !strings.HasSuffix(outputPath, "audio.mp3") { + t.Fatalf("outputPath = %q, want shared audio filename", outputPath) + } +} diff --git a/internal/processor/processor.go b/internal/processor/processor.go index d2a380b..3b53737 100644 --- a/internal/processor/processor.go +++ b/internal/processor/processor.go @@ -38,6 +38,8 @@ var newNanoBananaImageClient = func(config *image.NanoBananaConfig) image.ImageS return image.NewNanoBananaClient(config) } +var newAudioProvider = audio.NewProvider + // NewProcessor creates a new word processor func NewProcessor(flags *cli.Flags) *Processor { openAIKey := cli.GetOpenAIKey() @@ -343,7 +345,7 @@ func (p *Processor) generateAudioWithVoiceAndFilenameInDir(word, voice, filename } // Create the audio provider - provider, err := audio.NewProvider(providerConfig) + provider, err := newAudioProvider(providerConfig) if err != nil { return err } diff --git a/internal/processor/processor_test.go b/internal/processor/processor_test.go index 8ab13e7..1b86910 100644 --- a/internal/processor/processor_test.go +++ b/internal/processor/processor_test.go @@ -8,6 +8,7 @@ import ( "strings" "testing" + "codeberg.org/snonux/totalrecall/internal/audio" "codeberg.org/snonux/totalrecall/internal/cli" "codeberg.org/snonux/totalrecall/internal/gui" "codeberg.org/snonux/totalrecall/internal/image" @@ -56,6 +57,27 @@ func (s *stubImageSearcher) GetLastPrompt() string { return s.lastPrompt } +type fakeAudioProvider struct { + generateCalls int + lastText string + lastOutputFile string +} + +func (f *fakeAudioProvider) GenerateAudio(_ context.Context, text, outputFile string) error { + f.generateCalls++ + f.lastText = text + f.lastOutputFile = outputFile + return nil +} + +func (f *fakeAudioProvider) Name() string { + return "fake-audio" +} + +func (f *fakeAudioProvider) IsAvailable() error { + return nil +} + func TestNewProcessor(t *testing.T) { t.Setenv("OPENAI_API_KEY", "test-openai-key") t.Setenv("GOOGLE_API_KEY", "test-google-key") @@ -231,6 +253,56 @@ func TestGUIConfigForRunModeHonorsExplicitImageAPI(t *testing.T) { } } +func TestGenerateAudioUsesSharedOpenAIVoices(t *testing.T) { + originalFactory := newAudioProvider + t.Cleanup(func() { + newAudioProvider = originalFactory + }) + + originalVoices := append([]string(nil), audio.OpenAIVoices...) + t.Cleanup(func() { + audio.OpenAIVoices = originalVoices + }) + + audio.OpenAIVoices = []string{"sentinel-openai-voice"} + + fakeProvider := &fakeAudioProvider{} + var capturedConfig *audio.Config + newAudioProvider = func(config *audio.Config) (audio.Provider, error) { + copyConfig := *config + capturedConfig = ©Config + return fakeProvider, nil + } + + tempDir := t.TempDir() + flags := cli.NewFlags() + flags.OutputDir = tempDir + flags.AudioFormat = "mp3" + flags.AllVoices = true + + p := NewProcessor(flags) + + if err := p.generateAudio("ябълка"); err != nil { + t.Fatalf("generateAudio() unexpected error: %v", err) + } + + if capturedConfig == nil { + t.Fatal("expected audio provider config to be captured") + } + if capturedConfig.OpenAIVoice != "sentinel-openai-voice" { + t.Fatalf("captured OpenAIVoice = %q, want %q", capturedConfig.OpenAIVoice, "sentinel-openai-voice") + } + if fakeProvider.generateCalls != 1 { + t.Fatalf("GenerateAudio() calls = %d, want %d", fakeProvider.generateCalls, 1) + } + if fakeProvider.lastText != "ябълка" { + t.Fatalf("GenerateAudio() text = %q, want %q", fakeProvider.lastText, "ябълка") + } + if !strings.HasSuffix(fakeProvider.lastOutputFile, "audio_sentinel-openai-voice.mp3") { + t.Fatalf("GenerateAudio() output file = %q, want shared voice name in filename", fakeProvider.lastOutputFile) + } +} + func TestDownloadImagesWithTranslationUsesNanoBananaConfigAndSavesPrompt(t *testing.T) { t.Setenv("OPENAI_API_KEY", "test-openai-key") t.Setenv("GOOGLE_API_KEY", "test-google-key") |
