Files
nexarch/mail/internal/ocr/language.go
T
sysopsandClaude Sonnet 5 1a246abdb3 SRC-10: spracherkennung-ocr-qualitaetsbewertung
Spracherkennung für OCR-Texte und Qualitätsbewertung (Konfidenzwert), um
schlechte OCR-Ergebnisse kenntlich zu machen. Letztes Ticket vor QA-03.

- ocr/language.go: RecognizeWithLanguageAndConfidence erkennt ein Bild
  einzeln je Kandidatensprache (deu/eng) im Tesseract-TSV-Modus — die
  Sprache mit höherem Konfidenzwert gewinnt, derselbe Lauf liefert den
  Konfidenzwert direkt mit.
- search: neue Felder ocr_language/ocr_confidence (Migrationen 0006/0007,
  gleiches ALTER-Muster wie SRC-05), in Document/Result gespiegelt.
  Client.AttachmentsBelowConfidence filtert gezielt auf niedrige
  Konfidenz, schließt Dokumente ohne OCR-Anhang aus.
- Regressionsbug gefunden und behoben: reindex.go (SRC-09) kannte die
  neuen OCR-Spalten nicht, Reindex wäre mit "unknown column"
  fehlgeschlagen.

Prüfungen (alle real durchgeführt, siehe mail/docs/SRC-10-PRUEFPROTOKOLL.md):
1. TestRecognizeWithLanguageAndConfidence_MultilingualCorpus: deutsches
   und englisches Testbild real korrekt als deu/eng erkannt.
2. TestRecognizeWithLanguageAndConfidence_DegradedImageLowersConfidence:
   künstliche Verschlechterung senkt Konfidenz real von 91,76 auf 28,21.
3. TestAttachmentsBelowConfidence_QueryReturnsExpectedResults: Abfrage
   unterhalb Schwelle liefert real genau die erwarteten 2 von 4 Treffern.

Kein Umbau: Search/Facets/SearchWithFilters/Index/Delete-Verhalten sonst
unverändert, dedup/indexworker/storage/crypto/encstorage/savedsearch
unverändert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HhgFcLS8tYMhDJpP74C6AQ
2026-08-31 22:16:26 +02:00

125 lines
4.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 (0100) 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
}