// SRC-10: Spracherkennung & OCR-Qualitätsbewertung. Tesseract erkennt bei // kombinierten Sprachpaketen (Languages = "deu+eng") nicht, WELCHE Sprache // tatsächlich vorlag — daher wird das Bild hier bewusst EINZELN mit jedem // Kandidaten aus candidateLanguages erkannt, und die Sprache mit dem // höheren durchschnittlichen Worterkennungs-Konfidenzwert gewinnt // (Akzeptanzkriterium 1). Derselbe Tesseract-TSV-Lauf liefert den // Konfidenzwert direkt mit (Akzeptanzkriterium 2) — keine zweite externe // Bibliothek für Sprach- ODER Konfidenzerkennung nötig. package ocr import ( "bytes" "context" "fmt" "os" "os/exec" "path/filepath" "strconv" "strings" ) // candidateLanguages sind die unterstützten Einzelsprachen — muss eine // Teilmenge der in Languages kombinierten Sprachpakete sein. var candidateLanguages = []string{"deu", "eng"} // RecognizedText ist das Ergebnis einer sprach- und konfidenzbewussten // Texterkennung. type RecognizedText struct { Text string // Language ist der ISO-639-2-Sprachcode aus candidateLanguages mit dem // höchsten Konfidenzwert (Akzeptanzkriterium 1). Language string // Confidence ist der durchschnittliche Tesseract-Worterkennungs- // Konfidenzwert (0–100) der gewählten Sprache (Akzeptanzkriterium 2). Confidence float64 } // RecognizeWithLanguageAndConfidence erkennt Sprache und Text eines Bildes // und liefert einen auswertbaren Konfidenzwert. func RecognizeWithLanguageAndConfidence(ctx context.Context, imageBytes []byte) (RecognizedText, error) { var best RecognizedText haveResult := false for _, lang := range candidateLanguages { text, confidence, err := recognizeWithTSV(ctx, imageBytes, lang) if err != nil { return RecognizedText{}, err } if !haveResult || confidence > best.Confidence { best = RecognizedText{Text: text, Language: lang, Confidence: confidence} haveResult = true } } return best, nil } // recognizeWithTSV führt Tesseract mit GENAU einer Sprache im TSV-Ausgabe- // modus aus und liefert den zusammengesetzten Text sowie den // durchschnittlichen Konfidenzwert über alle erkannten Wörter (Tesseract- // TSV-Zeilen mit level=5, conf>=0 — negative Konfidenzwerte markieren // Struktur-/Layoutzeilen ohne eigenen Text, siehe Tesseract-TSV-Format). func recognizeWithTSV(ctx context.Context, imageBytes []byte, lang string) (text string, confidence float64, err error) { workDir, err := os.MkdirTemp("", "mail-ocr-lang-*") if err != nil { return "", 0, fmt.Errorf("ocr: arbeitsverzeichnis anlegen: %w", err) } defer func() { _ = os.RemoveAll(workDir) }() imagePath := filepath.Join(workDir, "input") if err := os.WriteFile(imagePath, imageBytes, 0o600); err != nil { return "", 0, fmt.Errorf("ocr: bild schreiben: %w", err) } outBase := filepath.Join(workDir, "output") cmd := exec.CommandContext(ctx, "tesseract", imagePath, outBase, "-l", lang, "tsv") var stderr bytes.Buffer cmd.Stderr = &stderr if err := cmd.Run(); err != nil { return "", 0, fmt.Errorf("ocr: tesseract (tsv, %s) fehlgeschlagen: %w (%s)", lang, err, stderr.String()) } tsvBytes, err := os.ReadFile(outBase + ".tsv") if err != nil { return "", 0, fmt.Errorf("ocr: tsv-ausgabe lesen: %w", err) } return parseTSV(string(tsvBytes)) } // tsvWordLevel ist der Tesseract-TSV-"level"-Wert für einzelne Wörter // (1=Seite, 2=Block, 3=Absatz, 4=Zeile, 5=Wort). const tsvWordLevel = "5" func parseTSV(tsv string) (text string, confidence float64, err error) { lines := strings.Split(tsv, "\n") var words []string var confSum float64 var confCount int for i, line := range lines { if i == 0 || strings.TrimSpace(line) == "" { continue // Kopfzeile bzw. Leerzeile überspringen } cols := strings.Split(line, "\t") if len(cols) < 12 || cols[0] != tsvWordLevel { continue } conf, convErr := strconv.ParseFloat(cols[10], 64) if convErr != nil || conf < 0 { continue } word := cols[11] if word == "" { continue } words = append(words, word) confSum += conf confCount++ } if confCount == 0 { return "", 0, nil } return strings.Join(words, " "), confSum / float64(confCount), nil }