diff options
| author | Paul Buetow <paul@buetow.org> | 2026-04-02 08:34:18 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-04-02 08:34:18 +0300 |
| commit | ea0df3747579fc1411a0b9533c86ed23d4ffb641 (patch) | |
| tree | 515ee66ec0a126e7587d8a4d17ec8356b83fccf8 /internal/processor | |
| parent | fdee8dfa21f32b8981bd048b5620cb4772322990 (diff) | |
Fix processor Gemini/OpenAI voice wiring
Diffstat (limited to 'internal/processor')
| -rw-r--r-- | internal/processor/processor.go | 40 | ||||
| -rw-r--r-- | internal/processor/processor_test.go | 161 |
2 files changed, 158 insertions, 43 deletions
diff --git a/internal/processor/processor.go b/internal/processor/processor.go index ecd4c1a..3c07806 100644 --- a/internal/processor/processor.go +++ b/internal/processor/processor.go @@ -291,6 +291,16 @@ func (p *Processor) geminiVoice() string { return "" } +func (p *Processor) openAIVoice() string { + if voice := strings.TrimSpace(viper.GetString("audio.openai_voice")); voice != "" { + return voice + } + if p != nil && p.flags != nil { + return strings.TrimSpace(p.flags.OpenAIVoice) + } + return "" +} + func (p *Processor) audioVoicesForProvider() []string { switch p.audioProviderName() { case "gemini": @@ -303,17 +313,14 @@ func (p *Processor) audioVoicesForProvider() []string { func (p *Processor) audioVoiceForProvider() string { switch p.audioProviderName() { case "gemini": - if voice := p.geminiVoice(); voice != "" { - return voice - } + return p.geminiVoice() default: - if p.flags.OpenAIVoice != "" { - return p.flags.OpenAIVoice + if voice := p.openAIVoice(); voice != "" { + return voice } + voices := p.audioVoicesForProvider() + return voices[rand.Intn(len(voices))] } - - voices := p.audioVoicesForProvider() - return voices[rand.Intn(len(voices))] } // generateAudio generates audio files for a word @@ -328,13 +335,13 @@ func (p *Processor) generateAudio(word string) error { voice := p.audioVoiceForProvider() switch provider { case "gemini": - if p.geminiVoice() != "" { + if voice != "" { fmt.Printf(" Using specified Gemini voice: %s\n", voice) } else { - fmt.Printf(" Using random Gemini voice: %s\n", voice) + fmt.Printf(" Using Gemini model default voice\n") } default: - if p.flags.OpenAIVoice != "" { + if p.openAIVoice() != "" { fmt.Printf(" Using specified voice: %s\n", voice) } else { fmt.Printf(" Using random voice: %s\n", voice) @@ -363,13 +370,13 @@ func (p *Processor) generateAudioBgBg(front, back string) error { voice := p.audioVoiceForProvider() switch provider { case "gemini": - if p.geminiVoice() != "" { + if voice != "" { fmt.Printf(" Using specified Gemini voice: %s\n", voice) } else { - fmt.Printf(" Using random Gemini voice: %s\n", voice) + fmt.Printf(" Using Gemini model default voice\n") } default: - if p.flags.OpenAIVoice != "" { + if p.openAIVoice() != "" { fmt.Printf(" Using specified voice: %s\n", voice) } else { fmt.Printf(" Using random voice: %s\n", voice) @@ -479,7 +486,7 @@ func (p *Processor) generateAudioWithVoiceAndFilenameInDir(word, voice, filename } // Save audio attribution - if err := p.saveAudioAttribution(word, outputFile, providerConfig); err != nil { + if err := p.saveAudioAttribution(word, outputFile, providerConfig, word); err != nil { fmt.Printf(" Warning: Failed to save audio attribution: %v\n", err) } @@ -980,8 +987,7 @@ func (p *Processor) isWordFullyProcessed(word string) bool { } return true // All required files exist } -func (p *Processor) saveAudioAttribution(word, audioFile string, config *audio.Config) error { - processedText := audio.ProcessedTextForWord(word) +func (p *Processor) saveAudioAttribution(word, audioFile string, config *audio.Config, processedText string) error { var attribution string switch strings.ToLower(strings.TrimSpace(config.Provider)) { case "gemini": diff --git a/internal/processor/processor_test.go b/internal/processor/processor_test.go index 64b8131..4d9f3ad 100644 --- a/internal/processor/processor_test.go +++ b/internal/processor/processor_test.go @@ -452,9 +452,9 @@ func TestGenerateAudioBgBgUsesSharedOpenAIVoices(t *testing.T) { t.Fatalf("expected attribution file %q: %v", attrPath, err) } attribution := string(attributionData) - wantText := []string{"ябълка...", "круша..."}[i] + wantText := []string{"ябълка", "круша"}[i] if !strings.Contains(attribution, "Processed text sent to TTS: "+wantText) { - t.Fatalf("bg-bg attribution missing processed text %q: %q", wantText, attribution) + t.Fatalf("bg-bg attribution missing exact processed text %q: %q", wantText, attribution) } } } @@ -575,24 +575,17 @@ func TestGenerateAudioUsesConfiguredGeminiVoiceAndModel(t *testing.T) { t.Fatalf("expected attribution file %q: %v", attrPath, err) } attribution := string(attributionData) - if !strings.Contains(attribution, "Processed text sent to TTS: ябълка...") { - t.Fatalf("gemini attribution missing processed text: %q", attribution) + if !strings.Contains(attribution, "Processed text sent to TTS: ябълка") { + t.Fatalf("gemini attribution missing exact processed text: %q", attribution) } } -func TestGenerateAudioUsesSharedGeminiVoicesWhenVoiceNotSet(t *testing.T) { +func TestGenerateAudioUsesGeminiModelDefaultWhenVoiceNotSet(t *testing.T) { originalFactory := newAudioProvider t.Cleanup(func() { newAudioProvider = originalFactory }) - originalVoices := append([]string(nil), audio.GeminiVoices...) - t.Cleanup(func() { - audio.GeminiVoices = originalVoices - }) - - audio.GeminiVoices = []string{"sentinel-gemini-voice"} - fakeProvider := &fakeAudioProvider{} var capturedConfig *audio.Config newAudioProvider = func(config *audio.Config) (audio.Provider, error) { @@ -622,24 +615,53 @@ func TestGenerateAudioUsesSharedGeminiVoicesWhenVoiceNotSet(t *testing.T) { if capturedConfig == nil { t.Fatal("expected audio provider config to be captured") } - if capturedConfig.GeminiVoice != "sentinel-gemini-voice" { - t.Fatalf("captured GeminiVoice = %q, want %q", capturedConfig.GeminiVoice, "sentinel-gemini-voice") + if capturedConfig.GeminiVoice != "" { + t.Fatalf("captured GeminiVoice = %q, want empty model-default voice", capturedConfig.GeminiVoice) + } + if fakeProvider.generateCalls != 1 { + t.Fatalf("GenerateAudio() calls = %d, want %d", fakeProvider.generateCalls, 1) + } + if !strings.HasSuffix(fakeProvider.lastOutputFile, "audio.wav") { + t.Fatalf("GenerateAudio() output file = %q, want wav output", fakeProvider.lastOutputFile) + } + + wordDir := p.findCardDirectory("ябълка") + if wordDir == "" { + t.Fatal("expected generated word directory") + } + metadataData, err := os.ReadFile(filepath.Join(wordDir, "audio_metadata.txt")) + if err != nil { + t.Fatalf("expected metadata file: %v", err) + } + metadata := string(metadataData) + for _, want := range []string{ + "provider=gemini", + "voice=model-default", + "audio_file=audio.wav", + "cardtype=en-bg", + } { + if !strings.Contains(metadata, want) { + t.Fatalf("metadata = %q, missing %q", metadata, want) + } + } + + attrPath := audio.AttributionPath(fakeProvider.lastOutputFile) + attributionData, err := os.ReadFile(attrPath) + if err != nil { + t.Fatalf("expected attribution file %q: %v", attrPath, err) + } + attribution := string(attributionData) + if !strings.Contains(attribution, "Processed text sent to TTS: ябълка") { + t.Fatalf("gemini attribution missing exact processed text: %q", attribution) } } -func TestGenerateAudioBgBgUsesSharedGeminiVoicesWhenVoiceNotSet(t *testing.T) { +func TestGenerateAudioBgBgUsesGeminiModelDefaultWhenVoiceNotSet(t *testing.T) { originalFactory := newAudioProvider t.Cleanup(func() { newAudioProvider = originalFactory }) - originalVoices := append([]string(nil), audio.GeminiVoices...) - t.Cleanup(func() { - audio.GeminiVoices = originalVoices - }) - - audio.GeminiVoices = []string{"sentinel-bg-gemini-voice"} - fakeProvider := &fakeAudioProvider{} var capturedConfigs []*audio.Config newAudioProvider = func(config *audio.Config) (audio.Provider, error) { @@ -670,13 +692,25 @@ func TestGenerateAudioBgBgUsesSharedGeminiVoicesWhenVoiceNotSet(t *testing.T) { t.Fatalf("captured config count = %d, want %d", len(capturedConfigs), 2) } for i, capturedConfig := range capturedConfigs { - if capturedConfig.GeminiVoice != "sentinel-bg-gemini-voice" { - t.Fatalf("captured config %d GeminiVoice = %q, want %q", i, capturedConfig.GeminiVoice, "sentinel-bg-gemini-voice") + if capturedConfig.GeminiVoice != "" { + t.Fatalf("captured config %d GeminiVoice = %q, want empty model-default voice", i, capturedConfig.GeminiVoice) } } if fakeProvider.generateCalls != 2 { t.Fatalf("GenerateAudio() calls = %d, want %d", fakeProvider.generateCalls, 2) } + for i, outputFile := range fakeProvider.outputFiles { + attrPath := audio.AttributionPath(outputFile) + attributionData, err := os.ReadFile(attrPath) + if err != nil { + t.Fatalf("expected attribution file %q: %v", attrPath, err) + } + attribution := string(attributionData) + wantText := []string{"ябълка", "круша"}[i] + if !strings.Contains(attribution, "Processed text sent to TTS: "+wantText) { + t.Fatalf("bg-bg attribution missing exact processed text %q: %q", wantText, attribution) + } + } } func TestGenerateAudioUsesConfiguredAudioFormatWhenOpenAIConfigIsSetOnly(t *testing.T) { @@ -749,8 +783,83 @@ func TestGenerateAudioUsesConfiguredAudioFormatWhenOpenAIConfigIsSetOnly(t *test t.Fatalf("expected attribution file %q: %v", attrPath, err) } attribution := string(attributionData) - if !strings.Contains(attribution, "Processed text sent to TTS: ябълка...") { - t.Fatalf("openai attribution missing processed text: %q", attribution) + if !strings.Contains(attribution, "Processed text sent to TTS: ябълка") { + t.Fatalf("openai attribution missing exact processed text: %q", attribution) + } +} + +func TestGenerateAudioUsesConfiguredOpenAIVoiceFromConfig(t *testing.T) { + originalFactory := newAudioProvider + t.Cleanup(func() { + newAudioProvider = originalFactory + }) + + fakeProvider := &fakeAudioProvider{} + var capturedConfig *audio.Config + newAudioProvider = func(config *audio.Config) (audio.Provider, error) { + copyConfig := *config + capturedConfig = ©Config + return fakeProvider, nil + } + + originalConfig := viper.New() + *originalConfig = *viper.GetViper() + defer func() { + *viper.GetViper() = *originalConfig + }() + viper.Reset() + viper.Set("audio.provider", "openai") + viper.Set("audio.openai_voice", "shimmer") + + flags := cli.NewFlags() + flags.OutputDir = t.TempDir() + flags.AudioFormat = "mp3" + + 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 != "shimmer" { + t.Fatalf("captured OpenAIVoice = %q, want %q", capturedConfig.OpenAIVoice, "shimmer") + } + if fakeProvider.generateCalls != 1 { + t.Fatalf("GenerateAudio() calls = %d, want %d", fakeProvider.generateCalls, 1) + } + if !strings.HasSuffix(fakeProvider.lastOutputFile, "audio.mp3") { + t.Fatalf("GenerateAudio() output file = %q, want single-voice output file", fakeProvider.lastOutputFile) + } + + wordDir := p.findCardDirectory("ябълка") + if wordDir == "" { + t.Fatal("expected generated word directory") + } + metadataData, err := os.ReadFile(filepath.Join(wordDir, "audio_metadata.txt")) + if err != nil { + t.Fatalf("expected metadata file: %v", err) + } + metadata := string(metadataData) + for _, want := range []string{ + "provider=openai", + "voice=shimmer", + "audio_file=audio.mp3", + } { + if !strings.Contains(metadata, want) { + t.Fatalf("metadata = %q, missing %q", metadata, want) + } + } + + attrPath := audio.AttributionPath(fakeProvider.lastOutputFile) + attributionData, err := os.ReadFile(attrPath) + if err != nil { + t.Fatalf("expected attribution file %q: %v", attrPath, err) + } + attribution := string(attributionData) + if !strings.Contains(attribution, "Processed text sent to TTS: ябълка") { + t.Fatalf("openai attribution missing exact processed text: %q", attribution) } } |
