summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-04-02 07:56:31 +0300
committerPaul Buetow <paul@buetow.org>2026-04-02 07:56:31 +0300
commitb7302580d840fc3d7e3d127df49d79ef12d79d9c (patch)
tree458132230aa1337be76701cb7af6f8c26b3e0d55 /internal
parent0f2b16d1ccf461b706dd426727ed188c4ea3b60a (diff)
Align GUI and CLI provider wiring
Diffstat (limited to 'internal')
-rw-r--r--internal/gui/app.go13
-rw-r--r--internal/gui/app_test.go7
-rw-r--r--internal/gui/generator.go16
-rw-r--r--internal/gui/generator_test.go17
-rw-r--r--internal/processor/processor.go42
-rw-r--r--internal/processor/processor_test.go42
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() {