summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-04-01 14:06:30 +0300
committerPaul Buetow <paul@buetow.org>2026-04-01 14:06:30 +0300
commita892b4913c233d0ef85dd67e3d1c415e383bd6fd (patch)
tree198aa3d727a9ae937391f2880cc248d9c22f9498
parent9dbf30e9eacccfc23d980c94c3b4d91b5cdb3459 (diff)
zu: switch GUI phonetics to shared fetcher
-rw-r--r--internal/gui/app.go68
-rw-r--r--internal/phonetic/fetcher.go13
-rw-r--r--internal/phonetic/fetcher_test.go56
-rw-r--r--internal/processor/processor.go10
4 files changed, 97 insertions, 50 deletions
diff --git a/internal/gui/app.go b/internal/gui/app.go
index 5d37879..1aa60d3 100644
--- a/internal/gui/app.go
+++ b/internal/gui/app.go
@@ -19,12 +19,12 @@ import (
"fyne.io/fyne/v2/widget"
fynetooltip "github.com/dweymouth/fyne-tooltip"
ttwidget "github.com/dweymouth/fyne-tooltip/widget"
- "github.com/sashabaranov/go-openai"
"codeberg.org/snonux/totalrecall/internal"
"codeberg.org/snonux/totalrecall/internal/anki"
"codeberg.org/snonux/totalrecall/internal/archive"
"codeberg.org/snonux/totalrecall/internal/audio"
+ "codeberg.org/snonux/totalrecall/internal/phonetic"
)
// Application represents the main GUI application
@@ -84,8 +84,9 @@ type Application struct {
autoPlayEnabled bool // Whether to automatically play audio when generated or navigated to
// Configuration
- config *Config
- audioConfig *audio.Config
+ config *Config
+ audioConfig *audio.Config
+ phoneticFetcher *phonetic.Fetcher
// Background processing
ctx context.Context
@@ -104,11 +105,13 @@ type Application struct {
// Config holds GUI application configuration
type Config struct {
- OutputDir string
- AudioFormat string
- ImageProvider string
- OpenAIKey string
- AutoPlay bool // Whether to automatically play audio when generated or navigated to
+ 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
}
// DefaultConfig returns default GUI configuration
@@ -118,10 +121,11 @@ func DefaultConfig() *Config {
outputDir := filepath.Join(homeDir, ".local", "state", "totalrecall", "cards")
return &Config{
- OutputDir: outputDir,
- AudioFormat: "mp3",
- ImageProvider: "openai",
- AutoPlay: true, // Auto-play enabled by default
+ OutputDir: outputDir,
+ AudioFormat: "mp3",
+ ImageProvider: "openai",
+ PhoneticProvider: phonetic.ProviderOpenAI,
+ AutoPlay: true, // Auto-play enabled by default
}
}
@@ -181,6 +185,11 @@ func New(config *Config) *Application {
OpenAISpeed: 0.9,
OpenAIInstruction: "You are speaking Bulgarian language (български език). Pronounce the Bulgarian text with authentic Bulgarian phonetics, not Russian. Speak slowly and clearly for language learners.",
}
+ app.phoneticFetcher = phonetic.NewFetcher(&phonetic.Config{
+ Provider: config.PhoneticProvider,
+ OpenAIKey: config.OpenAIKey,
+ GoogleAPIKey: config.GoogleAPIKey,
+ })
app.setupUI()
@@ -2707,41 +2716,16 @@ func (a *Application) handleWordChange(oldWord, newWord string) {
}
}
-// getPhoneticInfo fetches phonetic information for a Bulgarian word using OpenAI GPT-4o
+// getPhoneticInfo fetches phonetic information for a Bulgarian word using the shared phonetic package.
func (a *Application) getPhoneticInfo(word string) (string, error) {
- if a.config.OpenAIKey == "" {
- return "", fmt.Errorf("openai API key not configured")
- }
-
- client := openai.NewClient(a.config.OpenAIKey)
-
- ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
- defer cancel()
-
- req := openai.ChatCompletionRequest{
- Model: openai.GPT4o,
- Messages: []openai.ChatCompletionMessage{
- {
- Role: openai.ChatMessageRoleSystem,
- Content: "You are a Bulgarian language expert. Provide only the IPA (International Phonetic Alphabet) transcription for Bulgarian words. Return ONLY the IPA transcription in square brackets, nothing else. No explanations, no word labels, just the IPA.",
- },
- {
- Role: openai.ChatMessageRoleUser,
- Content: fmt.Sprintf(`%s`, word),
- },
- },
- Temperature: 0.3,
- MaxTokens: 50,
+ if a.phoneticFetcher == nil {
+ return "", fmt.Errorf("phonetic fetcher not initialized")
}
- resp, err := client.CreateChatCompletion(ctx, req)
+ phoneticInfo, err := a.phoneticFetcher.Fetch(word)
if err != nil {
return "", fmt.Errorf("failed to get phonetic info: %w", err)
}
- if len(resp.Choices) == 0 {
- return "", fmt.Errorf("no response from OpenAI")
- }
-
- return resp.Choices[0].Message.Content, nil
+ return phoneticInfo, nil
}
diff --git a/internal/phonetic/fetcher.go b/internal/phonetic/fetcher.go
index a59d1f4..e6f9694 100644
--- a/internal/phonetic/fetcher.go
+++ b/internal/phonetic/fetcher.go
@@ -131,10 +131,7 @@ func NewFetcher(config *Config) *Fetcher {
// FetchAndSave fetches phonetic information for a word and saves it to the word directory.
func (f *Fetcher) FetchAndSave(word, wordDir string) error {
- ctx, cancel := context.WithTimeout(context.Background(), phoneticTimeout)
- defer cancel()
-
- phoneticInfo, err := f.fetchPhoneticInfo(ctx, word)
+ phoneticInfo, err := f.Fetch(word)
if err != nil {
return err
}
@@ -147,6 +144,14 @@ func (f *Fetcher) FetchAndSave(word, wordDir string) error {
return nil
}
+// Fetch fetches phonetic information for a word.
+func (f *Fetcher) Fetch(word string) (string, error) {
+ ctx, cancel := context.WithTimeout(context.Background(), phoneticTimeout)
+ defer cancel()
+
+ return f.fetchPhoneticInfo(ctx, word)
+}
+
// Provider reports the configured phonetic backend.
func (f *Fetcher) Provider() Provider {
return f.provider
diff --git a/internal/phonetic/fetcher_test.go b/internal/phonetic/fetcher_test.go
index fedc5c2..c577d5d 100644
--- a/internal/phonetic/fetcher_test.go
+++ b/internal/phonetic/fetcher_test.go
@@ -64,6 +64,30 @@ func TestFetchAndSave_UnknownProvider(t *testing.T) {
}
}
+func TestFetch_OpenAIProvider(t *testing.T) {
+ originalFetch := fetchOpenAIPhonetic
+ fetchOpenAIPhonetic = func(context.Context, *openai.Client, string) (string, error) {
+ return "[ˈjɤbɐlkɐ]", nil
+ }
+ t.Cleanup(func() {
+ fetchOpenAIPhonetic = originalFetch
+ })
+
+ fetcher := NewFetcher(&Config{
+ Provider: ProviderOpenAI,
+ OpenAIKey: "test-openai-key",
+ })
+
+ got, err := fetcher.Fetch("ябълка")
+ if err != nil {
+ t.Fatalf("Fetch failed: %v", err)
+ }
+
+ if got != "[ˈjɤbɐlkɐ]" {
+ t.Fatalf("unexpected phonetic content %q", got)
+ }
+}
+
func TestFetchAndSave_OpenAIProvider_WritesFile(t *testing.T) {
originalFetch := fetchOpenAIPhonetic
fetchOpenAIPhonetic = func(context.Context, *openai.Client, string) (string, error) {
@@ -93,6 +117,38 @@ func TestFetchAndSave_OpenAIProvider_WritesFile(t *testing.T) {
}
}
+func TestFetch_GeminiProvider(t *testing.T) {
+ originalFetch := fetchGeminiPhonetic
+ fetchGeminiPhonetic = func(context.Context, *genai.Client, string) (string, error) {
+ return "[ˈkotka]", nil
+ }
+ t.Cleanup(func() {
+ fetchGeminiPhonetic = originalFetch
+ })
+
+ originalNewGeminiClient := newGeminiClient
+ newGeminiClient = func(context.Context, *genai.ClientConfig) (*genai.Client, error) {
+ return &genai.Client{}, nil
+ }
+ t.Cleanup(func() {
+ newGeminiClient = originalNewGeminiClient
+ })
+
+ fetcher := NewFetcher(&Config{
+ Provider: ProviderGemini,
+ GoogleAPIKey: "test-google-key",
+ })
+
+ got, err := fetcher.Fetch("котка")
+ if err != nil {
+ t.Fatalf("Fetch failed: %v", err)
+ }
+
+ if got != "[ˈkotka]" {
+ t.Fatalf("unexpected phonetic content %q", got)
+ }
+}
+
func TestFetchAndSave_GeminiProvider_WritesFile(t *testing.T) {
originalFetch := fetchGeminiPhonetic
fetchGeminiPhonetic = func(context.Context, *genai.Client, string) (string, error) {
diff --git a/internal/processor/processor.go b/internal/processor/processor.go
index 970dafd..5792119 100644
--- a/internal/processor/processor.go
+++ b/internal/processor/processor.go
@@ -543,10 +543,12 @@ 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(),
- 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(),
+ 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