From 0349a83743f8b8c8125a35b93b2271e402352b99 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Thu, 2 Apr 2026 08:06:13 +0300 Subject: Fix processor GUI parity gaps --- internal/processor/processor.go | 53 ++++++++++++++++++++++- internal/processor/processor_test.go | 83 +++++++++++++++++++++++++++++++++++- 2 files changed, 132 insertions(+), 4 deletions(-) (limited to 'internal/processor') diff --git a/internal/processor/processor.go b/internal/processor/processor.go index 3ecbce8..28b2352 100644 --- a/internal/processor/processor.go +++ b/internal/processor/processor.go @@ -495,6 +495,21 @@ func (p *Processor) downloadImagesWithTranslation(word, translationText string) searchOpts.Translation = translationText } + type promptSetter interface { + SetPromptCallback(func(prompt string)) + } + if promptAware, ok := searcher.(promptSetter); ok { + promptFile := filepath.Join(wordDir, "image_prompt.txt") + promptAware.SetPromptCallback(func(prompt string) { + if prompt == "" { + return + } + if err := os.WriteFile(promptFile, []byte(prompt), 0644); err != nil { + fmt.Printf(" Warning: Failed to save image prompt: %v\n", err) + } + }) + } + // Download single image ctx := context.Background() _, path, err := downloader.DownloadBestMatchWithOptions(ctx, searchOpts) @@ -987,7 +1002,7 @@ func (p *Processor) saveAudioAttribution(word, audioFile string, config *audio.C // Also save metadata for GUI display wordDir := filepath.Dir(audioFile) metadataFile := filepath.Join(wordDir, "audio_metadata.txt") - metadata := p.buildAudioMetadata(config) + metadata := p.buildAudioMetadata(config, audioFile) if err := os.WriteFile(metadataFile, []byte(metadata), 0644); err != nil { // Non-fatal error, just log it fmt.Printf("Warning: Failed to save audio metadata: %v\n", err) @@ -996,7 +1011,7 @@ func (p *Processor) saveAudioAttribution(word, audioFile string, config *audio.C return nil } -func (p *Processor) buildAudioMetadata(config *audio.Config) string { +func (p *Processor) buildAudioMetadata(config *audio.Config, audioFile string) string { var b strings.Builder provider := strings.ToLower(strings.TrimSpace(config.Provider)) if provider == "" { @@ -1029,6 +1044,40 @@ func (p *Processor) buildAudioMetadata(config *audio.Config) string { format = p.effectiveAudioFormat() } fmt.Fprintf(&b, "format=%s\n", format) + audioFileHint, audioFileBackHint := p.audioMetadataFileHints(audioFile) + if audioFileHint != "" { + fmt.Fprintf(&b, "audio_file=%s\n", filepath.Base(audioFileHint)) + } + if audioFileBackHint != "" { + fmt.Fprintf(&b, "audio_file_back=%s\n", filepath.Base(audioFileBackHint)) + } return b.String() } + +func (p *Processor) audioMetadataFileHints(audioFile string) (string, string) { + if strings.TrimSpace(audioFile) == "" { + return "", "" + } + + wordDir := filepath.Dir(audioFile) + base := filepath.Base(audioFile) + ext := filepath.Ext(base) + name := strings.TrimSuffix(base, ext) + + switch name { + case "audio": + return audioFile, "" + case "audio_front": + backFile := filepath.Join(wordDir, "audio_back"+ext) + if _, err := os.Stat(backFile); err == nil { + return audioFile, backFile + } + return audioFile, "" + case "audio_back": + frontFile := filepath.Join(wordDir, "audio_front"+ext) + return frontFile, audioFile + default: + return audioFile, "" + } +} diff --git a/internal/processor/processor_test.go b/internal/processor/processor_test.go index 8716e79..f658713 100644 --- a/internal/processor/processor_test.go +++ b/internal/processor/processor_test.go @@ -19,8 +19,10 @@ import ( ) type stubImageSearcher struct { - lastPrompt string - searchErr error + lastPrompt string + searchErr error + downloadErr error + promptCallback func(string) } func (s *stubImageSearcher) Search(ctx context.Context, opts *image.SearchOptions) ([]image.SearchResult, error) { @@ -29,6 +31,9 @@ func (s *stubImageSearcher) Search(ctx context.Context, opts *image.SearchOption } s.lastPrompt = "stub nanobanana prompt" + if s.promptCallback != nil { + s.promptCallback(s.lastPrompt) + } return []image.SearchResult{ { ID: "stub-image", @@ -44,6 +49,9 @@ func (s *stubImageSearcher) Search(ctx context.Context, opts *image.SearchOption } func (s *stubImageSearcher) Download(ctx context.Context, url string) (io.ReadCloser, error) { + if s.downloadErr != nil { + return nil, s.downloadErr + } return io.NopCloser(strings.NewReader("mock image data")), nil } @@ -59,6 +67,10 @@ func (s *stubImageSearcher) GetLastPrompt() string { return s.lastPrompt } +func (s *stubImageSearcher) SetPromptCallback(callback func(string)) { + s.promptCallback = callback +} + type fakeAudioProvider struct { generateCalls int texts []string @@ -411,6 +423,26 @@ func TestGenerateAudioBgBgUsesSharedOpenAIVoices(t *testing.T) { if !strings.HasSuffix(fakeProvider.outputFiles[1], "audio_back.mp3") { t.Fatalf("back output file = %q, want audio_back.mp3", fakeProvider.outputFiles[1]) } + + 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", + "audio_file=audio_front.mp3", + "audio_file_back=audio_back.mp3", + } { + if !strings.Contains(metadata, want) { + t.Fatalf("metadata = %q, missing %q", metadata, want) + } + } } func TestGenerateAudioProviderFactoryError(t *testing.T) { @@ -515,6 +547,7 @@ func TestGenerateAudioUsesConfiguredGeminiVoiceAndModel(t *testing.T) { "voice=Kore", "speed=1.00", "format=wav", + "audio_file=audio.wav", } { if !strings.Contains(metadata, want) { t.Fatalf("metadata = %q, missing %q", metadata, want) @@ -733,6 +766,52 @@ func TestDownloadImagesWithTranslationUsesNanoBananaConfigAndSavesPrompt(t *test } } +func TestDownloadImagesWithTranslationPersistsPromptWhenDownloadFails(t *testing.T) { + t.Setenv("OPENAI_API_KEY", "test-openai-key") + t.Setenv("GOOGLE_API_KEY", "test-google-key") + + originalConfig := viper.New() + *originalConfig = *viper.GetViper() + defer func() { + *viper.GetViper() = *originalConfig + }() + viper.Reset() + viper.Set("image.provider", "nanobanana") + + originalConstructor := newNanoBananaImageClient + stubSearcher := &stubImageSearcher{downloadErr: errors.New("download failed")} + newNanoBananaImageClient = func(config *image.NanoBananaConfig) image.ImageSearcher { + return stubSearcher + } + t.Cleanup(func() { + newNanoBananaImageClient = originalConstructor + }) + + flags := cli.NewFlags() + flags.OutputDir = t.TempDir() + flags.ImageAPI = "nanobanana" + flags.ImageAPISpecified = true + + p := NewProcessor(flags) + err := p.downloadImagesWithTranslation("ябълка", "apple") + if err == nil { + t.Fatal("downloadImagesWithTranslation() expected error from failed download") + } + + wordDir := p.findCardDirectory("ябълка") + if wordDir == "" { + t.Fatal("expected word directory to be created") + } + + promptData, err := os.ReadFile(filepath.Join(wordDir, "image_prompt.txt")) + if err != nil { + t.Fatalf("expected prompt file after failed download: %v", err) + } + if got := strings.TrimSpace(string(promptData)); got != "stub nanobanana prompt" { + t.Fatalf("prompt file = %q, want %q", got, "stub nanobanana prompt") + } +} + func TestDownloadImagesWithTranslationUsesConfiguredNanoBananaWhenImageAPINotSpecified(t *testing.T) { t.Setenv("OPENAI_API_KEY", "test-openai-key") t.Setenv("GOOGLE_API_KEY", "test-google-key") -- cgit v1.2.3