summaryrefslogtreecommitdiff
path: root/internal/processor
diff options
context:
space:
mode:
Diffstat (limited to 'internal/processor')
-rw-r--r--internal/processor/image_downloader.go50
-rw-r--r--internal/processor/processor.go23
-rw-r--r--internal/processor/processor_test.go8
3 files changed, 38 insertions, 43 deletions
diff --git a/internal/processor/image_downloader.go b/internal/processor/image_downloader.go
index d2bb399..74aa58a 100644
--- a/internal/processor/image_downloader.go
+++ b/internal/processor/image_downloader.go
@@ -59,20 +59,13 @@ func (p *Processor) downloadImagesWithTranslation(ctx context.Context, word, tra
return nil
}
-// registerPromptCallback wires a prompt-save callback into searchers that
-// support SetPromptCallback. The callback fires during the Search call so the
-// prompt is captured even if the subsequent download fails.
-func (p *Processor) registerPromptCallback(searcher image.ImageClient, wordDir string) {
- type promptSetter interface {
- SetPromptCallback(func(prompt string))
- }
- promptAware, ok := searcher.(promptSetter)
- if !ok {
- return
- }
-
+// registerPromptCallback wires a prompt-save callback into the searcher. The
+// callback fires during the Search call so the prompt is captured even if the
+// subsequent download fails. All searchers returned by newImageSearcher
+// implement image.PromptAwareClient, so no type-assertion is needed.
+func (p *Processor) registerPromptCallback(searcher image.PromptAwareClient, wordDir string) {
promptFile := filepath.Join(wordDir, "image_prompt.txt")
- promptAware.SetPromptCallback(func(prompt string) {
+ searcher.SetPromptCallback(func(prompt string) {
if prompt == "" {
return
}
@@ -84,8 +77,9 @@ func (p *Processor) registerPromptCallback(searcher image.ImageClient, wordDir s
// saveImagePrompt persists the last prompt used by a searcher that implements
// GetLastPrompt. This acts as a fallback when the prompt is not available via
-// the callback during the search call itself.
-func (p *Processor) saveImagePrompt(wordDir string, searcher image.ImageClient) {
+// the callback during the search call itself. The local promptGetter interface
+// is intentionally narrow: not all PromptAwareClients expose GetLastPrompt.
+func (p *Processor) saveImagePrompt(wordDir string, searcher image.PromptAwareClient) {
type promptGetter interface {
GetLastPrompt() string
}
@@ -106,9 +100,11 @@ func (p *Processor) saveImagePrompt(wordDir string, searcher image.ImageClient)
}
}
-// newImageSearcher creates the appropriate ImageClient based on the configured
-// image provider (openai or nanobanana).
-func (p *Processor) newImageSearcher() (image.ImageClient, error) {
+// newImageSearcher creates the appropriate PromptAwareClient based on the
+// configured image provider (openai or nanobanana). Returning PromptAwareClient
+// instead of ImageClient means callers can call SetPromptCallback directly
+// without a type-assertion.
+func (p *Processor) newImageSearcher() (image.PromptAwareClient, error) {
switch p.imageProviderForRunMode() {
case "openai":
return p.newOpenAIImageSearcher()
@@ -131,10 +127,10 @@ func (p *Processor) imageProviderForRunMode() string {
return strings.ToLower(strings.TrimSpace(p.flags.ImageAPI))
}
-// newOpenAIImageSearcher builds an OpenAI ImageClient from CLI flags and the
-// resolved processor Config. Config-file overrides are applied only when the
-// flag still holds its default value so explicit CLI flags always win.
-func (p *Processor) newOpenAIImageSearcher() (image.ImageClient, error) {
+// newOpenAIImageSearcher builds an OpenAI PromptAwareClient from CLI flags and
+// the resolved processor Config. Config-file overrides are applied only when
+// the flag still holds its default value so explicit CLI flags always win.
+func (p *Processor) newOpenAIImageSearcher() (image.PromptAwareClient, error) {
openaiConfig := &image.OpenAIConfig{
APIKey: cli.GetOpenAIKey(),
Model: p.flags.OpenAIImageModel,
@@ -161,13 +157,13 @@ func (p *Processor) newOpenAIImageSearcher() (image.ImageClient, error) {
return nil, fmt.Errorf("OpenAI API key is required for image generation")
}
- return p.newOpenAIImageClient(openaiConfig), nil
+ return p.imageFactories.NewOpenAIClient(openaiConfig), nil
}
-// newNanoBananaImageSearcher builds a NanoBanana ImageClient from CLI flags
-// and the resolved processor Config, applying overrides in the same
+// newNanoBananaImageSearcher builds a NanoBanana PromptAwareClient from CLI
+// flags and the resolved processor Config, applying overrides in the same
// flag-wins-over-config pattern.
-func (p *Processor) newNanoBananaImageSearcher() (image.ImageClient, error) {
+func (p *Processor) newNanoBananaImageSearcher() (image.PromptAwareClient, error) {
nanoBananaConfig := &image.NanoBananaConfig{
APIKey: cli.GetGoogleAPIKey(),
Model: p.flags.NanoBananaModel,
@@ -185,5 +181,5 @@ func (p *Processor) newNanoBananaImageSearcher() (image.ImageClient, error) {
return nil, fmt.Errorf("google API key is required for image generation")
}
- return p.newNanoBananaImageClient(nanoBananaConfig), nil
+ return p.imageFactories.NewNanoBananaClient(nanoBananaConfig), nil
}
diff --git a/internal/processor/processor.go b/internal/processor/processor.go
index 7d83149..14e5341 100644
--- a/internal/processor/processor.go
+++ b/internal/processor/processor.go
@@ -64,8 +64,9 @@ type Config struct {
// Processor handles the main word processing logic.
// Audio coordination is in audio_coordinator.go, card directory management is
// in card_store.go, and image downloading is in image_downloader.go.
-// The factory fields (newOpenAIImageClient, newNanoBananaImageClient, newAudioProvider)
-// are injected at construction time so tests can swap them without mutating global state.
+// Factory functions for image and audio providers are grouped in image.ClientFactories
+// and the audio.ProviderFactory type so the signatures are defined once and
+// shared with the gui package — eliminating parallel field duplication.
type Processor struct {
flags *cli.Flags
translator *translation.Translator
@@ -76,10 +77,13 @@ type Processor struct {
// so individual methods never call Viper directly.
cfg *Config
- // Factories — replaced by tests to inject fakes.
- newOpenAIImageClient func(*image.OpenAIConfig) image.ImageClient
- newNanoBananaImageClient func(*image.NanoBananaConfig) image.ImageClient
- newAudioProvider func(*audio.Config) (audio.Provider, error)
+ // imageFactories groups the two image-provider construction functions.
+ // Production code uses image.DefaultClientFactories(); tests replace fields.
+ imageFactories image.ClientFactories
+
+ // newAudioProvider constructs an audio.Provider from a Config.
+ // Production code uses audio.NewProvider; tests replace it with a fake.
+ newAudioProvider audio.ProviderFactory
}
// NewProcessor creates a new word processor with default production factories.
@@ -99,12 +103,7 @@ func NewProcessor(flags *cli.Flags, cfg *Config) *Processor {
translationCache: translation.NewTranslationCache(),
phoneticFetcher: phonetic.NewFetcher(&phonetic.Config{Provider: phoneticProvider, OpenAIKey: openAIKey, GoogleAPIKey: googleAPIKey}),
randomIntn: rand.Intn,
- newOpenAIImageClient: func(config *image.OpenAIConfig) image.ImageClient {
- return image.NewOpenAIClient(config)
- },
- newNanoBananaImageClient: func(config *image.NanoBananaConfig) image.ImageClient {
- return image.NewNanoBananaClient(config)
- },
+ imageFactories: image.DefaultClientFactories(),
newAudioProvider: audio.NewProvider,
}
}
diff --git a/internal/processor/processor_test.go b/internal/processor/processor_test.go
index 17b56e9..947f9d3 100644
--- a/internal/processor/processor_test.go
+++ b/internal/processor/processor_test.go
@@ -1058,7 +1058,7 @@ func TestDownloadImagesWithTranslationUsesNanoBananaConfigAndSavesPrompt(t *test
ImageNanoBananaTextModelSet: true,
}
p := NewProcessor(flags, cfg)
- p.newNanoBananaImageClient = func(config *image.NanoBananaConfig) image.ImageClient {
+ p.imageFactories.NewNanoBananaClient = func(config *image.NanoBananaConfig) image.PromptAwareClient {
*capturedConfig = *config
return stubSearcher
}
@@ -1102,7 +1102,7 @@ func TestDownloadImagesWithTranslationPersistsPromptWhenDownloadFails(t *testing
flags.ImageAPISpecified = true
p := NewProcessor(flags, &Config{ImageProvider: "nanobanana"})
- p.newNanoBananaImageClient = func(config *image.NanoBananaConfig) image.ImageClient {
+ p.imageFactories.NewNanoBananaClient = func(config *image.NanoBananaConfig) image.PromptAwareClient {
return stubSearcher
}
err := p.downloadImagesWithTranslation(context.Background(), "ябълка", "apple")
@@ -1144,7 +1144,7 @@ func TestDownloadImagesWithTranslationUsesConfiguredNanoBananaWhenImageAPINotSpe
ImageNanoBananaTextModelSet: true,
}
p := NewProcessor(flags, cfg)
- p.newNanoBananaImageClient = func(config *image.NanoBananaConfig) image.ImageClient {
+ p.imageFactories.NewNanoBananaClient = func(config *image.NanoBananaConfig) image.PromptAwareClient {
*capturedConfig = *config
return stubSearcher
}
@@ -1231,7 +1231,7 @@ func TestNewNanoBananaImageSearcherExplicitDefaultWinsOverConfig(t *testing.T) {
ImageNanoBananaTextModelSet: true,
}
p := NewProcessor(flags, cfg)
- p.newNanoBananaImageClient = func(config *image.NanoBananaConfig) image.ImageClient {
+ p.imageFactories.NewNanoBananaClient = func(config *image.NanoBananaConfig) image.PromptAwareClient {
*capturedConfig = *config
return &stubImageSearcher{}
}