coze-studio/backend/infra/impl/document/parser/ppstructure/parser.go

325 lines
10 KiB
Go

/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ppstructure
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
"regexp"
"strings"
"github.com/cloudwego/eino/components/document/parser"
"github.com/cloudwego/eino/schema"
"github.com/coze-dev/coze-studio/backend/infra/contract/document/ocr"
contract "github.com/coze-dev/coze-studio/backend/infra/contract/document/parser"
"github.com/coze-dev/coze-studio/backend/infra/contract/storage"
"github.com/coze-dev/coze-studio/backend/infra/impl/document/parser/builtin"
)
type ppstructureParser struct {
parserConfig *contract.Config
apiConfig *APIConfig
fileType int
ocr ocr.OCR
storage storage.Storage
}
type APIConfig struct {
Client *http.Client
URL string
// see: https://paddlepaddle.github.io/PaddleX/latest/pipeline_usage/tutorials/ocr_pipelines/PP-StructureV3.html#3
UseDocOrientationClassify *bool
UseDocUnwarping *bool
UseTextlineOrientation *bool
UseSealRecognition *bool
UseFormulaRecognition *bool
UseChartRecognition *bool
UseRegionDetection *bool
LayoutThreshold *float64
LayoutNms *bool
LayoutUnclipRatio *float64
LayoutMergeBboxesMode *string
TextDetLimitSideLen *int64
TextDetLimitType *string
TextDetThresh *float64
TextDetBoxThresh *float64
TextDetUnclipRatio *float64
TextRecScoreThresh *float64
SealDetLimitSideLen *int64
SealDetLimitType *string
SealDetThresh *float64
SealDetBoxThresh *float64
SealDetUnclipRatio *float64
SealRecScoreThresh *float64
UseWiredTableCellsTransToHtml *bool
UseWirelessTableCellsTransToHtml *bool
UseTableOrientationClassify *bool
UseOcrResultsWithTableCells *bool
UseE2eWiredTableRecModel *bool
UseE2eWirelessTableRecModel *bool
}
type ppstructureResponse struct {
Result *ppstructureInferResult `json:"result"`
}
type ppstructureInferResult struct {
LayoutParsingResults []*ppstructureInnerResult `json:"layoutParsingResults"`
}
type ppstructureInnerResult struct {
Markdown *ppstructureMarkdown `json:"markdown"`
}
type ppstructureMarkdown struct {
Text *string `json:"text"`
Images map[string]string `json:"images"`
IsStart *bool `json:"isStart"`
IsEnd *bool `json:"isEnd"`
}
func (p *ppstructureParser) Parse(ctx context.Context, reader io.Reader, opts ...parser.Option) (docs []*schema.Document, err error) {
// TODO(Bobholamovic): Current chunking strategy is rather naive; we should
// implement a more sophisticated one that at least takes tables and text
// extracted from the images into consideration.
options := parser.GetCommonOptions(&parser.Options{ExtraMeta: map[string]any{}}, opts...)
fileBytes, err := io.ReadAll(reader)
if err != nil {
return nil, fmt.Errorf("[Parse] failed to read the file bytes, %w", err)
}
b64 := base64.StdEncoding.EncodeToString(fileBytes)
reqBody := p.newRequestBody(b64, p.fileType, p.parserConfig.ParsingStrategy.ExtractImage, p.parserConfig.ParsingStrategy.ExtractTable)
bodyBytes, err := json.Marshal(reqBody)
if err != nil {
return nil, fmt.Errorf("[Parse] failed to serizalize the request body, %w", err)
}
req, err := http.NewRequest("POST", p.apiConfig.URL, bytes.NewReader(bodyBytes))
if err != nil {
return nil, fmt.Errorf("[Parse] failed to create a new request, %w", err)
}
req.Header.Set("Content-Type", "application/json")
resp, err := p.apiConfig.Client.Do(req)
if err != nil {
return nil, fmt.Errorf("[Parse] request failed, %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("[Parse] request failed, %w", err)
}
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("[Parse] failed to read the response body, %w", err)
}
var res ppstructureResponse
if err := json.Unmarshal(respBody, &res); err != nil {
return nil, fmt.Errorf("[Parse] failed to deserialize the response body, %w", err)
}
if res.Result == nil ||
res.Result.LayoutParsingResults == nil {
return nil, fmt.Errorf("[Parse] failed to get the layout parsing result, %w", err)
}
for i, item := range res.Result.LayoutParsingResults {
if item.Markdown == nil {
return nil, fmt.Errorf("[Parse] invalid response, %w", err)
}
if item.Markdown.Text == nil {
return nil, fmt.Errorf("[Parse] invalid response, %w", err)
}
should_skip := false
for _, v := range p.parserConfig.ParsingStrategy.FilterPages {
if i+1 == v {
should_skip = true
break
}
}
if should_skip {
continue
}
text := *item.Markdown.Text
// Convert the image in markdown to comments, as the image content will be added later.
pattern := `(?i)<img[^>]*>`
re := regexp.MustCompile(pattern)
// TODO(Bobholamovic): Add image numbering
text = re.ReplaceAllStringFunc(text, func(matched string) string {
return "<!-- image -->"
})
partDocs, err := builtin.ChunkCustom(ctx, text, p.parserConfig, opts...)
if err != nil {
return nil, fmt.Errorf("[Parse] chunk text failed, %w", err)
}
docs = append(docs, partDocs...)
if p.parserConfig.ParsingStrategy.ExtractImage {
if item.Markdown.Images == nil {
return nil, fmt.Errorf("[Parse] invalid response, %w", err)
}
for _, v := range item.Markdown.Images {
image, err := base64.StdEncoding.DecodeString(v)
if err != nil {
return nil, fmt.Errorf("[Parse] failed to decode an image, %w", err)
}
imgSrc, err := builtin.PutImageObject(ctx, p.storage, "png", builtin.GetCreatorIDFromExtraMeta(options.ExtraMeta), image)
if err != nil {
return nil, err
}
label := fmt.Sprintf("\n%s", imgSrc)
if p.parserConfig.ParsingStrategy.ImageOCR && p.ocr != nil {
texts, err := p.ocr.FromBase64(ctx, v)
if err != nil {
return nil, fmt.Errorf("[Parse] FromBase64 failed, %w", err)
}
label += strings.Join(texts, "\n")
}
doc := &schema.Document{
Content: label,
MetaData: map[string]any{},
}
for k, v := range options.ExtraMeta {
doc.MetaData[k] = v
}
docs = append(docs, doc)
}
}
}
return docs, nil
}
func (p *ppstructureParser) newRequestBody(file string, fileType int, extractImage bool, extractTable bool) map[string]interface{} {
payload := map[string]interface{}{
"file": file,
"fileType": fileType,
"useTableRecognition": extractTable,
"visualize": extractImage,
}
if p.apiConfig.UseDocOrientationClassify != nil {
payload["useDocOrientationClassify"] = *p.apiConfig.UseDocOrientationClassify
}
if p.apiConfig.UseDocUnwarping != nil {
payload["useDocUnwarping"] = *p.apiConfig.UseDocUnwarping
}
if p.apiConfig.UseTextlineOrientation != nil {
payload["useTextlineOrientation"] = *p.apiConfig.UseTextlineOrientation
}
if p.apiConfig.UseSealRecognition != nil {
payload["useSealRecognition"] = *p.apiConfig.UseSealRecognition
}
if p.apiConfig.UseFormulaRecognition != nil {
payload["useFormulaRecognition"] = *p.apiConfig.UseFormulaRecognition
}
if p.apiConfig.UseChartRecognition != nil {
payload["useChartRecognition"] = *p.apiConfig.UseChartRecognition
}
if p.apiConfig.UseRegionDetection != nil {
payload["useRegionDetection"] = *p.apiConfig.UseRegionDetection
}
if p.apiConfig.LayoutThreshold != nil {
payload["layoutThreshold"] = *p.apiConfig.LayoutThreshold
}
if p.apiConfig.LayoutNms != nil {
payload["layoutNms"] = *p.apiConfig.LayoutNms
}
if p.apiConfig.LayoutUnclipRatio != nil {
payload["layoutUnclipRatio"] = *p.apiConfig.LayoutUnclipRatio
}
if p.apiConfig.LayoutMergeBboxesMode != nil {
payload["layoutMergeBboxesMode"] = *p.apiConfig.LayoutMergeBboxesMode
}
if p.apiConfig.TextDetLimitSideLen != nil {
payload["textDetLimitSideLen"] = *p.apiConfig.TextDetLimitSideLen
}
if p.apiConfig.TextDetLimitType != nil {
payload["textDetLimitType"] = *p.apiConfig.TextDetLimitType
}
if p.apiConfig.TextDetThresh != nil {
payload["textDetThresh"] = *p.apiConfig.TextDetThresh
}
if p.apiConfig.TextDetBoxThresh != nil {
payload["textDetBoxThresh"] = *p.apiConfig.TextDetBoxThresh
}
if p.apiConfig.TextDetUnclipRatio != nil {
payload["textDetUnclipRatio"] = *p.apiConfig.TextDetUnclipRatio
}
if p.apiConfig.TextRecScoreThresh != nil {
payload["textRecScoreThresh"] = *p.apiConfig.TextRecScoreThresh
}
if p.apiConfig.SealDetLimitSideLen != nil {
payload["sealDetLimitSideLen"] = *p.apiConfig.SealDetLimitSideLen
}
if p.apiConfig.SealDetLimitType != nil {
payload["sealDetLimitType"] = *p.apiConfig.SealDetLimitType
}
if p.apiConfig.SealDetThresh != nil {
payload["sealDetThresh"] = *p.apiConfig.SealDetThresh
}
if p.apiConfig.SealDetBoxThresh != nil {
payload["sealDetBoxThresh"] = *p.apiConfig.SealDetBoxThresh
}
if p.apiConfig.SealDetUnclipRatio != nil {
payload["sealDetUnclipRatio"] = *p.apiConfig.SealDetUnclipRatio
}
if p.apiConfig.SealRecScoreThresh != nil {
payload["sealRecScoreThresh"] = *p.apiConfig.SealRecScoreThresh
}
if p.apiConfig.UseWiredTableCellsTransToHtml != nil {
payload["useWiredTableCellsTransToHtml"] = *p.apiConfig.UseWiredTableCellsTransToHtml
}
if p.apiConfig.UseWirelessTableCellsTransToHtml != nil {
payload["useWirelessTableCellsTransToHtml"] = *p.apiConfig.UseWirelessTableCellsTransToHtml
}
if p.apiConfig.UseTableOrientationClassify != nil {
payload["useTableOrientationClassify"] = *p.apiConfig.UseTableOrientationClassify
}
if p.apiConfig.UseOcrResultsWithTableCells != nil {
payload["useOcrResultsWithTableCells"] = *p.apiConfig.UseOcrResultsWithTableCells
}
if p.apiConfig.UseE2eWiredTableRecModel != nil {
payload["useE2eWiredTableRecModel"] = *p.apiConfig.UseE2eWiredTableRecModel
}
if p.apiConfig.UseE2eWirelessTableRecModel != nil {
payload["useE2eWirelessTableRecModel"] = *p.apiConfig.UseE2eWirelessTableRecModel
}
return payload
}