diff options
| author | Paul Buetow <paul@buetow.org> | 2025-07-22 16:03:13 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2025-07-22 16:03:13 +0300 |
| commit | 18a475657cbc7b2ff8ee537b082eeef25e9bf619 (patch) | |
| tree | 9295ccbd3e6e0f45f2ab361acc5800197de6265f /internal/gui/generator.go | |
| parent | df496b9888ec29bc86d2b5a8ebee1782e94e49f9 (diff) | |
Fix race conditions in background processing and prevent deletion of active cards
- Fix race condition where images, audio, and phonetic info could be saved to wrong flashcard when navigating quickly between cards
- Add pre-determined card directory that's passed to all background operations
- Track active operations per word to prevent deletion during generation
- Block deletion of cards that are queued or being processed
- Show appropriate error messages when deletion is blocked
This ensures files are always saved to the correct card directory and prevents data loss from deleting cards with active operations.
🤖 Generated with [opencode](https://opencode.ai)
Co-Authored-By: opencode <noreply@opencode.ai>
Diffstat (limited to 'internal/gui/generator.go')
| -rw-r--r-- | internal/gui/generator.go | 58 |
1 files changed, 16 insertions, 42 deletions
diff --git a/internal/gui/generator.go b/internal/gui/generator.go index 814e578..cfb8ad3 100644 --- a/internal/gui/generator.go +++ b/internal/gui/generator.go @@ -12,7 +12,6 @@ import ( "fyne.io/fyne/v2" "github.com/sashabaranov/go-openai" - "codeberg.org/snonux/totalrecall/internal" "codeberg.org/snonux/totalrecall/internal/audio" "codeberg.org/snonux/totalrecall/internal/image" ) @@ -84,12 +83,11 @@ func (a *Application) translateEnglishToBulgarian(word string) (string, error) { } // generateAudio generates audio for a word -func (a *Application) generateAudio(ctx context.Context, word string) (string, error) { +func (a *Application) generateAudio(ctx context.Context, word string, cardDir string) (string, error) { // Check if this is a regeneration by looking for existing audio file - wordDir := a.findCardDirectory(word) isRegeneration := false - if wordDir != "" { - audioFile := filepath.Join(wordDir, fmt.Sprintf("audio.%s", a.config.AudioFormat)) + if cardDir != "" { + audioFile := filepath.Join(cardDir, fmt.Sprintf("audio.%s", a.config.AudioFormat)) if _, err := os.Stat(audioFile); err == nil { isRegeneration = true } @@ -124,25 +122,13 @@ func (a *Application) generateAudio(ctx context.Context, word string) (string, e return "", err } - // Find existing card directory or create new one again after provider creation - wordDir = a.findCardDirectory(word) - if wordDir == "" { - // No existing directory, create new one with card ID - cardID := internal.GenerateCardID(word) - wordDir = filepath.Join(a.config.OutputDir, cardID) - if err := os.MkdirAll(wordDir, 0755); err != nil { - return "", fmt.Errorf("failed to create word directory: %w", err) - } - - // Save the original Bulgarian word in a metadata file - metadataFile := filepath.Join(wordDir, "word.txt") - if err := os.WriteFile(metadataFile, []byte(word), 0644); err != nil { - return "", fmt.Errorf("failed to save word metadata: %w", err) - } + // Use the provided card directory + if cardDir == "" { + return "", fmt.Errorf("card directory not provided") } // Generate filename in subdirectory - outputFile := filepath.Join(wordDir, fmt.Sprintf("audio.%s", a.config.AudioFormat)) + outputFile := filepath.Join(cardDir, fmt.Sprintf("audio.%s", a.config.AudioFormat)) // Generate audio err = provider.GenerateAudio(ctx, word, outputFile) @@ -157,7 +143,7 @@ func (a *Application) generateAudio(ctx context.Context, word string) (string, e } // Save voice metadata for GUI display - metadataFile := filepath.Join(wordDir, "audio_metadata.txt") + metadataFile := filepath.Join(cardDir, "audio_metadata.txt") metadata := fmt.Sprintf("voice=%s\nspeed=%.2f\n", voice, speed) if err := os.WriteFile(metadataFile, []byte(metadata), 0644); err != nil { fmt.Printf("Warning: Failed to save audio metadata: %v\n", err) @@ -167,12 +153,12 @@ func (a *Application) generateAudio(ctx context.Context, word string) (string, e } // generateImages downloads images for a word -func (a *Application) generateImages(ctx context.Context, word string) (string, error) { - return a.generateImagesWithPrompt(ctx, word, "", "") +func (a *Application) generateImages(ctx context.Context, word string, cardDir string) (string, error) { + return a.generateImagesWithPrompt(ctx, word, "", "", cardDir) } // generateImagesWithPrompt downloads a single image for a word with optional custom prompt and translation -func (a *Application) generateImagesWithPrompt(ctx context.Context, word string, customPrompt string, translation string) (string, error) { +func (a *Application) generateImagesWithPrompt(ctx context.Context, word string, customPrompt string, translation string, cardDir string) (string, error) { // Create image searcher based on provider var searcher image.ImageSearcher var err error @@ -196,26 +182,14 @@ func (a *Application) generateImagesWithPrompt(ctx context.Context, word string, return "", fmt.Errorf("unknown image provider: %s", a.config.ImageProvider) } - // Find existing card directory or create new one - wordDir := a.findCardDirectory(word) - if wordDir == "" { - // No existing directory, create new one with card ID - cardID := internal.GenerateCardID(word) - wordDir = filepath.Join(a.config.OutputDir, cardID) - if err := os.MkdirAll(wordDir, 0755); err != nil { - return "", fmt.Errorf("failed to create word directory: %w", err) - } - - // Save the original Bulgarian word in a metadata file - metadataFile := filepath.Join(wordDir, "word.txt") - if err := os.WriteFile(metadataFile, []byte(word), 0644); err != nil { - return "", fmt.Errorf("failed to save word metadata: %w", err) - } + // Use the provided card directory + if cardDir == "" { + return "", fmt.Errorf("card directory not provided") } // Create downloader downloadOpts := &image.DownloadOptions{ - OutputDir: wordDir, + OutputDir: cardDir, OverwriteExisting: true, CreateDir: true, FileNamePattern: "image", @@ -229,7 +203,7 @@ func (a *Application) generateImagesWithPrompt(ctx context.Context, word string, if openaiClient, ok := searcher.(*image.OpenAIClient); ok { openaiClient.SetPromptCallback(func(prompt string) { // Save the prompt to disk immediately for this word - promptFile := filepath.Join(wordDir, "image_prompt.txt") + promptFile := filepath.Join(cardDir, "image_prompt.txt") os.WriteFile(promptFile, []byte(prompt), 0644) // Only update UI if this word is still the current word |
