summaryrefslogtreecommitdiff
path: root/internal/processor/batch_processor.go
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-04-08 10:01:57 +0300
committerPaul Buetow <paul@buetow.org>2026-04-08 10:01:57 +0300
commit08d7d36ac6d49d7d5efd39a837bc08c9c52b6eb8 (patch)
tree8ce1eecdeb832dadf1d35a4edf496542e13de2a0 /internal/processor/batch_processor.go
parent35a3c860f63403d14c83668dcdd017b6ef517fbf (diff)
refactor(processor): split BatchProcessor, AnkiExporter, CLIConfigResolver
Extract CLIConfigResolver for flag/config precedence and GUI wiring; embed it on Processor so audio and image helpers use promoted accessors. Move batch file flow to BatchProcessor and Anki generation to AnkiExporter; Processor delegates while keeping per-word processing and translation cache. Made-with: Cursor
Diffstat (limited to 'internal/processor/batch_processor.go')
-rw-r--r--internal/processor/batch_processor.go129
1 files changed, 129 insertions, 0 deletions
diff --git a/internal/processor/batch_processor.go b/internal/processor/batch_processor.go
new file mode 100644
index 0000000..faa515f
--- /dev/null
+++ b/internal/processor/batch_processor.go
@@ -0,0 +1,129 @@
+package processor
+
+// BatchProcessor runs multi-word batch files: translation pass, validation,
+// per-word processing with timeouts, and summary output. It delegates
+// per-word work and skip detection to Processor.
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "path/filepath"
+ "time"
+
+ "codeberg.org/snonux/totalrecall/internal/audio"
+ "codeberg.org/snonux/totalrecall/internal/batch"
+)
+
+// BatchProcessor orchestrates batch file processing. It holds a reference to
+// the main Processor for shared services (translation, card directories,
+// ProcessWordWithTranslationAndType).
+type BatchProcessor struct {
+ p *Processor
+}
+
+// ProcessBatch processes multiple words from a batch file.
+// It first translates any entries that have English-to-Bulgarian translation
+// needs, then validates all Bulgarian words, and finally processes each word
+// with a per-word timeout to prevent a single hung API call from stalling the batch.
+func (b *BatchProcessor) ProcessBatch() error {
+ p := b.p
+ entries, err := batch.ReadBatchFile(p.Flags.BatchFile)
+ if err != nil {
+ return err
+ }
+
+ if err := os.MkdirAll(p.Flags.OutputDir, 0755); err != nil {
+ return fmt.Errorf("failed to create output directory: %w", err)
+ }
+
+ if err := b.translateBatchEntries(entries); err != nil {
+ return err
+ }
+
+ if err := b.validateBatchEntries(entries); err != nil {
+ return err
+ }
+
+ skipped, processed, errCount := b.processBatchEntries(entries)
+
+ b.printBatchSummary(len(entries), processed, skipped, errCount)
+ return nil
+}
+
+// translateBatchEntries runs the first pass over entries that need English→Bulgarian
+// translation and mutates the slice in place with the result.
+func (b *BatchProcessor) translateBatchEntries(entries []batch.WordEntry) error {
+ p := b.p
+ for i, entry := range entries {
+ if !entry.NeedsTranslation || entry.Translation == "" {
+ continue
+ }
+ bulgarian, err := p.translator.TranslateEnglishToBulgarian(entry.Translation)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Error translating '%s' to Bulgarian: %v\n", entry.Translation, err)
+ continue
+ }
+ entries[i].Bulgarian = bulgarian
+ fmt.Printf("Translated '%s' to Bulgarian: %s\n", entry.Translation, bulgarian)
+ }
+ return nil
+}
+
+// validateBatchEntries checks that every entry with a Bulgarian word contains
+// only valid Bulgarian text. Returns on the first validation failure.
+func (b *BatchProcessor) validateBatchEntries(entries []batch.WordEntry) error {
+ for _, entry := range entries {
+ if entry.Bulgarian == "" {
+ continue
+ }
+ if err := audio.ValidateBulgarianText(entry.Bulgarian); err != nil {
+ return fmt.Errorf("invalid word '%s': %w", entry.Bulgarian, err)
+ }
+ }
+ return nil
+}
+
+// processBatchEntries iterates the validated entries and processes each word,
+// skipping words that are already fully processed. Returns skip, process, and
+// error counts for the summary.
+func (b *BatchProcessor) processBatchEntries(entries []batch.WordEntry) (skipped, processed, errCount int) {
+ p := b.p
+ for i, entry := range entries {
+ if entry.Bulgarian == "" {
+ continue
+ }
+
+ fmt.Printf("\nProcessing %d/%d: %s\n", i+1, len(entries), entry.Bulgarian)
+
+ if p.isWordFullyProcessed(entry.Bulgarian) {
+ wordDir := p.findCardDirectory(entry.Bulgarian)
+ fmt.Printf(" ✓ Skipping '%s' - already fully processed in %s\n", entry.Bulgarian, filepath.Base(wordDir))
+ skipped++
+ continue
+ }
+
+ wordCtx, wordCancel := context.WithTimeout(context.Background(), 5*time.Minute)
+ err := p.ProcessWordWithTranslationAndType(wordCtx, entry.Bulgarian, entry.Translation, entry.CardType)
+ wordCancel()
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Error processing '%s': %v\n", entry.Bulgarian, err)
+ errCount++
+ } else {
+ processed++
+ }
+ }
+ return
+}
+
+// printBatchSummary prints a human-readable summary of the batch run.
+func (b *BatchProcessor) printBatchSummary(total, processed, skipped, errCount int) {
+ fmt.Printf("\n=== Batch Processing Summary ===\n")
+ fmt.Printf("Total words: %d\n", total)
+ fmt.Printf("Processed: %d\n", processed)
+ fmt.Printf("Skipped (already complete): %d\n", skipped)
+ if errCount > 0 {
+ fmt.Printf("Errors: %d\n", errCount)
+ }
+ fmt.Printf("================================\n")
+}