diff options
| author | Paul Buetow <paul@buetow.org> | 2025-07-16 21:44:28 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2025-07-16 21:44:28 +0300 |
| commit | e2e75315e5e7c3eaccdc38881bd2fa669bb9dda5 (patch) | |
| tree | c751e8c9b4d1efd858310fba54451e791c67737b | |
| parent | 6b0b4c9ce28b88ab0abbff03c5f367d89ba97c89 (diff) | |
only one img per card
| -rw-r--r-- | GUI.md | 71 | ||||
| -rw-r--r-- | TODO.md | 28 | ||||
| -rw-r--r-- | cmd/totalrecall/main.go | 23 | ||||
| -rw-r--r-- | internal/anki/generator.go | 49 | ||||
| -rw-r--r-- | internal/gui/app.go | 52 | ||||
| -rw-r--r-- | internal/gui/generator.go | 32 | ||||
| -rw-r--r-- | internal/gui/navigation.go | 20 | ||||
| -rw-r--r-- | internal/gui/queue.go | 6 | ||||
| -rw-r--r-- | internal/image/download.go | 84 | ||||
| -rw-r--r-- | internal/image/openai.go | 4 |
10 files changed, 77 insertions, 292 deletions
@@ -1,71 +0,0 @@ -# GUI Mode for TotalRecall - -TotalRecall now includes an interactive GUI mode for a more user-friendly flashcard generation experience. - -## Prerequisites - -The GUI mode requires Fyne, which has the following system dependencies: - -### Linux -```bash -# Debian/Ubuntu -sudo apt-get install gcc libgl1-mesa-dev xorg-dev - -# Fedora -sudo dnf install gcc mesa-libGL-devel libXcursor-devel libXrandr-devel libXinerama-devel libXi-devel libXxf86vm-devel -``` - -### macOS -No additional dependencies required (uses system frameworks). - -### Windows -No additional dependencies required if using MinGW or similar. - -## Running GUI Mode - -```bash -./totalrecall --gui -``` - -## Features - -The GUI provides: - -1. **Interactive Input**: Enter Bulgarian words one at a time -2. **Live Preview**: See generated images and hear audio pronunciation -3. **Fine-grained Regeneration**: - - Regenerate just the image (cycles through different results) - - Regenerate just the audio (uses a different voice) - - Regenerate both -4. **Session Management**: Keep track of all generated cards in a session -5. **Export to Anki**: Export all saved cards to CSV format - -## GUI Layout - -- **Top Section**: Input field for Bulgarian words with submit button -- **Middle Section**: - - Image display with navigation (if multiple images) - - Audio player with play controls - - Translation display -- **Bottom Section**: Action buttons - - "New Word" - saves the current card and clears for a new word - - "Regenerate Image" - gets a new image - - "Regenerate Audio" - generates with a different voice - - "Regenerate All" - regenerates everything - -## Building from Source - -If you're building from source and encounter issues with the GUI: - -1. Ensure you have the system dependencies installed (see Prerequisites) -2. The build might take longer the first time as it compiles Fyne -3. If the build times out, try building without the GUI first: - ```bash - go build -tags nogui ./cmd/totalrecall - ``` - -## Troubleshooting - -- **Build fails**: Check that you have the required system dependencies -- **GUI doesn't start**: Ensure you're running in a graphical environment -- **Audio doesn't play**: The current implementation shows audio controls but actual playback requires additional audio libraries
\ No newline at end of file diff --git a/TODO.md b/TODO.md deleted file mode 100644 index ab84a41..0000000 --- a/TODO.md +++ /dev/null @@ -1,28 +0,0 @@ -# TODO's - -## Completed -- [x] Added Fyne GUI mode with `--gui` flag -- [x] Interactive word input with Bulgarian validation -- [x] Live preview of generated images and audio -- [x] Fine-grained regeneration (image-only, audio-only, or both) -- [x] Session management with "New Word" functionality -- [x] Export to Anki CSV from GUI session -- [x] Progress indicators and status updates -- [x] Concurrent word processing in GUI - users can enter new words while previous ones are being generated -- [x] Word queue system with background processing -- [x] Queue status display showing pending, processing, and completed jobs -- [x] Navigation supports both disk files and queue-completed words - -## GUI Enhancements (Future) -- [ ] Implement actual audio playback (currently shows controls only) -- [ ] Add preferences dialog for GUI settings -- [ ] Add drag & drop support for batch word lists -- [ ] Add recent words history -- [ ] Add dark/light theme toggle -- [ ] Keyboard shortcuts (Enter to submit, Space to play audio) -- [ ] Save/restore GUI window size and position - -## Known Limitations -- Audio playback requires additional audio library integration (e.g., github.com/hajimehoshi/oto) -- First build with GUI may be slow due to Fyne compilation - diff --git a/cmd/totalrecall/main.go b/cmd/totalrecall/main.go index 89d0ad9..e6b74a0 100644 --- a/cmd/totalrecall/main.go +++ b/cmd/totalrecall/main.go @@ -31,7 +31,6 @@ var ( batchFile string skipAudio bool skipImages bool - imagesPerWord int generateAnki bool listModels bool allVoices bool @@ -82,7 +81,6 @@ func init() { rootCmd.Flags().StringVar(&batchFile, "batch", "", "Process words from file (one per line)") rootCmd.Flags().BoolVar(&skipAudio, "skip-audio", false, "Skip audio generation") rootCmd.Flags().BoolVar(&skipImages, "skip-images", false, "Skip image download") - rootCmd.Flags().IntVar(&imagesPerWord, "images-per-word", 1, "Number of images to download per word") rootCmd.Flags().BoolVar(&generateAnki, "anki", false, "Generate Anki import CSV file") rootCmd.Flags().BoolVar(&listModels, "list-models", false, "List available OpenAI models for the current API key") rootCmd.Flags().BoolVar(&allVoices, "all-voices", false, "Generate audio in all available voices (creates multiple files)") @@ -432,23 +430,13 @@ func downloadImages(word string) error { downloader := image.NewDownloader(searcher, downloadOpts) - // Download images + // Download single image ctx := context.Background() - if imagesPerWord == 1 { - _, path, err := downloader.DownloadBestMatch(ctx, word) - if err != nil { - return err - } - fmt.Printf(" Downloaded: %s\n", path) - } else { - paths, err := downloader.DownloadMultiple(ctx, word, imagesPerWord) - if err != nil { - return err - } - for _, path := range paths { - fmt.Printf(" Downloaded: %s\n", path) - } + _, path, err := downloader.DownloadBestMatch(ctx, word) + if err != nil { + return err } + fmt.Printf(" Downloaded: %s\n", path) return nil } @@ -726,7 +714,6 @@ func runGUIMode() error { OutputDir: outputDir, AudioFormat: audioFormat, ImageProvider: imageAPI, - ImagesPerWord: imagesPerWord, EnableCache: viper.GetBool("cache.enable"), OpenAIKey: getOpenAIKey(), PixabayKey: viper.GetString("image.pixabay_key"), diff --git a/internal/anki/generator.go b/internal/anki/generator.go index ad41903..3685e97 100644 --- a/internal/anki/generator.go +++ b/internal/anki/generator.go @@ -12,7 +12,7 @@ import ( type Card struct { Bulgarian string // The Bulgarian word/phrase AudioFile string // Path to audio file - ImageFiles []string // Paths to image files + ImageFile string // Path to image file Translation string // Optional translation Notes string // Optional notes } @@ -90,7 +90,7 @@ func (g *Generator) GenerateCSV() error { record := []string{ card.Bulgarian, g.formatAudioField(card.AudioFile), - g.formatImageField(card.ImageFiles), + g.formatImageField(card.ImageFile), card.Translation, card.Notes, } @@ -116,29 +116,15 @@ func (g *Generator) formatAudioField(audioFile string) string { return fmt.Sprintf("[sound:%s]", filename) } -// formatImageField formats image file references for Anki -func (g *Generator) formatImageField(imageFiles []string) string { - if len(imageFiles) == 0 { +// formatImageField formats image file reference for Anki +func (g *Generator) formatImageField(imageFile string) string { + if imageFile == "" { return "" } - // For multiple images, we'll use HTML to display them - if len(imageFiles) == 1 { - filename := filepath.Base(imageFiles[0]) - return fmt.Sprintf(`<img src="%s">`, filename) - } - - // Multiple images - create a simple layout - var html strings.Builder - html.WriteString(`<div style="display: flex; flex-wrap: wrap; gap: 10px;">`) - - for _, imageFile := range imageFiles { - filename := filepath.Base(imageFile) - html.WriteString(fmt.Sprintf(`<img src="%s" style="max-width: 200px; height: auto;">`, filename)) - } - - html.WriteString(`</div>`) - return html.String() + // Get just the filename + filename := filepath.Base(imageFile) + return fmt.Sprintf(`<img src="%s">`, filename) } // GenerateFromDirectory creates cards from a directory of materials @@ -179,8 +165,7 @@ func (g *Generator) GenerateFromDirectory(dir string) error { card, exists := wordFiles[word] if !exists { card = &Card{ - Bulgarian: word, - ImageFiles: make([]string, 0), + Bulgarian: word, } wordFiles[word] = card } @@ -192,7 +177,9 @@ func (g *Generator) GenerateFromDirectory(dir string) error { card.AudioFile = path } case ".jpg", ".jpeg", ".png", ".gif": - card.ImageFiles = append(card.ImageFiles, path) + if card.ImageFile == "" { // Use first image file found + card.ImageFile = path + } } return nil @@ -234,16 +221,14 @@ func (g *Generator) GeneratePackage(outputDir string) error { g.cards[i].AudioFile = newPath } - // Copy image files - newImagePaths := make([]string, 0, len(card.ImageFiles)) - for _, imagePath := range card.ImageFiles { - newPath, err := g.copyMediaFile(imagePath, mediaDir) + // Copy image file + if card.ImageFile != "" { + newPath, err := g.copyMediaFile(card.ImageFile, mediaDir) if err != nil { return fmt.Errorf("failed to copy image file: %w", err) } - newImagePaths = append(newImagePaths, newPath) + g.cards[i].ImageFile = newPath } - g.cards[i].ImageFiles = newImagePaths } // Update output path to package directory @@ -314,7 +299,7 @@ func (g *Generator) Stats() (totalCards, withAudio, withImages int) { if card.AudioFile != "" { withAudio++ } - if len(card.ImageFiles) > 0 { + if card.ImageFile != "" { withImages++ } } diff --git a/internal/gui/app.go b/internal/gui/app.go index 836ebca..3a8dd33 100644 --- a/internal/gui/app.go +++ b/internal/gui/app.go @@ -51,7 +51,7 @@ type Application struct { // State management currentWord string currentAudioFile string - currentImages []string + currentImage string currentTranslation string currentJobID int savedCards []anki.Card @@ -81,7 +81,6 @@ type Config struct { OutputDir string AudioFormat string ImageProvider string - ImagesPerWord int EnableCache bool OpenAIKey string PixabayKey string @@ -94,7 +93,6 @@ func DefaultConfig() *Config { OutputDir: "./anki_cards", AudioFormat: "mp3", ImageProvider: "openai", - ImagesPerWord: 1, EnableCache: true, } } @@ -361,7 +359,7 @@ func (a *Application) generateMaterials(word string) { // Get custom prompt from UI customPrompt := a.imagePromptEntry.Text - images, err := a.generateImagesWithPrompt(word, customPrompt) + imageFile, err := a.generateImagesWithPrompt(word, customPrompt) a.decrementProcessing() // Image processing ends if err != nil { @@ -371,10 +369,12 @@ func (a *Application) generateMaterials(word string) { }) return } - a.currentImages = images - fyne.Do(func() { - a.imageDisplay.SetImages(images) - }) + if imageFile != "" { + a.currentImage = imageFile + fyne.Do(func() { + a.imageDisplay.SetImages([]string{imageFile}) + }) + } // Enable action buttons fyne.Do(func() { @@ -388,12 +388,12 @@ func (a *Application) generateMaterials(word string) { // onKeepAndContinue saves the current card and clears for a new word func (a *Application) onKeepAndContinue() { // Check if we have a complete word to save - if a.currentWord != "" && a.currentAudioFile != "" && len(a.currentImages) > 0 { + if a.currentWord != "" && a.currentAudioFile != "" && a.currentImage != "" { // Save current card card := anki.Card{ Bulgarian: a.currentWord, AudioFile: a.currentAudioFile, - ImageFiles: a.currentImages, + ImageFile: a.currentImage, Translation: a.currentTranslation, } @@ -457,16 +457,18 @@ func (a *Application) onRegenerateImage() { defer a.wg.Done() defer a.decrementProcessing() // Image processing ends - images, err := a.generateImagesWithPrompt(a.currentWord, customPrompt) + imageFile, err := a.generateImagesWithPrompt(a.currentWord, customPrompt) if err != nil { fyne.Do(func() { a.showError(fmt.Errorf("Image regeneration failed: %w", err)) }) } else { - a.currentImages = images - fyne.Do(func() { - a.imageDisplay.SetImages(images) - }) + if imageFile != "" { + a.currentImage = imageFile + fyne.Do(func() { + a.imageDisplay.SetImages([]string{imageFile}) + }) + } } fyne.Do(func() { @@ -734,7 +736,7 @@ func (a *Application) processWordJob(job *WordJob) { }) // Use the custom prompt from the job - imageFiles, err := a.generateImagesWithPrompt(job.Word, job.CustomPrompt) + imageFile, err := a.generateImagesWithPrompt(job.Word, job.CustomPrompt) a.decrementProcessing() // Image processing ends if err != nil { @@ -749,18 +751,22 @@ func (a *Application) processWordJob(job *WordJob) { a.updateStatus(fmt.Sprintf("Finalizing '%s'...", job.Word)) }) - a.queue.CompleteJob(job.ID, translation, audioFile, imageFiles) + a.queue.CompleteJob(job.ID, translation, audioFile, imageFile) // Update UI with results if this is still the current job a.mu.Lock() if a.currentJobID == job.ID { a.currentTranslation = translation a.currentAudioFile = audioFile - a.currentImages = imageFiles + if imageFile != "" { + a.currentImage = imageFile + } fyne.Do(func() { a.translationText.SetText(fmt.Sprintf("%s = %s", job.Word, translation)) - a.imageDisplay.SetImages(imageFiles) + if imageFile != "" { + a.imageDisplay.SetImages([]string{imageFile}) + } a.audioPlayer.SetAudioFile(audioFile) a.hideProgress() a.setActionButtonsEnabled(true) @@ -817,7 +823,7 @@ func (a *Application) onJobComplete(job *WordJob) { // Only update UI if the current word is still empty (waiting for this job) if job.Word == a.currentWord && job.ID != a.currentJobID { // Check if the UI is still empty/waiting for content - hasContent := a.currentAudioFile != "" || len(a.currentImages) > 0 + hasContent := a.currentAudioFile != "" || a.currentImage != "" if !hasContent { // Update the UI with the completed results since it's still waiting @@ -831,9 +837,9 @@ func (a *Application) onJobComplete(job *WordJob) { a.audioPlayer.SetAudioFile(job.AudioFile) a.regenerateAudioBtn.Enable() } - if len(job.ImageFiles) > 0 && len(a.currentImages) == 0 { - a.currentImages = job.ImageFiles - a.imageDisplay.SetImages(job.ImageFiles) + if job.ImageFile != "" && a.currentImage == "" { + a.currentImage = job.ImageFile + a.imageDisplay.SetImages([]string{job.ImageFile}) a.regenerateImageBtn.Enable() } diff --git a/internal/gui/generator.go b/internal/gui/generator.go index 9738d88..7a6d26e 100644 --- a/internal/gui/generator.go +++ b/internal/gui/generator.go @@ -86,12 +86,12 @@ func (a *Application) generateAudio(word string) (string, error) { } // generateImages downloads images for a word -func (a *Application) generateImages(word string) ([]string, error) { +func (a *Application) generateImages(word string) (string, error) { return a.generateImagesWithPrompt(word, "") } -// generateImagesWithPrompt downloads images for a word with optional custom prompt -func (a *Application) generateImagesWithPrompt(word string, customPrompt string) ([]string, error) { +// generateImagesWithPrompt downloads a single image for a word with optional custom prompt +func (a *Application) generateImagesWithPrompt(word string, customPrompt string) (string, error) { // Create image searcher based on provider var searcher image.ImageSearcher var err error @@ -102,11 +102,11 @@ func (a *Application) generateImagesWithPrompt(word string, customPrompt string) case "unsplash": if a.config.UnsplashKey == "" { - return nil, fmt.Errorf("Unsplash API key is required") + return "", fmt.Errorf("Unsplash API key is required") } searcher, err = image.NewUnsplashClient(a.config.UnsplashKey) if err != nil { - return nil, err + return "", err } case "openai": @@ -127,7 +127,7 @@ func (a *Application) generateImagesWithPrompt(word string, customPrompt string) } default: - return nil, fmt.Errorf("unknown image provider: %s", a.config.ImageProvider) + return "", fmt.Errorf("unknown image provider: %s", a.config.ImageProvider) } // Create downloader @@ -147,20 +147,10 @@ func (a *Application) generateImagesWithPrompt(word string, customPrompt string) searchOpts.CustomPrompt = customPrompt } - // Download images - var paths []string - - if a.config.ImagesPerWord == 1 { - _, path, err := downloader.DownloadBestMatchWithOptions(a.ctx, searchOpts) - if err != nil { - return nil, err - } - paths = []string{path} - } else { - paths, err = downloader.DownloadMultipleWithOptions(a.ctx, searchOpts, a.config.ImagesPerWord) - if err != nil { - return nil, err - } + // Download single image + _, path, err := downloader.DownloadBestMatchWithOptions(a.ctx, searchOpts) + if err != nil { + return "", err } // If using OpenAI, get the last used prompt and update the UI @@ -175,7 +165,7 @@ func (a *Application) generateImagesWithPrompt(word string, customPrompt string) } } - return paths, nil + return path, nil } // saveAudioAttribution saves attribution info for generated audio diff --git a/internal/gui/navigation.go b/internal/gui/navigation.go index 05bf988..bc65c8f 100644 --- a/internal/gui/navigation.go +++ b/internal/gui/navigation.go @@ -175,7 +175,7 @@ func (a *Application) loadWordByIndex(index int) { // Load from queue job a.currentTranslation = job.Translation a.currentAudioFile = job.AudioFile - a.currentImages = job.ImageFiles + a.currentImage = job.ImageFile fyne.Do(func() { if job.Translation != "" { @@ -184,8 +184,8 @@ func (a *Application) loadWordByIndex(index int) { if job.AudioFile != "" { a.audioPlayer.SetAudioFile(job.AudioFile) } - if len(job.ImageFiles) > 0 { - a.imageDisplay.SetImages(job.ImageFiles) + if job.ImageFile != "" { + a.imageDisplay.SetImages([]string{job.ImageFile}) } a.updateStatus(fmt.Sprintf("Loaded from queue: %s", word)) }) @@ -234,8 +234,8 @@ func (a *Application) loadExistingFiles(word string) { }) } - // Load image files - a.currentImages = []string{} + // Load image file + a.currentImage = "" // Try to find images with different patterns patterns := []string{ fmt.Sprintf("%s.jpg", sanitized), @@ -249,20 +249,20 @@ func (a *Application) loadExistingFiles(word string) { for _, pattern := range patterns { imagePath := filepath.Join(a.config.OutputDir, pattern) if _, err := os.Stat(imagePath); err == nil { - a.currentImages = append(a.currentImages, imagePath) + a.currentImage = imagePath break // Just load the first image found } } - if len(a.currentImages) > 0 { + if a.currentImage != "" { fyne.Do(func() { - a.imageDisplay.SetImages(a.currentImages) + a.imageDisplay.SetImages([]string{a.currentImage}) }) // Try to load the prompt from attribution file if using OpenAI - if a.config.ImageProvider == "openai" && len(a.currentImages) > 0 { + if a.config.ImageProvider == "openai" { // Look for attribution file - baseImagePath := a.currentImages[0] + baseImagePath := a.currentImage attrPath := strings.TrimSuffix(baseImagePath, filepath.Ext(baseImagePath)) + "_attribution.txt" if data, err := os.ReadFile(attrPath); err == nil { // Parse prompt from attribution file diff --git a/internal/gui/queue.go b/internal/gui/queue.go index aaa0c55..df39908 100644 --- a/internal/gui/queue.go +++ b/internal/gui/queue.go @@ -13,7 +13,7 @@ type WordJob struct { Word string Translation string AudioFile string - ImageFiles []string + ImageFile string // Changed from ImageFiles []string to single image Status JobStatus Error error StartedAt time.Time @@ -194,7 +194,7 @@ func (q *WordQueue) Stop() { } // CompleteJob marks a job as completed with results -func (q *WordQueue) CompleteJob(jobID int, translation, audioFile string, imageFiles []string) { +func (q *WordQueue) CompleteJob(jobID int, translation, audioFile, imageFile string) { q.mu.Lock() defer q.mu.Unlock() @@ -202,7 +202,7 @@ func (q *WordQueue) CompleteJob(jobID int, translation, audioFile string, imageF job.Status = StatusCompleted job.Translation = translation job.AudioFile = audioFile - job.ImageFiles = imageFiles + job.ImageFile = imageFile job.CompletedAt = time.Now() delete(q.processing, jobID) diff --git a/internal/image/download.go b/internal/image/download.go index 7083a6f..fabb1bb 100644 --- a/internal/image/download.go +++ b/internal/image/download.go @@ -200,48 +200,6 @@ func sanitizeFileName(name string) string { return sanitized } -// DownloadMultiple downloads multiple images for a query -func (d *Downloader) DownloadMultiple(ctx context.Context, query string, count int) ([]string, error) { - // Search for images - opts := DefaultSearchOptions(query) - opts.PerPage = count * 2 // Get extra in case some fail - - results, err := d.searcher.Search(ctx, opts) - if err != nil { - return nil, fmt.Errorf("search failed: %w", err) - } - - if len(results) == 0 { - return nil, fmt.Errorf("no images found for query: %s", query) - } - - // Download up to 'count' images - var downloaded []string - for i, result := range results { - if len(downloaded) >= count { - break - } - - // Generate filename - filename := d.generateFileName(query, &result, i) - outputPath := filepath.Join(d.options.OutputDir, filename) - - // Try to download - err := d.DownloadImage(ctx, &result, outputPath) - if err == nil { - downloaded = append(downloaded, outputPath) - } else { - // Log error and continue - fmt.Fprintf(os.Stderr, "Warning: failed to download image %d: %v\n", i+1, err) - } - } - - if len(downloaded) == 0 { - return nil, fmt.Errorf("failed to download any images for query: %s", query) - } - - return downloaded, nil -} // DownloadBestMatchWithOptions downloads the best matching image for given search options func (d *Downloader) DownloadBestMatchWithOptions(ctx context.Context, opts *SearchOptions) (*SearchResult, string, error) { @@ -277,45 +235,3 @@ func (d *Downloader) DownloadBestMatchWithOptions(ctx context.Context, opts *Sea return nil, "", fmt.Errorf("failed to download any images for query: %s", opts.Query) } -// DownloadMultipleWithOptions downloads multiple images for given search options -func (d *Downloader) DownloadMultipleWithOptions(ctx context.Context, opts *SearchOptions, count int) ([]string, error) { - // Search for images - searchOpts := *opts // Copy to avoid modifying original - searchOpts.PerPage = count * 2 // Get extra in case some fail - - results, err := d.searcher.Search(ctx, &searchOpts) - if err != nil { - return nil, fmt.Errorf("search failed: %w", err) - } - - if len(results) == 0 { - return nil, fmt.Errorf("no images found for query: %s", opts.Query) - } - - // Download up to 'count' images - var downloaded []string - for i, result := range results { - if len(downloaded) >= count { - break - } - - // Generate filename - filename := d.generateFileName(opts.Query, &result, i) - outputPath := filepath.Join(d.options.OutputDir, filename) - - // Try to download - err := d.DownloadImage(ctx, &result, outputPath) - if err == nil { - downloaded = append(downloaded, outputPath) - } else { - // Log error and continue - fmt.Fprintf(os.Stderr, "Warning: failed to download image %d: %v\n", i+1, err) - } - } - - if len(downloaded) == 0 { - return nil, fmt.Errorf("failed to download any images for query: %s", opts.Query) - } - - return downloaded, nil -}
\ No newline at end of file diff --git a/internal/image/openai.go b/internal/image/openai.go index 2fbb043..dc82b90 100644 --- a/internal/image/openai.go +++ b/internal/image/openai.go @@ -261,7 +261,7 @@ 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 %s of: %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.", @@ -331,7 +331,7 @@ func (c *OpenAIClient) createEducationalPrompt(bulgarianWord, englishTranslation // Create a simple, clear prompt for educational images return fmt.Sprintf( - "Generate a %s of: %s. "+ + "Generate a %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.", |
