ihatebaselines commited on
Commit
4f7bf82
·
verified ·
1 Parent(s): 10830b0

Upload inference.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. inference.py +269 -0
inference.py ADDED
@@ -0,0 +1,269 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn.functional as F
3
+ import cv2
4
+ from PIL import Image
5
+ from torch.utils.data import DataLoader
6
+ from .dataset import Makeset
7
+
8
+ def generate(model, image, bos_id, eos_id, device="cuda", temp=0.5, max_iter=64, penalty=1.15, top_k=5):
9
+ """
10
+ Prediction function using greedy search / top-k sampling with repetition penalty.
11
+
12
+ Args:
13
+ model: SOCRATE model instance.
14
+ image (Tensor): Single image tensor [1, C, H, W].
15
+ bos_id (int): Begin-of-sequence token ID.
16
+ eos_id (int): End-of-sequence token ID.
17
+ device (str): Target device. Default: "cuda".
18
+ temp (float): Temperature for sampling. Lower = more greedy. Default: 0.5.
19
+ max_iter (int): Max number of tokens to generate. Default: 64.
20
+ penalty (float): Repetition penalty applied to already-seen tokens. Default: 1.15.
21
+ top_k (int): Number of top candidates to sample from at each step. Default: 5.
22
+ """
23
+ model.eval()
24
+ current_text = [bos_id]
25
+ generated = []
26
+ already_seen = set()
27
+
28
+ with torch.inference_mode():
29
+ memory_image = model.encode(image)
30
+
31
+ for i in range(max_iter):
32
+ x = torch.tensor([current_text], dtype=torch.long).to(device)
33
+ output = model.decode(memory_image, x)
34
+ output = output[:, -1, :]
35
+
36
+ for token_id in already_seen:
37
+ if output[0, token_id] < 0:
38
+ output[0, token_id] *= penalty
39
+ else:
40
+ output[0, token_id] /= penalty
41
+
42
+ output = output / temp
43
+ topk_vals, topk_idx = torch.topk(output, top_k, dim=-1)
44
+ probs = F.softmax(topk_vals, dim=-1)
45
+ idx = torch.multinomial(probs, 1)
46
+
47
+ idx = topk_idx.gather(-1, idx).item()
48
+ if idx == eos_id:
49
+ break
50
+
51
+ generated.append(idx)
52
+ current_text.append(idx)
53
+ already_seen.add(idx)
54
+
55
+ return generated
56
+
57
+ def generate_fast(model, image, bos_id, eos_id, device="cuda", max_iter=32):
58
+ """
59
+ Super-fast prediction using only argmax (no sampling).
60
+
61
+ Args:
62
+ model: SOCRATE model instance.
63
+ image (Tensor): Single image tensor [1, C, H, W].
64
+ bos_id (int): Begin-of-sequence token ID.
65
+ eos_id (int): End-of-sequence token ID.
66
+ device (str): Target device. Default: "cuda".
67
+ max_iter (int): Max number of tokens to generate. Default: 32.
68
+ """
69
+ model.eval()
70
+ current_text = [bos_id]
71
+ generated = []
72
+
73
+ with torch.inference_mode():
74
+ memory_image = model.encode(image)
75
+ for _ in range(max_iter):
76
+ x = torch.tensor([current_text], dtype=torch.long, device=device)
77
+ output = model.decode(memory_image, x)
78
+ logits = output[:, -1, :]
79
+ idx = logits.argmax(dim=-1).item()
80
+
81
+ if idx == eos_id:
82
+ break
83
+
84
+ generated.append(idx)
85
+ current_text.append(idx)
86
+
87
+ return generated
88
+
89
+ def beam_search(model, image, bos_id, eos_id, device="cuda", beam_width=4, max_iter=64):
90
+ """
91
+ Beam search decoding.
92
+
93
+ Args:
94
+ model: SOCRATE model instance.
95
+ image (Tensor): Single image tensor [1, C, H, W].
96
+ bos_id (int): Begin-of-sequence token ID.
97
+ eos_id (int): End-of-sequence token ID.
98
+ device (str): Target device. Default: "cuda".
99
+ beam_width (int): Number of beams. Default: 4.
100
+ max_iter (int): Max tokens per beam. Default: 64.
101
+
102
+ Note: Full beam search is coming soon. Currently uses generate_fast as a fallback.
103
+ """
104
+ print("WARNING: Beam search is not fully implemented yet. Using generate_fast as a fallback.")
105
+ return generate_fast(model, image, bos_id, eos_id, device, max_iter=max_iter)
106
+
107
+ def extract_crops_from_image(image_path, doctr_model=None):
108
+ """
109
+ Extracts words (crops) using doctr and sorts them
110
+ correctly from top-to-bottom and left-to-right.
111
+ """
112
+ if doctr_model is None:
113
+ from doctr.models import detection_predictor
114
+ doctr_model = detection_predictor(arch="db_resnet50", pretrained=True)
115
+
116
+ from doctr.io import DocumentFile
117
+
118
+ doc = DocumentFile.from_images(image_path)
119
+ result = doctr_model(doc)
120
+
121
+ boxes = result[0]["words"]
122
+ image = cv2.imread(image_path)
123
+ if image is None:
124
+ raise ValueError(f"Could not load image from {image_path}")
125
+
126
+ H, W = image.shape[:2]
127
+
128
+ # Sort by lines a la SOCRATE
129
+ boxes_info = []
130
+ for b in boxes:
131
+ xmin, ymin, xmax, ymax, score = b
132
+ cy = (ymin + ymax) / 2.0
133
+ h = ymax - ymin
134
+ boxes_info.append({'box': b, 'cy': cy, 'h': h, 'x': xmin})
135
+
136
+ boxes_info.sort(key=lambda item: item['cy'])
137
+
138
+ lines = []
139
+ current_line = []
140
+
141
+ for b in boxes_info:
142
+ if not current_line:
143
+ current_line.append(b)
144
+ else:
145
+ tolerance = current_line[0]['h'] * 0.5
146
+ if abs(b['cy'] - current_line[0]['cy']) < tolerance:
147
+ current_line.append(b)
148
+ else:
149
+ lines.append(current_line)
150
+ current_line = [b]
151
+ if current_line:
152
+ lines.append(current_line)
153
+
154
+ sorted_boxes = []
155
+ for line in lines:
156
+ line.sort(key=lambda item: item['x'])
157
+ for item in line:
158
+ sorted_boxes.append(item['box'])
159
+
160
+ crops = []
161
+ for b in sorted_boxes:
162
+ xmin, ymin, xmax, ymax, score = b
163
+ x1 = int(xmin * W)
164
+ y1 = int(ymin * H)
165
+ x2 = int(xmax * W)
166
+ y2 = int(ymax * H)
167
+
168
+ crop = image[y1:y2, x1:x2]
169
+ h, w = crop.shape[:2]
170
+
171
+ if h == 0 or w == 0:
172
+ continue
173
+
174
+ crops.append(crop)
175
+
176
+ return crops
177
+
178
+ def predict(model, tokenizer, image_paths, wpb=16, function="generate_fast", doctr_model=None, bos_id=None, eos_id=None, device="cuda",
179
+ # generate() params
180
+ temp=None, max_iter=None, penalty=None, top_k=None,
181
+ # generate_fast() params
182
+ fast_max_iter=None,
183
+ # beam_search() params
184
+ beam_width=None, beam_max_iter=None):
185
+ """
186
+ The main prediction function of the library.
187
+ Takes images and returns the text read from them.
188
+
189
+ Inference parameters (temp, max_iter, penalty, top_k, fast_max_iter, beam_width, beam_max_iter)
190
+ can be set here directly, OR they will be read from model.sx_config if you created the model via sx.init(config=...).
191
+
192
+ Args:
193
+ model: SOCRATE model instance.
194
+ tokenizer: SocrateXTokenizer instance.
195
+ image_paths (str | List[str]): Path(s) to the image(s).
196
+ wpb (int): Words per batch. Default: 16.
197
+ function (str | callable): 'generate', 'generate_fast', 'beam_search', or a custom callable.
198
+ doctr_model: Pre-loaded doctr model (avoids re-loading on each call).
199
+ bos_id (int): Override BOS token ID.
200
+ eos_id (int): Override EOS token ID.
201
+ device (str): Target device. Default: "cuda".
202
+ temp (float): Temperature for generate(). Default: from config or 0.5.
203
+ max_iter (int): Max tokens for generate(). Default: from config or 64.
204
+ penalty (float): Repetition penalty for generate(). Default: from config or 1.15.
205
+ top_k (int): Top-k for generate(). Default: from config or 5.
206
+ fast_max_iter (int): Max tokens for generate_fast(). Default: from config or 32.
207
+ beam_width (int): Number of beams for beam_search(). Default: from config or 4.
208
+ beam_max_iter (int): Max tokens for beam_search(). Default: from config or 64.
209
+ """
210
+ model.eval()
211
+
212
+ # Pull inference defaults from sx_config if they were set
213
+ sx_cfg = getattr(model, "sx_config", None)
214
+
215
+ _temp = temp if temp is not None else (sx_cfg.temp if sx_cfg else 0.5)
216
+ _max_iter = max_iter if max_iter is not None else (sx_cfg.max_iter if sx_cfg else 64)
217
+ _penalty = penalty if penalty is not None else (sx_cfg.penalty if sx_cfg else 1.15)
218
+ _top_k = top_k if top_k is not None else (sx_cfg.top_k if sx_cfg else 5)
219
+ _fast_max = fast_max_iter if fast_max_iter is not None else (sx_cfg.fast_max_iter if sx_cfg else 32)
220
+ _beam_width = beam_width if beam_width is not None else (sx_cfg.beam_width if sx_cfg else 4)
221
+ _beam_max = beam_max_iter if beam_max_iter is not None else (sx_cfg.beam_max_iter if sx_cfg else 64)
222
+
223
+ # Resolve tokens (default to tokenizer if not provided)
224
+ if bos_id is None:
225
+ bos_id = tokenizer.token_to_id("<bos>")
226
+ if eos_id is None:
227
+ eos_id = tokenizer.token_to_id("<eos>")
228
+
229
+ results = {}
230
+
231
+ if isinstance(image_paths, str):
232
+ image_paths = [image_paths]
233
+
234
+ for image_path in image_paths:
235
+ crops = extract_crops_from_image(image_path, doctr_model)
236
+ if not crops:
237
+ results[image_path] = ""
238
+ continue
239
+
240
+ dataset = Makeset(images=crops)
241
+ dataloader = DataLoader(dataset, batch_size=wpb, shuffle=False, collate_fn=dataset.collate_fn)
242
+
243
+ doc_text = []
244
+ for batch in dataloader:
245
+ batch = batch.to(device)
246
+ for img in batch:
247
+ img = img.unsqueeze(0) # [1, C, H, W]
248
+
249
+ if function == "generate":
250
+ pred_ids = generate(model, img, bos_id=bos_id, eos_id=eos_id, device=device,
251
+ temp=_temp, max_iter=_max_iter, penalty=_penalty, top_k=_top_k)
252
+ elif function == "generate_fast":
253
+ pred_ids = generate_fast(model, img, bos_id=bos_id, eos_id=eos_id, device=device,
254
+ max_iter=_fast_max)
255
+ elif function == "beam_search":
256
+ pred_ids = beam_search(model, img, bos_id=bos_id, eos_id=eos_id, device=device,
257
+ beam_width=_beam_width, max_iter=_beam_max)
258
+ else:
259
+ if callable(function):
260
+ pred_ids = function(model, img, bos_id, eos_id, device)
261
+ else:
262
+ raise ValueError(f"Unknown function: {function}")
263
+
264
+ text = tokenizer.decode(pred_ids)
265
+ doc_text.append(text)
266
+
267
+ results[image_path] = " ".join(doc_text)
268
+
269
+ return results