| """Image -> text extraction with a local vision model (MiniCPM-V-4.5). |
| |
| Runs in-Space on ZeroGPU via the @spaces.GPU decorator. The model reads a |
| document/ID image and returns the fields as plain text, which then flows |
| through the same PII detection + redaction core as typed text. |
| |
| MiniCPM-V-4.5 (model_type "minicpmv") uses trust_remote_code + model.chat() |
| and runs on transformers 4.x. The weights are downloaded/loaded on CPU |
| *before* entering the GPU window so the 16GB download can't blow the |
| ZeroGPU time budget; the GPU call only moves the model to CUDA and generates. |
| |
| Local dev: set REDAC_MOCK=1 to skip the model entirely and return a canned |
| extraction, so the Gradio UI runs on a laptop with no GPU. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import os |
| from functools import lru_cache |
|
|
| MODEL_ID = "openbmb/MiniCPM-V-4_5" |
|
|
| EXTRACTION_PROMPT = ( |
| "You are a document data extractor. Read this image and transcribe ALL " |
| "personal and sensitive information you can find. Output one field per " |
| "line as 'field: value'. Include, when present: full name, date of birth, " |
| "address, passport/ID/driver-license number, national ID or social " |
| "security number, phone, email, and any account or card numbers. " |
| "Transcribe values exactly as written. Do not invent fields." |
| ) |
|
|
| _MOCK_EXTRACTION = ( |
| "full name: John A. Doe\n" |
| "date of birth: 1985-04-12\n" |
| "address: 221B Baker Street, London\n" |
| "passport number: X1234567\n" |
| "national id number: 123-45-6789\n" |
| "email: john.doe@example.com\n" |
| "phone: +49 151 23456789" |
| ) |
|
|
|
|
| def _is_mock() -> bool: |
| return os.environ.get("REDAC_MOCK", "").strip() in {"1", "true", "True"} |
|
|
|
|
| |
| |
| try: |
| import spaces |
|
|
| _gpu = spaces.GPU(duration=120) |
| except Exception: |
| def _gpu(fn): |
| return fn |
|
|
|
|
| @lru_cache(maxsize=1) |
| def _load_model(): |
| """Download + load weights on CPU. Cached. Called outside the GPU window.""" |
| import torch |
| from transformers import AutoModel, AutoTokenizer |
|
|
| model = AutoModel.from_pretrained( |
| MODEL_ID, |
| trust_remote_code=True, |
| attn_implementation="sdpa", |
| torch_dtype=torch.bfloat16, |
| ).eval() |
| tokenizer = AutoTokenizer.from_pretrained(MODEL_ID, trust_remote_code=True) |
| return model, tokenizer |
|
|
|
|
| @_gpu |
| def _chat(image, prompt: str) -> str: |
| model, tokenizer = _load_model() |
| model = model.to("cuda") |
| msgs = [{"role": "user", "content": [image, prompt]}] |
| return model.chat(image=None, msgs=msgs, tokenizer=tokenizer, sampling=False) |
|
|
|
|
| def extract_text_from_image(image, prompt: str | None = None) -> str: |
| """Return extracted field text from a PIL image. Honors REDAC_MOCK.""" |
| if image is None: |
| return "" |
| if _is_mock(): |
| return _MOCK_EXTRACTION |
| _load_model() |
| return _chat(image.convert("RGB"), prompt or EXTRACTION_PROMPT).strip() |
|
|