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
318 lines
10 KiB
Go
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
|
|
}
|