paddle ocr suport
This commit is contained in:
@@ -24,6 +24,7 @@ require (
|
|||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0 // 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/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // 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/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 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
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 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
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=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/ledongthuc/pdf"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
@@ -25,6 +27,18 @@ func NewClient(endpoint string) *Client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Recognize(imageData []byte) ([]TextBlock, error) {
|
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)
|
b64 := base64.StdEncoding.EncodeToString(imageData)
|
||||||
|
|
||||||
reqBody, err := json.Marshal(OCRRequest{Image: b64})
|
reqBody, err := json.Marshal(OCRRequest{Image: b64})
|
||||||
@@ -63,6 +77,59 @@ func (c *Client) Recognize(imageData []byte) ([]TextBlock, error) {
|
|||||||
return flattenResults(ocrResp.Result), nil
|
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 {
|
func (c *Client) HealthCheck() error {
|
||||||
resp, err := c.httpClient.Get(c.endpoint + "/health")
|
resp, err := c.httpClient.Get(c.endpoint + "/health")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ func GenerateFileDownloadUrl(fileID string) string {
|
|||||||
sig := sign(fileID, expires, secret)
|
sig := sign(fileID, expires, secret)
|
||||||
|
|
||||||
url := fmt.Sprintf(
|
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,
|
fileID,
|
||||||
expires,
|
expires,
|
||||||
sig,
|
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)},
|
||||||
|
)
|
||||||
+6
-2
@@ -1,15 +1,19 @@
|
|||||||
services:
|
services:
|
||||||
paddleocr:
|
paddleocr:
|
||||||
image: paddlecloud/paddleocr:2.6-cpu-latest
|
build:
|
||||||
|
context: ./backend/ocr-server
|
||||||
|
dockerfile: Dockerfile
|
||||||
ports:
|
ports:
|
||||||
- "9090:8080"
|
- "9090:8080"
|
||||||
|
environment:
|
||||||
|
- OCR_LANG=fr
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "-sf", "http://localhost:8080/health"]
|
test: ["CMD", "curl", "-sf", "http://localhost:8080/health"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 5
|
retries: 5
|
||||||
start_period: 60s
|
start_period: 120s
|
||||||
|
|
||||||
# backend:
|
# backend:
|
||||||
# build:
|
# build:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export const API_BASE_URL = process.env.EXPO_PUBLIC_API_BASE_URL || 'http://192.168.1.142:8080/api/v1';
|
export const API_BASE_URL = process.env.EXPO_PUBLIC_API_BASE_URL || 'http://192.168.1.17:8080/api/v1';
|
||||||
|
|
||||||
export const ENDPOINTS = {
|
export const ENDPOINTS = {
|
||||||
FILES: '/files',
|
FILES: '/files',
|
||||||
|
|||||||
Reference in New Issue
Block a user