summaryrefslogtreecommitdiff
path: root/internal/processor
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-04-02 08:06:13 +0300
committerPaul Buetow <paul@buetow.org>2026-04-02 08:06:13 +0300
commit0349a83743f8b8c8125a35b93b2271e402352b99 (patch)
tree89dc07a29a475511316dc90765ca83a6f65b074d /internal/processor
parentb7302580d840fc3d7e3d127df49d79ef12d79d9c (diff)
Fix processor GUI parity gaps
Diffstat (limited to 'internal/processor')
-rw-r--r--internal/processor/processor.go53
-rw-r--r--internal/processor/processor_test.go83
2 files changed, 132 insertions, 4 deletions
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")