Files
wehub-resource-sync e904b667c6
Build/Publish Develop Docs / deploy (push) Failing after 1s
PaddleOCR Code Style Check / check-code-style (push) Failing after 1s
PaddleOCR PR Tests GPU / detect-changes (push) Failing after 1s
PaddleOCR PR Tests / detect-changes (push) Failing after 1s
PaddleOCR PR Tests GPU / test-pr-gpu (push) Has been cancelled
PaddleOCR PR Tests / test-pr (push) Has been cancelled
PaddleOCR PR Tests GPU / test-pr-gpu-impl (push) Has been cancelled
PaddleOCR PR Tests / test-pr-python (3.13) (push) Has been cancelled
PaddleOCR PR Tests / test-pr-python (3.8) (push) Has been cancelled
PaddleOCR PR Tests / test-pr-python (3.9) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 11:59:26 +08:00

318 lines
10 KiB
Go

// Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved.
//
// 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 paddleocr
import (
"context"
"encoding/json"
)
func (c *Client) OCR(ctx context.Context, req *OCRRequest) (*OCRResult, error) {
job, err := c.SubmitOCR(ctx, req)
if err != nil {
return nil, err
}
return c.WaitOCRResult(ctx, job.JobID)
}
// ParseDocument performs document parsing. Blocks until result is ready.
func (c *Client) ParseDocument(ctx context.Context, req *DocParsingRequest) (*DocParsingResult, error) {
job, err := c.SubmitDocumentParsing(ctx, req)
if err != nil {
return nil, err
}
return c.WaitDocumentParsingResult(ctx, job.JobID)
}
// SubmitOCR submits an OCR job and returns job metadata for tracking.
func (c *Client) SubmitOCR(ctx context.Context, req *OCRRequest) (*Job, error) {
if req == nil {
return nil, &InvalidRequestError{PaddleOCRAPIError{Message: "OCR request is nil"}}
}
model := req.Model
if model == "" {
model = PPOCRv6
}
if !IsOCRModel(model) {
return nil, &InvalidRequestError{PaddleOCRAPIError{Message: "model is not an OCR model: " + model}}
}
jobID, err := c.submit(ctx, model, req.FileURL, req.FilePath, req.Options, req.PageRanges, req.BatchID)
if err != nil {
return nil, err
}
return &Job{JobID: jobID, Model: model, Task: "ocr", PageRanges: req.PageRanges, BatchID: req.BatchID}, nil
}
// SubmitDocumentParsing submits a document parsing job and returns job metadata for tracking.
func (c *Client) SubmitDocumentParsing(ctx context.Context, req *DocParsingRequest) (*Job, error) {
if req == nil {
return nil, &InvalidRequestError{PaddleOCRAPIError{Message: "document parsing request is nil"}}
}
model := req.Model
if model == "" {
model = PaddleOCRVL16
}
if !IsDocumentParsingModel(model) {
return nil, &InvalidRequestError{PaddleOCRAPIError{Message: "model is not a document parsing model: " + model}}
}
jobID, err := c.submit(ctx, model, req.FileURL, req.FilePath, req.Options, req.PageRanges, req.BatchID)
if err != nil {
return nil, err
}
return &Job{JobID: jobID, Model: model, Task: "document_parsing", PageRanges: req.PageRanges, BatchID: req.BatchID}, nil
}
func (c *Client) WaitOCRResult(ctx context.Context, jobID string) (*OCRResult, error) {
jsonlData, err := c.pollUntilDone(ctx, jobID)
if err != nil {
return nil, err
}
return parseOCRResult(jobID, jsonlData)
}
func (c *Client) WaitDocumentParsingResult(ctx context.Context, jobID string) (*DocParsingResult, error) {
jsonlData, err := c.pollUntilDone(ctx, jobID)
if err != nil {
return nil, err
}
return parseDocParsingResult(jobID, jsonlData)
}
func (c *Client) GetStatus(ctx context.Context, jobID string) (*JobStatus, error) {
status, err := c.getJobStatus(ctx, jobID)
if err != nil {
return nil, err
}
return normalizeStatus(jobID, status)
}
func (c *Client) GetBatchStatus(ctx context.Context, batchID string) (*BatchStatus, error) {
if batchID == "" {
return nil, &InvalidRequestError{PaddleOCRAPIError{Message: "batchID is required"}}
}
return c.getBatchStatus(ctx, batchID)
}
func (c *Client) submit(ctx context.Context, model, fileURL, filePath string, options interface{}, pageRanges, batchID string) (string, error) {
if fileURL == "" && filePath == "" {
return "", &InvalidRequestError{PaddleOCRAPIError{Message: "Either FileURL or FilePath is required."}}
}
if fileURL != "" && filePath != "" {
return "", &InvalidRequestError{PaddleOCRAPIError{Message: "FileURL and FilePath are mutually exclusive."}}
}
payload := defaultPayload(model, options)
if fileURL != "" {
return c.submitURL(ctx, model, fileURL, payload, pageRanges, batchID)
}
return c.submitFile(ctx, model, filePath, payload, pageRanges, batchID)
}
func defaultPayload(model string, options interface{}) interface{} {
switch typed := options.(type) {
case *OCROptions:
if typed != nil {
return payloadWithExtraOptions(typed)
}
case *PPStructureV3Options:
if typed != nil {
return payloadWithExtraOptions(typed)
}
case *PaddleOCRVLOptions:
if typed != nil {
return payloadWithExtraOptions(typed)
}
default:
if options != nil {
return payloadWithExtraOptions(options)
}
}
if IsOCRModel(model) {
return payloadWithExtraOptions(&OCROptions{})
}
if IsVLModel(model) {
return payloadWithExtraOptions(&PaddleOCRVLOptions{})
}
return payloadWithExtraOptions(&PPStructureV3Options{})
}
func payloadWithExtraOptions(options interface{}) interface{} {
payloadBytes, _ := json.Marshal(options)
payload := map[string]interface{}{}
_ = json.Unmarshal(payloadBytes, &payload)
var extraOptions map[string]interface{}
switch typed := options.(type) {
case *OCROptions:
extraOptions = typed.ExtraOptions
case *PPStructureV3Options:
extraOptions = typed.ExtraOptions
case *PaddleOCRVLOptions:
extraOptions = typed.ExtraOptions
}
for key, value := range extraOptions {
payload[key] = value
}
return payload
}
func parseOCRResult(jobID string, jsonlData []map[string]interface{}) (*OCRResult, error) {
result := &OCRResult{JobID: jobID, DataInfo: map[string]interface{}{}}
for _, lineObj := range jsonlData {
resultData, ok := lineObj["result"].(map[string]interface{})
if !ok {
return nil, &ResultParseError{PaddleOCRAPIError{Message: "OCR result item is missing result"}}
}
if dataInfo, ok := resultData["dataInfo"].(map[string]interface{}); ok {
for key, value := range dataInfo {
result.DataInfo[key] = value
}
}
ocrResults, ok := resultData["ocrResults"].([]interface{})
if !ok {
return nil, &ResultParseError{PaddleOCRAPIError{Message: "OCR result item is missing ocrResults"}}
}
for _, item := range ocrResults {
itemMap, ok := item.(map[string]interface{})
if !ok {
return nil, &ResultParseError{PaddleOCRAPIError{Message: "OCR result page is malformed"}}
}
if _, ok := itemMap["prunedResult"]; !ok {
return nil, &ResultParseError{PaddleOCRAPIError{Message: "OCR result page is missing prunedResult"}}
}
page := OCRPage{
PrunedResult: itemMap["prunedResult"],
OCRImageURL: getString(itemMap, "ocrImage"),
DocPreprocessingImageURL: getString(itemMap, "docPreprocessingImage"),
InputImageURL: getString(itemMap, "inputImage"),
Raw: itemMap,
}
result.Pages = append(result.Pages, page)
}
}
return result, nil
}
func parseDocParsingResult(jobID string, jsonlData []map[string]interface{}) (*DocParsingResult, error) {
result := &DocParsingResult{JobID: jobID, DataInfo: map[string]interface{}{}}
for _, lineObj := range jsonlData {
resultData, ok := lineObj["result"].(map[string]interface{})
if !ok {
return nil, &ResultParseError{PaddleOCRAPIError{Message: "document parsing result item is missing result"}}
}
if dataInfo, ok := resultData["dataInfo"].(map[string]interface{}); ok {
for key, value := range dataInfo {
result.DataInfo[key] = value
}
}
lpResults, ok := resultData["layoutParsingResults"].([]interface{})
if !ok {
return nil, &ResultParseError{PaddleOCRAPIError{Message: "document parsing result item is missing layoutParsingResults"}}
}
for _, item := range lpResults {
itemMap, ok := item.(map[string]interface{})
if !ok {
return nil, &ResultParseError{PaddleOCRAPIError{Message: "document parsing result page is malformed"}}
}
markdown, ok := itemMap["markdown"].(map[string]interface{})
if !ok || getString(markdown, "text") == "" {
return nil, &ResultParseError{PaddleOCRAPIError{Message: "document parsing result page is missing markdown.text"}}
}
page := DocParsingPage{
MarkdownText: getString(markdown, "text"),
MarkdownImages: getStringMap(markdown, "images"),
OutputImages: getStringMap(itemMap, "outputImages"),
PrunedResult: itemMap["prunedResult"],
InputImageURL: getString(itemMap, "inputImage"),
Exports: getMap(itemMap, "exports"),
Markdown: markdown,
Raw: itemMap,
}
result.Pages = append(result.Pages, page)
}
}
return result, nil
}
func getString(m map[string]interface{}, key string) string {
if m == nil {
return ""
}
v, _ := m[key].(string)
return v
}
func getMap(m map[string]interface{}, key string) map[string]interface{} {
value, ok := m[key].(map[string]interface{})
if !ok {
return map[string]interface{}{}
}
return value
}
func getStringMap(m map[string]interface{}, key string) map[string]string {
if m == nil {
return nil
}
raw, ok := m[key]
if !ok {
return nil
}
switch v := raw.(type) {
case map[string]interface{}:
result := make(map[string]string, len(v))
for k, val := range v {
if s, ok := val.(string); ok {
result[k] = s
}
}
return result
default:
b, _ := json.Marshal(raw)
var result map[string]string
json.Unmarshal(b, &result)
return result
}
}
func normalizeStatus(jobID string, status *jobStatusResponse) (*JobStatus, error) {
if status == nil || status.State == "" {
return nil, &ResponseFormatError{PaddleOCRAPIError{Message: "status response is missing state"}}
}
if status.State != "pending" && status.State != "running" && status.State != "done" && status.State != "failed" {
return nil, &ResponseFormatError{PaddleOCRAPIError{Message: "unknown job state: " + status.State}}
}
result := &JobStatus{
JobID: jobID,
State: status.State,
ResultURL: status.ResultURL,
ErrorMsg: status.ErrorMsg,
}
if len(status.ExtractProgress) != 0 && string(status.ExtractProgress) != "null" {
var progress extractProgress
if err := json.Unmarshal(status.ExtractProgress, &progress); err != nil {
return nil, &ResponseFormatError{PaddleOCRAPIError{Message: "status progress is malformed", Cause: err}}
}
result.Progress = &Progress{
TotalPages: progress.TotalPages,
ExtractedPages: progress.ExtractedPages,
StartTime: progress.StartTime,
EndTime: progress.EndTime,
}
}
return result, nil
}