summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-04-01 14:14:19 +0300
committerPaul Buetow <paul@buetow.org>2026-04-01 14:14:19 +0300
commitbcdda1dcdff6afa42c3bf81b40413c2e6967ddc7 (patch)
tree420c3dde87aacc6278d34cb194ba16f36f26e337
parenta892b4913c233d0ef85dd67e3d1c415e383bd6fd (diff)
zt: route GUI translations through shared translator
-rw-r--r--internal/gui/app.go54
-rw-r--r--internal/gui/app_test.go86
-rw-r--r--internal/gui/generator.go62
-rw-r--r--internal/processor/processor.go13
4 files changed, 141 insertions, 74 deletions
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
diff --git a/internal/processor/processor.go b/internal/processor/processor.go
index 5792119..dd49c2b 100644
--- a/internal/processor/processor.go
+++ b/internal/processor/processor.go
@@ -543,12 +543,13 @@ func (p *Processor) GenerateAnkiFile() (string, error) {
func (p *Processor) RunGUIMode() error {
// Create GUI configuration from command line flags and viper config
guiConfig := &gui.Config{
- AudioFormat: p.flags.AudioFormat,
- ImageProvider: p.flags.ImageAPI,
- OpenAIKey: cli.GetOpenAIKey(),
- GoogleAPIKey: cli.GetGoogleAPIKey(),
- PhoneticProvider: phonetic.Provider(viper.GetString("phonetic.provider")),
- AutoPlay: !p.flags.NoAutoPlay, // Invert the flag (--no-auto-play disables auto-play)
+ AudioFormat: p.flags.AudioFormat,
+ ImageProvider: p.flags.ImageAPI,
+ OpenAIKey: cli.GetOpenAIKey(),
+ GoogleAPIKey: cli.GetGoogleAPIKey(),
+ TranslationProvider: translation.Provider(viper.GetString("translation.provider")),
+ PhoneticProvider: phonetic.Provider(viper.GetString("phonetic.provider")),
+ AutoPlay: !p.flags.NoAutoPlay, // Invert the flag (--no-auto-play disables auto-play)
}
// Only set OutputDir if it was explicitly provided via flag