diff options
| author | Paul Buetow <paul@buetow.org> | 2026-04-01 19:32:58 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-04-01 19:32:58 +0300 |
| commit | 008d6308ecb767df7194aa944328c6ead6473ca5 (patch) | |
| tree | ed73bb9601b02a896b1d3993dfc043afaada1854 /internal/gui/generator.go | |
| parent | 27b753192edca7005e6ee20b7be2a0c127294bd8 (diff) | |
z9: wire Nano Banana into GUI
Diffstat (limited to 'internal/gui/generator.go')
| -rw-r--r-- | internal/gui/generator.go | 114 |
1 files changed, 70 insertions, 44 deletions
diff --git a/internal/gui/generator.go b/internal/gui/generator.go index ee96fb1..7964e72 100644 --- a/internal/gui/generator.go +++ b/internal/gui/generator.go @@ -14,6 +14,19 @@ import ( "codeberg.org/snonux/totalrecall/internal/image" ) +type promptAwareImageClient interface { + image.ImageSearcher + SetPromptCallback(func(prompt string)) +} + +var newOpenAIImageClient = func(config *image.OpenAIConfig) promptAwareImageClient { + return image.NewOpenAIClient(config) +} + +var newNanoBananaImageClient = func(config *image.NanoBananaConfig) promptAwareImageClient { + return image.NewNanoBananaClient(config) +} + func randomVoiceAndSpeed(voices []string) (string, float64) { rng := rand.New(rand.NewSource(time.Now().UnixNano())) voice := voices[rng.Intn(len(voices))] @@ -232,27 +245,9 @@ func (a *Application) generateAudioBgBg(ctx context.Context, front, back, cardDi // generateImagesWithPrompt downloads a single image for a word with optional custom prompt and translation func (a *Application) generateImagesWithPrompt(ctx context.Context, word string, customPrompt string, translation string, cardDir string) (string, error) { - // Create image searcher based on provider - var searcher image.ImageSearcher - var err error - - switch a.config.ImageProvider { - case "openai": - openaiConfig := &image.OpenAIConfig{ - APIKey: a.config.OpenAIKey, - Model: "dall-e-2", // DALL-E 2 supports 512x512 - Size: "512x512", // Half of 1024x1024 - Quality: "standard", - Style: "natural", - } - - openaiClient := image.NewOpenAIClient(openaiConfig) - searcher = openaiClient - if openaiConfig.APIKey == "" { - return "", fmt.Errorf("OpenAI API key is required for image generation") - } - default: - return "", fmt.Errorf("unknown image provider: %s", a.config.ImageProvider) + searcher, err := a.newImageSearcher() + if err != nil { + return "", err } // Use the provided card directory @@ -271,29 +266,8 @@ func (a *Application) generateImagesWithPrompt(ctx context.Context, word string, downloader := image.NewDownloader(searcher, downloadOpts) - // Set up callback for OpenAI to update prompt immediately when it's generated - if a.config.ImageProvider == "openai" { - if openaiClient, ok := searcher.(*image.OpenAIClient); ok { - openaiClient.SetPromptCallback(func(prompt string) { - // Save the prompt to disk immediately for this word - promptFile := filepath.Join(cardDir, "image_prompt.txt") - if err := os.WriteFile(promptFile, []byte(prompt), 0644); err != nil { - fmt.Printf("Warning: Failed to save prompt for '%s': %v\n", word, err) - } - - // Only update UI if this word is still the current word - a.mu.Lock() - isCurrentWord := a.currentWord == word - a.mu.Unlock() - - if isCurrentWord { - fyne.Do(func() { - a.imagePromptEntry.SetText(prompt) - }) - } - }) - } - } + // Set up a prompt callback so the GUI and on-disk metadata update as soon as the prompt exists. + searcher.SetPromptCallback(a.imagePromptCallback(cardDir, word)) // Create search options with custom prompt and translation if provided searchOpts := image.DefaultSearchOptions(word) @@ -315,6 +289,58 @@ func (a *Application) generateImagesWithPrompt(ctx context.Context, word string, return path, nil } +func (a *Application) newImageSearcher() (promptAwareImageClient, error) { + switch a.config.ImageProvider { + case imageProviderOpenAI: + if a.config.OpenAIKey == "" { + return nil, fmt.Errorf("OpenAI API key is required for image generation") + } + + openaiConfig := &image.OpenAIConfig{ + APIKey: a.config.OpenAIKey, + Model: "dall-e-2", // DALL-E 2 supports 512x512 + Size: "512x512", // Half of 1024x1024 + Quality: "standard", + Style: "natural", + } + + return newOpenAIImageClient(openaiConfig), nil + case imageProviderNanoBanana: + if a.config.GoogleAPIKey == "" { + return nil, fmt.Errorf("Google API key is required for image generation") + } + + nanoBananaConfig := &image.NanoBananaConfig{ + APIKey: a.config.GoogleAPIKey, + } + + return newNanoBananaImageClient(nanoBananaConfig), nil + default: + return nil, fmt.Errorf("unknown image provider: %s", a.config.ImageProvider) + } +} + +func (a *Application) imagePromptCallback(cardDir, word string) func(prompt string) { + return func(prompt string) { + // Save the prompt to disk immediately for this word. + promptFile := filepath.Join(cardDir, "image_prompt.txt") + if err := os.WriteFile(promptFile, []byte(prompt), 0644); err != nil { + fmt.Printf("Warning: Failed to save prompt for '%s': %v\n", word, err) + } + + // Only update UI if this word is still the current word. + a.mu.Lock() + isCurrentWord := a.currentWord == word + a.mu.Unlock() + + if isCurrentWord && a.imagePromptEntry != nil { + fyne.Do(func() { + a.imagePromptEntry.SetText(prompt) + }) + } + } +} + // saveAudioAttribution saves attribution info for generated audio func (a *Application) saveAudioAttribution(word, audioFile, voice string, speed float64) error { attribution := audio.BuildOpenAIAttribution(audio.AttributionParams{ |
