mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-11 22:25:41 +08:00
feat(ingestion): mirror Go pipeline progress into the document table;
harden resume guards
- pipeline: bind the owning document via WithDocumentID; after each
TrackProgress event aggregate ingestion_task_log progress and mirror
progress/run/progress_msg back into the document table, so GET
/api/v1/datasets/{dataset_id}/documents reflects live Go pipeline
progress without a bespoke endpoint.
- canvas: extend the S3 resume guard to reject legacy no-op nodes (e.g.
ExitLoop) so component_total equals the count of progress-reporting
components and the aggregate percent can reach 100%.
- runtime/canvas: route progress through TrackProgress; add interrupt
test coverage (r3_interrupt_test.go).
- dao/entity: add IngestionTask.DocumentID column and AggregateProgress
support used by the mirror; IngestionTaskLog keeps a Checkpoint column
alongside the progress fields.
feat(deepdoc): cache DocAnalyzer inference results in Redis (1h TTL)
- Redis-backed DocAnalyzerCache decorator over inference.Client; cache
key = "ddoc:cache:<method>:" + sha256 of the JPEG-encoded image bytes
(deterministic).
- TTL = 1h; hits skip the inner HTTP call and return cached JSON; inner
errors are not cached.
refactor(deepdoc): align figure cropping with Python cropout + bounded
page caches
- CropSectionByDLA mirrors Python cropout: best-overlap DLA
figure/equation region, fallback to section bbox per page, vertical
concat on gray background.
- sliding-window page-image cache bounds peak memory to the recent
window instead of the whole PDF.
- rename DLADebug -> DLARegions across parser/chunker/tests.
refactor(parser): drop lib_type selector; align NewXxxParser with
NewPDFParser
- remove config["lib_type"] lookup and the libType param/field/switch
from all nine constructors; surface the CGO-required error at
ParseWithResult time instead of construction time; drop resolveLibType,
its test, and the four lib_type constants.
feat(utility): add a reusable workerpool for bounded concurrent
execution
- internal/utility/workerpool.go (+ tests).
refactor: translate Chinese prose comments to English in non-harness Go
files.
chore: upgrade github.com/cloudwego/eino from v0.9.9 to v0.9.12.
473 lines
17 KiB
Go
473 lines
17 KiB
Go
package pdf
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"image"
|
|
inf "ragflow/internal/deepdoc/parser/pdf/inference"
|
|
lyt "ragflow/internal/deepdoc/parser/pdf/layout"
|
|
tbl "ragflow/internal/deepdoc/parser/pdf/table"
|
|
pdf "ragflow/internal/deepdoc/parser/pdf/type"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// ── MockDocAnalyzer tests ──────────────────────────────────────────────
|
|
|
|
func TestMockDocAnalyzer(t *testing.T) {
|
|
mock := &MockDocAnalyzer{
|
|
Healthy: true,
|
|
DLARegions: []pdf.DLARegion{
|
|
{X0: 0, Y0: 0, X1: 100, Y1: 100, Label: "table", Confidence: 0.95},
|
|
},
|
|
TSRCells: []pdf.TSRCell{
|
|
{X0: 0, Y0: 0, X1: 50, Y1: 30, Text: "A"},
|
|
},
|
|
}
|
|
|
|
if !mock.Health() {
|
|
t.Error("mock should be healthy")
|
|
}
|
|
regions, _ := mock.DLA(context.Background(), nil)
|
|
if len(regions) != 1 || regions[0].Label != "table" {
|
|
t.Error("mock DLA returned wrong data")
|
|
}
|
|
cells, _ := mock.TSR(context.Background(), nil)
|
|
if len(cells) != 1 || cells[0].Text != "A" {
|
|
t.Error("mock TSR returned wrong data")
|
|
}
|
|
// OCRDetect + OCRRecognize replaces deprecated OCR — tested in TestOCR_scanPage/TestOCR_fallback.
|
|
_ = mock.OCRDetect
|
|
_ = mock.OCRRecognize
|
|
|
|
// Unhealthy mock
|
|
mock2 := &MockDocAnalyzer{Healthy: false}
|
|
if mock2.Health() {
|
|
t.Error("unhealthy mock should return false")
|
|
}
|
|
}
|
|
|
|
// ── enrichOnePageWithDeepDoc noop ───────────────────────────────────────
|
|
|
|
func TestEnrichOnePageWithDeepDoc_Noop(t *testing.T) {
|
|
boxes := []pdf.TextBox{
|
|
{PageNumber: 0, X0: 50, X1: 550, Top: 100, Bottom: 112, Text: "text"},
|
|
}
|
|
dummyImg := image.NewRGBA(image.Rect(0, 0, 900, 600))
|
|
|
|
p := NewParser(pdf.DefaultParserConfig())
|
|
mock := &MockDocAnalyzer{Healthy: false}
|
|
_, tables, _ := p.enrichOnePageWithDeepDoc(context.Background(), dummyImg, boxes, 0, nil, mock, NewTableBuilderFor(mock), pdf.DlaScale)
|
|
if len(tables) != 0 {
|
|
t.Error("unhealthy DeepDoc → 0 Tables")
|
|
}
|
|
}
|
|
|
|
// ── enrichOnePageWithDeepDoc with mock ───────────────────────────────────
|
|
|
|
func TestEnrichOnePageWithDeepDoc_Mock(t *testing.T) {
|
|
boxes := []pdf.TextBox{
|
|
{PageNumber: 0, X0: 80, X1: 500, Top: 200, Bottom: 550, Text: "cell 1"},
|
|
{PageNumber: 0, X0: 80, X1: 500, Top: 550, Bottom: 760, Text: "cell 2"},
|
|
{PageNumber: 0, X0: 50, X1: 550, Top: 100, Bottom: 180, Text: "heading"},
|
|
{PageNumber: 0, X0: 50, X1: 550, Top: 780, Bottom: 850, Text: "below"},
|
|
}
|
|
mock := &MockDocAnalyzer{
|
|
Healthy: true,
|
|
DLARegions: []pdf.DLARegion{
|
|
{X0: 250, Y0: 600, X1: 1500, Y1: 2300, Label: "table", Confidence: 0.95},
|
|
},
|
|
TSRCells: []pdf.TSRCell{
|
|
{X0: 0, Y0: 0, X1: 600, Y1: 400, Text: "A1"},
|
|
{X0: 600, Y0: 0, X1: 1240, Y1: 400, Text: "B1"},
|
|
{X0: 0, Y0: 410, X1: 600, Y1: 800, Text: "A2"},
|
|
{X0: 600, Y0: 410, X1: 1240, Y1: 800, Text: "B2"},
|
|
},
|
|
}
|
|
p := NewParser(pdf.DefaultParserConfig())
|
|
dummyImg := image.NewRGBA(image.Rect(0, 0, 2000, 3000))
|
|
|
|
_, tables, _ := p.enrichOnePageWithDeepDoc(context.Background(), dummyImg, boxes, 0, nil, mock, NewTableBuilderFor(mock), pdf.DlaScale)
|
|
if len(tables) != 1 {
|
|
t.Fatalf("expected 1 pdf.TableItem, got %d", len(tables))
|
|
}
|
|
tbl := tables[0]
|
|
if len(tbl.Cells) != 4 {
|
|
t.Errorf("expected 4 cells, got %d", len(tbl.Cells))
|
|
}
|
|
// Rows populated later by constructTable via extractTableAndReplace.
|
|
if tbl.ImageB64 == "" {
|
|
t.Error("ImageB64 empty")
|
|
}
|
|
if len(tbl.Positions) != 2 {
|
|
t.Errorf("expected 2 Positions, got %d", len(tbl.Positions))
|
|
}
|
|
}
|
|
|
|
func TestEnrichOnePageWithDeepDoc_NoTables(t *testing.T) {
|
|
mock := &MockDocAnalyzer{Healthy: true, DLARegions: []pdf.DLARegion{}}
|
|
p := NewParser(pdf.DefaultParserConfig())
|
|
dummy := image.NewRGBA(image.Rect(0, 0, 1000, 1000))
|
|
_, tables, _ := p.enrichOnePageWithDeepDoc(context.Background(), dummy, nil, 0, nil, mock, NewTableBuilderFor(mock), pdf.DlaScale)
|
|
if len(tables) != 0 {
|
|
t.Errorf("0 tables expected, got %d", len(tables))
|
|
}
|
|
}
|
|
|
|
func TestEnrichOnePageWithDeepDoc_NonTableRegions(t *testing.T) {
|
|
mock := &MockDocAnalyzer{
|
|
Healthy: true,
|
|
DLARegions: []pdf.DLARegion{
|
|
{X0: 150, Y0: 300, X1: 1650, Y1: 336, Label: "text", Confidence: 0.9},
|
|
{X0: 150, Y0: 600, X1: 1650, Y1: 900, Label: "figure", Confidence: 0.8},
|
|
},
|
|
}
|
|
p := NewParser(pdf.DefaultParserConfig())
|
|
dummy := image.NewRGBA(image.Rect(0, 0, 2000, 2000))
|
|
_, tables, _ := p.enrichOnePageWithDeepDoc(context.Background(), dummy, nil, 0, nil, mock, NewTableBuilderFor(mock), pdf.DlaScale)
|
|
if len(tables) != 0 {
|
|
t.Errorf("non-table regions → 0 tables, got %d", len(tables))
|
|
}
|
|
}
|
|
|
|
func TestEnrichOnePageWithDeepDoc_NoOverlap(t *testing.T) {
|
|
boxes := []pdf.TextBox{
|
|
{PageNumber: 0, X0: 50, X1: 550, Top: 10, Bottom: 30, Text: "far away"},
|
|
}
|
|
mock := &MockDocAnalyzer{
|
|
Healthy: true,
|
|
DLARegions: []pdf.DLARegion{
|
|
{X0: 150, Y0: 1500, X1: 1500, Y1: 2300, Label: "table", Confidence: 0.95},
|
|
},
|
|
}
|
|
p := NewParser(pdf.DefaultParserConfig())
|
|
dummy := image.NewRGBA(image.Rect(0, 0, 2000, 3000))
|
|
_, tables, _ := p.enrichOnePageWithDeepDoc(context.Background(), dummy, boxes, 0, nil, mock, NewTableBuilderFor(mock), pdf.DlaScale)
|
|
if len(tables) != 0 {
|
|
t.Errorf("no overlap → 0 tables, got %d", len(tables))
|
|
}
|
|
}
|
|
|
|
func TestEnrichOnePageWithDeepDoc_TSRError(t *testing.T) {
|
|
boxes := []pdf.TextBox{
|
|
{PageNumber: 0, X0: 80, X1: 500, Top: 210, Bottom: 660, Text: "cell"},
|
|
}
|
|
mock := &MockDocAnalyzer{
|
|
Healthy: true,
|
|
DLARegions: []pdf.DLARegion{
|
|
{X0: 250, Y0: 600, X1: 1500, Y1: 2000, Label: "table", Confidence: 0.95},
|
|
},
|
|
TSRCells: nil, // TSR returns nothing
|
|
}
|
|
p := NewParser(pdf.DefaultParserConfig())
|
|
dummy := image.NewRGBA(image.Rect(0, 0, 2000, 3000))
|
|
_, tables, _ := p.enrichOnePageWithDeepDoc(context.Background(), dummy, boxes, 0, nil, mock, NewTableBuilderFor(mock), pdf.DlaScale)
|
|
if len(tables) != 1 {
|
|
t.Fatalf("TSR failure: expected 1 pdf.TableItem with image+positions, got %d", len(tables))
|
|
}
|
|
if tables[0].ImageB64 == "" {
|
|
t.Error("should have image despite TSR failure")
|
|
}
|
|
if len(tables[0].Positions) == 0 {
|
|
t.Error("should have positions despite TSR failure")
|
|
}
|
|
if len(tables[0].Rows) != 0 {
|
|
t.Errorf("TSR failure → 0 rows, got %d", len(tables[0].Rows))
|
|
}
|
|
}
|
|
|
|
func TestEnrichOnePageWithDeepDoc_DLAError(t *testing.T) {
|
|
// DLA returns only non-table regions → 0 tables
|
|
mock := &MockDocAnalyzer{Healthy: true, DLARegions: []pdf.DLARegion{
|
|
{X0: 0, Y0: 0, X1: 100, Y1: 100, Label: "text", Confidence: 0.9},
|
|
}}
|
|
p := NewParser(pdf.DefaultParserConfig())
|
|
dummy := image.NewRGBA(image.Rect(0, 0, 1000, 1000))
|
|
_, tables, _ := p.enrichOnePageWithDeepDoc(context.Background(), dummy, nil, 0, nil, mock, NewTableBuilderFor(mock), pdf.DlaScale)
|
|
if len(tables) != 0 {
|
|
t.Errorf("non-table DLA → 0 tables, got %d", len(tables))
|
|
}
|
|
}
|
|
|
|
func TestParse_TableLinkedToSections(t *testing.T) {
|
|
// Simulate enrichOnePageWithDeepDoc → extractTableAndReplace → boxesToSections:
|
|
// table boxes are popped and replaced with one HTML box.
|
|
boxes := []pdf.TextBox{
|
|
{PageNumber: 0, X0: 50, X1: 200, Top: 50, Bottom: 80, Text: "heading"},
|
|
{PageNumber: 0, X0: 50, X1: 500, Top: 250, Bottom: 400, Text: "table text", LayoutType: "table"},
|
|
{PageNumber: 0, X0: 50, X1: 200, Top: 450, Bottom: 480, Text: "after"},
|
|
}
|
|
tableItem := pdf.TableItem{
|
|
Cells: []pdf.TSRCell{
|
|
{X0: 0, Y0: 0, X1: 200, Y1: 50, Label: "table row"},
|
|
{X0: 0, Y0: 51, X1: 200, Y1: 100, Label: "table row"},
|
|
},
|
|
Positions: []pdf.Position{{PageNumbers: []int{0}, Left: 50, Right: 500, Top: 250, Bottom: 400}},
|
|
Scale: 1.0,
|
|
}
|
|
|
|
boxes = tbl.ExtractTableAndReplace(boxes, []pdf.TableItem{tableItem})
|
|
sections := lyt.BoxesToSections(boxes, nil)
|
|
|
|
// 3 boxes (heading, table, after) → 3 sections (heading, HTML, after).
|
|
if len(sections) != 3 {
|
|
t.Errorf("expected 3 sections, got %d", len(sections))
|
|
}
|
|
tableFound := false
|
|
for _, s := range sections {
|
|
if s.LayoutType == "table" && strings.Contains(s.Text, "<table>") {
|
|
tableFound = true
|
|
}
|
|
}
|
|
if !tableFound {
|
|
t.Errorf("expected at least one section with HTML table")
|
|
for _, s := range sections {
|
|
t.Logf(" section text=%q LayoutType=%q", s.Text[:min(40, len(s.Text))], s.LayoutType)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── cropImageRegion ────────────────────────────────────────────────────
|
|
// ── enrichOnePageWithDeepDoc: invalid DLA region ────────────────────────
|
|
|
|
func TestEnrichOnePageWithDeepDoc_InvalidRegion(t *testing.T) {
|
|
// DLA returns a table region with x1 < x0. The pipeline should skip
|
|
// this table gracefully (Python raises ValueError from PIL.Image.crop).
|
|
mock := &MockDocAnalyzer{
|
|
Healthy: true,
|
|
DLARegions: []pdf.DLARegion{
|
|
{X0: 500, Y0: 100, X1: 100, Y1: 300, Label: "table", Confidence: 0.9},
|
|
},
|
|
}
|
|
p := NewParser(pdf.DefaultParserConfig())
|
|
dummy := image.NewRGBA(image.Rect(0, 0, 1000, 1000))
|
|
_, tables, _ := p.enrichOnePageWithDeepDoc(context.Background(), dummy, nil, 0, nil, mock, NewTableBuilderFor(mock), pdf.DlaScale)
|
|
if len(tables) != 0 {
|
|
t.Errorf("invalid DLA region should be skipped, got %d tables", len(tables))
|
|
}
|
|
}
|
|
|
|
// ── DLA → figure end-to-end ───────────────────────────────────────────
|
|
|
|
func TestParse_CollectsFigures(t *testing.T) {
|
|
// End-to-end: Parse() with mock DeepDoc that labels a box as "figure".
|
|
// Verify p.Figures is populated.
|
|
|
|
eng := &MockEngine{NumPages: 1, Chars: map[int][]pdf.TextChar{0: {{X0: 50, X1: 550, Top: 100, Bottom: 112, Text: "chart image"}}}}
|
|
mock := &MockDocAnalyzer{
|
|
Healthy: true,
|
|
DLARegions: []pdf.DLARegion{
|
|
{X0: 50, Y0: 200, X1: 2000, Y1: 1000, Label: "figure", Confidence: 0.85},
|
|
},
|
|
}
|
|
p := NewParser(pdf.DefaultParserConfig())
|
|
|
|
result, err := p.ParseRaw(context.Background(), eng, mock)
|
|
if err != nil {
|
|
t.Fatalf("Parse: %v", err)
|
|
}
|
|
if len(result.Sections) == 0 {
|
|
t.Fatal("expected at least 1 section")
|
|
}
|
|
if len(result.Figures()) != 1 {
|
|
t.Fatalf("expected 1 figure, got %d", len(result.Figures()))
|
|
}
|
|
if result.Figures()[0].LayoutType != "figure" {
|
|
t.Errorf("figure LayoutType = %q, want 'figure'", result.Figures()[0].LayoutType)
|
|
}
|
|
if result.Figures()[0].Text == "" {
|
|
t.Error("figure Text should not be empty")
|
|
}
|
|
}
|
|
|
|
func TestParse_NoFigures(t *testing.T) {
|
|
// Parse() with no DLA figure regions → p.Figures should be empty.
|
|
|
|
eng := &MockEngine{NumPages: 1, Chars: map[int][]pdf.TextChar{0: {{X0: 50, X1: 550, Top: 100, Bottom: 112, Text: "just text"}}}}
|
|
mock := &MockDocAnalyzer{
|
|
DLARegions: []pdf.DLARegion{
|
|
{X0: 150, Y0: 300, X1: 1500, Y1: 600, Label: "text", Confidence: 0.8},
|
|
},
|
|
}
|
|
p := NewParser(pdf.DefaultParserConfig())
|
|
|
|
result, err := p.ParseRaw(context.Background(), eng, mock)
|
|
if err != nil {
|
|
t.Fatalf("Parse: %v", err)
|
|
}
|
|
if len(result.Figures()) != 0 {
|
|
t.Fatalf("expected 0 figures, got %d", len(result.Figures()))
|
|
}
|
|
}
|
|
|
|
func TestParse_NoDeepDoc_NoFigures(t *testing.T) {
|
|
// Parse() with mock DeepDoc → Figures should be empty (no DLA-detected figures).
|
|
|
|
eng := &MockEngine{NumPages: 1, Chars: map[int][]pdf.TextChar{0: {{X0: 50, X1: 550, Top: 100, Bottom: 112, Text: "text"}}}}
|
|
mock := &MockDocAnalyzer{Healthy: true}
|
|
p := NewParser(pdf.DefaultParserConfig())
|
|
|
|
result, err := p.ParseRaw(context.Background(), eng, mock)
|
|
if err != nil {
|
|
t.Fatalf("Parse: %v", err)
|
|
}
|
|
if len(result.Figures()) != 0 {
|
|
t.Fatalf("expected 0 Figures (no DLA-detected figures), got %d", len(result.Figures()))
|
|
}
|
|
}
|
|
|
|
// ── Parse + ocrMergeChars (full-page detect) ──────────────────────────
|
|
|
|
func TestParse_UsesOCRDetectForEmbeddedChars(t *testing.T) {
|
|
// When DeepDoc is available and the page has embedded chars,
|
|
// Parse should use ocrMergeChars (detect → merge → recognize).
|
|
eng := &MockEngine{
|
|
NumPages: 1,
|
|
Chars: map[int][]pdf.TextChar{0: {
|
|
{X0: 10, X1: 30, Top: 10, Bottom: 30, Text: "Hello", PageNumber: 0},
|
|
}},
|
|
}
|
|
mock := &MockDocAnalyzer{
|
|
Healthy: true,
|
|
OCRBoxes: []pdf.OCRBox{
|
|
{X0: 5, Y0: 5, X1: 50, Y1: 5, X2: 50, Y2: 50, X3: 5, Y3: 50},
|
|
},
|
|
}
|
|
p := NewParser(pdf.DefaultParserConfig())
|
|
|
|
result, err := p.ParseRaw(context.Background(), eng, mock)
|
|
if err != nil {
|
|
t.Fatalf("Parse: %v", err)
|
|
}
|
|
if len(result.Sections) == 0 {
|
|
t.Fatal("expected at least 1 section")
|
|
}
|
|
// The box should come from OCR detect, not charsToBoxes.
|
|
// Verifying that ocrMergeChars was used (sections exist).
|
|
if result.Metrics.BoxesInitial == 0 {
|
|
t.Error("expected BoxesInitial > 0 (OCR detect path)")
|
|
}
|
|
}
|
|
|
|
func TestParse_FallsBackToCharsToBoxes_NoDeepDoc(t *testing.T) {
|
|
// Without DeepDoc, Parse should use charsToBoxes (unchanged behavior).
|
|
eng := &MockEngine{
|
|
NumPages: 1,
|
|
Chars: map[int][]pdf.TextChar{0: {
|
|
{X0: 10, X1: 30, Top: 10, Bottom: 30, Text: "Hello", PageNumber: 0},
|
|
}},
|
|
}
|
|
mock := &MockDocAnalyzer{Healthy: true}
|
|
p := NewParser(pdf.DefaultParserConfig())
|
|
|
|
result, err := p.ParseRaw(context.Background(), eng, mock)
|
|
if err != nil {
|
|
t.Fatalf("Parse: %v", err)
|
|
}
|
|
if len(result.Sections) == 0 {
|
|
t.Fatal("expected at least 1 section (charsToBoxes)")
|
|
}
|
|
}
|
|
|
|
func TestParse_FallsBackToCharsToBoxes_EmptyOCRBoxes(t *testing.T) {
|
|
// OCRDetect returns no boxes → falls through to charsToBoxes.
|
|
eng := &MockEngine{
|
|
NumPages: 1,
|
|
Chars: map[int][]pdf.TextChar{0: {
|
|
{X0: 10, X1: 30, Top: 10, Bottom: 30, Text: "Hello", PageNumber: 0},
|
|
}},
|
|
}
|
|
mock := &MockDocAnalyzer{
|
|
Healthy: true,
|
|
OCRBoxes: []pdf.OCRBox{}, // empty detect
|
|
}
|
|
p := NewParser(pdf.DefaultParserConfig())
|
|
|
|
result, err := p.ParseRaw(context.Background(), eng, mock)
|
|
if err != nil {
|
|
t.Fatalf("Parse: %v", err)
|
|
}
|
|
if len(result.Sections) == 0 {
|
|
t.Fatal("expected at least 1 section (charsToBoxes fallback)")
|
|
}
|
|
}
|
|
|
|
// ── Error path coverage ────────────────────────────────────────────────
|
|
|
|
func TestMockDocAnalyzer_DLAError_DoesNotCrash(t *testing.T) {
|
|
mock := &MockDocAnalyzer{
|
|
Healthy: true,
|
|
DLAErr: fmt.Errorf("DLA service unavailable"),
|
|
}
|
|
p := NewParser(pdf.DefaultParserConfig())
|
|
img := image.NewRGBA(image.Rect(0, 0, 100, 100))
|
|
boxes := []pdf.TextBox{
|
|
{PageNumber: 0, X0: 0, X1: 100, Top: 0, Bottom: 50, Text: "text"},
|
|
}
|
|
// enrichOnePageWithDeepDoc should return nil (not panic) on DLA error.
|
|
_, tables, _ := p.enrichOnePageWithDeepDoc(context.Background(), img, boxes, 0, nil, mock, NewTableBuilderFor(mock), pdf.DlaScale)
|
|
if len(tables) != 0 {
|
|
t.Errorf("DLA error should produce 0 tables, got %d", len(tables))
|
|
}
|
|
}
|
|
|
|
func TestMockDocAnalyzer_TSRError_DoesNotCrash(t *testing.T) {
|
|
// TSR error: DLA succeeds, TSR fails. The table region is detected
|
|
// but no cells are returned — the table is skipped gracefully.
|
|
mock := &MockDocAnalyzer{
|
|
Healthy: true,
|
|
DLARegions: []pdf.DLARegion{
|
|
{X0: 0, Y0: 0, X1: 400, Y1: 400, Label: "table", Confidence: 0.95},
|
|
},
|
|
TSRErr: fmt.Errorf("TSR model timeout"),
|
|
}
|
|
p := NewParser(pdf.DefaultParserConfig())
|
|
img := image.NewRGBA(image.Rect(0, 0, 100, 100))
|
|
boxes := []pdf.TextBox{
|
|
{PageNumber: 0, X0: 10, X1: 90, Top: 10, Bottom: 90, Text: "in table region"},
|
|
}
|
|
_, tables, _ := p.enrichOnePageWithDeepDoc(context.Background(), img, boxes, 0, nil, mock, NewTableBuilderFor(mock), pdf.DlaScale)
|
|
// DLA detects the table region → 1 pdf.TableItem is created. TSR failure
|
|
// means it has no cells, but the pipeline must not panic.
|
|
if len(tables) != 1 {
|
|
t.Errorf("TSR error: expected 1 table (DLA region found), got %d", len(tables))
|
|
}
|
|
if len(tables[0].Cells) != 0 {
|
|
t.Errorf("TSR error: Cells should be empty, got %d", len(tables[0].Cells))
|
|
}
|
|
}
|
|
|
|
func TestMockDocAnalyzer_OCRDetectError_DoesNotCrash(t *testing.T) {
|
|
// OCRDetect failure path: processPageBoxes uses ocrDetectAndRecognize which
|
|
// calls doc.OCRDetect. When it fails, the page is skipped gracefully.
|
|
mock := &MockDocAnalyzer{Healthy: true, OCRDetectErr: fmt.Errorf("OCR model OOM")}
|
|
eng := &MockEngine{
|
|
NumPages: 1,
|
|
Chars: map[int][]pdf.TextChar{}, // empty → triggers OCR path
|
|
}
|
|
p := NewParser(pdf.DefaultParserConfig())
|
|
_, err := p.ParseRaw(context.Background(), eng, mock)
|
|
if err != nil {
|
|
t.Fatalf("Parse returned error: %v", err)
|
|
}
|
|
// Parse should succeed — the page with OCRDetect error is just skipped.
|
|
}
|
|
|
|
// TestTSRLabels verifies Go inf.DefaultTSRLabels() matches Python's table_structure_recognizer.py labels.
|
|
// Order must be exact — the ONNX model returns class IDs that index into this array.
|
|
func TestTSRLabels(t *testing.T) {
|
|
want := []string{
|
|
"table", "table column", "table row",
|
|
"table column header", "table projected row header",
|
|
"table spanning cell",
|
|
}
|
|
if len(inf.DefaultTSRLabels()) != len(want) {
|
|
t.Fatalf("inf.DefaultTSRLabels() length %d, want %d", len(inf.DefaultTSRLabels()), len(want))
|
|
}
|
|
for i := range want {
|
|
if inf.DefaultTSRLabels()[i] != want[i] {
|
|
t.Errorf("inf.DefaultTSRLabels()[%d] = %q, want %q", i, inf.DefaultTSRLabels()[i], want[i])
|
|
}
|
|
}
|
|
}
|