Redac / redac /vision.py
barath19's picture
fix: use MiniCPM-V-4.5 to resolve unsupported architecture on Space
988ede0
Raw
History Blame Contribute Delete
3.12 kB
"""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"}
# ZeroGPU decorator; degrade to a no-op decorator when `spaces` is absent
# (local dev) so the module imports cleanly off-Space.
try:
import spaces # type: ignore
_gpu = spaces.GPU(duration=120)
except Exception: # pragma: no cover - local fallback
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() # cached -> instant inside GPU window
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() # warm the cache on CPU before claiming the GPU
return _chat(image.convert("RGB"), prompt or EXTRACTION_PROMPT).strip()