AdityaAjithKumar commited on
Commit
844cd97
·
1 Parent(s): 681e2b1

PaddleOCR-VL

Browse files
Files changed (3) hide show
  1. Dockerfile +19 -10
  2. app.py +80 -146
  3. requirements.txt +6 -6
Dockerfile CHANGED
@@ -1,15 +1,24 @@
1
- FROM python:3.12-slim
2
- WORKDIR /app
3
- COPY . .
 
 
4
 
5
- RUN apt-get update && apt-get install -y --no-install-recommends \
6
- libgl1 libglib2.0-0 && \
7
- rm -rf /var/lib/apt/lists/*
 
 
8
 
9
- RUN pip install --no-cache-dir torch --index-url https://download.pytorch.org/whl/cpu
 
 
10
 
11
- RUN pip install --no-cache-dir \
12
- fastapi uvicorn python-multipart pillow transformers
13
 
 
14
  EXPOSE 7860
15
- CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
 
 
 
1
+ # Use a slim Python base image for CPU-only deployment
2
+ FROM python:3.10-slim
3
+
4
+ # Set working directory
5
+ WORKDIR /code
6
 
7
+ # Install system dependencies (if needed for PIL or other libs)
8
+ RUN apt-get update && apt-get install -y \
9
+ libgl1-mesa-glx \
10
+ libglib2.0-0 \
11
+ && rm -rf /var/lib/apt/lists/*
12
 
13
+ # Copy and install Python dependencies
14
+ COPY requirements.txt .
15
+ RUN pip install --no-cache-dir -r requirements.txt
16
 
17
+ # Copy the application code
18
+ COPY . .
19
 
20
+ # Expose the port (Hugging Face Spaces expects 7860 for FastAPI/Gradio)
21
  EXPOSE 7860
22
+
23
+ # Run the FastAPI app with uvicorn
24
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860", "--reload"]
app.py CHANGED
@@ -1,77 +1,36 @@
1
- import logging
2
  import io
3
- import time
4
- import json
5
- from fastapi import FastAPI, File, Form, UploadFile, HTTPException
6
- from fastapi.middleware.gzip import GZipMiddleware
7
- from PIL import Image
8
  import torch
9
- from transformers import AutoProcessor, AutoModelForImageTextToText
10
-
11
- logging.basicConfig(level=logging.INFO, format="%(asctime)s | %(levelname)s | %(message)s")
12
- logger = logging.getLogger(__name__)
13
-
14
- app = FastAPI(title="GLM-OCR Structured Extraction API", version="1.0.0")
15
- app.add_middleware(GZipMiddleware, minimum_size=1000)
16
-
17
- # ── Built-in schemas ──────────────────────────────────────────────
18
- SCHEMAS = {
19
- "pan_card": {
20
- "name": "string",
21
- "father_name": "string",
22
- "date_of_birth": "string",
23
- "pan_number": "string"
24
- },
25
- "aadhaar_card": {
26
- "name": "string",
27
- "date_of_birth": "string",
28
- "gender": "string",
29
- "aadhaar_number": "string",
30
- "address": "string"
31
- },
32
- "invoice": {
33
- "invoice_number": "string",
34
- "date": "string",
35
- "vendor_name": "string",
36
- "total_amount": "string",
37
- "line_items": "array"
38
- },
39
- "business_card": {
40
- "name": "string",
41
- "title": "string",
42
- "company": "string",
43
- "email": "string",
44
- "phone": "string",
45
- "website": "string"
46
- },
47
- "receipt": {
48
- "store_name": "string",
49
- "date": "string",
50
- "items": "array",
51
- "subtotal": "string",
52
- "tax": "string",
53
- "total": "string"
54
- }
55
- }
56
 
57
- DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
58
- MODEL_ID = "zai-org/GLM-OCR"
59
 
60
- logger.info(f"=== LOADING GLM-OCR on {DEVICE} ===")
61
- processor = AutoProcessor.from_pretrained(MODEL_ID, trust_remote_code=True)
62
- model = AutoModelForImageTextToText.from_pretrained(
63
- MODEL_ID,
64
- torch_dtype=torch.bfloat16 if DEVICE == "cuda" else torch.float32,
65
- trust_remote_code=True,
66
- ).to(DEVICE)
67
- model.eval()
68
- logger.info("✅ GLM-OCR ready!")
 
 
 
69
 
70
 
71
- # ── Routes ────────────────────────────────────────────────────────
72
  @app.get("/")
73
  async def root():
74
- return {"message": "GLM-OCR API ready!", "endpoints": ["GET /schemas", "POST /ocr/extract"]}
75
 
76
 
77
  @app.get("/health")
@@ -80,98 +39,73 @@ async def health():
80
 
81
 
82
  @app.get("/schemas")
83
- async def list_schemas():
84
- return {"available": list(SCHEMAS.keys()), "schemas": SCHEMAS}
 
 
 
 
 
 
85
 
86
 
87
  @app.post("/ocr/extract")
88
- async def extract(
89
- image: UploadFile = File(...),
90
- schema_name: str = Form(default="", description="Built-in: pan_card, aadhaar_card, invoice, business_card, receipt"),
91
- custom_schema: str = Form(default="", description='Custom JSON e.g. {"company":"string","gst":"string"}'),
92
  ):
93
- # ── Validate inputs ───────────────────────────────────────────
94
- if not schema_name and not custom_schema:
95
- raise HTTPException(status_code=422, detail='Provide schema_name or custom_schema.')
96
-
97
- if schema_name and schema_name not in SCHEMAS:
98
- raise HTTPException(status_code=422, detail=f"Unknown schema. Available: {list(SCHEMAS.keys())}")
99
-
100
- if image.content_type not in {"image/jpeg", "image/png", "image/webp", "image/bmp", "image/tiff"}:
101
- raise HTTPException(status_code=415, detail=f"Unsupported type: {image.content_type}")
102
-
103
- raw = await image.read()
104
- if len(raw) > 10 * 1024 * 1024:
105
- raise HTTPException(status_code=413, detail="File too large. Max 10MB.")
106
 
107
- # ── Resolve schema ────────────────────────────────────────────
108
- if schema_name:
109
- schema = SCHEMAS[schema_name]
110
- schema_label = schema_name
111
- else:
112
- try:
113
- schema = json.loads(custom_schema)
114
- schema_label = "custom"
115
- except json.JSONDecodeError:
116
- raise HTTPException(status_code=422, detail="custom_schema must be valid JSON.")
117
-
118
- logger.info(f"📸 Extract | file: {image.filename} | schema: {schema_label}")
119
- t0 = time.perf_counter()
120
-
121
- # ── Run GLM-OCR with JSON schema prompt ───────────────────────
122
  try:
123
- img = Image.open(io.BytesIO(raw)).convert("RGB")
124
-
125
- # GLM-OCR natively understands JSON schema extraction prompts
126
- prompt = (
127
- f"Extract information from this document and return ONLY a valid JSON object "
128
- f"matching this schema: {json.dumps(schema)}. "
129
- f"Use null for missing fields. No explanation, no markdown."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
  )
131
 
132
- messages = [{"role": "user", "content": [
133
- {"type": "image", "image": img},
134
- {"type": "text", "text": prompt}
135
- ]}]
136
-
137
- text = processor.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
138
- inputs = processor(text=text, images=[img], return_tensors="pt").to(DEVICE)
139
-
140
- with torch.inference_mode():
141
- outputs = model.generate(
142
- **inputs,
143
- max_new_tokens=1024,
144
- do_sample=False,
145
- )
146
-
147
- input_len = inputs["input_ids"].shape[1]
148
- raw_output = processor.decode(outputs[0][input_len:], skip_special_tokens=True).strip()
149
-
150
- # ── Parse JSON from response ──────────────────────────────
151
- try:
152
- # Strip markdown fences if present
153
- clean = raw_output.replace("```json", "").replace("```", "").strip()
154
- structured = json.loads(clean)
155
- except json.JSONDecodeError:
156
- # Fallback: extract JSON block
157
- start, end = clean.find("{"), clean.rfind("}") + 1
158
- structured = json.loads(clean[start:end]) if start != -1 else {"raw": raw_output}
159
-
160
- except Exception as e:
161
- logger.error(f"❌ Error: {e}")
162
- raise HTTPException(status_code=500, detail="Extraction failed.")
163
 
164
- elapsed = round((time.perf_counter() - t0) * 1000)
165
- logger.info(f"✅ Done in {elapsed}ms")
 
 
 
166
 
167
  return {
168
- "data": structured,
169
- "schema_used": schema_label,
170
- "inference_ms": elapsed,
171
- "model": MODEL_ID,
172
  }
173
 
174
 
175
  if __name__ == "__main__":
176
- import uvicorn
177
- uvicorn.run("app:app", host="0.0.0.0", port=7860, reload=False)
 
 
1
  import io
 
 
 
 
 
2
  import torch
3
+ from fastapi import FastAPI, File, UploadFile, HTTPException,Form
4
+ from fastapi.responses import JSONResponse
5
+ from PIL import Image
6
+ from transformers import AutoModelForCausalLM, AutoProcessor
7
+ import uvicorn
8
+
9
+ app = FastAPI(title="PaddleOCR-VL API", version="1.0.0")
10
+
11
+ MODEL_ID = "PaddlePaddle/PaddleOCR-VL"
12
+ DEVICE = "cpu"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
 
14
+ model = None
15
+ processor = None
16
 
17
+
18
+ @app.on_event("startup")
19
+ async def load_model():
20
+ global model, processor
21
+ print(f"Loading model {MODEL_ID} on {DEVICE}...")
22
+ model = AutoModelForCausalLM.from_pretrained(
23
+ MODEL_ID,
24
+ trust_remote_code=True,
25
+ torch_dtype=torch.float32 # Use float32 for reliable CPU inference
26
+ ).to(DEVICE).eval()
27
+ processor = AutoProcessor.from_pretrained(MODEL_ID, trust_remote_code=True)
28
+ print("Model loaded successfully!")
29
 
30
 
 
31
  @app.get("/")
32
  async def root():
33
+ return {"message": "PaddleOCR-VL API ready!", "endpoints": ["GET /schemas", "POST /ocr/extract"]}
34
 
35
 
36
  @app.get("/health")
 
39
 
40
 
41
  @app.get("/schemas")
42
+ async def schemas():
43
+ from fastapi.openapi.utils import get_openapi
44
+ return get_openapi(
45
+ title="PaddleOCR-VL API",
46
+ version="1.0.0",
47
+ description="API for OCR extraction using PaddleOCR-VL model",
48
+ routes=app.routes,
49
+ )
50
 
51
 
52
  @app.post("/ocr/extract")
53
+ async def ocr_extract(
54
+ file: UploadFile = File(..., description="Image file to process"),
55
+ query: str = Form("OCR:",
56
+ description="Specific query about the image (e.g., 'What is the date on this document?')")
57
  ):
58
+ if not file.content_type.startswith("image/"):
59
+ raise HTTPException(status_code=400, detail="File must be an image (e.g., PNG, JPEG)")
 
 
 
 
 
 
 
 
 
 
 
60
 
61
+ contents = await file.read()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  try:
63
+ image = Image.open(io.BytesIO(contents)).convert("RGB")
64
+ except Exception as e:
65
+ raise HTTPException(status_code=400, detail=f"Invalid image file: {str(e)}")
66
+
67
+ # Use the provided query as the prompt
68
+ messages = [
69
+ {
70
+ "role": "user",
71
+ "content": [
72
+ {"type": "image", "image": image},
73
+ {"type": "text", "text": query}
74
+ ]
75
+ }
76
+ ]
77
+
78
+ inputs = processor.apply_chat_template(
79
+ messages,
80
+ tokenize=True,
81
+ add_generation_prompt=True,
82
+ return_dict=True,
83
+ return_tensors="pt"
84
+ ).to(DEVICE)
85
+
86
+ with torch.no_grad():
87
+ outputs = model.generate(
88
+ **inputs,
89
+ max_new_tokens=1024,
90
+ do_sample=False, # Greedy decoding for deterministic OCR-like responses
91
+ temperature=0.1 # Low temp for factual extraction
92
  )
93
 
94
+ result = processor.batch_decode(outputs, skip_special_tokens=True)[0]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
 
96
+ # Clean up: Remove the query prefix from the response
97
+ if query in result:
98
+ extracted_text = result.split(query, 1)[1].strip()
99
+ else:
100
+ extracted_text = result.strip()
101
 
102
  return {
103
+ "query": query,
104
+ "extracted_text": extracted_text,
105
+ "full_response": result,
106
+ "model": MODEL_ID
107
  }
108
 
109
 
110
  if __name__ == "__main__":
111
+ uvicorn.run(app, host="0.0.0.0", port=7860)
 
requirements.txt CHANGED
@@ -1,6 +1,6 @@
1
- fastapi
2
- uvicorn
3
- python-multipart
4
- pillow
5
- transformers
6
- torch --index-url https://download.pytorch.org/whl/cpu
 
1
+ fastapi==0.115.0
2
+ uvicorn[standard]==0.32.0
3
+ transformers==4.46.3
4
+ torch==2.5.1
5
+ pillow==11.1.0
6
+ python-multipart==0.0.12