1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
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")
}
|