diff options
| author | Paul Buetow <paul@buetow.org> | 2025-07-16 21:27:08 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2025-07-16 21:27:08 +0300 |
| commit | 6b0b4c9ce28b88ab0abbff03c5f367d89ba97c89 (patch) | |
| tree | fc102068a7e41c8042bdf2d17674405e715da8be /internal/image | |
| parent | d46669426aa6b0ece71d0d05d0b6f2966686b17a (diff) | |
fix: update button labels to show lowercase hotkeys and improve export location
- Change all button labels to show lowercase letters (g, n, i, a, r, d, p)
- Update delete confirmation dialog to show lowercase y/n
- Set default export location to anki_cards directory
- Add note about CSV needing to be in same directory as media files
- Fix prompt generation to remove Bulgarian word reference
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
Diffstat (limited to 'internal/image')
| -rw-r--r-- | internal/image/openai.go | 104 |
1 files changed, 51 insertions, 53 deletions
diff --git a/internal/image/openai.go b/internal/image/openai.go index add1c96..2fbb043 100644 --- a/internal/image/openai.go +++ b/internal/image/openai.go @@ -12,7 +12,7 @@ import ( "path/filepath" "strings" "time" - + "github.com/sashabaranov/go-openai" ) @@ -46,9 +46,9 @@ func NewOpenAIClient(config *OpenAIConfig) *OpenAIClient { // Return nil client that will fail on operations return &OpenAIClient{} } - + client := openai.NewClient(config.APIKey) - + // Set defaults if config.Model == "" { config.Model = "dall-e-3" @@ -65,7 +65,7 @@ func NewOpenAIClient(config *OpenAIConfig) *OpenAIClient { if config.CacheDir == "" { config.CacheDir = "./.image_cache" } - + oc := &OpenAIClient{ client: client, apiKey: config.APIKey, @@ -76,12 +76,12 @@ func NewOpenAIClient(config *OpenAIConfig) *OpenAIClient { cacheDir: config.CacheDir, enableCache: config.EnableCache, } - + // Create cache directory if caching is enabled if oc.enableCache && oc.cacheDir != "" { os.MkdirAll(oc.cacheDir, 0755) } - + return oc } @@ -94,7 +94,7 @@ func (c *OpenAIClient) Search(ctx context.Context, opts *SearchOptions) ([]Searc Message: "OpenAI API key not configured", } } - + // Check cache first if c.enableCache { cacheFile := c.getCacheFilePath(opts.Query) @@ -114,7 +114,7 @@ func (c *OpenAIClient) Search(ctx context.Context, opts *SearchOptions) ([]Searc return []SearchResult{result}, nil } } - + // Translate Bulgarian word to English for better results translatedWord, err := c.translateBulgarianToEnglish(ctx, opts.Query) if err != nil { @@ -122,7 +122,7 @@ func (c *OpenAIClient) Search(ctx context.Context, opts *SearchOptions) ([]Searc fmt.Printf("Translation failed: %v, using original word\n", err) translatedWord = opts.Query } - + // Create prompt - use custom if provided, otherwise generate educational prompt var prompt string if opts.CustomPrompt != "" { @@ -131,14 +131,14 @@ func (c *OpenAIClient) Search(ctx context.Context, opts *SearchOptions) ([]Searc } else { prompt = c.createEducationalPrompt(opts.Query, translatedWord) } - + // Store the prompt for attribution c.lastPrompt = prompt - + // Log the prompt to stdout for debugging fmt.Printf("OpenAI Image Generation Prompt: %s\n", prompt) fmt.Printf("OpenAI Image Generation: Using model '%s' with size '%s'\n", c.model, c.size) - + // Create the image generation request req := openai.ImageRequest{ Prompt: prompt, @@ -147,13 +147,13 @@ func (c *OpenAIClient) Search(ctx context.Context, opts *SearchOptions) ([]Searc ResponseFormat: openai.CreateImageResponseFormatURL, N: 1, } - + // Add model-specific parameters if c.model == "dall-e-3" { req.Quality = c.quality req.Style = c.style } - + // Generate the image resp, err := c.client.CreateImage(ctx, req) if err != nil { @@ -163,7 +163,7 @@ func (c *OpenAIClient) Search(ctx context.Context, opts *SearchOptions) ([]Searc Message: fmt.Sprintf("Failed to generate image: %v", err), } } - + if len(resp.Data) == 0 { return nil, &SearchError{ Provider: "openai", @@ -171,10 +171,10 @@ func (c *OpenAIClient) Search(ctx context.Context, opts *SearchOptions) ([]Searc Message: "No image generated", } } - + // Get the generated image URL imageURL := resp.Data[0].URL - + // Download and cache the image if caching is enabled if c.enableCache { cacheFile := c.getCacheFilePath(opts.Query) @@ -184,7 +184,7 @@ func (c *OpenAIClient) Search(ctx context.Context, opts *SearchOptions) ([]Searc } // Continue even if caching fails } - + // Create result result := SearchResult{ ID: c.generateImageID(opts.Query), @@ -196,7 +196,7 @@ func (c *OpenAIClient) Search(ctx context.Context, opts *SearchOptions) ([]Searc Attribution: "Generated by OpenAI DALL-E", Source: "openai", } - + return []SearchResult{result}, nil } @@ -210,23 +210,23 @@ func (c *OpenAIClient) Download(ctx context.Context, url string) (io.ReadCloser, } return file, nil } - + // Otherwise download from URL req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil, err } - + resp, err := http.DefaultClient.Do(req) if err != nil { return nil, err } - + if resp.StatusCode != http.StatusOK { resp.Body.Close() return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, resp.Status) } - + return resp.Body, nil } @@ -261,16 +261,15 @@ func (c *OpenAIClient) createEducationalPrompt(bulgarianWord, englishTranslation if creativeStyle := c.getCreativeStyleFromOpenAI(context.Background(), englishTranslation); creativeStyle != "" { fmt.Printf(" Using OpenAI-suggested style: %s\n", creativeStyle) return fmt.Sprintf( - "Generate a %s of: %s. "+ - "This is for the Bulgarian word '%s' which means %s. "+ + "Generate %s of: %s. "+ "The image should be educational and suitable for language learning flashcards. "+ "Requirements: single main subject, plain background, clear and recognizable. "+ "IMPORTANT: No text whatsoever. Do not include any words, letters, typography, labels, captions, or writing of any kind. Image only, without any text elements.", - creativeStyle, englishTranslation, bulgarianWord, englishTranslation, + creativeStyle, englishTranslation, ) } } - + // Define different art styles for variety (42 styles total) styles := []string{ // Original styles (1-10) @@ -284,12 +283,12 @@ func (c *OpenAIClient) createEducationalPrompt(bulgarianWord, englishTranslation "oil painting, classical art style", "paper cut art, layered craft style", "isometric illustration, technical drawing style", - + // Requested styles (11-13) "superhero comic book style, dynamic action pose, bold colors, Marvel/DC aesthetic", "super-realistic person practicing yoga, serene wellness photography", "cute illustration with cats interacting with the subject, whimsical cat-themed", - + // Additional artistic styles (14-25) "impressionist painting style, Monet-inspired brushstrokes", "art nouveau style, decorative organic forms, Mucha-inspired", @@ -303,7 +302,7 @@ func (c *OpenAIClient) createEducationalPrompt(bulgarianWord, englishTranslation "mosaic tile art style, Byzantine-inspired patterns", "art deco style, geometric patterns, 1920s aesthetic", "surrealist style, Salvador Dali inspired dreamlike quality", - + // Photography styles (26-32) "macro photography style, extreme close-up detail", "vintage polaroid photograph, retro instant camera aesthetic", @@ -312,7 +311,7 @@ func (c *OpenAIClient) createEducationalPrompt(bulgarianWord, englishTranslation "underwater photography style, ethereal aquatic atmosphere", "aerial drone photography, bird's eye view perspective", "long exposure photography, motion blur effects", - + // Modern digital styles (33-42) "vaporwave aesthetic, 80s-90s retro digital art", "low poly 3D art style, geometric simplified forms", @@ -325,19 +324,18 @@ func (c *OpenAIClient) createEducationalPrompt(bulgarianWord, englishTranslation "origami paper folding art style, geometric paper craft", "chalk art style, sidewalk drawing aesthetic", } - + // Select a random style selectedStyle := styles[rand.Intn(len(styles))] fmt.Printf(" Using image style: %s\n", selectedStyle) - + // Create a simple, clear prompt for educational images return fmt.Sprintf( "Generate a %s of: %s. "+ - "This is for the Bulgarian word '%s' which means %s. "+ "The image should be educational and suitable for language learning flashcards. "+ "Requirements: single main subject, plain background, clear and recognizable. "+ "IMPORTANT: No text whatsoever. Do not include any words, letters, typography, labels, captions, or writing of any kind. Image only, without any text elements.", - selectedStyle, englishTranslation, bulgarianWord, englishTranslation, + selectedStyle, englishTranslation, ) } @@ -345,7 +343,7 @@ func (c *OpenAIClient) createEducationalPrompt(bulgarianWord, englishTranslation func (c *OpenAIClient) translateBulgarianToEnglish(ctx context.Context, word string) (string, error) { // Use OpenAI chat completion to translate fmt.Printf("OpenAI Translation: Using model 'gpt-4o-mini' to translate '%s'\n", word) - + req := openai.ChatCompletionRequest{ Model: openai.GPT4oMini, Messages: []openai.ChatCompletionMessage{ @@ -357,19 +355,19 @@ func (c *OpenAIClient) translateBulgarianToEnglish(ctx context.Context, word str Temperature: 0.3, // Lower temperature for more consistent translations MaxTokens: 50, } - + resp, err := c.client.CreateChatCompletion(ctx, req) if err != nil { return "", fmt.Errorf("translation failed: %w", err) } - + if len(resp.Choices) == 0 || resp.Choices[0].Message.Content == "" { return "", fmt.Errorf("no translation received") } - + translation := strings.TrimSpace(resp.Choices[0].Message.Content) fmt.Printf("Translated '%s' to '%s'\n", word, translation) - + return translation, nil } @@ -383,11 +381,11 @@ func (c *OpenAIClient) getCacheFilePath(word string) string { h.Write([]byte(c.quality)) h.Write([]byte(c.style)) hash := hex.EncodeToString(h.Sum(nil)) - + // Use first 2 chars as subdirectory for better file system performance subdir := hash[:2] filename := hash[2:] + ".png" - + return filepath.Join(c.cacheDir, subdir, filename) } @@ -398,21 +396,21 @@ func (c *OpenAIClient) downloadAndCache(ctx context.Context, url, cacheFile stri if err := os.MkdirAll(dir, 0755); err != nil { return err } - + // Download the image resp, err := c.Download(ctx, url) if err != nil { return err } defer resp.Close() - + // Create the cache file out, err := os.Create(cacheFile) if err != nil { return err } defer out.Close() - + // Copy the data _, err = io.Copy(out, resp) return err @@ -466,37 +464,37 @@ func (c *OpenAIClient) getSizeHeight() int { // getCreativeStyleFromOpenAI asks OpenAI for a creative photo style suggestion func (c *OpenAIClient) getCreativeStyleFromOpenAI(ctx context.Context, subject string) string { fmt.Printf(" Asking OpenAI for creative style suggestion for '%s'...\n", subject) - + req := openai.ChatCompletionRequest{ Model: openai.GPT4oMini, Messages: []openai.ChatCompletionMessage{ { - Role: openai.ChatMessageRoleSystem, + Role: openai.ChatMessageRoleSystem, Content: "You are a creative art director. Suggest unique, interesting photo/art styles for educational flashcard images. Be creative and varied. Respond with ONLY the style description, nothing else. Keep it concise (max 15 words).", }, { - Role: openai.ChatMessageRoleUser, + Role: openai.ChatMessageRoleUser, Content: fmt.Sprintf("Suggest a creative visual style for an educational image of: %s", subject), }, }, Temperature: 0.9, // Higher temperature for more creativity MaxTokens: 30, } - + resp, err := c.client.CreateChatCompletion(ctx, req) if err != nil { fmt.Printf(" Failed to get creative style: %v\n", err) return "" } - + if len(resp.Choices) == 0 || resp.Choices[0].Message.Content == "" { return "" } - + style := strings.TrimSpace(resp.Choices[0].Message.Content) // Remove any trailing punctuation style = strings.TrimSuffix(style, ".") style = strings.TrimSuffix(style, "!") - + return style -}
\ No newline at end of file +} |
