diff options
| author | Paul Buetow <paul@buetow.org> | 2025-07-21 16:34:16 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2025-07-21 16:34:16 +0300 |
| commit | 18390dc18f397207e6eb8e67991dc7078b5e7515 (patch) | |
| tree | 75a5b4862346adebe6616ef157c94b9b00cac6fb /internal | |
| parent | f3e301168ad742a556b336761e28f739eb1d7143 (diff) | |
more on this
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/gui/app.go | 485 | ||||
| -rw-r--r-- | internal/gui/audio_player.go | 5 | ||||
| -rw-r--r-- | internal/gui/generator.go | 47 | ||||
| -rw-r--r-- | internal/gui/widgets.go | 45 | ||||
| -rw-r--r-- | internal/image/openai.go | 13 |
5 files changed, 399 insertions, 196 deletions
diff --git a/internal/gui/app.go b/internal/gui/app.go index 1b66440..b423852 100644 --- a/internal/gui/app.go +++ b/internal/gui/app.go @@ -400,22 +400,45 @@ func (a *Application) setupUI() { // Now that tooltip layer is created, set all tooltips a.setupTooltips() - // Set tooltips for export and help buttons - exportButton.SetToolTip("Export to Anki (x)") - helpButton.SetToolTip("Show hotkeys (?)") + // Set tooltips for export and help buttons with a delay + go func() { + time.Sleep(500 * time.Millisecond) + fyne.Do(func() { + if exportButton != nil { + exportButton.SetToolTip("Export to Anki (x)") + } + if helpButton != nil { + helpButton.SetToolTip("Show hotkeys (?)") + } + }) + }() a.window.SetOnClosed(func() { // Stop file check ticker if a.fileCheckTicker != nil { a.fileCheckTicker.Stop() } - // Stop log capture - if a.logViewer != nil { - a.logViewer.StopCapture() + // Cancel any ongoing operations + if a.cancel != nil { + a.cancel() } - a.cancel() - a.queue.Stop() - a.wg.Wait() + // Wait for all goroutines to finish with timeout + done := make(chan struct{}) + go func() { + a.wg.Wait() + close(done) + }() + + select { + case <-done: + // All goroutines finished + case <-time.After(2 * time.Second): + // Timeout after 2 seconds + fmt.Println("Warning: Some operations did not complete before window close") + } + + // Close the application + a.app.Quit() }) // Set up keyboard shortcuts @@ -567,71 +590,70 @@ func (a *Application) generateMaterials(word string) { } } - // Generate audio - fyne.Do(func() { - a.updateStatus("Generating audio...") - a.incrementProcessing() // Audio processing starts - }) - audioFile, err := a.generateAudio(cardCtx, word) - a.decrementProcessing() // Audio processing ends - - if err != nil { - fyne.Do(func() { - a.showError(fmt.Errorf("Audio generation failed: %w", err)) - a.setUIEnabled(true) - }) - return + // Create channels for parallel operations + type audioResult struct { + file string + err error } - - // Only update UI if this word is still the current word - a.mu.Lock() - if a.currentWord == word { - a.currentAudioFile = audioFile - fyne.Do(func() { - a.audioPlayer.SetAudioFile(audioFile) - }) + type imageResult struct { + file string + err error + } + type phoneticResult struct { + info string + err error } - a.mu.Unlock() - // Generate images with custom prompt if provided - fyne.Do(func() { - a.updateStatus("Waiting for/downloading images...") - a.incrementProcessing() // Image processing starts - }) + audioChan := make(chan audioResult, 1) + imageChan := make(chan imageResult, 1) + phoneticChan := make(chan phoneticResult, 1) - // Get custom prompt from UI + // Get custom prompt and translation before starting goroutines customPrompt := a.imagePromptEntry.Text - - // Pass the current translation to avoid re-translating translation := a.currentTranslation if translation == "" { // Use the text from translationEntry if currentTranslation is not set translation = strings.TrimSpace(a.translationEntry.Text) } - imageFile, err := a.generateImagesWithPrompt(cardCtx, word, customPrompt, translation) - a.decrementProcessing() // Image processing ends - if err != nil { + // Update status to show parallel processing + fyne.Do(func() { + a.updateStatus("Generating audio, images, and phonetics in parallel...") + }) + + // Start all three operations in parallel + + // 1. Audio generation + go func() { fyne.Do(func() { - a.showError(fmt.Errorf("Image download failed: %w", err)) - a.setUIEnabled(true) + a.incrementProcessing() // Audio processing starts }) - return - } - // Only update UI if this word is still the current word - if imageFile != "" { - a.mu.Lock() - if a.currentWord == word { - a.currentImage = imageFile - fyne.Do(func() { - a.imageDisplay.SetImages([]string{imageFile}) - }) - } - a.mu.Unlock() - } + audioFile, err := a.generateAudio(cardCtx, word) + a.decrementProcessing() // Audio processing ends + + audioChan <- audioResult{file: audioFile, err: err} + }() - // Fetch phonetic information in a separate goroutine + // 2. Image generation + go func() { + fyne.Do(func() { + a.incrementProcessing() // Image processing starts + // Show generating status if this is still the current word + a.mu.Lock() + if a.currentWord == word { + a.imageDisplay.SetGenerating() + } + a.mu.Unlock() + }) + + imageFile, err := a.generateImagesWithPrompt(cardCtx, word, customPrompt, translation) + a.decrementProcessing() // Image processing ends + + imageChan <- imageResult{file: imageFile, err: err} + }() + + // 3. Phonetic information fetching go func() { fyne.Do(func() { a.incrementProcessing() // Phonetic processing starts @@ -642,31 +664,89 @@ func (a *Application) generateMaterials(word string) { // Log error but don't fail - phonetic info is optional fmt.Printf("Warning: Failed to get phonetic info: %v\n", err) phoneticInfo = "Failed to fetch phonetic information" + } else { + fmt.Printf("Successfully fetched phonetic info for '%s': %s\n", word, phoneticInfo) } - // Update UI with phonetic info if this is still the current word + // Save phonetic info to disk + if phoneticInfo != "" && phoneticInfo != "Failed to fetch phonetic information" { + a.savePhoneticInfoForWord(word, phoneticInfo) + } + + a.decrementProcessing() // Phonetic processing ends + phoneticChan <- phoneticResult{info: phoneticInfo, err: nil} + }() + + // Wait for all operations to complete + var hasError bool + + // Collect audio result + audioRes := <-audioChan + if audioRes.err != nil { + fyne.Do(func() { + a.showError(fmt.Errorf("Audio generation failed: %w", audioRes.err)) + }) + hasError = true + } else { + // Only update UI if this word is still the current word a.mu.Lock() if a.currentWord == word { - a.currentPhonetic = phoneticInfo + a.currentAudioFile = audioRes.file fyne.Do(func() { - // Extract and display just the IPA - // Display the IPA directly - if phoneticInfo != "" { - a.audioPlayer.SetPhonetic(phoneticInfo) - } else { - a.audioPlayer.SetPhonetic("") - } + a.audioPlayer.SetAudioFile(audioRes.file) }) } a.mu.Unlock() + } - // Save phonetic info to disk - if phoneticInfo != "" && phoneticInfo != "Failed to fetch phonetic information" { - a.savePhoneticInfoForWord(word, phoneticInfo) + // Collect image result + imageRes := <-imageChan + if imageRes.err != nil { + fyne.Do(func() { + a.showError(fmt.Errorf("Image download failed: %w", imageRes.err)) + }) + hasError = true + } else if imageRes.file != "" { + // Only update UI if this word is still the current word + a.mu.Lock() + if a.currentWord == word { + a.currentImage = imageRes.file + fyne.Do(func() { + a.imageDisplay.SetImages([]string{imageRes.file}) + }) } + a.mu.Unlock() + } - a.decrementProcessing() // Phonetic processing ends - }() + // Collect phonetic result + phoneticRes := <-phoneticChan + if phoneticRes.info != "" { + // Update UI with phonetic info if this is still the current word + a.mu.Lock() + shouldUpdate := a.currentWord == word + if shouldUpdate { + a.currentPhonetic = phoneticRes.info + } + a.mu.Unlock() + + if shouldUpdate { + fmt.Printf("Updating phonetic display in UI for word '%s': %s\n", word, phoneticRes.info) + fyne.Do(func() { + // Display the IPA directly + a.audioPlayer.SetPhonetic(phoneticRes.info) + }) + } else { + fmt.Printf("Not updating phonetic display - word mismatch (current: %s, this: %s)\n", a.currentWord, word) + } + } + + // If any critical operation failed, re-enable UI + if hasError { + fyne.Do(func() { + a.setUIEnabled(true) + }) + return + } // Enable action buttons fyne.Do(func() { @@ -747,8 +827,8 @@ func (a *Application) onRegenerateImage() { a.regenerateAllBtn.Disable() a.showProgress("Regenerating image...") - // Clear the current image immediately - a.imageDisplay.Clear() + // Show generating status immediately + a.imageDisplay.SetGenerating() // Get custom prompt from UI customPrompt := a.imagePromptEntry.Text @@ -811,8 +891,8 @@ func (a *Application) onRegenerateRandomImage() { a.regenerateAllBtn.Disable() a.showProgress("Generating random image...") - // Clear the current image immediately - a.imageDisplay.Clear() + // Show generating status immediately + a.imageDisplay.SetGenerating() // Clear the custom prompt to let the system generate a new one customPrompt := "" @@ -910,8 +990,8 @@ func (a *Application) onRegenerateAll() { a.setUIEnabled(false) a.showProgress("Regenerating all materials...") - // Clear the current image immediately - a.imageDisplay.Clear() + // Show generating status immediately + a.imageDisplay.SetGenerating() a.wg.Add(1) go func() { @@ -1386,25 +1466,54 @@ func (a *Application) clearUI() { // setupTooltips sets up all tooltips after the tooltip layer has been created func (a *Application) setupTooltips() { - // Navigation button tooltips - a.submitButton.SetToolTip("Generate word (g)") - a.prevWordBtn.SetToolTip("Previous word (← / h/х)") - a.nextWordBtn.SetToolTip("Next word (→ / l/л)") - - // Action button tooltips - a.keepButton.SetToolTip("Keep card and new word (n)") - a.regenerateImageBtn.SetToolTip("Regenerate image (i)") - a.regenerateRandomImageBtn.SetToolTip("Random image (m)") - a.regenerateAudioBtn.SetToolTip("Regenerate audio (a)") - a.regenerateAllBtn.SetToolTip("Regenerate all (r)") - a.deleteButton.SetToolTip("Delete word (d)") - - // Export and help button tooltips need to be set after creation - // We'll handle this in setupUI where they are created - - // Audio player tooltips - a.audioPlayer.playButton.SetToolTip("Play audio (p)") - a.audioPlayer.stopButton.SetToolTip("Stop audio") + // Use a goroutine with a delay to ensure the tooltip layer is fully initialized + go func() { + time.Sleep(500 * time.Millisecond) + + fyne.Do(func() { + // Navigation button tooltips + if a.submitButton != nil { + a.submitButton.SetToolTip("Generate word (g)") + } + if a.prevWordBtn != nil { + a.prevWordBtn.SetToolTip("Previous word (← / h/х)") + } + if a.nextWordBtn != nil { + a.nextWordBtn.SetToolTip("Next word (→ / l/л)") + } + + // Action button tooltips + if a.keepButton != nil { + a.keepButton.SetToolTip("Keep card and new word (n)") + } + if a.regenerateImageBtn != nil { + a.regenerateImageBtn.SetToolTip("Regenerate image (i)") + } + if a.regenerateRandomImageBtn != nil { + a.regenerateRandomImageBtn.SetToolTip("Random image (m)") + } + if a.regenerateAudioBtn != nil { + a.regenerateAudioBtn.SetToolTip("Regenerate audio (a)") + } + if a.regenerateAllBtn != nil { + a.regenerateAllBtn.SetToolTip("Regenerate all (r)") + } + if a.deleteButton != nil { + a.deleteButton.SetToolTip("Delete word (d)") + } + + // Export and help button tooltips need to be set after creation + // They are set in the main window setup + + // Audio player tooltips + if a.audioPlayer != nil && a.audioPlayer.playButton != nil { + a.audioPlayer.playButton.SetToolTip("Play audio (p)") + } + if a.audioPlayer != nil && a.audioPlayer.stopButton != nil { + a.audioPlayer.stopButton.SetToolTip("Stop audio") + } + }) + }() } // processNextInQueue processes the next word in the queue @@ -1536,20 +1645,76 @@ func (a *Application) processWordJob(job *WordJob) { } a.mu.Unlock() - // Start fetching phonetic information concurrently - phoneticDone := make(chan struct{}) + // Create channels for parallel operations + type audioResult struct { + file string + err error + } + type imageResult struct { + file string + err error + } + type phoneticResult struct { + info string + err error + } + + audioChan := make(chan audioResult, 1) + imageChan := make(chan imageResult, 1) + phoneticChan := make(chan phoneticResult, 1) + + // Update status to show parallel processing + fyne.Do(func() { + a.updateStatus(fmt.Sprintf("Processing '%s' - generating audio, images, and phonetics in parallel...", job.Word)) + }) + + // Start all three operations in parallel + + // 1. Audio generation go func() { - defer close(phoneticDone) + fyne.Do(func() { + a.incrementProcessing() // Audio processing starts + }) + + audioFile, err := a.generateAudio(cardCtx, job.Word) + a.decrementProcessing() // Audio processing ends + + audioChan <- audioResult{file: audioFile, err: err} + }() + + // 2. Image generation (includes scene description) + go func() { + fyne.Do(func() { + a.incrementProcessing() // Image processing starts + // Show generating status if this is still the current job + a.mu.Lock() + if a.currentJobID == job.ID { + a.imageDisplay.SetGenerating() + } + a.mu.Unlock() + }) + + // Use the custom prompt from the job + // The translation variable already contains the correct translation (either from job or translated) + imageFile, err := a.generateImagesWithPrompt(cardCtx, job.Word, job.CustomPrompt, translation) + a.decrementProcessing() // Image processing ends + + imageChan <- imageResult{file: imageFile, err: err} + }() + // 3. Phonetic information fetching + go func() { fyne.Do(func() { a.incrementProcessing() // Phonetic processing starts }) phoneticInfo, err := a.getPhoneticInfo(job.Word) if err != nil { - // Log error but don't fail the job - phonetic info is optional + // Log error but don't fail - phonetic info is optional fmt.Printf("Warning: Failed to get phonetic info: %v\n", err) phoneticInfo = "Failed to fetch phonetic information" + } else { + fmt.Printf("Successfully fetched phonetic info for '%s': %s\n", job.Word, phoneticInfo) } // Save phonetic info to disk immediately for this specific word @@ -1569,76 +1734,77 @@ func (a *Application) processWordJob(job *WordJob) { os.WriteFile(phoneticFile, []byte(phoneticInfo), 0644) } - // Update UI with phonetic info if this is still the current job - a.mu.Lock() - if a.currentJobID == job.ID { - a.currentPhonetic = phoneticInfo - fyne.Do(func() { - // Extract and display just the IPA - // Display the IPA directly - if phoneticInfo != "" { - a.audioPlayer.SetPhonetic(phoneticInfo) - } else { - a.audioPlayer.SetPhonetic("") - } - }) - } - a.mu.Unlock() - a.decrementProcessing() // Phonetic processing ends + phoneticChan <- phoneticResult{info: phoneticInfo, err: nil} }() - // Generate audio - fyne.Do(func() { - a.updateStatus(fmt.Sprintf("Generating audio for '%s'...", job.Word)) - a.incrementProcessing() // Audio processing starts - }) + // Wait for all operations to complete + var audioFile, imageFile string + var phoneticInfo string + var hasError bool - audioFile, err := a.generateAudio(cardCtx, job.Word) - a.decrementProcessing() // Audio processing ends + // Collect audio result + audioRes := <-audioChan + if audioRes.err != nil { + a.queue.FailJob(job.ID, fmt.Errorf("audio generation failed: %w", audioRes.err)) + hasError = true + } else { + audioFile = audioRes.file - if err != nil { - a.queue.FailJob(job.ID, fmt.Errorf("audio generation failed: %w", err)) - a.finishCurrentJob() - return + // Update UI with audio immediately if this is still the current job + a.mu.Lock() + isCurrentJob := a.currentJobID == job.ID + if isCurrentJob { + a.currentAudioFile = audioFile + } + a.mu.Unlock() + + if isCurrentJob { + fyne.Do(func() { + a.audioPlayer.SetAudioFile(audioFile) + // Enable audio-related actions + a.regenerateAudioBtn.Enable() + }) + } } - // Update UI with audio immediately if this is still the current job + // Collect image result + imageRes := <-imageChan + if imageRes.err != nil { + a.queue.FailJob(job.ID, fmt.Errorf("image download failed: %w", imageRes.err)) + hasError = true + } else { + imageFile = imageRes.file + } + + // Collect phonetic result + phoneticRes := <-phoneticChan + phoneticInfo = phoneticRes.info + + // Update UI with phonetic info if this is still the current job a.mu.Lock() - isCurrentJob := a.currentJobID == job.ID - if isCurrentJob { - a.currentAudioFile = audioFile + shouldUpdate := a.currentJobID == job.ID && phoneticInfo != "" + if shouldUpdate { + a.currentPhonetic = phoneticInfo } a.mu.Unlock() - if isCurrentJob { + if shouldUpdate { + fmt.Printf("Updating phonetic display in UI for job %d: %s\n", job.ID, phoneticInfo) fyne.Do(func() { - a.audioPlayer.SetAudioFile(audioFile) - // Enable audio-related actions - a.regenerateAudioBtn.Enable() + // Display the IPA directly + a.audioPlayer.SetPhonetic(phoneticInfo) }) + } else { + fmt.Printf("Not updating phonetic display - job mismatch or empty info (current job: %d, this job: %d, info: %s)\n", a.currentJobID, job.ID, phoneticInfo) } - // Generate images - fyne.Do(func() { - a.updateStatus(fmt.Sprintf("Waiting for/downloading images for '%s'...", job.Word)) - a.incrementProcessing() // Image processing starts - }) - - // Use the custom prompt from the job - // The translation variable already contains the correct translation (either from job or translated) - imageFile, err := a.generateImagesWithPrompt(cardCtx, job.Word, job.CustomPrompt, translation) - a.decrementProcessing() // Image processing ends - - if err != nil { - a.queue.FailJob(job.ID, fmt.Errorf("image download failed: %w", err)) + // If any critical operation failed, finish the job and return + if hasError { a.finishCurrentJob() return } - // Wait for phonetic fetching to complete before finalizing - <-phoneticDone - // Mark job as completed fyne.Do(func() { a.updateStatus(fmt.Sprintf("Finalizing '%s'...", job.Word)) @@ -1648,13 +1814,17 @@ func (a *Application) processWordJob(job *WordJob) { // Update UI with results if this is still the current job a.mu.Lock() - isCurrentJob = a.currentJobID == job.ID + isCurrentJob := a.currentJobID == job.ID if isCurrentJob { a.currentTranslation = translation a.currentAudioFile = audioFile if imageFile != "" { a.currentImage = imageFile } + // Make sure we have the phonetic info too + if phoneticInfo != "" && phoneticInfo != "Failed to fetch phonetic information" { + a.currentPhonetic = phoneticInfo + } } a.mu.Unlock() @@ -1665,6 +1835,13 @@ func (a *Application) processWordJob(job *WordJob) { a.imageDisplay.SetImages([]string{imageFile}) } a.audioPlayer.SetAudioFile(audioFile) + // Make sure phonetic info is displayed if we have it + if a.currentPhonetic != "" { + fmt.Printf("Setting phonetic in final UI update: %s\n", a.currentPhonetic) + a.audioPlayer.SetPhonetic(a.currentPhonetic) + } else { + fmt.Printf("No phonetic info available in final UI update\n") + } a.hideProgress() a.setActionButtonsEnabled(true) a.updateStatus(fmt.Sprintf("Completed: %s", job.Word)) diff --git a/internal/gui/audio_player.go b/internal/gui/audio_player.go index 8b2637b..c0b3a0d 100644 --- a/internal/gui/audio_player.go +++ b/internal/gui/audio_player.go @@ -143,6 +143,11 @@ func (p *AudioPlayer) Clear() { // SetPhonetic sets the phonetic transcription text func (p *AudioPlayer) SetPhonetic(phonetic string) { p.phoneticLabel.SetText(phonetic) + p.phoneticLabel.Refresh() + // Also refresh the container to ensure layout updates + if p.container != nil { + p.container.Refresh() + } } // SetAutoPlayEnabled sets the reference to the auto-play state diff --git a/internal/gui/generator.go b/internal/gui/generator.go index a26aaa4..814e578 100644 --- a/internal/gui/generator.go +++ b/internal/gui/generator.go @@ -187,11 +187,11 @@ func (a *Application) generateImagesWithPrompt(ctx context.Context, word string, Style: "natural", } - searcher = image.NewOpenAIClient(openaiConfig) + openaiClient := image.NewOpenAIClient(openaiConfig) + searcher = openaiClient if openaiConfig.APIKey == "" { return "", fmt.Errorf("OpenAI API key is required for image generation") } - default: return "", fmt.Errorf("unknown image provider: %s", a.config.ImageProvider) } @@ -224,29 +224,13 @@ func (a *Application) generateImagesWithPrompt(ctx context.Context, word string, downloader := image.NewDownloader(searcher, downloadOpts) - // Create search options with custom prompt and translation if provided - searchOpts := image.DefaultSearchOptions(word) - if customPrompt != "" { - searchOpts.CustomPrompt = customPrompt - } - if translation != "" { - searchOpts.Translation = translation - } - - // Download single image - _, path, err := downloader.DownloadBestMatchWithOptions(ctx, searchOpts) - if err != nil { - return "", err - } - - // If using OpenAI, get the last used prompt + // Set up callback for OpenAI to update prompt immediately when it's generated if a.config.ImageProvider == "openai" { if openaiClient, ok := searcher.(*image.OpenAIClient); ok { - usedPrompt := openaiClient.GetLastPrompt() - if usedPrompt != "" { + openaiClient.SetPromptCallback(func(prompt string) { // Save the prompt to disk immediately for this word promptFile := filepath.Join(wordDir, "image_prompt.txt") - os.WriteFile(promptFile, []byte(usedPrompt), 0644) + os.WriteFile(promptFile, []byte(prompt), 0644) // Only update UI if this word is still the current word a.mu.Lock() @@ -255,13 +239,30 @@ func (a *Application) generateImagesWithPrompt(ctx context.Context, word string, if isCurrentWord { fyne.Do(func() { - a.imagePromptEntry.SetText(usedPrompt) + a.imagePromptEntry.SetText(prompt) }) } - } + }) } } + // Create search options with custom prompt and translation if provided + searchOpts := image.DefaultSearchOptions(word) + if customPrompt != "" { + searchOpts.CustomPrompt = customPrompt + } + if translation != "" { + searchOpts.Translation = translation + } + + // Download single image + _, path, err := downloader.DownloadBestMatchWithOptions(ctx, searchOpts) + if err != nil { + return "", err + } + + // The prompt has already been saved and UI updated via the callback + return path, nil } diff --git a/internal/gui/widgets.go b/internal/gui/widgets.go index eb9818c..6ad428c 100644 --- a/internal/gui/widgets.go +++ b/internal/gui/widgets.go @@ -18,27 +18,27 @@ import ( // ImageDisplay is a custom widget for displaying images type ImageDisplay struct { widget.BaseWidget - - container *fyne.Container - imageCanvas *canvas.Image - imageLabel *widget.Label - - currentImage string + + container *fyne.Container + imageCanvas *canvas.Image + imageLabel *widget.Label + + currentImage string } // NewImageDisplay creates a new image display widget func NewImageDisplay() *ImageDisplay { d := &ImageDisplay{} - + // Create image canvas d.imageCanvas = canvas.NewImageFromResource(nil) d.imageCanvas.FillMode = canvas.ImageFillContain - d.imageCanvas.SetMinSize(fyne.NewSize(200, 150)) // Half the size - + d.imageCanvas.SetMinSize(fyne.NewSize(200, 150)) // Half the size + // Create label d.imageLabel = widget.NewLabel("No image") d.imageLabel.Alignment = fyne.TextAlignCenter - + // Create main container - no navigation buttons here d.container = container.NewBorder( nil, @@ -46,7 +46,7 @@ func NewImageDisplay() *ImageDisplay { nil, nil, d.imageCanvas, ) - + d.ExtendBaseWidget(d) return d } @@ -62,9 +62,9 @@ func (d *ImageDisplay) SetImage(imagePath string) { d.Clear() return } - + d.currentImage = imagePath - + // Load image from file file, err := os.Open(imagePath) if err != nil { @@ -72,17 +72,17 @@ func (d *ImageDisplay) SetImage(imagePath string) { return } defer file.Close() - + img, _, err := image.Decode(file) if err != nil { d.imageLabel.SetText(fmt.Sprintf("Error decoding image: %v", err)) return } - + // Update canvas d.imageCanvas.Image = img d.imageCanvas.Refresh() - + // Update label d.imageLabel.SetText(filepath.Base(imagePath)) } @@ -104,6 +104,13 @@ func (d *ImageDisplay) Clear() { d.imageLabel.SetText("No image") } +// SetGenerating shows a generating status +func (d *ImageDisplay) SetGenerating() { + d.currentImage = "" + d.imageCanvas.Image = nil + d.imageCanvas.Refresh() + d.imageLabel.SetText("Generating...") +} // ResourceFromPath creates a Fyne resource from a file path func ResourceFromPath(path string) (fyne.Resource, error) { @@ -112,11 +119,11 @@ func ResourceFromPath(path string) (fyne.Resource, error) { return nil, err } defer file.Close() - + data, err := os.ReadFile(path) if err != nil { return nil, err } - + return fyne.NewStaticResource(filepath.Base(path), data), nil -}
\ No newline at end of file +} diff --git a/internal/image/openai.go b/internal/image/openai.go index 637b8a1..d964b55 100644 --- a/internal/image/openai.go +++ b/internal/image/openai.go @@ -23,6 +23,9 @@ type OpenAIClient struct { quality string // standard or hd (dall-e-3 only) style string // natural or vivid (dall-e-3 only) lastPrompt string // Store the last used prompt for attribution + + // PromptCallback is called when the prompt is generated, before the image is created + PromptCallback func(prompt string) } // OpenAIConfig holds configuration for the OpenAI image provider @@ -120,6 +123,11 @@ func (c *OpenAIClient) Search(ctx context.Context, opts *SearchOptions) ([]Searc // Store the prompt for attribution c.lastPrompt = prompt + // Call the callback if set + if c.PromptCallback != nil { + c.PromptCallback(prompt) + } + // Log the prompt to stdout for debugging fmt.Printf("OpenAI Image Generation Prompt (%d chars): %s\n", len(prompt), prompt) fmt.Printf("OpenAI Image Generation: Using model '%s' with size '%s'\n", c.model, c.size) @@ -220,6 +228,11 @@ func (c *OpenAIClient) GetLastPrompt() string { return c.lastPrompt } +// SetPromptCallback sets a callback function that will be called when the prompt is generated +func (c *OpenAIClient) SetPromptCallback(callback func(prompt string)) { + c.PromptCallback = callback +} + // createEducationalPrompt generates a prompt optimized for language learning func (c *OpenAIClient) createEducationalPrompt(bulgarianWord, englishTranslation string) string { // Generate a scene description for the word |
