diff options
| author | Paul Buetow <paul@buetow.org> | 2025-07-18 14:00:13 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2025-07-18 14:00:13 +0300 |
| commit | a6e9947b904406ec5b49e88c77689d5c6ef6d04b (patch) | |
| tree | fc89df6a57d6ee3e8ef3487d9d07d01797b4c696 /cmd | |
| parent | e2f45921c45c9322c2ddb679d0511ba20772dcae (diff) | |
feat: major refactor - APKG export support and subdirectory organization
Major Features:
- Added native .apkg (Anki package) export format with embedded media
- Reorganized file structure to use subdirectories per word
- Enhanced GUI export dialog with format selection (APKG/CSV)
APKG Export Implementation:
- Created apkg_generator.go with full SQLite-based Anki package generation
- Includes custom card templates with professional CSS styling
- Front side: Image + English word
- Back side: Image + Bulgarian word + Audio + Notes
- All media files automatically embedded in package
- Custom deck names supported via --deck-name flag
Directory Structure Changes:
- Each word now gets its own subdirectory (e.g., anki_cards/ябълка/)
- All related files (audio, images, translations, prompts) stored together
- Cleaner organization and easier management
- Prevents file naming conflicts
GUI Updates:
- Export dialog no longer shows file browser, exports directly to anki_cards
- Format selection between APKG (recommended) and CSV (legacy)
- Fixed navigation to properly load image prompts from subdirectories
- Delete function now moves entire word directory to trash
CLI Updates:
- --anki flag now generates APKG by default
- --anki-csv flag for legacy CSV format
- All file generation uses subdirectory structure
Bug Fixes:
- Fixed handling of multi-word entries (e.g., "картоф картофи")
- Fixed GenerateFromDirectory to properly handle words with underscores
- Fixed phonetic files being treated as separate cards
- Fixed image prompt preservation during navigation
Breaking Changes:
- File structure changed from flat to subdirectory-based
- Existing files need to be reorganized into subdirectories
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
Diffstat (limited to 'cmd')
| -rw-r--r-- | cmd/totalrecall/main.go | 62 |
1 files changed, 49 insertions, 13 deletions
diff --git a/cmd/totalrecall/main.go b/cmd/totalrecall/main.go index 83695e8..04174bc 100644 --- a/cmd/totalrecall/main.go +++ b/cmd/totalrecall/main.go @@ -32,6 +32,8 @@ var ( skipAudio bool skipImages bool generateAnki bool + ankiCSV bool + deckName string listModels bool allVoices bool guiMode bool @@ -81,7 +83,9 @@ func init() { rootCmd.Flags().StringVar(&batchFile, "batch", "", "Process words from file (one per line)") rootCmd.Flags().BoolVar(&skipAudio, "skip-audio", false, "Skip audio generation") rootCmd.Flags().BoolVar(&skipImages, "skip-images", false, "Skip image download") - rootCmd.Flags().BoolVar(&generateAnki, "anki", false, "Generate Anki import CSV file") + rootCmd.Flags().BoolVar(&generateAnki, "anki", false, "Generate Anki import file (APKG format by default, use --anki-csv for legacy CSV)") + rootCmd.Flags().BoolVar(&ankiCSV, "anki-csv", false, "Generate legacy CSV format instead of APKG when using --anki") + rootCmd.Flags().StringVar(&deckName, "deck-name", "Bulgarian Vocabulary", "Deck name for APKG export") rootCmd.Flags().BoolVar(&listModels, "list-models", false, "List available OpenAI models for the current API key") rootCmd.Flags().BoolVar(&allVoices, "all-voices", false, "Generate audio in all available voices (creates multiple files)") rootCmd.Flags().BoolVar(&guiMode, "gui", false, "Launch interactive GUI mode") @@ -203,13 +207,17 @@ func runCommand(cmd *cobra.Command, args []string) error { } } - // Generate Anki CSV if requested + // Generate Anki file if requested if generateAnki { fmt.Printf("\nGenerating Anki import file...\n") - if err := generateAnkiCSV(); err != nil { - fmt.Fprintf(os.Stderr, "Warning: Failed to generate Anki CSV: %v\n", err) + if err := generateAnkiFile(); err != nil { + fmt.Fprintf(os.Stderr, "Warning: Failed to generate Anki file: %v\n", err) } else { - fmt.Println("Anki import file created: anki_import.csv") + if ankiCSV { + fmt.Println("Anki import file created: anki_import.csv") + } else { + fmt.Printf("Anki package created: %s.apkg\n", deckName) + } } } @@ -332,12 +340,18 @@ func generateAudioWithVoice(word, voice string) error { ctx := context.Background() filename := sanitizeFilename(word) + // Create subdirectory for this word + wordDir := filepath.Join(outputDir, filename) + if err := os.MkdirAll(wordDir, 0755); err != nil { + return fmt.Errorf("failed to create word directory: %w", err) + } + // Add voice name to filename if generating multiple voices var outputFile string if allVoices { - outputFile = filepath.Join(outputDir, fmt.Sprintf("%s_%s.%s", filename, voice, audioFormat)) + outputFile = filepath.Join(wordDir, fmt.Sprintf("%s_%s.%s", filename, voice, audioFormat)) } else { - outputFile = filepath.Join(outputDir, fmt.Sprintf("%s.%s", filename, audioFormat)) + outputFile = filepath.Join(wordDir, fmt.Sprintf("%s.%s", filename, audioFormat)) } // Generate the audio @@ -403,9 +417,16 @@ func downloadImages(word string) error { return fmt.Errorf("unknown image provider: %s", imageAPI) } + // Create subdirectory for this word + filename := sanitizeFilename(word) + wordDir := filepath.Join(outputDir, filename) + if err := os.MkdirAll(wordDir, 0755); err != nil { + return fmt.Errorf("failed to create word directory: %w", err) + } + // Create downloader downloadOpts := &image.DownloadOptions{ - OutputDir: outputDir, + OutputDir: wordDir, OverwriteExisting: true, // Allow overwriting existing files CreateDir: true, FileNamePattern: "{word}_{index}", @@ -484,7 +505,7 @@ func isSpace(r rune) bool { return r == ' ' || r == '\t' || r == '\n' || r == '\r' } -func generateAnkiCSV() error { +func generateAnkiFile() error { // Create Anki generator gen := anki.NewGenerator(&anki.GeneratorOptions{ OutputPath: filepath.Join(outputDir, "anki_import.csv"), @@ -505,9 +526,17 @@ func generateAnkiCSV() error { } } - // Generate CSV - if err := gen.GenerateCSV(); err != nil { - return fmt.Errorf("failed to generate CSV: %w", err) + if ankiCSV { + // Generate CSV + if err := gen.GenerateCSV(); err != nil { + return fmt.Errorf("failed to generate CSV: %w", err) + } + } else { + // Generate APKG + outputPath := filepath.Join(outputDir, fmt.Sprintf("%s.apkg", sanitizeFilename(deckName))) + if err := gen.GenerateAPKG(outputPath, deckName); err != nil { + return fmt.Errorf("failed to generate APKG: %w", err) + } } // Print stats @@ -646,7 +675,14 @@ func translateWord(word string) (string, error) { func saveTranslation(word, translation string) error { // Save translation to a text file filename := sanitizeFilename(word) - outputFile := filepath.Join(outputDir, fmt.Sprintf("%s_translation.txt", filename)) + wordDir := filepath.Join(outputDir, filename) + + // Ensure directory exists + if err := os.MkdirAll(wordDir, 0755); err != nil { + return fmt.Errorf("failed to create word directory: %w", err) + } + + outputFile := filepath.Join(wordDir, fmt.Sprintf("%s_translation.txt", filename)) content := fmt.Sprintf("%s = %s\n", word, translation) |
