paddle ocr suport
This commit is contained in:
@@ -24,6 +24,7 @@ require (
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/ledongthuc/pdf v0.0.0-20250511090121-5959a4027728 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
|
||||
@@ -45,6 +45,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/ledongthuc/pdf v0.0.0-20250511090121-5959a4027728 h1:QwWKgMY28TAXaDl+ExRDqGQltzXqN/xypdKP86niVn8=
|
||||
github.com/ledongthuc/pdf v0.0.0-20250511090121-5959a4027728/go.mod h1:1fEHWurg7pvf5SG6XNE5Q8UZmOwex51Mkx3SLhrW5B4=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/ledongthuc/pdf"
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
@@ -25,6 +27,18 @@ func NewClient(endpoint string) *Client {
|
||||
}
|
||||
|
||||
func (c *Client) Recognize(imageData []byte) ([]TextBlock, error) {
|
||||
|
||||
docType := DetectDocumentType(imageData)
|
||||
|
||||
switch docType {
|
||||
case PDFScanned:
|
||||
fmt.Println("PDFScanned -> PaddleOCR")
|
||||
case Image:
|
||||
fmt.Println("Image -> PaddleOCR")
|
||||
default:
|
||||
return []TextBlock{}, fmt.Errorf("DOCTYPE not supported => %s", docType)
|
||||
}
|
||||
|
||||
b64 := base64.StdEncoding.EncodeToString(imageData)
|
||||
|
||||
reqBody, err := json.Marshal(OCRRequest{Image: b64})
|
||||
@@ -63,6 +77,59 @@ func (c *Client) Recognize(imageData []byte) ([]TextBlock, error) {
|
||||
return flattenResults(ocrResp.Result), nil
|
||||
}
|
||||
|
||||
type DocType string
|
||||
|
||||
const (
|
||||
Image DocType = "image"
|
||||
PDFText DocType = "pdf_text"
|
||||
PDFScanned DocType = "pdf_scanned"
|
||||
Unknown DocType = "unknown"
|
||||
)
|
||||
|
||||
func isPDFText(data []byte) bool {
|
||||
reader := bytes.NewReader(data)
|
||||
|
||||
r, err := pdf.NewReader(reader, int64(len(data)))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for i := 1; i <= r.NumPage(); i++ {
|
||||
page := r.Page(i)
|
||||
if page.V.IsNull() {
|
||||
continue
|
||||
}
|
||||
|
||||
text, _ := page.GetPlainText(nil)
|
||||
if len(text) > 20 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func DetectDocumentType(data []byte) DocType {
|
||||
mime := http.DetectContentType(data)
|
||||
|
||||
switch {
|
||||
case mime == "application/pdf":
|
||||
if isPDFText(data) {
|
||||
return PDFText
|
||||
}
|
||||
return PDFScanned
|
||||
|
||||
case bytes.HasPrefix(data, []byte{0xFF, 0xD8}): // JPEG
|
||||
return Image
|
||||
|
||||
case bytes.HasPrefix(data, []byte{0x89, 0x50, 0x4E, 0x47}): // PNG
|
||||
return Image
|
||||
|
||||
default:
|
||||
return Unknown
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) HealthCheck() error {
|
||||
resp, err := c.httpClient.Get(c.endpoint + "/health")
|
||||
if err != nil {
|
||||
|
||||
@@ -22,7 +22,7 @@ func GenerateFileDownloadUrl(fileID string) string {
|
||||
sig := sign(fileID, expires, secret)
|
||||
|
||||
url := fmt.Sprintf(
|
||||
"http://192.168.1.142:8080/api/v1/files/%s?expires=%d&sig=%s",
|
||||
"http://192.168.1.17:8080/api/v1/files/%s?expires=%d&sig=%s",
|
||||
fileID,
|
||||
expires,
|
||||
sig,
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
FROM python:3.10-slim
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
libglib2.0-0 libgl1 libgomp1 curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN pip install --no-cache-dir \
|
||||
paddlepaddle==3.0.0 \
|
||||
paddleocr==3.3.3 \
|
||||
paddlex==3.3.13 \
|
||||
fastapi \
|
||||
uvicorn \
|
||||
python-multipart \
|
||||
Pillow \
|
||||
numpy
|
||||
|
||||
COPY server.py /workspace/server.py
|
||||
|
||||
WORKDIR /workspace
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
CMD ["uvicorn", "server:app", "--host", "0.0.0.0", "--port", "8080"]
|
||||
@@ -0,0 +1,96 @@
|
||||
import os
|
||||
import base64
|
||||
import logging
|
||||
from io import BytesIO
|
||||
|
||||
import numpy as np
|
||||
from fastapi import FastAPI, UploadFile, File
|
||||
from fastapi.responses import JSONResponse
|
||||
from paddleocr import PaddleOCR
|
||||
from pydantic import BaseModel
|
||||
from PIL import Image
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger("paddleocr-server")
|
||||
|
||||
OCR_LANG = os.getenv("OCR_LANG", "fr")
|
||||
|
||||
print(f"[INIT] Initializing PaddleOCR (lang={OCR_LANG})...", flush=True)
|
||||
ocr_engine = PaddleOCR(
|
||||
use_doc_orientation_classify=False,
|
||||
use_doc_unwarping=False,
|
||||
use_textline_orientation=False,
|
||||
lang=OCR_LANG,
|
||||
)
|
||||
print("[INIT] PaddleOCR ready.", flush=True)
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
class OCRRequest(BaseModel):
|
||||
image: str
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
def health():
|
||||
return {"status": "healthy", "service": "PaddleOCR Server"}
|
||||
|
||||
|
||||
@app.post("/ocr")
|
||||
def ocr_json(req: OCRRequest):
|
||||
print(f"[OCR] Request received, image field length: {len(req.image)}", flush=True)
|
||||
try:
|
||||
img_bytes = base64.b64decode(req.image)
|
||||
except Exception as e:
|
||||
print(f"[OCR] Base64 decode failed: {e}", flush=True)
|
||||
return JSONResponse(
|
||||
status_code=400,
|
||||
content={"errorCode": 1, "message": "invalid base64 image"},
|
||||
)
|
||||
print(f"[OCR] Decoded {len(img_bytes)} bytes, header: {img_bytes[:32].hex()}", flush=True)
|
||||
return run_ocr(img_bytes)
|
||||
|
||||
|
||||
@app.post("/ocr/upload")
|
||||
def ocr_upload(file: UploadFile = File(...)):
|
||||
img_bytes = file.file.read()
|
||||
print(f"[OCR] Upload received, {len(img_bytes)} bytes, header: {img_bytes[:32].hex()}", flush=True)
|
||||
return run_ocr(img_bytes)
|
||||
|
||||
|
||||
def run_ocr(img_bytes: bytes):
|
||||
try:
|
||||
image = Image.open(BytesIO(img_bytes))
|
||||
if image.mode != "RGB":
|
||||
image = image.convert("RGB")
|
||||
img_array = np.array(image)
|
||||
print(f"[OCR] Image decoded: {img_array.shape}", flush=True)
|
||||
except Exception as e:
|
||||
print(f"[OCR] Image decode FAILED: {e}", flush=True)
|
||||
return JSONResponse(
|
||||
status_code=400,
|
||||
content={"errorCode": 1, "message": f"failed to decode image: {e}"},
|
||||
)
|
||||
|
||||
try:
|
||||
result = list(ocr_engine.predict(img_array))
|
||||
|
||||
pages = []
|
||||
for r in result:
|
||||
raw = r._to_json()
|
||||
data = raw.get("res", raw)
|
||||
pages.append({
|
||||
"rec_texts": data.get("rec_texts", []),
|
||||
"rec_scores": [float(s) for s in data.get("rec_scores", [])],
|
||||
"rec_boxes": data.get("rec_boxes", []),
|
||||
"rec_polys": data.get("rec_polys", []),
|
||||
})
|
||||
|
||||
return {"errorCode": 0, "result": {"ocrResults": pages}}
|
||||
|
||||
except Exception as e:
|
||||
logger.exception("OCR failed")
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={"errorCode": 2, "message": str(e)},
|
||||
)
|
||||
Reference in New Issue
Block a user