diff options
| author | Paul Buetow <paul@buetow.org> | 2026-04-02 07:56:31 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-04-02 07:56:31 +0300 |
| commit | b7302580d840fc3d7e3d127df49d79ef12d79d9c (patch) | |
| tree | 458132230aa1337be76701cb7af6f8c26b3e0d55 | |
| parent | 0f2b16d1ccf461b706dd426727ed188c4ea3b60a (diff) | |
Align GUI and CLI provider wiring
| -rw-r--r-- | internal/gui/app.go | 13 | ||||
| -rw-r--r-- | internal/gui/app_test.go | 7 | ||||
| -rw-r--r-- | internal/gui/generator.go | 16 | ||||
| -rw-r--r-- | internal/gui/generator_test.go | 17 | ||||
| -rw-r--r-- | internal/processor/processor.go | 42 | ||||
| -rw-r--r-- | internal/processor/processor_test.go | 42 |
6 files changed, 131 insertions, 6 deletions
diff --git a/internal/gui/app.go b/internal/gui/app.go index 7c9ecd7..a69b178 100644 --- a/internal/gui/app.go +++ b/internal/gui/app.go @@ -24,6 +24,7 @@ import ( "codeberg.org/snonux/totalrecall/internal/anki" "codeberg.org/snonux/totalrecall/internal/archive" "codeberg.org/snonux/totalrecall/internal/audio" + "codeberg.org/snonux/totalrecall/internal/image" "codeberg.org/snonux/totalrecall/internal/phonetic" "codeberg.org/snonux/totalrecall/internal/translation" ) @@ -114,6 +115,10 @@ type Config struct { ImageProvider string OpenAIKey string GoogleAPIKey string + // NanoBananaModel selects the Gemini image model for Nano Banana generation. + NanoBananaModel string + // NanoBananaTextModel selects the Gemini text model for Nano Banana prompt generation. + NanoBananaTextModel string // GeminiTTSModel selects the Gemini TTS model when Gemini audio is active. GeminiTTSModel string // GeminiVoice selects a specific Gemini voice; empty uses the model default. @@ -139,6 +144,8 @@ func DefaultConfig() *Config { OutputDir: outputDir, AudioFormat: audioDefaults.OutputFormat, AudioProvider: audioDefaults.Provider, + NanoBananaModel: image.DefaultNanoBananaModel, + NanoBananaTextModel: image.DefaultNanoBananaTextModel, GeminiTTSModel: audioDefaults.GeminiTTSModel, ImageProvider: imageProviderNanoBanana, TranslationProvider: translation.ProviderOpenAI, @@ -170,6 +177,12 @@ func New(config *Config) *Application { if config.ImageProvider == "" { config.ImageProvider = defaults.ImageProvider } + if config.NanoBananaModel == "" { + config.NanoBananaModel = defaults.NanoBananaModel + } + if config.NanoBananaTextModel == "" { + config.NanoBananaTextModel = defaults.NanoBananaTextModel + } if config.GeminiTTSModel == "" { config.GeminiTTSModel = defaults.GeminiTTSModel } diff --git a/internal/gui/app_test.go b/internal/gui/app_test.go index 06525b2..057c5e0 100644 --- a/internal/gui/app_test.go +++ b/internal/gui/app_test.go @@ -4,6 +4,7 @@ import ( "testing" "codeberg.org/snonux/totalrecall/internal/audio" + "codeberg.org/snonux/totalrecall/internal/image" "codeberg.org/snonux/totalrecall/internal/translation" ) @@ -23,6 +24,12 @@ func TestDefaultConfigUsesOpenAITranslationProvider(t *testing.T) { if config.AudioFormat != audioDefaults.OutputFormat { t.Fatalf("DefaultConfig() audio format = %q, want %q", config.AudioFormat, audioDefaults.OutputFormat) } + if config.NanoBananaModel != image.DefaultNanoBananaModel { + t.Fatalf("DefaultConfig() NanoBananaModel = %q, want %q", config.NanoBananaModel, image.DefaultNanoBananaModel) + } + if config.NanoBananaTextModel != image.DefaultNanoBananaTextModel { + t.Fatalf("DefaultConfig() NanoBananaTextModel = %q, want %q", config.NanoBananaTextModel, image.DefaultNanoBananaTextModel) + } if config.GeminiTTSModel != audioDefaults.GeminiTTSModel { t.Fatalf("DefaultConfig() GeminiTTSModel = %q, want %q", config.GeminiTTSModel, audioDefaults.GeminiTTSModel) } diff --git a/internal/gui/generator.go b/internal/gui/generator.go index 704020d..7143cff 100644 --- a/internal/gui/generator.go +++ b/internal/gui/generator.go @@ -378,12 +378,18 @@ func (a *Application) newImageSearcher() (promptAwareImageClient, error) { return newOpenAIImageClient(openaiConfig), nil case imageProviderNanoBanana: - if a.config.GoogleAPIKey == "" { + config := a.config + if config == nil { + config = DefaultConfig() + } + if config.GoogleAPIKey == "" { return nil, fmt.Errorf("Google API key is required for image generation") } nanoBananaConfig := &image.NanoBananaConfig{ - APIKey: a.config.GoogleAPIKey, + APIKey: config.GoogleAPIKey, + Model: config.NanoBananaModel, + TextModel: config.NanoBananaTextModel, } return newNanoBananaImageClient(nanoBananaConfig), nil @@ -487,7 +493,11 @@ func (a *Application) saveAudioMetadata(cardDir string, audioConfig audio.Config } fmt.Fprintf(&metadata, "model=%s\n", model) } - fmt.Fprintf(&metadata, "voice=%s\n", voice) + voiceLine := strings.TrimSpace(voice) + if voiceLine == "" && strings.ToLower(strings.TrimSpace(audioConfig.Provider)) == "gemini" { + voiceLine = "model-default" + } + fmt.Fprintf(&metadata, "voice=%s\n", voiceLine) fmt.Fprintf(&metadata, "speed=%.2f\n", speed) fmt.Fprintf(&metadata, "format=%s\n", audioConfig.OutputFormat) fmt.Fprintf(&metadata, "cardtype=%s\n", cardType) diff --git a/internal/gui/generator_test.go b/internal/gui/generator_test.go index f9cb2a0..31b4aa2 100644 --- a/internal/gui/generator_test.go +++ b/internal/gui/generator_test.go @@ -107,9 +107,11 @@ func TestGenerateImagesWithPromptUsesNanoBananaProvider(t *testing.T) { tempDir := t.TempDir() app := &Application{ config: &Config{ - ImageProvider: imageProviderNanoBanana, - GoogleAPIKey: "google-key", - OutputDir: tempDir, + ImageProvider: imageProviderNanoBanana, + GoogleAPIKey: "google-key", + NanoBananaModel: "custom-image-model", + NanoBananaTextModel: "custom-text-model", + OutputDir: tempDir, }, currentWord: "друго", } @@ -125,6 +127,12 @@ func TestGenerateImagesWithPromptUsesNanoBananaProvider(t *testing.T) { if capturedConfig.APIKey != "google-key" { t.Fatalf("Nano Banana API key = %q, want %q", capturedConfig.APIKey, "google-key") } + if capturedConfig.Model != "custom-image-model" { + t.Fatalf("Nano Banana model = %q, want %q", capturedConfig.Model, "custom-image-model") + } + if capturedConfig.TextModel != "custom-text-model" { + t.Fatalf("Nano Banana text model = %q, want %q", capturedConfig.TextModel, "custom-text-model") + } if fakeClient.searchOpts == nil { t.Fatal("expected search options to be captured") } @@ -319,6 +327,9 @@ func TestGenerateAudioUsesGeminiModelDefaultVoiceAndAttribution(t *testing.T) { if !strings.Contains(metadata, "audio_file=audio.wav") { t.Fatalf("gemini metadata missing fresh audio file reference: %q", metadata) } + if !strings.Contains(metadata, "voice=model-default") { + t.Fatalf("gemini metadata missing model-default voice marker: %q", metadata) + } if !strings.Contains(metadata, "format=wav") { t.Fatalf("gemini metadata missing format: %q", metadata) } diff --git a/internal/processor/processor.go b/internal/processor/processor.go index 7d18aab..3ecbce8 100644 --- a/internal/processor/processor.go +++ b/internal/processor/processor.go @@ -634,6 +634,8 @@ func (p *Processor) guiConfigForRunMode() *gui.Config { ImageProvider: imageProvider, OpenAIKey: cli.GetOpenAIKey(), GoogleAPIKey: cli.GetGoogleAPIKey(), + NanoBananaModel: p.nanoBananaModelForRunMode(), + NanoBananaTextModel: p.nanoBananaTextModelForRunMode(), GeminiTTSModel: p.geminiTTSModel(), GeminiVoice: p.geminiVoice(), TranslationProvider: translation.Provider(viper.GetString("translation.provider")), @@ -642,6 +644,46 @@ func (p *Processor) guiConfigForRunMode() *gui.Config { } } +func (p *Processor) nanoBananaModelForRunMode() string { + if p != nil && p.flags != nil && p.flags.NanoBananaModelSpecified { + if model := strings.TrimSpace(p.flags.NanoBananaModel); model != "" { + return model + } + } + + if model := strings.TrimSpace(viper.GetString("image.nanobanana_model")); model != "" { + return model + } + + if p != nil && p.flags != nil { + if model := strings.TrimSpace(p.flags.NanoBananaModel); model != "" { + return model + } + } + + return image.DefaultNanoBananaModel +} + +func (p *Processor) nanoBananaTextModelForRunMode() string { + if p != nil && p.flags != nil && p.flags.NanoBananaTextModelSpecified { + if model := strings.TrimSpace(p.flags.NanoBananaTextModel); model != "" { + return model + } + } + + if model := strings.TrimSpace(viper.GetString("image.nanobanana_text_model")); model != "" { + return model + } + + if p != nil && p.flags != nil { + if model := strings.TrimSpace(p.flags.NanoBananaTextModel); model != "" { + return model + } + } + + return image.DefaultNanoBananaTextModel +} + func (p *Processor) newImageSearcher() (image.ImageSearcher, error) { provider := p.imageProviderForRunMode() diff --git a/internal/processor/processor_test.go b/internal/processor/processor_test.go index 2a4b331..8716e79 100644 --- a/internal/processor/processor_test.go +++ b/internal/processor/processor_test.go @@ -215,6 +215,8 @@ func TestGUIConfigForRunModeUsesNanoBananaDefaultWhenImageAPIIsNotSpecified(t *t *viper.GetViper() = *originalConfig }() viper.Reset() + viper.Set("image.nanobanana_model", "config-image-model") + viper.Set("image.nanobanana_text_model", "config-text-model") flags := cli.NewFlags() flags.AudioFormat = "wav" @@ -232,6 +234,12 @@ func TestGUIConfigForRunModeUsesNanoBananaDefaultWhenImageAPIIsNotSpecified(t *t if guiConfig.AudioFormat != "wav" { t.Fatalf("guiConfig.AudioFormat = %q, want %q", guiConfig.AudioFormat, "wav") } + if guiConfig.NanoBananaModel != "config-image-model" { + t.Fatalf("guiConfig.NanoBananaModel = %q, want %q", guiConfig.NanoBananaModel, "config-image-model") + } + if guiConfig.NanoBananaTextModel != "config-text-model" { + t.Fatalf("guiConfig.NanoBananaTextModel = %q, want %q", guiConfig.NanoBananaTextModel, "config-text-model") + } if guiConfig.GeminiTTSModel != "gemini-2.5-flash-preview-tts" { t.Fatalf("guiConfig.GeminiTTSModel = %q, want %q", guiConfig.GeminiTTSModel, "gemini-2.5-flash-preview-tts") } @@ -268,6 +276,40 @@ func TestGUIConfigForRunModeHonorsExplicitImageAPI(t *testing.T) { } } +func TestGUIConfigForRunModeHonorsExplicitNanoBananaModelFlags(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.nanobanana_model", "config-image-model") + viper.Set("image.nanobanana_text_model", "config-text-model") + + flags := cli.NewFlags() + flags.ImageAPI = "openai" + flags.ImageAPISpecified = false + flags.NanoBananaModel = "flag-image-model" + flags.NanoBananaModelSpecified = true + flags.NanoBananaTextModel = "flag-text-model" + flags.NanoBananaTextModelSpecified = true + p := NewProcessor(flags) + + guiConfig := p.guiConfigForRunMode() + if guiConfig.NanoBananaModel != "flag-image-model" { + t.Fatalf("guiConfig.NanoBananaModel = %q, want %q", guiConfig.NanoBananaModel, "flag-image-model") + } + if guiConfig.NanoBananaTextModel != "flag-text-model" { + t.Fatalf("guiConfig.NanoBananaTextModel = %q, want %q", guiConfig.NanoBananaTextModel, "flag-text-model") + } + if guiConfig.GoogleAPIKey != "test-google-key" { + t.Fatalf("guiConfig.GoogleAPIKey = %q, want %q", guiConfig.GoogleAPIKey, "test-google-key") + } +} + func TestGenerateAudioUsesSharedOpenAIVoices(t *testing.T) { originalFactory := newAudioProvider t.Cleanup(func() { |
