summaryrefslogtreecommitdiff
path: root/cmd
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2025-07-18 14:00:13 +0300
committerPaul Buetow <paul@buetow.org>2025-07-18 14:00:13 +0300
commita6e9947b904406ec5b49e88c77689d5c6ef6d04b (patch)
treefc89df6a57d6ee3e8ef3487d9d07d01797b4c696 /cmd
parente2f45921c45c9322c2ddb679d0511ba20772dcae (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.go62
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)