diff options
| author | Paul Buetow <paul@buetow.org> | 2026-01-21 22:41:08 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-01-21 22:41:08 +0200 |
| commit | 5f9f32f089fcc3cd827db4d707003acefa2a8cca (patch) | |
| tree | 5b295dbc88ac8fefb9661fffd442ebc846af269f | |
| parent | 962dd1f77c0b38bf5333e3328ba136ff6c456285 (diff) | |
Fix: Auto-play only regenerated audio (front or back, not both) for bg-bg cardsv0.8.0
- Removed duplicate fyne.KeyA handler that was triggering both front and back audio
- Added SetAudioFileNoAutoPlay() method for controlled playback
- Front audio (a key) now auto-plays only front audio
- Back audio (A key) now auto-plays only back audio
- Refactored startPlayback to use startPlaybackForFile for better control
- Fixed icon reset when playback finishes for each audio type separately
| -rw-r--r-- | PLAN.md | 147 | ||||
| -rw-r--r-- | TROUBLESHOOTING.md | 165 | ||||
| -rw-r--r-- | cmd/test_audio_regen/main.go | 251 | ||||
| -rw-r--r-- | internal/gui/app.go | 100 | ||||
| -rw-r--r-- | internal/gui/audio_player.go | 95 | ||||
| -rw-r--r-- | internal/gui/generator.go | 14 | ||||
| -rw-r--r-- | internal/gui/navigation.go | 35 | ||||
| -rw-r--r-- | internal/version.go | 2 | ||||
| -rw-r--r-- | output | 239 |
9 files changed, 699 insertions, 349 deletions
diff --git a/PLAN.md b/PLAN.md deleted file mode 100644 index 35b7f6e..0000000 --- a/PLAN.md +++ /dev/null @@ -1,147 +0,0 @@ -# Plan: Bulgarian-Bulgarian Flashcard Support - -## Status: ✅ IMPLEMENTED - -All features have been implemented and tested. - -## Overview -Add support for Bulgarian-Bulgarian flashcards alongside the existing English-Bulgarian mode. This enables monolingual learning where both sides of the flashcard are in Bulgarian (e.g., word and definition, or word and synonym). - -## Current State -- ✅ English-Bulgarian and Bulgarian-Bulgarian flashcards are supported -- ✅ File-based storage uses `translation.txt` and `cardtype.txt` -- ✅ Audio is generated for both sides of bg-bg cards -- ✅ Batch import supports: `english=bulgarian` (en-bg) and `bulgarian1==bulgarian2` (bg-bg) - ---- - -## 1. Internal Database/Storage Changes ✅ - -### 1.1 Add Card Type Indicator -**File:** `internal/cardtype.go` (NEW) - -- Created new `CardType` type with `CardTypeEnBg` and `CardTypeBgBg` constants -- Added `SaveCardType()` and `LoadCardType()` functions -- Backwards compatible: missing `cardtype.txt` defaults to `en-bg` - -### 1.2 Update Structs ✅ -**Files:** `internal/batch/processor.go`, `internal/anki/generator.go`, `internal/gui/queue.go` - -Added `CardType` field to: -- `WordEntry` struct -- `Card` struct (with `AudioFileBack` for bg-bg) -- `WordJob` struct (with `AudioFileBack` and `CardType`) - -### 1.3 Second Audio File ✅ -**File:** `internal/processor/processor.go`, `internal/gui/generator.go` - -For `bg-bg` cards, stores two audio files: -- `audio_front.mp3` - pronunciation of first Bulgarian term -- `audio_back.mp3` - pronunciation of second Bulgarian term - ---- - -## 2. Audio Generation Changes ✅ - -### 2.1 Generate Audio for Both Sides -**Files:** `internal/processor/processor.go`, `internal/gui/generator.go` - -- Added `generateAudioBgBg()` function for CLI processor -- Added `generateAudioBgBg()` function for GUI -- Both sides use the same voice for consistency - -### 2.2 Update Processor ✅ -- Detects card type and calls appropriate audio generation -- Saves to `audio_front.mp3` and `audio_back.mp3` for `bg-bg` - ---- - -## 3. GUI Support ✅ - -### 3.1 Card Type Selector -**File:** `internal/gui/app.go` - -Added dropdown selector: -- "English → Bulgarian" (default) -- "Bulgarian → Bulgarian" - -### 3.2 Update Input Labels ✅ -When `bg-bg` is selected: -- Translation placeholder changes to "Bulgarian definition..." - -### 3.3 Audio Preview ✅ -**File:** `internal/gui/audio_player.go` - -For `bg-bg` cards: -- Added "Play Back Audio" button (skip next icon) -- Button only visible for bg-bg cards - -### 3.4 Navigation Updates ✅ -**File:** `internal/gui/navigation.go` - -- Loads card type when navigating to existing cards -- Updates card type selector based on loaded card -- Loads both front and back audio for bg-bg cards - ---- - -## 4. Batch Importer Support ✅ - -### 4.1 Input Format Detection -**File:** `internal/batch/processor.go` - -Detects card type from line format: -- `bulgarian1==bulgarian2` → `bg-bg` mode (double equals) -- `english=bulgarian` or `bulgarian=english` → `en-bg` mode (single equals) - -### 4.2 Parsing Logic ✅ -``` -Line contains "==" → Split on "==" → bg-bg card -Line contains "=" (single) → Split on "=" → en-bg card -``` - -### 4.3 Processing Pipeline ✅ -- Parses and detects card type per line -- Passes card type through the processing pipeline -- Generates appropriate audio files based on type - ---- - -## 5. Anki Export Changes ✅ - -### 5.1 Update Export Format -**File:** `internal/anki/apkg_generator.go` - -For `bg-bg` cards: -- Created separate note type "Bulgarian-Bulgarian from TotalRecall" -- Fields: BulgarianFront, BulgarianBack, Image, AudioFront, AudioBack, Notes -- Includes both audio files in the Anki package - -### 5.2 Card Templates ✅ -- Forward: Shows front Bulgarian word + front audio, answer shows back + back audio -- Reverse: Shows back Bulgarian word + back audio, answer shows front + front audio -- Different CSS styling for front/back Bulgarian text - ---- - -## Test Coverage ✅ - -Added new tests in `internal/batch/processor_test.go`: -- `bulgarian-bulgarian_format_with_double_equals` -- `mixed_en-bg_and_bg-bg_formats` - -All tests pass: -``` -=== RUN TestReadBatchFile/bulgarian-bulgarian_format_with_double_equals ---- PASS: TestReadBatchFile/bulgarian-bulgarian_format_with_double_equals -=== RUN TestReadBatchFile/mixed_en-bg_and_bg-bg_formats ---- PASS: TestReadBatchFile/mixed_en-bg_and_bg-bg_formats -``` - ---- - -## Migration/Compatibility ✅ - -- Existing cards without `cardtype.txt` default to `en-bg` -- No migration needed for existing data -- Batch files can mix `en-bg` and `bg-bg` entries in the same file diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md deleted file mode 100644 index 2ff017c..0000000 --- a/TROUBLESHOOTING.md +++ /dev/null @@ -1,165 +0,0 @@ -# TotalRecall Troubleshooting Guide - -## Audio Regeneration Issue - Bulgarian-Bulgarian Cards - -### Issue Description - -When using keyboard shortcuts to regenerate audio on Bulgarian-Bulgarian (bg-bg) cards: -- **Expected**: `a` regenerates front audio, `A` regenerates back audio -- **Problem**: User reports that `A` is regenerating the "other side" instead of the back audio - -### What Should Happen - -For a bg-bg card with: -- Front word: "котка" (cat) -- Back definition: "домашно животно" (domestic animal) - -**Playback:** -- `p` (lowercase p) should play audio_front.mp3 (pronunciation of "котка") -- `P` (uppercase P) should play audio_back.mp3 (pronunciation of "домашно животно") - -**Regeneration:** -- `a` (lowercase a) should regenerate audio_front.mp3 for "котка" -- `A` (uppercase A) should regenerate audio_back.mp3 for "домашно животно" - -### Current Implementation - -#### File Structure (Correct as of Jan 21, 2025) -``` -~/.local/state/totalrecall/cards/[CARD_ID]/ -├── word.txt # "котка" -├── translation.txt # "котка = домашно животно" -├── cardtype.txt # "bg-bg" -├── audio_front.mp3 # Audio for "котка" -├── audio_back.mp3 # Audio for "домашно животно" -└── [other files...] -``` - -#### Code Logic - -**Playback** (working correctly): -- `SetOnTypedRune()` in app.go (lines 2445-2452) - - `p/п` → calls `a.audioPlayer.Play()` → plays `audio_front.mp3` - - `P/П` → calls `a.audioPlayer.PlayBack()` → plays `audio_back.mp3` - -**Regeneration** (potentially buggy): -- `onRegenerateAudio()` (app.go lines 1034-1125) - - For bg-bg: calls `generateAudioFront(cardCtx, wordForGeneration, cardDir)` - - `wordForGeneration = a.currentWord` (the front word) - - Generates audio for the front word ✓ CORRECT - -- `onRegenerateBackAudio()` (app.go lines 1128-1191) - - Extracts: `translation = a.currentTranslation` - - Calls: `generateAudioBack(cardCtx, translation, cardDir)` - - Should generate audio for the back definition - - **VERIFY**: Is `a.currentTranslation` actually the back definition? - -### Debugging Steps - -#### 1. Check Card Files -```bash -# Navigate to an existing bg-bg card directory -cd ~/.local/state/totalrecall/cards/[CARD_ID]/ - -# Verify card type -cat cardtype.txt # Should be "bg-bg" - -# Verify translation file format -cat translation.txt # Should be "front_word = back_definition" - -# Verify audio files exist -ls -lh audio_*.mp3 # Should see audio_front.mp3 and audio_back.mp3 -``` - -#### 2. Check UI State When Loading Card -When you open a bg-bg card in the GUI: -1. Check the "Card Type" dropdown - should show "Bulgarian → Bulgarian" -2. Check the Bulgarian input field - should show the front word -3. Check the translation field - should show the back definition (not English!) -4. The placeholder text should say "Bulgarian definition..." not "English translation..." - -#### 3. Test Playback (Working Baseline) -Open a bg-bg card and test: -- Press `p` (lowercase) - which audio plays? (Front word or definition?) -- Press `P` (uppercase) - which audio plays? (Front word or definition?) - -#### 4. Test Regeneration -After confirming playback works correctly: -- Press `a` (lowercase) - which TEXT is sent for audio generation? Check console output for: - ``` - Generating front audio for '[TEXT]' - ``` -- Press `A` (uppercase) - which TEXT is sent for audio generation? Check console output for: - ``` - Generating back audio for '[TEXT]' - ``` - -### Data Flow for `A` (Regenerate Back Audio) - -``` -User presses 'A' - ↓ -SetOnTypedRune() case 'A', 'А' - ↓ -onRegenerateBackAudio() - ↓ -Get translation: - - translation = a.currentTranslation - - IF empty: translation = a.translationEntry.Text - ↓ -generateAudioBack(cardCtx, translation, cardDir) - ↓ -Generate audio for [translation] → audio_back.mp3 -``` - -### Potential Issues to Investigate - -1. **`a.currentTranslation` not set correctly** - - Check if `LoadExistingFiles()` properly extracts back definition from translation.txt - - Verify the split on "=" correctly extracts the part after the equals sign - - Confirm the value is actually the Bulgarian definition, not English - -2. **Translation field contains wrong value** - - When bg-bg card loads, does `a.translationEntry.SetText()` set the back definition or something else? - - Is the UI field showing the correct value? - -3. **Card type not detected correctly** - - Confirm `cardtype.txt` exists and contains "bg-bg" - - Verify `internal.LoadCardType()` works correctly - -4. **Field names confusion** - - The `translationEntry` field is named misleadingly - - For en-bg: it contains English translation - - For bg-bg: it contains Bulgarian definition - - Placeholder text should change to reflect this - -### Related Code Files - -- `internal/gui/app.go` - Hotkey handlers (lines 1034-1191 for audio, 2405-2453 for playback) -- `internal/gui/navigation.go` - Card loading logic (lines 364-460) -- `internal/gui/generator.go` - Audio generation functions (lines 156-220) -- `internal/gui/audio_player.go` - Audio playback implementation -- `internal/processor/processor.go` - Batch processing audio generation - -### Test Case - -Use this batch input to create a test card: -``` -котка == домашно животно -``` - -Then in the GUI: -1. Navigate to the "котка" card -2. Verify UI shows correct values -3. Press `p` and listen - should hear "котка" -4. Press `P` and listen - should hear "домашно животно" -5. Press `a` - check console for "Generating front audio for 'котка'" -6. Press `A` - check console for "Generating back audio for 'домашно животно'" (NOT "котка") - -### Notes - -- Batch processing works correctly (both audio files generated in correct locations as of Jan 21, 2025) -- GUI playback buttons work correctly and show proper labels ("Front" / "Back") -- The issue appears to be isolated to the `A` key regeneration logic -- Audio file generation itself is not the problem - the issue is which TEXT is being passed to the generator - diff --git a/cmd/test_audio_regen/main.go b/cmd/test_audio_regen/main.go new file mode 100644 index 0000000..7f0367c --- /dev/null +++ b/cmd/test_audio_regen/main.go @@ -0,0 +1,251 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + "time" +) + +// TestState mimics the Application state for a bg-bg card +type TestState struct { + currentWord string + currentCardType string + currentTranslation string + cardDir string +} + +// SimulateKey_a simulates pressing 'a' (regenerate front audio) +func (ts *TestState) SimulateKey_a() { + fmt.Println("\n" + repeatStr("=", 80)) + fmt.Println("SIMULATING: Key pressed 'a' (regenerate front audio)") + fmt.Println(repeatStr("=", 80)) + + fmt.Printf("Conditions check:\n") + fmt.Printf(" - currentWord: %s\n", ts.currentWord) + fmt.Printf(" - currentCardType: %s\n", ts.currentCardType) + + if ts.currentCardType != "bg-bg" { + fmt.Printf(" ✗ NOT a bg-bg card, would return\n") + return + } + + fmt.Printf(" ✓ Is bg-bg card, proceeding...\n") + + // Simulate generateAudioFront + frontFile := filepath.Join(ts.cardDir, "audio_front.mp3") + fmt.Printf("\nGenerating front audio for '%s'\n", ts.currentWord) + fmt.Printf(" Output file: %s\n", frontFile) + + // Create file + if err := createAudioFile(frontFile, "FRONT_"+ts.currentWord); err != nil { + fmt.Printf(" ✗ Error: %v\n", err) + return + } + + fmt.Printf(" ✓ Successfully wrote front audio\n") + fmt.Printf(" 📁 File: audio_front.mp3\n") +} + +// SimulateKey_A simulates pressing 'A' (regenerate back audio) +func (ts *TestState) SimulateKey_A() { + fmt.Println("\n" + repeatStr("=", 80)) + fmt.Println("SIMULATING: Key pressed 'A' (regenerate back audio)") + fmt.Println(repeatStr("=", 80)) + + fmt.Printf("Conditions check:\n") + fmt.Printf(" - currentWord: %s\n", ts.currentWord) + fmt.Printf(" - currentCardType: %s\n", ts.currentCardType) + fmt.Printf(" - currentTranslation: %s\n", ts.currentTranslation) + + if ts.currentCardType != "bg-bg" { + fmt.Printf(" ✗ NOT a bg-bg card, would return\n") + return + } + + fmt.Printf(" ✓ Is bg-bg card, proceeding...\n") + + translation := ts.currentTranslation + if translation == "" { + fmt.Printf(" ⚠ Translation empty, should fallback to UI field\n") + return + } + + fmt.Printf("\nGenerating back audio for '%s'\n", translation) + + // Simulate generateAudioBack + backFile := filepath.Join(ts.cardDir, "audio_back.mp3") + fmt.Printf(" Output file: %s\n", backFile) + + // Create file + if err := createAudioFile(backFile, "BACK_"+translation); err != nil { + fmt.Printf(" ✗ Error: %v\n", err) + return + } + + fmt.Printf(" ✓ Successfully wrote back audio\n") + fmt.Printf(" 📁 File: audio_back.mp3\n") +} + +// createAudioFile creates a fake audio file with content marker +func createAudioFile(path, marker string) error { + content := fmt.Sprintf("FAKE_AUDIO[%s]_GENERATED_AT_%d", marker, time.Now().Unix()) + return os.WriteFile(path, []byte(content), 0644) +} + +// VerifyResult checks which files were actually created/modified +func (ts *TestState) VerifyResult() { + fmt.Println("\n" + repeatStr("=", 80)) + fmt.Println("VERIFICATION: Which audio files were regenerated?") + fmt.Println(repeatStr("=", 80)) + + frontFile := filepath.Join(ts.cardDir, "audio_front.mp3") + backFile := filepath.Join(ts.cardDir, "audio_back.mp3") + + // Check which files exist and their content + frontContent, _ := os.ReadFile(frontFile) + backContent, _ := os.ReadFile(backFile) + + fmt.Printf("\nFront audio (audio_front.mp3):\n") + if len(frontContent) > 0 { + fmt.Printf(" ✓ Exists: %s\n", string(frontContent)) + if containsString(string(frontContent), "FRONT_"+ts.currentWord) { + fmt.Printf(" ✓ CORRECT: Contains front word '%s'\n", ts.currentWord) + } else if containsString(string(frontContent), "BACK_") { + fmt.Printf(" ✗ WRONG: Contains back definition (should be front!)\n") + } + } else { + fmt.Printf(" ✗ File does not exist or is empty\n") + } + + fmt.Printf("\nBack audio (audio_back.mp3):\n") + if len(backContent) > 0 { + fmt.Printf(" ✓ Exists: %s\n", string(backContent)) + if containsString(string(backContent), "BACK_"+ts.currentTranslation) { + fmt.Printf(" ✓ CORRECT: Contains back definition '%s'\n", ts.currentTranslation) + } else if containsString(string(backContent), "FRONT_") { + fmt.Printf(" ✗ WRONG: Contains front word (should be back!)\n") + } + } else { + fmt.Printf(" ✗ File does not exist or is empty\n") + } +} + +// TestSequence runs a complete test sequence +func (ts *TestState) TestSequence() { + fmt.Println("\n" + "╔" + repeatStr("=", 78) + "╗") + fmt.Println("║" + centerText("TEST: Audio Regeneration for bg-bg Cards", 78) + "║") + fmt.Println("╚" + repeatStr("=", 78) + "╝") + + fmt.Printf("\nInitial State:\n") + fmt.Printf(" Word: %s\n", ts.currentWord) + fmt.Printf(" Card Type: %s\n", ts.currentCardType) + fmt.Printf(" Translation: %s\n", ts.currentTranslation) + fmt.Printf(" Card Dir: %s\n", ts.cardDir) + + // Simulate pressing 'a' then 'A' + ts.SimulateKey_a() + fmt.Printf("\n[Simulating 'a' completing...]\n") + time.Sleep(500 * time.Millisecond) + + ts.SimulateKey_A() + fmt.Printf("\n[Simulating 'A' completing...]\n") + time.Sleep(500 * time.Millisecond) + + // Verify results + ts.VerifyResult() + + // Summary + printSummary(ts) +} + +func printSummary(ts *TestState) { + fmt.Println("\n" + repeatStr("=", 80)) + fmt.Println("SUMMARY") + fmt.Println(repeatStr("=", 80)) + + frontFile := filepath.Join(ts.cardDir, "audio_front.mp3") + backFile := filepath.Join(ts.cardDir, "audio_back.mp3") + + frontContent, _ := os.ReadFile(frontFile) + backContent, _ := os.ReadFile(backFile) + + frontCorrect := containsString(string(frontContent), "FRONT_"+ts.currentWord) + backCorrect := containsString(string(backContent), "BACK_"+ts.currentTranslation) + + fmt.Printf("\nTest Result: ") + if frontCorrect && backCorrect { + fmt.Printf("✓ PASS - Both sides regenerated correctly!\n\n") + fmt.Printf(" ✓ Front audio contains: %s\n", ts.currentWord) + fmt.Printf(" ✓ Back audio contains: %s\n", ts.currentTranslation) + } else if !frontCorrect && !backCorrect { + fmt.Printf("✗ FAIL - Both sides regenerated incorrectly!\n\n") + if containsString(string(frontContent), "BACK_") { + fmt.Printf(" ✗ Front audio contains BACK definition (wrong!)\n") + } + if containsString(string(backContent), "FRONT_") { + fmt.Printf(" ✗ Back audio contains FRONT word (wrong!)\n") + } + } else if !frontCorrect { + fmt.Printf("✗ FAIL - Front audio is wrong!\n\n") + fmt.Printf(" ✗ Front audio: %s\n", string(frontContent)) + } else if !backCorrect { + fmt.Printf("✗ FAIL - Back audio is wrong!\n\n") + fmt.Printf(" ✗ Back audio: %s\n", string(backContent)) + } +} + +func containsString(haystack, needle string) bool { + return len(haystack) > 0 && len(needle) > 0 && ( + containsSubstring(haystack, needle)) +} + +func containsSubstring(s, substr string) bool { + for i := 0; i <= len(s)-len(substr); i++ { + if s[i:i+len(substr)] == substr { + return true + } + } + return false +} + +func centerText(text string, width int) string { + padding := (width - len(text)) / 2 + left := "" + for i := 0; i < padding; i++ { + left += " " + } + right := "" + for i := 0; i < width-len(text)-padding; i++ { + right += " " + } + return left + text + right +} + +func repeatStr(s string, count int) string { + result := "" + for i := 0; i < count; i++ { + result += s + } + return result +} + +func main() { + // Create temp directory for test + tmpDir := "/tmp/totalrecall_test" + os.MkdirAll(tmpDir, 0755) + + // Test case: котка == домашно животно + state := &TestState{ + currentWord: "котка", + currentCardType: "bg-bg", + currentTranslation: "домашно животно", + cardDir: tmpDir, + } + + state.TestSequence() + + fmt.Println("\n" + repeatStr("=", 80)) + fmt.Printf("Test files created in: %s\n", tmpDir) + fmt.Println(repeatStr("=", 80) + "\n") +} diff --git a/internal/gui/app.go b/internal/gui/app.go index 7d48f75..ea4d840 100644 --- a/internal/gui/app.go +++ b/internal/gui/app.go @@ -1032,14 +1032,23 @@ func (a *Application) onRegenerateRandomImage() { // onRegenerateAudio regenerates front audio (or single audio for en-bg cards) func (a *Application) onRegenerateAudio() { + fmt.Printf("DEBUG: ████████████████████████████████████████████████████████████████\n") + fmt.Printf("DEBUG: ████ ENTERED onRegenerateAudio() - REGENERATING FRONT AUDIO\n") + fmt.Printf("DEBUG: ████████████████████████████████████████████████████████████████\n") + fmt.Printf("DEBUG (onRegenerateAudio): Starting front audio regeneration\n") + fmt.Printf(" - currentWord: %s\n", a.currentWord) + fmt.Printf(" - currentCardType: %s\n", a.currentCardType) + // Only disable the audio-related buttons a.regenerateAudioBtn.Disable() a.regenerateAllBtn.Disable() isBgBg := a.currentCardType == "bg-bg" if isBgBg { + fmt.Printf("DEBUG (onRegenerateAudio): Card type is bg-bg, regenerating FRONT audio only\n") a.showProgress("Regenerating front audio...") } else { + fmt.Printf("DEBUG (onRegenerateAudio): Card type is en-bg, regenerating single audio\n") a.showProgress("Regenerating audio...") } @@ -1052,6 +1061,7 @@ func (a *Application) onRegenerateAudio() { // Store the word we're generating for wordForGeneration := a.currentWord + fmt.Printf("DEBUG (onRegenerateAudio): In goroutine - wordForGeneration: %s\n", wordForGeneration) a.startOperation(wordForGeneration) defer a.endOperation(wordForGeneration) @@ -1083,7 +1093,10 @@ func (a *Application) onRegenerateAudio() { fyne.Do(func() { a.mu.Lock() if a.currentWord == wordForGeneration { - a.audioPlayer.SetAudioFile(audioFile) + // Set front audio WITHOUT auto-play initially + a.audioPlayer.SetAudioFileNoAutoPlay(audioFile) + // Then explicitly play ONLY the front audio + a.audioPlayer.Play() } a.mu.Unlock() }) @@ -1126,7 +1139,17 @@ func (a *Application) onRegenerateAudio() { // onRegenerateBackAudio regenerates back audio for bg-bg cards func (a *Application) onRegenerateBackAudio() { + fmt.Printf("DEBUG: ████████████████████████████████████████████████████████████████\n") + fmt.Printf("DEBUG: ████ ENTERED onRegenerateBackAudio() - REGENERATING BACK AUDIO\n") + fmt.Printf("DEBUG: ████████████████████████████████████████████████████████████████\n") + fmt.Printf("DEBUG (onRegenerateBackAudio): Starting back audio regeneration\n") + fmt.Printf(" - currentWord: %s\n", a.currentWord) + fmt.Printf(" - currentCardType: %s\n", a.currentCardType) + fmt.Printf(" - currentTranslation (state var): %s\n", a.currentTranslation) + fmt.Printf(" - translationEntry.Text (UI field): %s\n", a.translationEntry.Text) + if a.currentCardType != "bg-bg" { + fmt.Printf("DEBUG (onRegenerateBackAudio): Not a bg-bg card, returning\n") return } @@ -1141,17 +1164,30 @@ func (a *Application) onRegenerateBackAudio() { defer a.wg.Done() defer a.decrementProcessing() + // CRITICAL: Get translation from state variable first translation := a.currentTranslation + fmt.Printf("DEBUG (onRegenerateBackAudio): In goroutine - translation from a.currentTranslation: %s\n", translation) + fmt.Printf("DEBUG (onRegenerateBackAudio): In goroutine - translation UI field: %s\n", a.translationEntry.Text) + if translation == "" { + fmt.Printf("DEBUG (onRegenerateBackAudio): WARNING - translation state was empty, falling back to UI field\n") translation = strings.TrimSpace(a.translationEntry.Text) + fmt.Printf("DEBUG (onRegenerateBackAudio): Using UI field translation: %s\n", translation) } + wordForGeneration := a.currentWord - + fmt.Printf("DEBUG (onRegenerateBackAudio): Final decision - will generate back audio for: %s\n", translation) + fmt.Printf("DEBUG (onRegenerateBackAudio): (NOT for word: %s)\n", wordForGeneration) + + // For back audio, we need to use the main context, not create a new card context + // because the front audio regeneration already has an active context for this word. + // Creating a new context would cancel the front audio operation. + fmt.Printf("DEBUG (onRegenerateBackAudio): Using main context (not creating new card context)\n") + fmt.Printf("DEBUG (onRegenerateBackAudio): This prevents cancelling ongoing front audio operation\n") + a.startOperation(wordForGeneration) defer a.endOperation(wordForGeneration) - cardCtx, _ := a.getOrCreateCardContext(wordForGeneration) - cardDir, err := a.ensureCardDirectory(wordForGeneration) if err != nil { fyne.Do(func() { @@ -1160,7 +1196,17 @@ func (a *Application) onRegenerateBackAudio() { return } - audioFile, err := a.generateAudioBack(cardCtx, translation, cardDir) + fmt.Printf("DEBUG (onRegenerateBackAudio): Calling generateAudioBack with:\n") + fmt.Printf(" - ctx: a.ctx (main app context)\n") + fmt.Printf(" - translation: %s\n", translation) + fmt.Printf(" - cardDir: %s\n", cardDir) + + audioFile, err := a.generateAudioBack(a.ctx, translation, cardDir) + + fmt.Printf("DEBUG (onRegenerateBackAudio): generateAudioBack returned:\n") + fmt.Printf(" - err: %v\n", err) + fmt.Printf(" - audioFile: %s\n", audioFile) + if err != nil { fyne.Do(func() { a.showError(fmt.Errorf("Back audio regeneration failed: %w", err)) @@ -1174,6 +1220,8 @@ func (a *Application) onRegenerateBackAudio() { a.mu.Lock() if a.currentWord == wordForGeneration { a.audioPlayer.SetBackAudioFile(audioFile) + // Auto-play the regenerated back audio + a.audioPlayer.PlayBack() } a.mu.Unlock() }) @@ -2427,12 +2475,30 @@ func (a *Application) setupKeyboardShortcuts() { a.onRegenerateRandomImage() } case 'a', 'а': // a = regenerate front audio + fmt.Printf("DEBUG: ╔════════════════════════════════════════════════════════════════\n") + fmt.Printf("DEBUG: ║ KEY PRESSED: 'a' (lowercase, regenerate FRONT audio)\n") + fmt.Printf("DEBUG: ╚════════════════════════════════════════════════════════════════\n") + fmt.Printf(" - currentWord: %s\n", a.currentWord) + fmt.Printf(" - currentCardType: %s\n", a.currentCardType) + fmt.Printf(" - regenerateAudioBtn.Disabled(): %v\n", a.regenerateAudioBtn.Disabled()) + fmt.Printf(" - CALLING: onRegenerateAudio() for FRONT audio\n") if !a.regenerateAudioBtn.Disabled() { a.onRegenerateAudio() } case 'A', 'А': // A = regenerate back audio (for bg-bg cards) - if a.currentCardType == "bg-bg" && !a.regenerateAudioBtn.Disabled() { + fmt.Printf("DEBUG: ╔════════════════════════════════════════════════════════════════\n") + fmt.Printf("DEBUG: ║ KEY PRESSED: 'A' (uppercase, regenerate BACK audio)\n") + fmt.Printf("DEBUG: ╚════════════════════════════════════════════════════════════════\n") + fmt.Printf(" - currentWord: %s\n", a.currentWord) + fmt.Printf(" - currentCardType: %s\n", a.currentCardType) + fmt.Printf(" - currentTranslation: %s\n", a.currentTranslation) + fmt.Printf(" - translationEntry.Text: %s\n", a.translationEntry.Text) + fmt.Printf(" - regenerateAudioBtn.Disabled(): %v\n", a.regenerateAudioBtn.Disabled()) + if a.currentCardType == "bg-bg" { + fmt.Printf(" - CALLING: onRegenerateBackAudio() for BACK audio\n") a.onRegenerateBackAudio() + } else { + fmt.Printf("DEBUG: Skipping back audio regen - not a bg-bg card\n") } case 'р', 'Р': // р = r if !a.regenerateAllBtn.Disabled() { @@ -2443,12 +2509,24 @@ func (a *Application) setupKeyboardShortcuts() { a.onDelete() } case 'p', 'п': // p = play front audio + fmt.Printf("DEBUG: Key pressed 'p' (play front audio)\n") + fmt.Printf(" - currentWord: %s\n", a.currentWord) + fmt.Printf(" - currentCardType: %s\n", a.currentCardType) + fmt.Printf(" - currentAudioFile: %s\n", a.currentAudioFile) if a.currentAudioFile != "" { a.audioPlayer.Play() + } else { + fmt.Printf("DEBUG: No front audio file to play\n") } case 'P', 'П': // P = play back audio (for bg-bg cards) + fmt.Printf("DEBUG: Key pressed 'P' (play back audio)\n") + fmt.Printf(" - currentWord: %s\n", a.currentWord) + fmt.Printf(" - currentCardType: %s\n", a.currentCardType) + fmt.Printf(" - currentAudioFileBack: %s\n", a.currentAudioFileBack) if a.currentAudioFileBack != "" { a.audioPlayer.PlayBack() + } else { + fmt.Printf("DEBUG: No back audio file to play\n") } case 'ж', 'Ж': // ж = x a.onExportToAnki() @@ -2562,11 +2640,11 @@ func (a *Application) handleShortcutKey(key fyne.KeyName) { } a.onRegenerateRandomImage() - case fyne.KeyA: // Regenerate Audio - if a.regenerateAudioBtn.Disabled() { - return - } - a.onRegenerateAudio() + case fyne.KeyA: // Regenerate Audio (handled by custom OnTypedRune for proper case sensitivity) + // NOTE: This handler is disabled to use character-based handler instead + // For bg-bg cards: shift+A = back audio, a = front audio + // For en-bg cards: a/A = regenerate audio + // See handleTypedRune for actual implementation case fyne.KeyR: // Regenerate All if a.regenerateAllBtn.Disabled() { diff --git a/internal/gui/audio_player.go b/internal/gui/audio_player.go index 4846615..8ab7f76 100644 --- a/internal/gui/audio_player.go +++ b/internal/gui/audio_player.go @@ -96,8 +96,19 @@ func (p *AudioPlayer) CreateRenderer() fyne.WidgetRenderer { return widget.NewSimpleRenderer(p.container) } -// SetAudioFile sets the audio file to play +// SetAudioFile sets the audio file to play and optionally auto-plays it func (p *AudioPlayer) SetAudioFile(audioFile string) { + p.setAudioFileInternal(audioFile, true) // Enable auto-play +} + +// SetAudioFileNoAutoPlay sets the audio file without auto-playing +// Used when regenerating audio on bg-bg cards - we only want to play when explicitly requested +func (p *AudioPlayer) SetAudioFileNoAutoPlay(audioFile string) { + p.setAudioFileInternal(audioFile, false) // Disable auto-play +} + +// setAudioFileInternal is the internal implementation for setting audio files +func (p *AudioPlayer) setAudioFileInternal(audioFile string, allowAutoPlay bool) { p.audioFile = audioFile p.isPlaying = false @@ -139,8 +150,8 @@ func (p *AudioPlayer) SetAudioFile(audioFile string) { statusText := fmt.Sprintf("Audio: %s%s", filepath.Base(audioFile), p.voiceInfo) p.statusLabel.SetText(statusText) - // Auto-play if enabled - if p.autoPlayEnabled != nil && *p.autoPlayEnabled { + // Auto-play if enabled and allowed + if allowAutoPlay && p.autoPlayEnabled != nil && *p.autoPlayEnabled { // Small delay to ensure UI is ready go func() { // Wait a tiny bit for UI to be ready @@ -166,6 +177,9 @@ func (p *AudioPlayer) SetBackAudioFile(audioFile string) { p.playBackLabel.Show() // Update front label now that we know it's bg-bg p.playButtonLabel.SetText("Front") + // NOTE: Do NOT auto-play back audio + // Back audio regeneration just prepares the file + // User should press 'P' to listen to it } else { p.isBgBg = false p.playBackButton.Disable() @@ -221,18 +235,28 @@ func (p *AudioPlayer) SetAutoPlayEnabled(autoPlayEnabled *bool) { // onPlay handles play button click func (p *AudioPlayer) onPlay() { + fmt.Printf("DEBUG (onPlay): Starting playback\n") + fmt.Printf(" - audioFile: %s\n", p.audioFile) + fmt.Printf(" - audioFileBack: %s\n", p.audioFileBack) + fmt.Printf(" - isBgBg: %v\n", p.isBgBg) + fmt.Printf(" - isPlaying: %v\n", p.isPlaying) + if p.audioFile == "" { + fmt.Printf("DEBUG (onPlay): No audioFile set, returning\n") return } if p.isPlaying { // Pause functionality - just stop for now + fmt.Printf("DEBUG (onPlay): Already playing, stopping\n") p.onStop() return } // Start playing + fmt.Printf("DEBUG (onPlay): About to start playback for: %s\n", filepath.Base(p.audioFile)) if err := p.startPlayback(); err != nil { + fmt.Printf("DEBUG (onPlay): Error starting playback: %v\n", err) p.statusLabel.SetText(fmt.Sprintf("Error: %v", err)) return } @@ -241,35 +265,40 @@ func (p *AudioPlayer) onPlay() { p.playButton.SetIcon(theme.MediaPauseIcon()) p.stopButton.Enable() p.statusLabel.SetText(fmt.Sprintf("Playing: %s%s", filepath.Base(p.audioFile), p.voiceInfo)) + fmt.Printf("DEBUG (onPlay): Playback started successfully\n") } // onPlayBack handles back audio button click (for bg-bg cards) func (p *AudioPlayer) onPlayBack() { + fmt.Printf("DEBUG (onPlayBack): Starting back audio playback\n") + fmt.Printf(" - audioFile: %s\n", p.audioFile) + fmt.Printf(" - audioFileBack: %s\n", p.audioFileBack) + fmt.Printf(" - isBgBg: %v\n", p.isBgBg) + fmt.Printf(" - isPlaying: %v\n", p.isPlaying) + if p.audioFileBack == "" { + fmt.Printf("DEBUG (onPlayBack): No audioFileBack set, returning\n") return } if p.isPlaying { + fmt.Printf("DEBUG (onPlayBack): Already playing, stopping first\n") p.onStop() } - // Temporarily swap audio files to play the back audio - originalFile := p.audioFile - p.audioFile = p.audioFileBack - - if err := p.startPlayback(); err != nil { + // Start playback using back audio file directly + fmt.Printf("DEBUG (onPlayBack): About to start playback for back audio: %s\n", filepath.Base(p.audioFileBack)) + if err := p.startPlaybackForFile(p.audioFileBack); err != nil { + fmt.Printf("DEBUG (onPlayBack): Error starting playback: %v\n", err) p.statusLabel.SetText(fmt.Sprintf("Error: %v", err)) - p.audioFile = originalFile return } p.isPlaying = true - p.playButton.SetIcon(theme.MediaPauseIcon()) + p.playBackButton.SetIcon(theme.MediaPauseIcon()) // Back button, not front p.stopButton.Enable() p.statusLabel.SetText(fmt.Sprintf("Playing back audio: %s", filepath.Base(p.audioFileBack))) - - // Restore original file after playback starts - p.audioFile = originalFile + fmt.Printf("DEBUG (onPlayBack): Back audio playback started successfully\n") } // onStop handles stop button click @@ -280,7 +309,12 @@ func (p *AudioPlayer) onStop() { } p.isPlaying = false - p.playButton.SetIcon(theme.MediaPlayIcon()) + // Set correct button icon based on which audio was playing + if p.isBgBg && p.audioFileBack != "" { + p.playBackButton.SetIcon(theme.MediaPlayIcon()) // Back button if it was playing + } else { + p.playButton.SetIcon(theme.MediaPlayIcon()) // Front button otherwise + } p.stopButton.Disable() p.statusLabel.SetText(fmt.Sprintf("Stopped: %s%s", filepath.Base(p.audioFile), p.voiceInfo)) } @@ -304,32 +338,39 @@ func (p *AudioPlayer) PlayBack() { } // startPlayback starts audio playback using platform-specific commands +// This plays the front audio file (p.audioFile) func (p *AudioPlayer) startPlayback() error { + return p.startPlaybackForFile(p.audioFile) +} + +// startPlaybackForFile starts playback of a specific audio file +// This allows playing either front or back audio without modifying state +func (p *AudioPlayer) startPlaybackForFile(audioFile string) error { var cmd *exec.Cmd switch runtime.GOOS { case "darwin": // macOS - cmd = exec.Command("afplay", p.audioFile) + cmd = exec.Command("afplay", audioFile) case "linux": // Try multiple commands in order of preference // mpg123 first since it handles MP3 files best if _, err := exec.LookPath("mpg123"); err == nil { - cmd = exec.Command("mpg123", "-q", p.audioFile) // -q for quiet mode + cmd = exec.Command("mpg123", "-q", audioFile) // -q for quiet mode } else if _, err := exec.LookPath("ffplay"); err == nil { - cmd = exec.Command("ffplay", "-nodisp", "-autoexit", "-loglevel", "quiet", p.audioFile) + cmd = exec.Command("ffplay", "-nodisp", "-autoexit", "-loglevel", "quiet", audioFile) } else if _, err := exec.LookPath("play"); err == nil { // SoX play command - cmd = exec.Command("play", "-q", p.audioFile) + cmd = exec.Command("play", "-q", audioFile) } else if _, err := exec.LookPath("paplay"); err == nil { - cmd = exec.Command("paplay", p.audioFile) + cmd = exec.Command("paplay", audioFile) } else if _, err := exec.LookPath("aplay"); err == nil { - cmd = exec.Command("aplay", "-q", p.audioFile) + cmd = exec.Command("aplay", "-q", audioFile) } else { return fmt.Errorf("no audio player found. Install mpg123, ffplay, sox, paplay, or aplay") } case "windows": // Use Windows Media Player - cmd = exec.Command("cmd", "/c", "start", "/min", p.audioFile) + cmd = exec.Command("cmd", "/c", "start", "/min", audioFile) default: return fmt.Errorf("unsupported platform: %s", runtime.GOOS) } @@ -338,15 +379,23 @@ func (p *AudioPlayer) startPlayback() error { p.playCmd = cmd // Start playback in background + // Capture whether this is playing back audio or front audio for proper icon reset + isPlayingBack := audioFile == p.audioFileBack go func() { err := cmd.Run() if err == nil { // Playback finished normally fyne.Do(func() { p.isPlaying = false - p.playButton.SetIcon(theme.MediaPlayIcon()) + // Reset correct button icon based on which audio was playing + if isPlayingBack { + p.playBackButton.SetIcon(theme.MediaPlayIcon()) + p.statusLabel.SetText(fmt.Sprintf("Finished: %s", filepath.Base(p.audioFileBack))) + } else { + p.playButton.SetIcon(theme.MediaPlayIcon()) + p.statusLabel.SetText(fmt.Sprintf("Finished: %s%s", filepath.Base(p.audioFile), p.voiceInfo)) + } p.stopButton.Disable() - p.statusLabel.SetText(fmt.Sprintf("Finished: %s%s", filepath.Base(p.audioFile), p.voiceInfo)) }) } }() diff --git a/internal/gui/generator.go b/internal/gui/generator.go index a3a0a4d..4569191 100644 --- a/internal/gui/generator.go +++ b/internal/gui/generator.go @@ -154,7 +154,10 @@ func (a *Application) generateAudio(ctx context.Context, word string, cardDir st // generateAudioFront generates front audio for a bg-bg card func (a *Application) generateAudioFront(ctx context.Context, word string, cardDir string) (string, error) { + fmt.Printf("DEBUG (generateAudioFront): Called with word: %s, cardDir: %s\n", word, cardDir) + if cardDir == "" { + fmt.Printf("DEBUG (generateAudioFront): Card directory not provided, returning error\n") return "", fmt.Errorf("card directory not provided") } @@ -170,14 +173,18 @@ func (a *Application) generateAudioFront(ctx context.Context, word string, cardD provider, err := audio.NewProvider(&audioConfig) if err != nil { + fmt.Printf("DEBUG (generateAudioFront): Failed to create audio provider: %v\n", err) return "", err } + fmt.Printf("DEBUG (generateAudioFront): Generating front audio for '%s' with voice: %s, speed: %.2f\n", word, voice, speed) fmt.Printf("Generating front audio for '%s' with voice: %s, speed: %.2f\n", word, voice, speed) frontFile := filepath.Join(cardDir, fmt.Sprintf("audio_front.%s", a.config.AudioFormat)) + fmt.Printf("DEBUG (generateAudioFront): Will write to: %s\n", frontFile) if err := provider.GenerateAudio(ctx, word, frontFile); err != nil { return "", fmt.Errorf("failed to generate front audio: %w", err) } + fmt.Printf("DEBUG (generateAudioFront): Successfully wrote front audio to: %s\n", frontFile) // Update metadata metadataFile := filepath.Join(cardDir, "audio_metadata.txt") @@ -191,7 +198,10 @@ func (a *Application) generateAudioFront(ctx context.Context, word string, cardD // generateAudioBack generates back audio for a bg-bg card func (a *Application) generateAudioBack(ctx context.Context, text string, cardDir string) (string, error) { + fmt.Printf("DEBUG (generateAudioBack): Called with text: %s, cardDir: %s\n", text, cardDir) + if cardDir == "" { + fmt.Printf("DEBUG (generateAudioBack): Card directory not provided, returning error\n") return "", fmt.Errorf("card directory not provided") } @@ -207,14 +217,18 @@ func (a *Application) generateAudioBack(ctx context.Context, text string, cardDi provider, err := audio.NewProvider(&audioConfig) if err != nil { + fmt.Printf("DEBUG (generateAudioBack): Failed to create audio provider: %v\n", err) return "", err } + fmt.Printf("DEBUG (generateAudioBack): Generating back audio for '%s' with voice: %s, speed: %.2f\n", text, voice, speed) fmt.Printf("Generating back audio for '%s' with voice: %s, speed: %.2f\n", text, voice, speed) backFile := filepath.Join(cardDir, fmt.Sprintf("audio_back.%s", a.config.AudioFormat)) + fmt.Printf("DEBUG (generateAudioBack): Will write to: %s\n", backFile) if err := provider.GenerateAudio(ctx, text, backFile); err != nil { return "", fmt.Errorf("failed to generate back audio: %w", err) } + fmt.Printf("DEBUG (generateAudioBack): Successfully wrote back audio to: %s\n", backFile) return backFile, nil } diff --git a/internal/gui/navigation.go b/internal/gui/navigation.go index 1755397..79703de 100644 --- a/internal/gui/navigation.go +++ b/internal/gui/navigation.go @@ -377,13 +377,29 @@ func (a *Application) loadExistingFiles(word string) { if data, err := os.ReadFile(translationFile); err == nil { // Parse translation from "word = translation" format content := string(data) + fmt.Printf("DEBUG (loadExistingFiles): Read translation.txt: %s\n", content) parts := strings.Split(content, "=") + fmt.Printf("DEBUG (loadExistingFiles): Split into %d parts\n", len(parts)) if len(parts) >= 2 { - a.currentTranslation = strings.TrimSpace(parts[1]) + translation := strings.TrimSpace(parts[1]) + fmt.Printf("DEBUG (loadExistingFiles): Extracted translation (part 1, after '='): %s\n", translation) + + // CRITICAL: Set the state BEFORE SetText so it's available when needed + a.currentTranslation = translation + fmt.Printf("DEBUG (loadExistingFiles): Set a.currentTranslation state variable to: %s\n", a.currentTranslation) + fyne.Do(func() { - a.translationEntry.SetText(a.currentTranslation) + a.translationEntry.SetText(translation) + fmt.Printf("DEBUG (loadExistingFiles): Set translationEntry UI field to: %s\n", translation) + + // CRITICAL: After SetText, verify the state is correct + fmt.Printf("DEBUG (loadExistingFiles): After SetText, a.currentTranslation is: %s\n", a.currentTranslation) }) + } else { + fmt.Printf("DEBUG (loadExistingFiles): Translation file did not have '=' separator\n") } + } else { + fmt.Printf("DEBUG (loadExistingFiles): Could not read translation.txt: %v\n", err) } // Load image prompt file @@ -413,46 +429,61 @@ func (a *Application) loadExistingFiles(word string) { // Load card type and audio files cardType := internal.LoadCardType(wordDir) a.currentCardType = string(cardType) + fmt.Printf("DEBUG (loadExistingFiles): Loaded card type: %s (isBgBg: %v)\n", cardType, cardType.IsBgBg()) // Update UI card type selector fyne.Do(func() { if cardType.IsBgBg() { + fmt.Printf("DEBUG (loadExistingFiles): Setting UI to bg-bg (Bulgarian → Bulgarian)\n") a.cardTypeSelect.SetSelected("Bulgarian → Bulgarian") } else { + fmt.Printf("DEBUG (loadExistingFiles): Setting UI to en-bg (English → Bulgarian)\n") a.cardTypeSelect.SetSelected("English → Bulgarian") } }) // Load audio file(s) if cardType.IsBgBg() { + fmt.Printf("DEBUG (loadExistingFiles): Loading audio files for bg-bg card\n") // For bg-bg cards, load both front and back audio frontAudio := filepath.Join(wordDir, fmt.Sprintf("audio_front.%s", a.config.AudioFormat)) backAudio := filepath.Join(wordDir, fmt.Sprintf("audio_back.%s", a.config.AudioFormat)) if _, err := os.Stat(frontAudio); err == nil { a.currentAudioFile = frontAudio + fmt.Printf("DEBUG (loadExistingFiles): Found front audio: %s\n", frontAudio) fyne.Do(func() { a.audioPlayer.SetAudioFile(frontAudio) }) + } else { + fmt.Printf("DEBUG (loadExistingFiles): Front audio not found: %s\n", frontAudio) } if _, err := os.Stat(backAudio); err == nil { a.currentAudioFileBack = backAudio + fmt.Printf("DEBUG (loadExistingFiles): Found back audio: %s\n", backAudio) fyne.Do(func() { a.audioPlayer.SetBackAudioFile(backAudio) }) + } else { + fmt.Printf("DEBUG (loadExistingFiles): Back audio not found: %s\n", backAudio) } } else { + fmt.Printf("DEBUG (loadExistingFiles): Loading audio files for en-bg card\n") // For en-bg cards, load standard audio file audioFile := filepath.Join(wordDir, fmt.Sprintf("audio.%s", a.config.AudioFormat)) if _, err := os.Stat(audioFile); err == nil { a.currentAudioFile = audioFile + fmt.Printf("DEBUG (loadExistingFiles): Found audio: %s\n", audioFile) fyne.Do(func() { a.audioPlayer.SetAudioFile(audioFile) }) + } else { + fmt.Printf("DEBUG (loadExistingFiles): Audio not found: %s\n", audioFile) } // Hide back audio button for en-bg cards a.currentAudioFileBack = "" + fmt.Printf("DEBUG (loadExistingFiles): Clearing back audio for en-bg card\n") fyne.Do(func() { a.audioPlayer.SetBackAudioFile("") }) diff --git a/internal/version.go b/internal/version.go index 17deabe..7f93254 100644 --- a/internal/version.go +++ b/internal/version.go @@ -1,3 +1,3 @@ package internal -const Version = "0.7.5" +const Version = "0.8.0" @@ -0,0 +1,239 @@ +Loading files from directory: /home/paul/.local/state/totalrecall/cards/1769025513854_d6abfb17 +DEBUG (loadExistingFiles): Read translation.txt: котка = домашно животно + +DEBUG (loadExistingFiles): Split into 2 parts +DEBUG (loadExistingFiles): Extracted translation (part 1, after '='): домашно животно +DEBUG (loadExistingFiles): Set a.currentTranslation state variable to: домашно животно +DEBUG (loadExistingFiles): Set translationEntry UI field to: домашно животно +DEBUG (loadExistingFiles): After SetText, a.currentTranslation is: домашно животно +Loaded prompt from file: /home/paul/.local/state/totalrecall/cards/1769025513854_d6abfb17/image_prompt.txt +Loaded phonetic info from file: /home/paul/.local/state/totalrecall/cards/1769025513854_d6abfb17/phonetic.txt +DEBUG (loadExistingFiles): Loaded card type: bg-bg (isBgBg: true) +DEBUG (loadExistingFiles): Setting UI to bg-bg (Bulgarian → Bulgarian) +DEBUG (loadExistingFiles): Loading audio files for bg-bg card +DEBUG (loadExistingFiles): Found front audio: /home/paul/.local/state/totalrecall/cards/1769025513854_d6abfb17/audio_front.mp3 +DEBUG (loadExistingFiles): Found back audio: /home/paul/.local/state/totalrecall/cards/1769025513854_d6abfb17/audio_back.mp3 +DEBUG (onPlay): Starting playback + - audioFile: /home/paul/.local/state/totalrecall/cards/1769025513854_d6abfb17/audio_front.mp3 + - audioFileBack: /home/paul/.local/state/totalrecall/cards/1769025513854_d6abfb17/audio_back.mp3 + - isBgBg: true + - isPlaying: false +DEBUG (onPlay): About to start playback for: audio_front.mp3 +DEBUG (onPlay): Playback started successfully +DEBUG: ╔════════════════════════════════════════════════════════════════ +DEBUG: ║ KEY PRESSED: 'a' (lowercase, regenerate FRONT audio) +DEBUG: ╚════════════════════════════════════════════════════════════════ + - currentWord: котка + - currentCardType: bg-bg + - regenerateAudioBtn.Disabled(): false + - CALLING: onRegenerateAudio() for FRONT audio +DEBUG: ████████████████████████████████████████████████████████████████ +DEBUG: ████ ENTERED onRegenerateAudio() - REGENERATING FRONT AUDIO +DEBUG: ████████████████████████████████████████████████████████████████ +DEBUG (onRegenerateAudio): Starting front audio regeneration + - currentWord: котка + - currentCardType: bg-bg +DEBUG (onRegenerateAudio): Card type is bg-bg, regenerating FRONT audio only +DEBUG (onRegenerateAudio): In goroutine - wordForGeneration: котка +DEBUG (generateAudioFront): Called with word: котка, cardDir: /home/paul/.local/state/totalrecall/cards/1769025513854_d6abfb17 +DEBUG (generateAudioFront): Generating front audio for 'котка' with voice: onyx, speed: 0.92 +Generating front audio for 'котка' with voice: onyx, speed: 0.92 +DEBUG (generateAudioFront): Will write to: /home/paul/.local/state/totalrecall/cards/1769025513854_d6abfb17/audio_front.mp3 +OpenAI TTS: Using model 'gpt-4o-mini-tts' with voice 'onyx' at speed 0.92 +OpenAI TTS Instruction: 'You are speaking Bulgarian language (български език). Pronounce the Bulgarian text with authentic Bulgarian phonetics, not Russian. Speak slowly and clearly for language learners.' +OpenAI TTS Input: 'котка' +DEBUG (generateAudioFront): Successfully wrote front audio to: /home/paul/.local/state/totalrecall/cards/1769025513854_d6abfb17/audio_front.mp3 +DEBUG (onPlay): Starting playback + - audioFile: /home/paul/.local/state/totalrecall/cards/1769025513854_d6abfb17/audio_front.mp3 + - audioFileBack: /home/paul/.local/state/totalrecall/cards/1769025513854_d6abfb17/audio_back.mp3 + - isBgBg: true + - isPlaying: false +DEBUG (onPlay): About to start playback for: audio_front.mp3 +DEBUG (onPlay): Playback started successfully +DEBUG: ╔════════════════════════════════════════════════════════════════ +DEBUG: ║ KEY PRESSED: 'A' (uppercase, regenerate BACK audio) +DEBUG: ╚════════════════════════════════════════════════════════════════ + - currentWord: котка + - currentCardType: bg-bg + - currentTranslation: домашно животно + - translationEntry.Text: домашно животно + - regenerateAudioBtn.Disabled(): false + - CALLING: onRegenerateBackAudio() for BACK audio +DEBUG: ████████████████████████████████████████████████████████████████ +DEBUG: ████ ENTERED onRegenerateBackAudio() - REGENERATING BACK AUDIO +DEBUG: ████████████████████████████████████████████████████████████████ +DEBUG (onRegenerateBackAudio): Starting back audio regeneration + - currentWord: котка + - currentCardType: bg-bg + - currentTranslation (state var): домашно животно + - translationEntry.Text (UI field): домашно животно +DEBUG (onRegenerateBackAudio): In goroutine - translation from a.currentTranslation: домашно животно +DEBUG (onRegenerateBackAudio): In goroutine - translation UI field: домашно животно +DEBUG (onRegenerateBackAudio): Final decision - will generate back audio for: домашно животно +DEBUG (onRegenerateBackAudio): (NOT for word: котка) +DEBUG (onRegenerateBackAudio): Using main context (not creating new card context) +DEBUG (onRegenerateBackAudio): This prevents cancelling ongoing front audio operation +DEBUG (onRegenerateBackAudio): Calling generateAudioBack with: + - ctx: a.ctx (main app context) + - translation: домашно животно + - cardDir: /home/paul/.local/state/totalrecall/cards/1769025513854_d6abfb17 +DEBUG (generateAudioBack): Called with text: домашно животно, cardDir: /home/paul/.local/state/totalrecall/cards/1769025513854_d6abfb17 +DEBUG (generateAudioBack): Generating back audio for 'домашно животно' with voice: ash, speed: 1.00 +Generating back audio for 'домашно животно' with voice: ash, speed: 1.00 +DEBUG (generateAudioBack): Will write to: /home/paul/.local/state/totalrecall/cards/1769025513854_d6abfb17/audio_back.mp3 +OpenAI TTS: Using model 'gpt-4o-mini-tts' with voice 'ash' at speed 1.00 +OpenAI TTS Instruction: 'You are speaking Bulgarian language (български език). Pronounce the Bulgarian text with authentic Bulgarian phonetics, not Russian. Speak slowly and clearly for language learners.' +OpenAI TTS Input: 'домашно животно' +DEBUG (generateAudioBack): Successfully wrote back audio to: /home/paul/.local/state/totalrecall/cards/1769025513854_d6abfb17/audio_back.mp3 +DEBUG (onRegenerateBackAudio): generateAudioBack returned: + - err: <nil> + - audioFile: /home/paul/.local/state/totalrecall/cards/1769025513854_d6abfb17/audio_back.mp3 +DEBUG (onPlayBack): Starting back audio playback + - audioFile: /home/paul/.local/state/totalrecall/cards/1769025513854_d6abfb17/audio_front.mp3 + - audioFileBack: /home/paul/.local/state/totalrecall/cards/1769025513854_d6abfb17/audio_back.mp3 + - isBgBg: true + - isPlaying: false +DEBUG (onPlayBack): About to start playback for back audio: audio_back.mp3 +DEBUG (onPlayBack): Back audio playback started successfully +DEBUG: ╔════════════════════════════════════════════════════════════════ +DEBUG: ║ KEY PRESSED: 'A' (uppercase, regenerate BACK audio) +DEBUG: ╚════════════════════════════════════════════════════════════════ + - currentWord: котка + - currentCardType: bg-bg + - currentTranslation: домашно животно + - translationEntry.Text: домашно животно + - regenerateAudioBtn.Disabled(): false + - CALLING: onRegenerateBackAudio() for BACK audio +DEBUG: ████████████████████████████████████████████████████████████████ +DEBUG: ████ ENTERED onRegenerateBackAudio() - REGENERATING BACK AUDIO +DEBUG: ████████████████████████████████████████████████████████████████ +DEBUG (onRegenerateBackAudio): Starting back audio regeneration + - currentWord: котка + - currentCardType: bg-bg + - currentTranslation (state var): домашно животно + - translationEntry.Text (UI field): домашно животно +DEBUG (onRegenerateBackAudio): In goroutine - translation from a.currentTranslation: домашно животно +DEBUG (onRegenerateBackAudio): In goroutine - translation UI field: домашно животно +DEBUG (onRegenerateBackAudio): Final decision - will generate back audio for: домашно животно +DEBUG (onRegenerateBackAudio): (NOT for word: котка) +DEBUG (onRegenerateBackAudio): Using main context (not creating new card context) +DEBUG (onRegenerateBackAudio): This prevents cancelling ongoing front audio operation +DEBUG (onRegenerateBackAudio): Calling generateAudioBack with: + - ctx: a.ctx (main app context) + - translation: домашно животно + - cardDir: /home/paul/.local/state/totalrecall/cards/1769025513854_d6abfb17 +DEBUG (generateAudioBack): Called with text: домашно животно, cardDir: /home/paul/.local/state/totalrecall/cards/1769025513854_d6abfb17 +DEBUG (generateAudioBack): Generating back audio for 'домашно животно' with voice: echo, speed: 0.93 +Generating back audio for 'домашно животно' with voice: echo, speed: 0.93 +DEBUG (generateAudioBack): Will write to: /home/paul/.local/state/totalrecall/cards/1769025513854_d6abfb17/audio_back.mp3 +OpenAI TTS: Using model 'gpt-4o-mini-tts' with voice 'echo' at speed 0.93 +OpenAI TTS Instruction: 'You are speaking Bulgarian language (български език). Pronounce the Bulgarian text with authentic Bulgarian phonetics, not Russian. Speak slowly and clearly for language learners.' +OpenAI TTS Input: 'домашно животно' +DEBUG (generateAudioBack): Successfully wrote back audio to: /home/paul/.local/state/totalrecall/cards/1769025513854_d6abfb17/audio_back.mp3 +DEBUG (onRegenerateBackAudio): generateAudioBack returned: + - err: <nil> + - audioFile: /home/paul/.local/state/totalrecall/cards/1769025513854_d6abfb17/audio_back.mp3 +DEBUG (onPlayBack): Starting back audio playback + - audioFile: /home/paul/.local/state/totalrecall/cards/1769025513854_d6abfb17/audio_front.mp3 + - audioFileBack: /home/paul/.local/state/totalrecall/cards/1769025513854_d6abfb17/audio_back.mp3 + - isBgBg: true + - isPlaying: false +DEBUG (onPlayBack): About to start playback for back audio: audio_back.mp3 +DEBUG (onPlayBack): Back audio playback started successfully +DEBUG: Key pressed 'p' (play front audio) + - currentWord: котка + - currentCardType: bg-bg + - currentAudioFile: /home/paul/.local/state/totalrecall/cards/1769025513854_d6abfb17/audio_front.mp3 +DEBUG (onPlay): Starting playback + - audioFile: /home/paul/.local/state/totalrecall/cards/1769025513854_d6abfb17/audio_front.mp3 + - audioFileBack: /home/paul/.local/state/totalrecall/cards/1769025513854_d6abfb17/audio_back.mp3 + - isBgBg: true + - isPlaying: false +DEBUG (onPlay): About to start playback for: audio_front.mp3 +DEBUG (onPlay): Playback started successfully +DEBUG: ╔════════════════════════════════════════════════════════════════ +DEBUG: ║ KEY PRESSED: 'A' (uppercase, regenerate BACK audio) +DEBUG: ╚════════════════════════════════════════════════════════════════ + - currentWord: котка + - currentCardType: bg-bg + - currentTranslation: домашно животно + - translationEntry.Text: домашно животно + - regenerateAudioBtn.Disabled(): false + - CALLING: onRegenerateBackAudio() for BACK audio +DEBUG: ████████████████████████████████████████████████████████████████ +DEBUG: ████ ENTERED onRegenerateBackAudio() - REGENERATING BACK AUDIO +DEBUG: ████████████████████████████████████████████████████████████████ +DEBUG (onRegenerateBackAudio): Starting back audio regeneration + - currentWord: котка + - currentCardType: bg-bg + - currentTranslation (state var): домашно животно + - translationEntry.Text (UI field): домашно животно +DEBUG (onRegenerateBackAudio): In goroutine - translation from a.currentTranslation: домашно животно +DEBUG (onRegenerateBackAudio): In goroutine - translation UI field: домашно животно +DEBUG (onRegenerateBackAudio): Final decision - will generate back audio for: домашно животно +DEBUG (onRegenerateBackAudio): (NOT for word: котка) +DEBUG (onRegenerateBackAudio): Using main context (not creating new card context) +DEBUG (onRegenerateBackAudio): This prevents cancelling ongoing front audio operation +DEBUG (onRegenerateBackAudio): Calling generateAudioBack with: + - ctx: a.ctx (main app context) + - translation: домашно животно + - cardDir: /home/paul/.local/state/totalrecall/cards/1769025513854_d6abfb17 +DEBUG (generateAudioBack): Called with text: домашно животно, cardDir: /home/paul/.local/state/totalrecall/cards/1769025513854_d6abfb17 +DEBUG (generateAudioBack): Generating back audio for 'домашно животно' with voice: shimmer, speed: 0.98 +Generating back audio for 'домашно животно' with voice: shimmer, speed: 0.98 +DEBUG (generateAudioBack): Will write to: /home/paul/.local/state/totalrecall/cards/1769025513854_d6abfb17/audio_back.mp3 +OpenAI TTS: Using model 'gpt-4o-mini-tts' with voice 'shimmer' at speed 0.98 +OpenAI TTS Instruction: 'You are speaking Bulgarian language (български език). Pronounce the Bulgarian text with authentic Bulgarian phonetics, not Russian. Speak slowly and clearly for language learners.' +OpenAI TTS Input: 'домашно животно' +DEBUG (generateAudioBack): Successfully wrote back audio to: /home/paul/.local/state/totalrecall/cards/1769025513854_d6abfb17/audio_back.mp3 +DEBUG (onRegenerateBackAudio): generateAudioBack returned: + - err: <nil> + - audioFile: /home/paul/.local/state/totalrecall/cards/1769025513854_d6abfb17/audio_back.mp3 +DEBUG (onPlayBack): Starting back audio playback + - audioFile: /home/paul/.local/state/totalrecall/cards/1769025513854_d6abfb17/audio_front.mp3 + - audioFileBack: /home/paul/.local/state/totalrecall/cards/1769025513854_d6abfb17/audio_back.mp3 + - isBgBg: true + - isPlaying: false +DEBUG (onPlayBack): About to start playback for back audio: audio_back.mp3 +DEBUG (onPlayBack): Back audio playback started successfully +DEBUG: Key pressed 'p' (play front audio) + - currentWord: котка + - currentCardType: bg-bg + - currentAudioFile: /home/paul/.local/state/totalrecall/cards/1769025513854_d6abfb17/audio_front.mp3 +DEBUG (onPlay): Starting playback + - audioFile: /home/paul/.local/state/totalrecall/cards/1769025513854_d6abfb17/audio_front.mp3 + - audioFileBack: /home/paul/.local/state/totalrecall/cards/1769025513854_d6abfb17/audio_back.mp3 + - isBgBg: true + - isPlaying: false +DEBUG (onPlay): About to start playback for: audio_front.mp3 +DEBUG (onPlay): Playback started successfully +DEBUG: ╔════════════════════════════════════════════════════════════════ +DEBUG: ║ KEY PRESSED: 'a' (lowercase, regenerate FRONT audio) +DEBUG: ╚════════════════════════════════════════════════════════════════ + - currentWord: котка + - currentCardType: bg-bg + - regenerateAudioBtn.Disabled(): false + - CALLING: onRegenerateAudio() for FRONT audio +DEBUG: ████████████████████████████████████████████████████████████████ +DEBUG: ████ ENTERED onRegenerateAudio() - REGENERATING FRONT AUDIO +DEBUG: ████████████████████████████████████████████████████████████████ +DEBUG (onRegenerateAudio): Starting front audio regeneration + - currentWord: котка + - currentCardType: bg-bg +DEBUG (onRegenerateAudio): Card type is bg-bg, regenerating FRONT audio only +DEBUG (onRegenerateAudio): In goroutine - wordForGeneration: котка +DEBUG (generateAudioFront): Called with word: котка, cardDir: /home/paul/.local/state/totalrecall/cards/1769025513854_d6abfb17 +DEBUG (generateAudioFront): Generating front audio for 'котка' with voice: coral, speed: 0.91 +Generating front audio for 'котка' with voice: coral, speed: 0.91 +DEBUG (generateAudioFront): Will write to: /home/paul/.local/state/totalrecall/cards/1769025513854_d6abfb17/audio_front.mp3 +OpenAI TTS: Using model 'gpt-4o-mini-tts' with voice 'coral' at speed 0.91 +OpenAI TTS Instruction: 'You are speaking Bulgarian language (български език). Pronounce the Bulgarian text with authentic Bulgarian phonetics, not Russian. Speak slowly and clearly for language learners.' +OpenAI TTS Input: 'котка' +DEBUG (generateAudioFront): Successfully wrote front audio to: /home/paul/.local/state/totalrecall/cards/1769025513854_d6abfb17/audio_front.mp3 +DEBUG (onPlay): Starting playback + - audioFile: /home/paul/.local/state/totalrecall/cards/1769025513854_d6abfb17/audio_front.mp3 + - audioFileBack: /home/paul/.local/state/totalrecall/cards/1769025513854_d6abfb17/audio_back.mp3 + - isBgBg: true + - isPlaying: false +DEBUG (onPlay): About to start playback for: audio_front.mp3 +DEBUG (onPlay): Playback started successfully |
