summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-04-01 20:29:50 +0300
committerPaul Buetow <paul@buetow.org>2026-04-01 20:29:50 +0300
commit5ae64f1658fb8e1daa9400efab96f3fcee5d17c0 (patch)
tree7cf31ca579cd010594431af0e37679c2a8a8da93
parent3c82415b3b9bca4b376d3044bb677438b17e491e (diff)
zg: add consumer coverage for shared voice lists
-rw-r--r--internal/gui/generator.go8
-rw-r--r--internal/gui/generator_test.go87
-rw-r--r--internal/processor/processor.go4
-rw-r--r--internal/processor/processor_test.go72
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 = &copyConfig
+ 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 = &copyConfig
+ 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")