Align Go parser backends and PDF pipeline with Python (#16676)

Ports remaining Go parser wiring and PDF backends, adds tenant-aware VLM
dispatch, aligns post-processing with Python, and adds end-to-end
pipeline coverage with a generated six-page PDF.
This commit is contained in:
Zhichang Yu
2026-07-06 19:50:54 +08:00
committed by GitHub
parent 3044283442
commit 55af1d70f3
36 changed files with 4918 additions and 28 deletions

View File

@@ -21,8 +21,11 @@ import (
"encoding/base64"
"errors"
"fmt"
"image"
"os"
"sort"
"strings"
"time"
deepdocpdf "ragflow/internal/deepdoc/parser/pdf"
"ragflow/internal/deepdoc/parser/pdf/inference"
@@ -36,17 +39,84 @@ import (
// is compiled behind `//go:build cgo`.
var ErrPDFEngineUnavailable = errors.New("parser: PDF backend unavailable in this build")
var supportedPDFParseMethods = map[string]struct{}{
"": {},
"deepdoc": {},
"plain_text": {},
"mineru": {},
"paddleocr": {},
"docling": {},
"opendataloader": {},
"somark": {},
"tcadp": {},
}
type PDFParser struct {
ParserType string // DeepDoc, PaddleOCR, MinerU
Model string // DeepDoc@buildin@ragflow
LibType string // pdf_oxide, used by DeepDoc
FlattenMediaToText bool
RemoveTOC bool
RemoveHeaderFooter bool
EnableMultiColumn bool
OutputFormat string
ParseMethod string
MinerUAPIServer string
MinerUAPIKey string
MinerUBackend string
MinerUPollTimeout time.Duration
PaddleOCRBaseURL string
PaddleOCRAPIKey string
PaddleOCRAlgorithm string
DoclingServerURL string
DoclingAPIKey string
OpenDataLoaderAPIServer string
OpenDataLoaderAPIKey string
OpenDataLoaderTimeout int
OpenDataLoaderHybrid string
OpenDataLoaderImageOutput string
OpenDataLoaderSanitize *bool
SoMarkBaseURL string
SoMarkAPIKey string
SoMarkImageFormat string
SoMarkFormulaFormat string
SoMarkTableFormat string
SoMarkCSFormat string
SoMarkEnableTextCrossPage bool
SoMarkEnableTableCrossPage bool
SoMarkEnableTitleLevelRecognition bool
SoMarkEnableInlineImage bool
SoMarkEnableTableImage bool
SoMarkEnableImageUnderstanding bool
SoMarkKeepHeaderFooter bool
TCADPAPIServer string
TCADPAPIKey string
TCADPTableResultType string
TCADPMarkdownImageResponseType string
}
func NewPDFParser() *PDFParser {
return &PDFParser{
ParserType: "DeepDoc",
Model: "DeepDoc@buildin@ragflow",
LibType: "pdf_oxide",
ParserType: "DeepDoc",
Model: "DeepDoc@buildin@ragflow",
LibType: "pdf_oxide",
ParseMethod: "deepdoc",
OutputFormat: "json",
MinerUBackend: "pipeline",
MinerUPollTimeout: minerUPollTimeout,
PaddleOCRAlgorithm: "PaddleOCR-VL",
OpenDataLoaderTimeout: 600,
SoMarkBaseURL: "https://somark.tech/api/v1",
SoMarkImageFormat: "url",
SoMarkFormulaFormat: "latex",
SoMarkTableFormat: "html",
SoMarkCSFormat: "image",
SoMarkEnableInlineImage: true,
SoMarkEnableTableImage: true,
SoMarkEnableImageUnderstanding: true,
TCADPTableResultType: "1",
TCADPMarkdownImageResponseType: "1",
}
}
@@ -54,6 +124,161 @@ func (p *PDFParser) String() string {
return "PDFParser"
}
func (p *PDFParser) ConfigureFromSetup(setup map[string]any) {
if p == nil || setup == nil {
return
}
if v, ok := setup["flatten_media_to_text"].(bool); ok {
p.FlattenMediaToText = v
}
if v, ok := setup["remove_toc"].(bool); ok {
p.RemoveTOC = v
}
if v, ok := setup["remove_header_footer"].(bool); ok {
p.RemoveHeaderFooter = v
}
if v, ok := setup["enable_multi_column"].(bool); ok {
p.EnableMultiColumn = v
}
if v, ok := setup["parse_method"].(string); ok && v != "" {
p.ParseMethod = v
}
if v, ok := setup["mineru_apiserver"].(string); ok && v != "" {
p.MinerUAPIServer = v
}
if v, ok := setup["mineru_api_key"].(string); ok {
p.MinerUAPIKey = v
}
if v, ok := setup["mineru_backend"].(string); ok && v != "" {
p.MinerUBackend = v
}
if v, ok := setup["mineru_timeout_seconds"].(int); ok && v > 0 {
p.MinerUPollTimeout = time.Duration(v) * time.Second
}
if v, ok := setup["mineru_timeout_seconds"].(float64); ok && v > 0 {
p.MinerUPollTimeout = time.Duration(v * float64(time.Second))
}
if v, ok := setup["output_format"].(string); ok && v != "" {
p.OutputFormat = v
}
if v, ok := setup["paddleocr_base_url"].(string); ok && v != "" {
p.PaddleOCRBaseURL = v
}
if v, ok := setup["paddleocr_api_key"].(string); ok {
p.PaddleOCRAPIKey = v
}
if v, ok := setup["paddleocr_algorithm"].(string); ok && v != "" {
p.PaddleOCRAlgorithm = v
}
if v, ok := setup["docling_server_url"].(string); ok && v != "" {
p.DoclingServerURL = v
}
if v, ok := setup["docling_api_key"].(string); ok {
p.DoclingAPIKey = v
}
if v, ok := setup["opendataloader_apiserver"].(string); ok && v != "" {
p.OpenDataLoaderAPIServer = v
}
if v, ok := setup["opendataloader_api_key"].(string); ok {
p.OpenDataLoaderAPIKey = v
}
if v, ok := setup["opendataloader_timeout"].(int); ok && v > 0 {
p.OpenDataLoaderTimeout = v
}
if v, ok := setup["opendataloader_timeout"].(float64); ok && v > 0 {
p.OpenDataLoaderTimeout = int(v)
}
if v, ok := setup["hybrid"].(string); ok && v != "" {
p.OpenDataLoaderHybrid = v
}
if v, ok := setup["image_output"].(string); ok && v != "" {
p.OpenDataLoaderImageOutput = v
}
if v, ok := setup["sanitize"].(bool); ok {
p.OpenDataLoaderSanitize = &v
}
if v, ok := setup["somark_base_url"].(string); ok && v != "" {
p.SoMarkBaseURL = v
}
if v, ok := setup["somark_api_key"].(string); ok {
p.SoMarkAPIKey = v
}
if v, ok := setup["somark_image_format"].(string); ok && v != "" {
p.SoMarkImageFormat = v
}
if v, ok := setup["somark_formula_format"].(string); ok && v != "" {
p.SoMarkFormulaFormat = v
}
if v, ok := setup["somark_table_format"].(string); ok && v != "" {
p.SoMarkTableFormat = v
}
if v, ok := setup["somark_cs_format"].(string); ok && v != "" {
p.SoMarkCSFormat = v
}
if v, ok := setup["somark_enable_text_cross_page"].(bool); ok {
p.SoMarkEnableTextCrossPage = v
}
if v, ok := setup["somark_enable_table_cross_page"].(bool); ok {
p.SoMarkEnableTableCrossPage = v
}
if v, ok := setup["somark_enable_title_level_recognition"].(bool); ok {
p.SoMarkEnableTitleLevelRecognition = v
}
if v, ok := setup["somark_enable_inline_image"].(bool); ok {
p.SoMarkEnableInlineImage = v
}
if v, ok := setup["somark_enable_table_image"].(bool); ok {
p.SoMarkEnableTableImage = v
}
if v, ok := setup["somark_enable_image_understanding"].(bool); ok {
p.SoMarkEnableImageUnderstanding = v
}
if v, ok := setup["somark_keep_header_footer"].(bool); ok {
p.SoMarkKeepHeaderFooter = v
}
if v, ok := setup["tcadp_apiserver"].(string); ok && v != "" {
p.TCADPAPIServer = v
}
if v, ok := setup["tcadp_api_key"].(string); ok {
p.TCADPAPIKey = v
}
if v, ok := setup["table_result_type"].(string); ok && v != "" {
p.TCADPTableResultType = v
}
if v, ok := setup["markdown_image_response_type"].(string); ok && v != "" {
p.TCADPMarkdownImageResponseType = v
}
}
func normalizePDFParseMethod(raw string) string {
method := strings.ToLower(strings.TrimSpace(raw))
switch {
case strings.HasSuffix(method, "@mineru"):
return "mineru"
case strings.HasSuffix(method, "@paddleocr"):
return "paddleocr"
case strings.HasSuffix(method, "@somark"):
return "somark"
case strings.HasSuffix(method, "@opendataloader"):
return "opendataloader"
}
switch method {
case "plaintext":
return "plain_text"
case "tcadp parser":
return "tcadp"
}
return method
}
func (p *PDFParser) validateParseMethod() error {
method := normalizePDFParseMethod(p.ParseMethod)
if _, ok := supportedPDFParseMethods[method]; ok {
return nil
}
return fmt.Errorf("parser: unsupported PDF parse_method %q (Go currently supports: deepdoc, plain_text, mineru, paddleocr, docling, opendataloader, somark, tcadp; tenant-resolved custom IMAGE2TEXT/VLM model names are not supported in the Go parser layer)", p.ParseMethod)
}
func emptyPDFResult(filename string) ParseResult {
return ParseResult{
OutputFormat: "json",
@@ -85,10 +310,22 @@ func deepDocAnalyzerFromEnv() deepdoctype.DocAnalyzer {
}
func pdfParseResultToJSON(filename string, parsed *deepdoctype.ParseResult) ParseResult {
return pdfParseResultToJSONWithOptions(filename, parsed, pdfPostProcessOptions{})
}
func pdfParseResultToJSONWithOptions(filename string, parsed *deepdoctype.ParseResult, opts pdfPostProcessOptions) ParseResult {
if parsed == nil {
return ParseResult{Err: fmt.Errorf("parser: nil DeepDOC PDF result for %s", filename)}
}
items := pdflayout.SectionsToJSON(parsed.Sections)
processed := *parsed
processed.Sections = append([]deepdoctype.Section(nil), parsed.Sections...)
processed.Outlines = append([]deepdoctype.Outline(nil), parsed.Outlines...)
if opts.enableMultiColumn && opts.pageWidth <= 0 {
opts.pageWidth = firstPDFPageWidth(processed.PageImages, opts.zoom)
}
applyPDFPostProcess(&processed, opts)
items := pdflayout.SectionsToJSON(processed.Sections)
if len(items) == 0 {
items = []map[string]any{{"text": "", "doc_type_kwd": "text"}}
}
@@ -96,9 +333,14 @@ func pdfParseResultToJSON(filename string, parsed *deepdoctype.ParseResult) Pars
if layoutType, _ := items[i]["layout_type"].(string); layoutType != "" {
items[i]["layout"] = layoutType
}
if _, ok := items[i]["page_number"]; !ok {
items[i]["page_number"] = firstPageNumber(items[i]["_pdf_positions"])
if normalized := normalizePDFPositions(items[i]["_pdf_positions"]); len(normalized) > 0 {
items[i]["_pdf_positions"] = normalized
items[i]["positions"] = normalized
if _, ok := items[i]["page_number"]; !ok {
items[i]["page_number"] = firstPageNumber(normalized)
}
}
normalizePDFDocType(items[i])
if img, _ := items[i]["image"].(string); img != "" {
items[i]["image"] = "data:image/png;base64," + img
}
@@ -107,13 +349,36 @@ func pdfParseResultToJSON(filename string, parsed *deepdoctype.ParseResult) Pars
OutputFormat: "json",
File: map[string]any{
"name": filename,
"page_count": len(parsed.PageImages),
"outline": outlinesToFileMeta(parsed.Outlines),
"page_count": len(processed.PageImages),
"outline": outlinesToFileMeta(processed.Outlines),
},
JSON: items,
}
}
func pdfParseResultToMarkdownWithOptions(filename string, parsed *deepdoctype.ParseResult, opts pdfPostProcessOptions) ParseResult {
if parsed == nil {
return ParseResult{Err: fmt.Errorf("parser: nil DeepDOC PDF result for %s", filename)}
}
processed := *parsed
processed.Sections = append([]deepdoctype.Section(nil), parsed.Sections...)
processed.Outlines = append([]deepdoctype.Outline(nil), parsed.Outlines...)
if opts.enableMultiColumn && opts.pageWidth <= 0 {
opts.pageWidth = firstPDFPageWidth(processed.PageImages, opts.zoom)
}
applyPDFPostProcess(&processed, opts)
return ParseResult{
OutputFormat: "markdown",
File: map[string]any{
"name": filename,
"page_count": len(processed.PageImages),
"outline": outlinesToFileMeta(processed.Outlines),
},
Markdown: sectionsToMarkdown(processed.Sections),
}
}
func outlinesToFileMeta(outlines []deepdoctype.Outline) []map[string]any {
if len(outlines) == 0 {
return []map[string]any{}
@@ -134,11 +399,7 @@ func firstPageNumber(raw any) int {
if !ok || len(positions) == 0 || len(positions[0]) == 0 {
return 0
}
pages, ok := positions[0][0].([]any)
if !ok || len(pages) == 0 {
return 0
}
switch v := pages[0].(type) {
switch v := positions[0][0].(type) {
case int:
return v
case int64:
@@ -163,7 +424,136 @@ func inlinePNGDataURL(raw string) string {
return "data:image/png;base64," + raw
}
func sectionsToMarkdown(sections []deepdoctype.Section) string {
var b strings.Builder
for _, section := range sections {
layoutType := strings.TrimSpace(section.LayoutType)
if layoutType == deepdoctype.LayoutTypeTitle {
b.WriteString("\n## ")
}
if layoutType == deepdoctype.LayoutTypeFigure && section.Image != "" {
b.WriteString("\n![Image](")
b.WriteString(inlinePNGDataURL(section.Image))
b.WriteString(")")
continue
}
b.WriteString(section.Text)
b.WriteByte('\n')
}
return b.String()
}
func firstPDFPageWidth(pageImages map[int]image.Image, zoom float64) float64 {
if len(pageImages) == 0 {
return 0
}
if zoom <= 0 {
zoom = deepdoctype.DefaultParserConfig().Zoom
}
pages := make([]int, 0, len(pageImages))
for page := range pageImages {
pages = append(pages, page)
}
sort.Ints(pages)
img := pageImages[pages[0]]
if img == nil {
return 0
}
return float64(img.Bounds().Dx()) / zoom
}
func normalizePDFPositions(raw any) [][]any {
positions, ok := raw.([][]any)
if !ok || len(positions) == 0 {
return nil
}
normalized := make([][]any, 0, len(positions))
for _, pos := range positions {
if len(pos) < 5 {
continue
}
pageNumber, ok := normalizePDFPageNumber(pos[0])
if !ok {
continue
}
left, lok := numericAny(pos[1])
right, rok := numericAny(pos[2])
top, tok := numericAny(pos[3])
bottom, bok := numericAny(pos[4])
if !lok || !rok || !tok || !bok {
continue
}
normalized = append(normalized, []any{pageNumber, left, right, top, bottom})
}
return normalized
}
func normalizePDFPageNumber(raw any) (int, bool) {
switch v := raw.(type) {
case int:
if v <= 0 {
return v + 1, true
}
return v, true
case int64:
return normalizePDFPageNumber(int(v))
case float64:
return normalizePDFPageNumber(int(v))
case []any:
if len(v) == 0 {
return 0, false
}
return normalizePDFPageNumber(v[len(v)-1])
case []int:
if len(v) == 0 {
return 0, false
}
return normalizePDFPageNumber(v[len(v)-1])
default:
return 0, false
}
}
func numericAny(raw any) (float64, bool) {
switch v := raw.(type) {
case int:
return float64(v), true
case int64:
return float64(v), true
case float64:
return v, true
default:
return 0, false
}
}
func normalizePDFDocType(item map[string]any) {
if item == nil {
return
}
if docType, _ := item["doc_type_kwd"].(string); docType != "" {
return
}
layoutType, _ := item["layout_type"].(string)
switch layoutType {
case "table":
item["doc_type_kwd"] = "table"
case "figure", "image":
item["doc_type_kwd"] = "image"
default:
if img, _ := item["image"].(string); img != "" {
item["doc_type_kwd"] = "image"
return
}
item["doc_type_kwd"] = "text"
}
}
func parsePDFWithDeepDoc(ctx context.Context, filename string, data []byte, parseFn func(context.Context, []byte, deepdoctype.DocAnalyzer) (*deepdoctype.ParseResult, error)) ParseResult {
return parsePDFWithDeepDocOptions(ctx, filename, data, pdfPostProcessOptions{}, parseFn)
}
func parsePDFWithDeepDocOptions(ctx context.Context, filename string, data []byte, opts pdfPostProcessOptions, parseFn func(context.Context, []byte, deepdoctype.DocAnalyzer) (*deepdoctype.ParseResult, error)) ParseResult {
if len(data) == 0 {
return emptyPDFResult(filename)
}
@@ -171,7 +561,15 @@ func parsePDFWithDeepDoc(ctx context.Context, filename string, data []byte, pars
if err != nil {
return ParseResult{Err: err}
}
res := pdfParseResultToJSON(filename, parsed)
var res ParseResult
switch strings.ToLower(strings.TrimSpace(opts.outputFormat)) {
case "", "json":
res = pdfParseResultToJSONWithOptions(filename, parsed, opts)
case "markdown":
res = pdfParseResultToMarkdownWithOptions(filename, parsed, opts)
default:
return ParseResult{Err: fmt.Errorf("parser: unsupported PDF output_format %q", opts.outputFormat)}
}
for i := range res.JSON {
if img, _ := res.JSON[i]["image"].(string); img != "" {
res.JSON[i]["image"] = inlinePNGDataURL(img)