summaryrefslogtreecommitdiff
path: root/internal/processor
diff options
context:
space:
mode:
Diffstat (limited to 'internal/processor')
-rw-r--r--internal/processor/processor.go40
-rw-r--r--internal/processor/processor_test.go161
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 = &copyConfig
+ 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)
}
}