Repository navigation
Expand file tree
/
Copy pathdonut_processor.py
More file actions
165 lines (134 loc) · 6.07 KB
/
Copy pathdonut_processor.py
File metadata and controls
165 lines (134 loc) · 6.07 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
"""
Provides functionality for processing images using the Donut model.
This module implements a DonutProcessor class that uses the Donut model
for document understanding tasks, particularly OCR and information extraction
from images of documents.
"""
import json
import logging
import os
import re
from typing import TYPE_CHECKING, Any
from .image_downloader import ImageDownloader
if TYPE_CHECKING:
from PIL import Image
# Check if we're running in a test environment
# More robust test environment detection
IN_TEST_ENV = "PYTEST_CURRENT_TEST" in os.environ or "TOX_ENV_NAME" in os.environ
# Check if the PYTEST_DONUT flag is set to enable OCR testing
DONUT_TESTING_ENABLED = os.environ.get("PYTEST_DONUT", "").lower() == "yes"
class DonutProcessor:
"""
Handles image processing using the Donut model.
Provides methods for loading models, preprocessing images, parsing images
for text extraction, and managing dependencies. Supports processing both
local images and images from URLs.
"""
def __init__(self, model_path="naver-clova-ix/donut-base-finetuned-cord-v2"):
# Store model path for lazy loading
self.model_path = model_path
self.downloader = ImageDownloader()
@staticmethod
def _missing_dependency_message(package_name: str) -> str:
return (
f"Donut OCR requires {package_name}. "
"Install with: pip install datafog[nlp-advanced,ocr]"
)
def preprocess_image(self, image: "Image.Image") -> Any:
import numpy as np
# Convert to RGB if the image is not already in RGB mode
if image.mode != "RGB":
image = image.convert("RGB")
# Convert to numpy array
image_np = np.array(image)
# Ensure the image is 3D (height, width, channels)
if image_np.ndim == 2:
image_np = np.expand_dims(image_np, axis=-1)
image_np = np.repeat(image_np, 3, axis=-1)
return image_np
async def extract_text_from_image(self, image: "Image.Image") -> str:
"""Extract text from an image using the Donut model"""
logging.info("DonutProcessor.extract_text_from_image called")
# If we're in a test environment and PYTEST_DONUT is not enabled, return a mock response
if IN_TEST_ENV and not DONUT_TESTING_ENABLED:
logging.info(
"Running in test environment without PYTEST_DONUT=yes, returning mock OCR result"
)
mock_result = {"text": "Mock OCR text for testing"}
return json.dumps(mock_result)
# If PYTEST_DONUT is enabled, log that we're running real OCR in test mode
if IN_TEST_ENV and DONUT_TESTING_ENABLED:
logging.info(
"PYTEST_DONUT=yes is set, running actual OCR in test environment"
)
try:
try:
import torch
except ImportError as exc:
raise ImportError(self._missing_dependency_message("torch")) from exc
try:
from transformers import DonutProcessor as TransformersDonutProcessor
from transformers import VisionEncoderDecoderModel
except ImportError as e:
raise ImportError(
self._missing_dependency_message("transformers")
) from e
# Preprocess the image
image_np = self.preprocess_image(image)
# Initialize model components
try:
processor = TransformersDonutProcessor.from_pretrained(
self.model_path,
local_files_only=True,
)
model = VisionEncoderDecoderModel.from_pretrained(
self.model_path,
local_files_only=True,
)
except OSError as exc:
raise RuntimeError(
f"Donut model {self.model_path!r} is not available locally. "
"Download it explicitly before using Donut OCR, or pass a local "
"model path."
) from exc
device = "cuda" if torch.cuda.is_available() else "cpu"
model.to(device)
model.eval()
# Process the image
task_prompt = "<s_cord-v2>"
decoder_input_ids = processor.tokenizer(
task_prompt, add_special_tokens=False, return_tensors="pt"
).input_ids
pixel_values = processor(images=image_np, return_tensors="pt").pixel_values
outputs = model.generate(
pixel_values.to(device),
decoder_input_ids=decoder_input_ids.to(device),
max_length=model.decoder.config.max_position_embeddings,
early_stopping=True,
pad_token_id=processor.tokenizer.pad_token_id,
eos_token_id=processor.tokenizer.eos_token_id,
use_cache=True,
num_beams=1,
bad_words_ids=[[processor.tokenizer.unk_token_id]],
return_dict_in_generate=True,
)
sequence = processor.batch_decode(outputs.sequences)[0]
sequence = sequence.replace(processor.tokenizer.eos_token, "").replace(
processor.tokenizer.pad_token, ""
)
sequence = re.sub(r"<.*?>", "", sequence, count=1).strip()
result = processor.token2json(sequence)
return json.dumps(result)
except (ImportError, RuntimeError):
raise
except Exception as e:
logging.error(f"Error in extract_text_from_image: {e}")
# Return a placeholder in case of error
return "Error processing image with Donut model"
async def process_url(self, url: str) -> str:
"""Download an image from URL and process it to extract text."""
image = await self.downloader.download_image(url)
return await self.extract_text_from_image(image)
async def download_image(self, url: str) -> "Image.Image":
"""Download an image from URL."""
return await self.downloader.download_image(url)