From bcdda1dcdff6afa42c3bf81b40413c2e6967ddc7 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Wed, 1 Apr 2026 14:14:19 +0300 Subject: zt: route GUI translations through shared translator --- internal/gui/app.go | 54 ++++++++++++++++++++++------- internal/gui/app_test.go | 86 +++++++++++++++++++++++++++++++++++++++++++++++ internal/gui/generator.go | 62 ++++------------------------------ 3 files changed, 134 insertions(+), 68 deletions(-) create mode 100644 internal/gui/app_test.go (limited to 'internal/gui') diff --git a/internal/gui/app.go b/internal/gui/app.go index 1aa60d3..52fc044 100644 --- a/internal/gui/app.go +++ b/internal/gui/app.go @@ -25,6 +25,7 @@ import ( "codeberg.org/snonux/totalrecall/internal/archive" "codeberg.org/snonux/totalrecall/internal/audio" "codeberg.org/snonux/totalrecall/internal/phonetic" + "codeberg.org/snonux/totalrecall/internal/translation" ) // Application represents the main GUI application @@ -87,6 +88,7 @@ type Application struct { config *Config audioConfig *audio.Config phoneticFetcher *phonetic.Fetcher + translator *translation.Translator // Background processing ctx context.Context @@ -105,13 +107,14 @@ type Application struct { // Config holds GUI application configuration type Config struct { - OutputDir string - AudioFormat string - ImageProvider string - OpenAIKey string - GoogleAPIKey string - PhoneticProvider phonetic.Provider - AutoPlay bool // Whether to automatically play audio when generated or navigated to + OutputDir string + AudioFormat string + ImageProvider string + OpenAIKey string + GoogleAPIKey string + TranslationProvider translation.Provider + PhoneticProvider phonetic.Provider + AutoPlay bool // Whether to automatically play audio when generated or navigated to } // DefaultConfig returns default GUI configuration @@ -121,11 +124,12 @@ func DefaultConfig() *Config { outputDir := filepath.Join(homeDir, ".local", "state", "totalrecall", "cards") return &Config{ - OutputDir: outputDir, - AudioFormat: "mp3", - ImageProvider: "openai", - PhoneticProvider: phonetic.ProviderOpenAI, - AutoPlay: true, // Auto-play enabled by default + OutputDir: outputDir, + AudioFormat: "mp3", + ImageProvider: "openai", + TranslationProvider: translation.ProviderGemini, + PhoneticProvider: phonetic.ProviderOpenAI, + AutoPlay: true, // Auto-play enabled by default } } @@ -190,6 +194,7 @@ func New(config *Config) *Application { OpenAIKey: config.OpenAIKey, GoogleAPIKey: config.GoogleAPIKey, }) + app.translator = translation.NewTranslator(translationConfigForApp(config)) app.setupUI() @@ -202,6 +207,31 @@ func New(config *Config) *Application { return app } +// translationConfigForApp normalizes the GUI translation settings. +// When no provider is explicitly configured, Gemini is preferred if a Google +// API key is available; otherwise the GUI falls back to OpenAI so existing +// OpenAI-only setups continue to work. +func translationConfigForApp(config *Config) *translation.Config { + if config == nil { + config = DefaultConfig() + } + + provider := config.TranslationProvider + if provider == "" { + if strings.TrimSpace(config.GoogleAPIKey) != "" { + provider = translation.ProviderGemini + } else { + provider = translation.ProviderOpenAI + } + } + + return &translation.Config{ + Provider: provider, + OpenAIKey: config.OpenAIKey, + GoogleAPIKey: config.GoogleAPIKey, + } +} + // setupUI creates the main user interface func (a *Application) setupUI() { a.window = a.app.NewWindow("TotalRecall") diff --git a/internal/gui/app_test.go b/internal/gui/app_test.go new file mode 100644 index 0000000..c89ebee --- /dev/null +++ b/internal/gui/app_test.go @@ -0,0 +1,86 @@ +package gui + +import ( + "testing" + + "codeberg.org/snonux/totalrecall/internal/translation" +) + +func TestDefaultConfigPrefersGeminiTranslationProvider(t *testing.T) { + config := DefaultConfig() + + if config.TranslationProvider != translation.ProviderGemini { + t.Fatalf("DefaultConfig() translation provider = %q, want %q", config.TranslationProvider, translation.ProviderGemini) + } +} + +func TestTranslationConfigForApp(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + config *Config + wantProv translation.Provider + wantOpen string + wantGoogle string + }{ + { + name: "default to gemini when google key is available", + config: &Config{ + GoogleAPIKey: "google-key", + }, + wantProv: translation.ProviderGemini, + wantOpen: "", + wantGoogle: "google-key", + }, + { + name: "fallback to openai when only openai key is available", + config: &Config{ + OpenAIKey: "openai-key", + }, + wantProv: translation.ProviderOpenAI, + wantOpen: "openai-key", + wantGoogle: "", + }, + { + name: "honor explicit gemini provider", + config: &Config{ + TranslationProvider: translation.ProviderGemini, + GoogleAPIKey: "google-key", + OpenAIKey: "openai-key", + }, + wantProv: translation.ProviderGemini, + wantOpen: "openai-key", + wantGoogle: "google-key", + }, + { + name: "honor explicit openai provider", + config: &Config{ + TranslationProvider: translation.ProviderOpenAI, + OpenAIKey: "openai-key", + GoogleAPIKey: "google-key", + }, + wantProv: translation.ProviderOpenAI, + wantOpen: "openai-key", + wantGoogle: "google-key", + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + got := translationConfigForApp(tt.config) + if got.Provider != tt.wantProv { + t.Fatalf("Provider = %q, want %q", got.Provider, tt.wantProv) + } + if got.OpenAIKey != tt.wantOpen { + t.Fatalf("OpenAIKey = %q, want %q", got.OpenAIKey, tt.wantOpen) + } + if got.GoogleAPIKey != tt.wantGoogle { + t.Fatalf("GoogleAPIKey = %q, want %q", got.GoogleAPIKey, tt.wantGoogle) + } + }) + } +} diff --git a/internal/gui/generator.go b/internal/gui/generator.go index 09ff534..ee96fb1 100644 --- a/internal/gui/generator.go +++ b/internal/gui/generator.go @@ -6,11 +6,9 @@ import ( "math/rand" "os" "path/filepath" - "strings" "time" "fyne.io/fyne/v2" - "github.com/sashabaranov/go-openai" "codeberg.org/snonux/totalrecall/internal/audio" "codeberg.org/snonux/totalrecall/internal/image" @@ -25,68 +23,20 @@ func randomVoiceAndSpeed(voices []string) (string, float64) { // translateWord translates a Bulgarian word to English func (a *Application) translateWord(word string) (string, error) { - if a.config.OpenAIKey == "" { - return "", fmt.Errorf("OpenAI API key not configured") + if a.translator == nil { + return "", fmt.Errorf("translation service not configured") } - client := openai.NewClient(a.config.OpenAIKey) - - req := openai.ChatCompletionRequest{ - Model: openai.GPT4oMini, - Messages: []openai.ChatCompletionMessage{ - { - Role: openai.ChatMessageRoleUser, - Content: fmt.Sprintf("Translate the Bulgarian word '%s' to English. Respond with only the English translation, nothing else.", word), - }, - }, - MaxTokens: 50, - Temperature: 0.3, - } - - resp, err := client.CreateChatCompletion(a.ctx, req) - if err != nil { - return "", fmt.Errorf("OpenAI API error: %w", err) - } - - if len(resp.Choices) == 0 { - return "", fmt.Errorf("no translation returned") - } - - translation := strings.TrimSpace(resp.Choices[0].Message.Content) - return translation, nil + return a.translator.TranslateWord(word) } // translateEnglishToBulgarian translates an English word to Bulgarian func (a *Application) translateEnglishToBulgarian(word string) (string, error) { - if a.config.OpenAIKey == "" { - return "", fmt.Errorf("OpenAI API key not configured") - } - - client := openai.NewClient(a.config.OpenAIKey) - - req := openai.ChatCompletionRequest{ - Model: openai.GPT4oMini, - Messages: []openai.ChatCompletionMessage{ - { - Role: openai.ChatMessageRoleUser, - Content: fmt.Sprintf("Translate the English word '%s' to Bulgarian. Respond with only the Bulgarian translation in Cyrillic script, nothing else.", word), - }, - }, - MaxTokens: 50, - Temperature: 0.3, - } - - resp, err := client.CreateChatCompletion(a.ctx, req) - if err != nil { - return "", fmt.Errorf("OpenAI API error: %w", err) - } - - if len(resp.Choices) == 0 { - return "", fmt.Errorf("no translation returned") + if a.translator == nil { + return "", fmt.Errorf("translation service not configured") } - translation := strings.TrimSpace(resp.Choices[0].Message.Content) - return translation, nil + return a.translator.TranslateEnglishToBulgarian(word) } // generateAudio generates audio for a word -- cgit v1.2.3