1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
|
package image
import (
"context"
"io"
)
// SearchResult represents a single image search result
type SearchResult struct {
ID string // Unique identifier
URL string // Direct URL to the image
ThumbnailURL string // URL to thumbnail version
Width int // Image width in pixels
Height int // Image height in pixels
Description string // Image description or tags
Attribution string // Attribution text if required
Source string // Source provider (e.g., "pixabay", "unsplash")
}
// SearchOptions configures the image search
type SearchOptions struct {
Query string // Search query (Bulgarian word)
Translation string // English translation (if already available)
Language string // Language code (default: "bg")
SafeSearch bool // Enable safe search filtering
PerPage int // Number of results per page
Page int // Page number (1-based)
ImageType string // Type: "photo", "illustration", "vector", "all"
Orientation string // Orientation: "horizontal", "vertical", "all"
CustomPrompt string // Custom prompt for AI image generation
AspectRatio string // Override aspect ratio (e.g. "9:16"); empty = provider default
// ReferenceImages holds raw PNG bytes of previously generated images.
// When non-empty, the NanoBanana client sends them as multimodal content
// alongside the text prompt so the model can match character appearance
// across pages (iterative chaining technique).
ReferenceImages [][]byte
}
// DefaultSearchOptions returns sensible defaults for Bulgarian word searches
func DefaultSearchOptions(query string) *SearchOptions {
return &SearchOptions{
Query: query,
Language: "bg",
SafeSearch: true,
PerPage: 10,
Page: 1,
ImageType: "photo",
Orientation: "all",
}
}
// AttributionProvider returns required attribution text for a search result.
// Kept separate from ImageSearcher so callers that only need attribution
// do not depend on Search/Download/Name.
type AttributionProvider interface {
GetAttribution(result *SearchResult) string
}
// ImageSearcher defines the interface for image search providers.
// Providers that also carry attribution text implement AttributionProvider
// in addition to this interface.
type ImageSearcher interface {
// Search performs an image search with the given options.
Search(ctx context.Context, opts *SearchOptions) ([]SearchResult, error)
// Download downloads an image from the given URL.
Download(ctx context.Context, url string) (io.ReadCloser, error)
// Name returns the name of the search provider.
Name() string
}
// ImageClient combines ImageSearcher and AttributionProvider for callers
// that need full provider capabilities (search, download, attribution).
type ImageClient interface {
ImageSearcher
AttributionProvider
}
// Image-generation provider names (AI backends). Use these keys when
// registering GUI/processor image client factories so string literals are not
// scattered across packages.
const (
ImageProviderOpenAI = "openai"
ImageProviderNanoBanana = "nanobanana"
)
// PromptAwareClient extends ImageClient with a callback for receiving the
// generated image prompt before the actual image download begins. Both
// OpenAIClient and NanoBananaClient implement this interface. It is the
// preferred return type for factory functions so callers (processor, gui) can
// register a prompt callback without a type-assertion.
type PromptAwareClient interface {
ImageClient
// SetPromptCallback registers a function that is called with the generated
// prompt text before the image download begins.
SetPromptCallback(func(prompt string))
}
// OpenAIClientFactory is the canonical type for functions that construct an
// OpenAI image client from config. Using a named type avoids duplicating the
// raw function signature in every package that needs to inject or replace the
// factory (processor, gui).
type OpenAIClientFactory func(*OpenAIConfig) PromptAwareClient
// NanoBananaClientFactory is the canonical type for functions that construct a
// NanoBanana (Gemini) image client from config. Using a named type avoids
// duplicating the raw function signature in every package that needs to inject
// or replace the factory (processor, gui).
type NanoBananaClientFactory func(*NanoBananaConfig) PromptAwareClient
// ClientFactories groups the two image-provider construction functions.
// Embedding or holding a ClientFactories value is the single source of truth
// for the image-factory test seams; packages no longer redeclare the same
// fields independently. Audio factory injection is kept separate (audio.ProviderFactory)
// to avoid an import cycle between the image and audio packages.
type ClientFactories struct {
// NewOpenAIClient constructs a PromptAwareClient from an OpenAI config.
// Production code uses the real constructor; tests replace it with a fake.
NewOpenAIClient OpenAIClientFactory
// NewNanoBananaClient constructs a PromptAwareClient from a NanoBanana config.
// Production code uses the real constructor; tests replace it with a fake.
NewNanoBananaClient NanoBananaClientFactory
}
// DefaultClientFactories returns a ClientFactories wired to the real production
// constructors. Callers that need test doubles replace individual fields before
// passing the value to a constructor.
func DefaultClientFactories() ClientFactories {
return ClientFactories{
NewOpenAIClient: func(c *OpenAIConfig) PromptAwareClient {
// NewOpenAIClient returns *OpenAIClient which implements PromptAwareClient.
return NewOpenAIClient(c)
},
NewNanoBananaClient: func(c *NanoBananaConfig) PromptAwareClient {
// NewNanoBananaClient returns *NanoBananaClient which implements PromptAwareClient.
return NewNanoBananaClient(c)
},
}
}
// SearchError represents an error from an image search provider
type SearchError struct {
Provider string
Code string
Message string
}
func (e *SearchError) Error() string {
return e.Provider + ": " + e.Message
}
// RateLimitError indicates that the API rate limit has been exceeded
type RateLimitError struct {
Provider string
RetryAfter int // Seconds to wait before retry
LimitPerHour int
LimitPerDay int
}
func (e *RateLimitError) Error() string {
return e.Provider + ": rate limit exceeded"
}
|