diff options
Diffstat (limited to 'internal/processor')
| -rw-r--r-- | internal/processor/image_downloader.go | 50 | ||||
| -rw-r--r-- | internal/processor/processor.go | 23 | ||||
| -rw-r--r-- | internal/processor/processor_test.go | 8 |
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{} } |
