IncreasingLoss commited on
Commit
02fcd9c
·
verified ·
1 Parent(s): 731df37

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +533 -570
app.py CHANGED
@@ -1,571 +1,534 @@
1
- import gradio as gr
2
- import torch
3
- import torch.nn as nn
4
- import torch.nn.functional as FU
5
- from torchvision import transforms
6
- import numpy as np
7
- from ultralytics import YOLO
8
- from llama_cpp import Llama
9
- from pypinyin import pinyin
10
- from PIL import Image, ImageDraw, ImageEnhance
11
- import json
12
- import os
13
- import torchvision.transforms.functional as F
14
-
15
-
16
- """yolo model"""
17
- user_device = "cuda" if torch.cuda.is_available() else "cpu"
18
- detection_model = YOLO("model/yolo_chinese_m.pt").to(user_device).eval()
19
-
20
-
21
-
22
- """LW-Vit Classifier"""
23
-
24
- class HSwish(nn.Module):
25
- """Hard Swish activation function"""
26
- def forward(self, x):
27
- out = x * FU.relu6(x + 3, inplace=True) / 6
28
- return out
29
-
30
- class MV2_Block(nn.Module):
31
- """MobileNetV2 Inverted Residual Block with h-swish activation"""
32
- def __init__(self, in_channels, expand_channels, out_channels, stride):
33
- super().__init__()
34
- self.stride = stride
35
- self.use_res_connect = self.stride == 1 and in_channels == out_channels
36
-
37
- self.expand = nn.Sequential(
38
- nn.Conv2d(in_channels, expand_channels, kernel_size=1, stride=1, padding=0, bias=False),
39
- nn.BatchNorm2d(expand_channels),
40
- HSwish()
41
- )
42
-
43
- self.depthwise = nn.Sequential(
44
- nn.Conv2d(expand_channels, expand_channels, kernel_size=3, stride=stride,
45
- padding=1, groups=expand_channels, bias=False),
46
- nn.BatchNorm2d(expand_channels),
47
- HSwish()
48
- )
49
-
50
- self.project = nn.Sequential(
51
- nn.Conv2d(expand_channels, out_channels, kernel_size=1, stride=1, padding=0, bias=False),
52
- nn.BatchNorm2d(out_channels)
53
- )
54
-
55
- def forward(self, x):
56
- if self.use_res_connect:
57
- return x + self.project(self.depthwise(self.expand(x)))
58
- else:
59
- return self.project(self.depthwise(self.expand(x)))
60
-
61
- class LW_ViT_Transformer_Block(nn.Module):
62
- """Lightweight Vision Transformer Block"""
63
- def __init__(self, in_channels, out_channels, patch_size=2, heads=4, dim=128):
64
- super().__init__()
65
- self.patch_size = patch_size
66
- self.in_channels = in_channels
67
- self.out_channels = out_channels
68
- self.dim = dim
69
-
70
- # Downsampling and channel adjustment
71
- self.downsample = nn.Conv2d(in_channels, in_channels, kernel_size=3, stride=2, padding=1)
72
- self.channel_adjust = nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=1)
73
-
74
- # Calculate patch dimension
75
- patch_dim = out_channels * patch_size * patch_size
76
- # Add projection layer if needed
77
- self.projection = nn.Identity() if patch_dim == dim else nn.Linear(patch_dim, dim)
78
-
79
- # Transformer components
80
- self.norm1 = nn.LayerNorm(dim)
81
- self.attn = nn.MultiheadAttention(embed_dim=dim, num_heads=heads, batch_first=True)
82
- self.norm2 = nn.LayerNorm(dim)
83
- self.ffn = nn.Sequential(
84
- nn.Linear(dim, dim * 4),
85
- nn.GELU(),
86
- nn.Linear(dim * 4, dim)
87
- )
88
-
89
- # Final processing
90
- self.final_conv = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=1, padding=1, bias=False)
91
- self.norm_out = nn.BatchNorm2d(out_channels)
92
- self.act = HSwish()
93
-
94
- def forward(self, x):
95
- # Initial downsampling and channel adjustment
96
- x = self.downsample(x)
97
- x = self.channel_adjust(x)
98
-
99
- B, C, H, W = x.shape
100
-
101
- # Convert feature map to patches and embed
102
- # Reshape to [B, C, H/P, P, W/P, P]
103
- x_reshaped = x.reshape(B, C, H // self.patch_size, self.patch_size,
104
- W // self.patch_size, self.patch_size)
105
- # Permute to [B, H/P, W/P, C, P, P]
106
- x_permuted = x_reshaped.permute(0, 2, 4, 1, 3, 5)
107
- # Reshape to [B, H/P * W/P, C * P * P]
108
- patches = x_permuted.reshape(B, (H // self.patch_size) * (W // self.patch_size), -1)
109
-
110
- # Apply projection if needed
111
- patches = self.projection(patches)
112
-
113
- # Apply transformer operations
114
- normed_patches = self.norm1(patches)
115
- attn_out, _ = self.attn(normed_patches, normed_patches, normed_patches)
116
- patches = patches + attn_out
117
-
118
- normed_patches = self.norm2(patches)
119
- ffn_out = self.ffn(normed_patches)
120
- patches = patches + ffn_out
121
-
122
- # Reshape back to feature map
123
- # Reshape to [B, H/P, W/P, C, P, P]
124
- x_back = patches.reshape(B, H // self.patch_size, W // self.patch_size,
125
- C, self.patch_size, self.patch_size)
126
- # Permute to [B, C, H/P, P, W/P, P]
127
- x_back = x_back.permute(0, 3, 1, 4, 2, 5)
128
- # Reshape to [B, C, H, W]
129
- x_out = x_back.reshape(B, C, H, W)
130
-
131
- # Final processing
132
- out = self.final_conv(x_out)
133
- out = self.norm_out(out)
134
- out = self.act(out)
135
-
136
- return out
137
-
138
- class LW_ViT(nn.Module):
139
- """Lightweight Vision Transformer for Chinese Character Recognition"""
140
- def __init__(self, base_channels=16, num_classes=3892):
141
- super().__init__()
142
-
143
- # Stem layer
144
- self.stem = nn.Sequential(
145
- nn.Conv2d(3, base_channels, 3, stride=2, padding=1, bias=False),
146
- nn.BatchNorm2d(base_channels),
147
- HSwish()
148
- )
149
-
150
- # Feature extraction layers
151
- self.features = nn.Sequential(
152
- # MV2 blocks
153
- MV2_Block(base_channels, 4*base_channels, base_channels, stride=1),
154
- MV2_Block(base_channels, 4*base_channels, base_channels, stride=2),
155
- MV2_Block(base_channels, 4*base_channels, base_channels, stride=1),
156
- MV2_Block(base_channels, 4*base_channels, base_channels, stride=2),
157
-
158
- # Transformer block
159
- LW_ViT_Transformer_Block(
160
- in_channels=base_channels,
161
- out_channels=2*base_channels,
162
- patch_size=2,
163
- heads=4,
164
- dim=128
165
- ),
166
-
167
- # Final convolutional layer
168
- nn.Conv2d(2*base_channels, 4*base_channels, kernel_size=1, stride=1),
169
- nn.BatchNorm2d(4*base_channels),
170
- HSwish()
171
- )
172
-
173
- # Global pooling and classifier
174
- self.global_pool = nn.AdaptiveAvgPool2d(1)
175
- self.dropout = nn.Dropout(0.1) # Added dropout for regularization
176
-
177
- self.classifier = nn.Sequential(
178
- nn.Linear(4*base_channels, 8*base_channels),
179
- nn.BatchNorm1d(8*base_channels),
180
- HSwish(),
181
- nn.Dropout(0.1), # Added dropout for regularization
182
- nn.Linear(8*base_channels, num_classes)
183
- )
184
-
185
- def forward(self, x):
186
- x = self.stem(x)
187
- x = self.features(x)
188
- x = self.global_pool(x)
189
- x = torch.flatten(x, 1)
190
- x = self.dropout(x)
191
- x = self.classifier(x)
192
- return x
193
-
194
- classifier = LW_ViT()
195
- classifier.load_state_dict(torch.load("model/chinese_Character_Classification_handwritten_LW_ViT.pth", weights_only=True, map_location=user_device))
196
- classifier.eval()
197
-
198
- """load classes dict"""
199
- with open("model/chinese_classes.json", "r", encoding="utf-8") as f:
200
- classes_dict = json.load(f)
201
-
202
- """transforms"""
203
- class ToSquare(object):
204
- """
205
- Transform to make images square by padding the shorter dimension
206
- """
207
- def __init__(self, fill=0):
208
- self.fill = fill # Fill value for padding (0 = black)
209
-
210
- def __call__(self, img):
211
- w, h = img.size
212
-
213
- # If already square, return as is
214
- if w == h:
215
- return img
216
-
217
- # Calculate target size (max dimension)
218
- max_dim = max(w, h)
219
-
220
- # Calculate padding
221
- pad_w = (max_dim - w) // 2
222
- pad_h = (max_dim - h) // 2
223
-
224
- # Handle odd dimensions (extra pixel on one side)
225
- pad_w_extra = (max_dim - w) % 2
226
- pad_h_extra = (max_dim - h) % 2
227
-
228
- # Create padding list (left, top, right, bottom) - FIX #1: Use list instead of tuple
229
- padding = [pad_w, pad_h, pad_w + pad_w_extra, pad_h + pad_h_extra]
230
-
231
- # Create new padded image
232
- padded_img = F.pad(img, padding, self.fill)
233
-
234
- return padded_img
235
-
236
- class ConvertToRGB(object):
237
- """Convert image to RGB mode"""
238
- def __call__(self, img):
239
- # Convert any image mode to RGB (including RGBA)
240
- return img.convert('RGB')
241
-
242
- class Contrast(object):
243
- """Randomly adjust contrast"""
244
- def __init__(self, p=0.3, factor_range=(0.5, 1.5)):
245
- self.p = p
246
- self.factor_range = factor_range
247
-
248
- def __call__(self, img):
249
- enhancer = ImageEnhance.Contrast(img)
250
- return enhancer.enhance(1.5)
251
-
252
- test_transforms = transforms.Compose([
253
- ToSquare(fill=255),
254
- transforms.Resize(128),
255
- ConvertToRGB(),
256
- transforms.ToTensor(),
257
- transforms.Normalize(mean=[0.5], std=[0.5])
258
- ])
259
-
260
-
261
- """import llm"""
262
- llm = Llama.from_pretrained(
263
- repo_id="IncreasingLoss/FineTunedTranslation_Yi-1.5-9B-Chat-Q6_K",
264
- filename="*.gguf", # or specify exact file name if only one GGUF in repo
265
- n_ctx=4096,
266
- n_gpu_layers=-1,
267
- verbose=False
268
- )
269
-
270
- """app functions"""
271
- def convert_to_pil(inputimg):
272
- # convert input to pil image
273
- if inputimg is None:
274
- return None
275
- if isinstance(inputimg, np.ndarray):
276
- pil_img = Image.fromarray(inputimg)
277
- elif hasattr(inputimg, 'convert'): # Check if it's already a PIL Image
278
- pil_img = inputimg
279
- else:
280
- pil_img = Image.fromarray(np.array(inputimg))
281
- pil_img = pil_img.convert("RGB")
282
- return pil_img
283
-
284
- def draw_bboxes(img, results):
285
- # Create a copy of the input image to draw on
286
- draw_img = img.copy()
287
- draw = ImageDraw.Draw(draw_img)
288
- # Draw bounding boxes for detected objects
289
- for box in results[0].boxes.xyxy:
290
- x1, y1, x2, y2 = map(int, box) # Convert to integers
291
- draw.rectangle([x1, y1, x2, y2], outline=(255, 0, 0), width=2)
292
- return draw_img
293
-
294
-
295
- from statistics import mean
296
-
297
- def crop_bboxes_by_reading_order(img, bboxes, written_vertical=False):
298
- """
299
- Crop and return image regions in proper reading order.
300
-
301
- For vertical Chinese:
302
- - Group into columns by x-coordinate (clusters whose centers are within avg_width/2)
303
- - Order columns right-to-left
304
- - Within each column, order top-to-bottom by y
305
-
306
- For horizontal:
307
- - Group into rows by y-coordinate (clusters within avg_height/2)
308
- - Order rows top-to-bottom
309
- - Within each row, order left-to-right by x
310
- """
311
- # 1) If bboxes is a Tensor, convert to Python list
312
- if hasattr(bboxes, "tolist"):
313
- bboxes = bboxes.tolist()
314
-
315
- # 2) Single box -> trivial crop
316
- if len(bboxes) <= 1:
317
- return [img.crop(tuple(map(int, bbox))) for bbox in bboxes]
318
-
319
- # 3) Compute centers and average glyph size
320
- centers = [((x1+x2)/2, (y1+y2)/2) for x1,y1,x2,y2 in bboxes]
321
- widths = [x2 - x1 for x1,y1,x2,y2 in bboxes]
322
- heights = [y2 - y1 for x1,y1,x2,y2 in bboxes]
323
- avg_w = mean(widths)
324
- avg_h = mean(heights)
325
-
326
- # 4) Choose clustering axis and threshold
327
- if written_vertical:
328
- coords = [c[0] for c in centers] # x-coords
329
- threshold = avg_w / 2
330
- else:
331
- coords = [c[1] for c in centers] # y-coords
332
- threshold = avg_h / 2
333
-
334
- # 5) Sort glyphs by the clustering axis
335
- order = sorted(range(len(coords)), key=lambda i: coords[i])
336
-
337
- # 6) Build clusters
338
- clusters = []
339
- current = [order[0]]
340
- current_mean = coords[order[0]]
341
-
342
- for idx in order[1:]:
343
- c = coords[idx]
344
- # if within threshold of this cluster's mean, add; else start new
345
- if abs(c - current_mean) <= threshold:
346
- current.append(idx)
347
- # update cluster mean incrementally
348
- current_mean = mean([coords[i] for i in current])
349
- else:
350
- clusters.append(current)
351
- current = [idx]
352
- current_mean = c
353
- clusters.append(current)
354
-
355
- # 7) Order clusters
356
- if written_vertical:
357
- # Chinese vertical: rightmost column first
358
- clusters.sort(
359
- key=lambda grp: mean(coords[i] for i in grp),
360
- reverse=True
361
- )
362
- else:
363
- # Horizontal: top row first
364
- clusters.sort(
365
- key=lambda grp: mean(coords[i] for i in grp)
366
- )
367
-
368
- # 8) Within each cluster, sort by the orthogonal axis
369
- final_indices = []
370
- for grp in clusters:
371
- if written_vertical:
372
- # sort top-to-bottom by y
373
- grp.sort(key=lambda i: centers[i][1])
374
- else:
375
- # sort left-to-right by x
376
- grp.sort(key=lambda i: centers[i][0])
377
- final_indices.extend(grp)
378
-
379
- # 9) Crop and return
380
- crops = []
381
- for i in final_indices:
382
- x1, y1, x2, y2 = map(int, bboxes[i])
383
- crops.append(img.crop((x1, y1, x2, y2)))
384
-
385
- return crops
386
-
387
-
388
- def move_slider(threshhold_slider, input_img):
389
- pil_img = convert_to_pil(input_img)
390
- # FIX #2: Add null check for pil_img
391
- if pil_img is None:
392
- return None
393
- with torch.inference_mode():
394
- results = detection_model(source=pil_img, conf=threshhold_slider)
395
- draw_img = draw_bboxes(pil_img, results)
396
- return draw_img
397
-
398
- def select_image(evt: gr.SelectData):
399
- # Get the selected index from the gallery
400
- selected_index = evt.index
401
- # Return the path of the selected image
402
- return example_images[selected_index]
403
-
404
- def translate_text(threshhold_slider, is_vertical, input_img):
405
- # convert input to pil image
406
- pil_img = convert_to_pil(input_img)
407
- # FIX #2: Add null check for pil_img
408
- if pil_img is None:
409
- return "", "", ""
410
-
411
- with torch.inference_mode():
412
- results = detection_model(source=pil_img, conf=threshhold_slider)
413
- sorted_cropped_images = crop_bboxes_by_reading_order(img=pil_img, bboxes=results[0].boxes.xyxy, written_vertical=is_vertical)
414
- chinese_text = ""
415
- for crop in sorted_cropped_images:
416
- crop = test_transforms(crop)
417
- crop = crop.to(user_device)
418
- logits = classifier(crop.unsqueeze(dim=0))
419
- class_idx = logits.argmax(dim=1)
420
- chinese_charcter = classes_dict[class_idx]
421
- chinese_text += chinese_charcter
422
-
423
- pinyin_text = pinyin(chinese_text)
424
- pinyin_sentence = " "
425
- for pin in pinyin_text:
426
- pinyin_sentence += f"{pin[0]} "
427
-
428
- # Enhanced prompt for better translation quality
429
- prompt = f""" You are a professional Chinese to English translator.
430
- 1. Translate the following Chinese text to natural, fluent English:
431
- "{chinese_text}"
432
- 2. Respond only with the translated English text.
433
- English translation: """
434
-
435
- # FIX #3: Disable streaming to get direct dict access
436
- output = llm(
437
- prompt,
438
- max_tokens=min(len(chinese_text)*2, 512), # More reasonable token limit with safety cap
439
- stop=["</s>", "\n\n", "Chinese:", "Chinese text:"], # Enhanced stop tokens
440
- echo=False,
441
- temperature=0.3, # Slightly higher temperature for more natural language
442
- frequency_penalty=.5, # Penalize repetition of tokens - higher values discourage repetition
443
- presence_penalty=.5, # Penalize tokens that have appeared at all - prevents topic repetition
444
- top_p=0.9, # Nucleus sampling - restricts to top 90% most likely tokens for more coherent output
445
- stream=False # Explicitly disable streaming to get a dict response
446
- )
447
-
448
- # Extract the translation
449
- translation = output["choices"][0]["text"].strip()
450
- try:
451
- start = translation.index('"') + 1
452
- # find index of last double-quote
453
- end = translation.rindex('"')
454
- translation = translation[start:end]
455
- except:
456
- pass
457
-
458
- return chinese_text, pinyin_sentence, translation
459
-
460
-
461
- css = """
462
- .centered-examples {
463
- margin: 0 auto !important;
464
- justify-content: center !important;
465
- gap: 8px !important;
466
- min-height: 150px !important;
467
- }
468
- .centered-examples .thumb {
469
- height: 100px !important;
470
- width: 100px !important;
471
- object-fit: cover !important;
472
- margin: 5px !important;
473
- }
474
-
475
- /* Universal solution - targets any element with both fixed-height and svelte classes */
476
- [class*="fixed-height"][class*="svelte-"] {
477
- min-height: 0 !important;
478
- height: auto !important;
479
- overflow-y: auto !important;
480
- }
481
-
482
- /* More specific for grid-wrap containers */
483
- [class*="grid-wrap"][class*="svelte-"][class*="fixed-height"] {
484
- min-height: 0 !important;
485
- height: auto !important;
486
- overflow-y: auto !important;
487
- }
488
-
489
- /* Target gallery containers specifically */
490
- #my_media_gallery [class*="grid-wrap"] {
491
- min-height: 0 !important;
492
- height: auto !important;
493
- overflow-y: auto !important;
494
- }
495
-
496
- /* Media query version */
497
- @media (min-width: 1280px) {
498
- [class*="fixed-height"][class*="svelte-"] {
499
- min-height: 0 !important;
500
- height: auto !important;
501
- }
502
-
503
- [class*="grid-wrap"][class*="svelte-"][class*="fixed-height"] {
504
- min-height: 0 !important;
505
- height: auto !important;
506
- }
507
- }
508
-
509
- /* Fallback with even higher specificity */
510
- div[class*="fixed-height"][class*="svelte-"]:not([style*="min-height"]) {
511
- min-height: 0 !important;
512
- }
513
-
514
- /* Target the specific gallery by ID */
515
- #my_media_gallery div[class*="grid-wrap"] {
516
- min-height: 0 !important;
517
- height: auto !important;
518
- overflow-y: auto !important;
519
- }
520
- """
521
-
522
-
523
- """gradio app"""
524
- example_dir = "examples"
525
- example_images = [os.path.join(example_dir, f) for f in os.listdir(example_dir)
526
- if f.lower().endswith(('.png', '.jpg', '.jpeg', '.webp'))]
527
- with gr.Blocks(css=css) as program:
528
- gr.Markdown("## DeepTranslate: Chinese OCR with translation to english")
529
- gr.Markdown("Upload or select an image and move the slider to detect characters.")
530
- gr.Markdown("Make sure that the input image is high resolution and not rotated in any way!")
531
- gr.Markdown("The tranlation in spaces takes a lot of time since its running on a cpu & uses a llm for the tranlation.")
532
- gr.Markdown("For Faster useage please download the files and run it locally (takes 20 seconds/translation on a 4080). On cpu it takes around 10 min/tranlation!")
533
- #inputs
534
- with gr.Column(scale=1):
535
- gallery = gr.Gallery(
536
- value=example_images,
537
- label="Example Images (Click to Select)",
538
- columns=6,
539
- rows=None, # Let it auto-size based on content
540
- height="auto", # Auto height based on content
541
- allow_preview=False,
542
- container=False, # Remove container constraints
543
- elem_id="my_media_gallery",
544
- elem_classes=["centered-examples"],
545
- interactive=True,
546
- show_label=True,
547
- object_fit="cover"
548
- )
549
- with gr.Row(scale=2):
550
- input_img = gr.Image(label="Input Image ")
551
- detection_img = gr.Image(label="Detection Image", interactive= False)
552
-
553
- # slider and button
554
- with gr.Column(scale=1):
555
- with gr.Row(scale=3):
556
- threshhold_slider = gr.Slider(value=0.25, minimum=0, maximum=0.75, label="Detection Threshhold", step = 0.01)
557
- translate_button = gr.Button("Translate To English", variant="primary")
558
- is_vertical = gr.Checkbox(value=False, label="Vertiacl Chinese Text?", interactive=True)
559
- # outputs
560
- with gr.Column(scale=1):
561
- with gr.Row(scale=3):
562
- chinese_text = gr.TextArea(label="Chinese Text", max_lines= 1000, interactive=False)
563
- pinyin_text = gr.TextArea(label="Chinese Pinyin", max_lines= 1000, interactive=False)
564
- english_text = gr.TextArea(label="English Text", max_lines= 1000, interactive=False)
565
- #function, calling
566
- threshhold_slider.change(fn= move_slider, inputs=[threshhold_slider, input_img], outputs=[detection_img])
567
- translate_button.click(fn=translate_text, inputs= [threshhold_slider, is_vertical, input_img], outputs=[chinese_text, pinyin_text, english_text])
568
- gallery.select(fn=select_image, inputs=None, outputs=input_img)
569
-
570
- if __name__ == "__main__":
571
  program.launch(share=False)
 
1
+ import gradio as gr
2
+ import torch
3
+ import torch.nn as nn
4
+ import torch.nn.functional as FU
5
+ from torchvision import transforms
6
+ import numpy as np
7
+ from ultralytics import YOLO
8
+ from llama_cpp import Llama
9
+ from pypinyin import pinyin
10
+ from PIL import Image, ImageDraw, ImageEnhance
11
+ import json
12
+ import os
13
+ import torchvision.transforms.functional as F
14
+ from statistics import mean
15
+
16
+
17
+ """yolo model"""
18
+ user_device = "cuda" if torch.cuda.is_available() else "cpu"
19
+ detection_model = YOLO("model/yolo/yolo_chinese_m.pt").to(user_device).eval()
20
+
21
+
22
+
23
+ """LW-Vit Classifier"""
24
+
25
+ class HSwish(nn.Module):
26
+ """Hard Swish activation function"""
27
+ def forward(self, x):
28
+ out = x * FU.relu6(x + 3, inplace=True) / 6
29
+ return out
30
+
31
+ class MV2_Block(nn.Module):
32
+ """MobileNetV2 Inverted Residual Block with h-swish activation"""
33
+ def __init__(self, in_channels, expand_channels, out_channels, stride):
34
+ super().__init__()
35
+ self.stride = stride
36
+ self.use_res_connect = self.stride == 1 and in_channels == out_channels
37
+
38
+ self.expand = nn.Sequential(
39
+ nn.Conv2d(in_channels, expand_channels, kernel_size=1, stride=1, padding=0, bias=False),
40
+ nn.BatchNorm2d(expand_channels),
41
+ HSwish()
42
+ )
43
+
44
+ self.depthwise = nn.Sequential(
45
+ nn.Conv2d(expand_channels, expand_channels, kernel_size=3, stride=stride,
46
+ padding=1, groups=expand_channels, bias=False),
47
+ nn.BatchNorm2d(expand_channels),
48
+ HSwish()
49
+ )
50
+
51
+ self.project = nn.Sequential(
52
+ nn.Conv2d(expand_channels, out_channels, kernel_size=1, stride=1, padding=0, bias=False),
53
+ nn.BatchNorm2d(out_channels)
54
+ )
55
+
56
+ def forward(self, x):
57
+ if self.use_res_connect:
58
+ return x + self.project(self.depthwise(self.expand(x)))
59
+ else:
60
+ return self.project(self.depthwise(self.expand(x)))
61
+
62
+ class LW_ViT_Transformer_Block(nn.Module):
63
+ """Lightweight Vision Transformer Block"""
64
+ def __init__(self, in_channels, out_channels, patch_size=2, heads=4, dim=128):
65
+ super().__init__()
66
+ self.patch_size = patch_size
67
+ self.in_channels = in_channels
68
+ self.out_channels = out_channels
69
+ self.dim = dim
70
+
71
+ # Downsampling and channel adjustment
72
+ self.downsample = nn.Conv2d(in_channels, in_channels, kernel_size=3, stride=2, padding=1)
73
+ self.channel_adjust = nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=1)
74
+
75
+ # Calculate patch dimension
76
+ patch_dim = out_channels * patch_size * patch_size
77
+ # Add projection layer if needed
78
+ self.projection = nn.Identity() if patch_dim == dim else nn.Linear(patch_dim, dim)
79
+
80
+ # Transformer components
81
+ self.norm1 = nn.LayerNorm(dim)
82
+ self.attn = nn.MultiheadAttention(embed_dim=dim, num_heads=heads, batch_first=True)
83
+ self.norm2 = nn.LayerNorm(dim)
84
+ self.ffn = nn.Sequential(
85
+ nn.Linear(dim, dim * 4),
86
+ nn.GELU(),
87
+ nn.Linear(dim * 4, dim)
88
+ )
89
+
90
+ # Final processing
91
+ self.final_conv = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=1, padding=1, bias=False)
92
+ self.norm_out = nn.BatchNorm2d(out_channels)
93
+ self.act = HSwish()
94
+
95
+ def forward(self, x):
96
+ # Initial downsampling and channel adjustment
97
+ x = self.downsample(x)
98
+ x = self.channel_adjust(x)
99
+
100
+ B, C, H, W = x.shape
101
+
102
+ # Convert feature map to patches and embed
103
+ # Reshape to [B, C, H/P, P, W/P, P]
104
+ x_reshaped = x.reshape(B, C, H // self.patch_size, self.patch_size,
105
+ W // self.patch_size, self.patch_size)
106
+ # Permute to [B, H/P, W/P, C, P, P]
107
+ x_permuted = x_reshaped.permute(0, 2, 4, 1, 3, 5)
108
+ # Reshape to [B, H/P * W/P, C * P * P]
109
+ patches = x_permuted.reshape(B, (H // self.patch_size) * (W // self.patch_size), -1)
110
+
111
+ # Apply projection if needed
112
+ patches = self.projection(patches)
113
+
114
+ # Apply transformer operations
115
+ normed_patches = self.norm1(patches)
116
+ attn_out, _ = self.attn(normed_patches, normed_patches, normed_patches)
117
+ patches = patches + attn_out
118
+
119
+ normed_patches = self.norm2(patches)
120
+ ffn_out = self.ffn(normed_patches)
121
+ patches = patches + ffn_out
122
+
123
+ # Reshape back to feature map
124
+ # Reshape to [B, H/P, W/P, C, P, P]
125
+ x_back = patches.reshape(B, H // self.patch_size, W // self.patch_size,
126
+ C, self.patch_size, self.patch_size)
127
+ # Permute to [B, C, H/P, P, W/P, P]
128
+ x_back = x_back.permute(0, 3, 1, 4, 2, 5)
129
+ # Reshape to [B, C, H, W]
130
+ x_out = x_back.reshape(B, C, H, W)
131
+
132
+ # Final processing
133
+ out = self.final_conv(x_out)
134
+ out = self.norm_out(out)
135
+ out = self.act(out)
136
+
137
+ return out
138
+
139
+ class LW_ViT(nn.Module):
140
+ """Lightweight Vision Transformer for Chinese Character Recognition"""
141
+ def __init__(self, base_channels=16, num_classes=3892):
142
+ super().__init__()
143
+
144
+ # Stem layer
145
+ self.stem = nn.Sequential(
146
+ nn.Conv2d(3, base_channels, 3, stride=2, padding=1, bias=False),
147
+ nn.BatchNorm2d(base_channels),
148
+ HSwish()
149
+ )
150
+
151
+ # Feature extraction layers
152
+ self.features = nn.Sequential(
153
+ # MV2 blocks
154
+ MV2_Block(base_channels, 4*base_channels, base_channels, stride=1),
155
+ MV2_Block(base_channels, 4*base_channels, base_channels, stride=2),
156
+ MV2_Block(base_channels, 4*base_channels, base_channels, stride=1),
157
+ MV2_Block(base_channels, 4*base_channels, base_channels, stride=2),
158
+
159
+ # Transformer block
160
+ LW_ViT_Transformer_Block(
161
+ in_channels=base_channels,
162
+ out_channels=2*base_channels,
163
+ patch_size=2,
164
+ heads=4,
165
+ dim=128
166
+ ),
167
+
168
+ # Final convolutional layer
169
+ nn.Conv2d(2*base_channels, 4*base_channels, kernel_size=1, stride=1),
170
+ nn.BatchNorm2d(4*base_channels),
171
+ HSwish()
172
+ )
173
+
174
+ # Global pooling and classifier
175
+ self.global_pool = nn.AdaptiveAvgPool2d(1)
176
+ self.dropout = nn.Dropout(0.1) # Added dropout for regularization
177
+
178
+ self.classifier = nn.Sequential(
179
+ nn.Linear(4*base_channels, 8*base_channels),
180
+ nn.BatchNorm1d(8*base_channels),
181
+ HSwish(),
182
+ nn.Dropout(0.1), # Added dropout for regularization
183
+ nn.Linear(8*base_channels, num_classes)
184
+ )
185
+
186
+ def forward(self, x):
187
+ x = self.stem(x)
188
+ x = self.features(x)
189
+ x = self.global_pool(x)
190
+ x = torch.flatten(x, 1)
191
+ x = self.dropout(x)
192
+ x = self.classifier(x)
193
+ return x
194
+
195
+ classifier = LW_ViT()
196
+ classifier.load_state_dict(torch.load("model/LW_Vit/chinese_Character_Classification_handwritten_LW_ViT.pth", weights_only=True, map_location=user_device))
197
+ classifier = classifier.to(user_device)
198
+ classifier.eval()
199
+
200
+ """load classes dict"""
201
+ with open("model/LW_Vit/chinese_classes.json", "r", encoding="utf-8") as f:
202
+ classes_dict = json.load(f)
203
+
204
+ """transforms"""
205
+ class ToSquare(object):
206
+ """
207
+ Transform to make images square by padding the shorter dimension
208
+ """
209
+ def __init__(self, fill=0):
210
+ self.fill = fill # Fill value for padding (0 = black)
211
+
212
+ def __call__(self, img):
213
+ w, h = img.size
214
+
215
+ # If already square, return as is
216
+ if w == h:
217
+ return img
218
+
219
+ # Calculate target size (max dimension)
220
+ max_dim = max(w, h)
221
+
222
+ # Calculate padding
223
+ pad_w = (max_dim - w) // 2
224
+ pad_h = (max_dim - h) // 2
225
+
226
+ # Handle odd dimensions (extra pixel on one side)
227
+ pad_w_extra = (max_dim - w) % 2
228
+ pad_h_extra = (max_dim - h) % 2
229
+
230
+ # Create padding list (left, top, right, bottom) - FIX #1: Use list instead of tuple
231
+ padding = [pad_w, pad_h, pad_w + pad_w_extra, pad_h + pad_h_extra]
232
+
233
+ # Create new padded image
234
+ padded_img = F.pad(img, padding, self.fill)
235
+
236
+ return padded_img
237
+
238
+ class ConvertToRGB(object):
239
+ """Convert image to RGB mode"""
240
+ def __call__(self, img):
241
+ # Convert any image mode to RGB (including RGBA)
242
+ return img.convert('RGB')
243
+
244
+ class Contrast(object):
245
+ """Randomly adjust contrast"""
246
+ def __init__(self, p=0.3, factor_range=(0.5, 1.5)):
247
+ self.p = p
248
+ self.factor_range = factor_range
249
+
250
+ def __call__(self, img):
251
+ enhancer = ImageEnhance.Contrast(img)
252
+ return enhancer.enhance(1.5)
253
+
254
+ test_transforms = transforms.Compose([
255
+ ToSquare(fill=255),
256
+ transforms.Resize(128),
257
+ ConvertToRGB(),
258
+ transforms.ToTensor(),
259
+ transforms.Normalize(mean=[0.5], std=[0.5])
260
+ ])
261
+
262
+
263
+ """import llm"""
264
+ llm = Llama(
265
+ model_path="model/llms/Yi-1.5-9B-Chat-Q6_K.gguf",
266
+ n_ctx=4096, # Context window size
267
+ n_gpu_layers=-1, # -1 means use all available GPU layers
268
+ verbose=False # Set to True for debugging information
269
+ )
270
+
271
+
272
+ """app functions"""
273
+ def convert_to_pil(inputimg):
274
+ # convert input to pil image
275
+ if inputimg is None:
276
+ return None
277
+ if isinstance(inputimg, np.ndarray):
278
+ pil_img = Image.fromarray(inputimg)
279
+ elif hasattr(inputimg, 'convert'): # Check if it's already a PIL Image
280
+ pil_img = inputimg
281
+ else:
282
+ pil_img = Image.fromarray(np.array(inputimg))
283
+ pil_img = pil_img.convert("RGB")
284
+ return pil_img
285
+
286
+ def draw_bboxes(img, results):
287
+ # Create a copy of the input image to draw on
288
+ draw_img = img.copy()
289
+ draw = ImageDraw.Draw(draw_img)
290
+ # Draw bounding boxes for detected objects
291
+ for box in results[0].boxes.xyxy:
292
+ x1, y1, x2, y2 = map(int, box) # Convert to integers
293
+ draw.rectangle([x1, y1, x2, y2], outline=(255, 0, 0), width=2)
294
+ return draw_img
295
+
296
+
297
+
298
+ def crop_bboxes_by_reading_order(img, bboxes, written_vertical=False):
299
+ """
300
+ Crop and return image regions in proper reading order.
301
+
302
+ For vertical Chinese:
303
+ - Group into columns by x-coordinate (clusters whose centers are within avg_width/2)
304
+ - Order columns right-to-left
305
+ - Within each column, order top-to-bottom by y
306
+
307
+ For horizontal:
308
+ - Group into rows by y-coordinate (clusters within avg_height/2)
309
+ - Order rows top-to-bottom
310
+ - Within each row, order left-to-right by x
311
+ """
312
+ # 1) If bboxes is a Tensor, convert to Python list
313
+ if hasattr(bboxes, "tolist"):
314
+ bboxes = bboxes.tolist()
315
+
316
+ # 2) Single box -> trivial crop
317
+ if len(bboxes) <= 1:
318
+ return [img.crop(tuple(map(int, bbox))) for bbox in bboxes]
319
+
320
+ # 3) Compute centers and average glyph size
321
+ centers = [((x1+x2)/2, (y1+y2)/2) for x1,y1,x2,y2 in bboxes]
322
+ widths = [x2 - x1 for x1,y1,x2,y2 in bboxes]
323
+ heights = [y2 - y1 for x1,y1,x2,y2 in bboxes]
324
+ avg_w = mean(widths)
325
+ avg_h = mean(heights)
326
+
327
+ # 4) Choose clustering axis and threshold
328
+ if written_vertical:
329
+ coords = [c[0] for c in centers] # x-coords
330
+ threshold = avg_w / 2
331
+ else:
332
+ coords = [c[1] for c in centers] # y-coords
333
+ threshold = avg_h / 2
334
+
335
+ # 5) Sort glyphs by the clustering axis
336
+ order = sorted(range(len(coords)), key=lambda i: coords[i])
337
+
338
+ # 6) Build clusters
339
+ clusters = []
340
+ current = [order[0]]
341
+ current_mean = coords[order[0]]
342
+
343
+ for idx in order[1:]:
344
+ c = coords[idx]
345
+ # if within threshold of this cluster's mean, add; else start new
346
+ if abs(c - current_mean) <= threshold:
347
+ current.append(idx)
348
+ # update cluster mean incrementally
349
+ current_mean = mean([coords[i] for i in current])
350
+ else:
351
+ clusters.append(current)
352
+ current = [idx]
353
+ current_mean = c
354
+ clusters.append(current)
355
+
356
+ # 7) Order clusters
357
+ if written_vertical:
358
+ # Chinese vertical: rightmost column first
359
+ clusters.sort(
360
+ key=lambda grp: mean(coords[i] for i in grp),
361
+ reverse=True
362
+ )
363
+ else:
364
+ # Horizontal: top row first
365
+ clusters.sort(
366
+ key=lambda grp: mean(coords[i] for i in grp)
367
+ )
368
+
369
+ # 8) Within each cluster, sort by the orthogonal axis
370
+ final_indices = []
371
+ for grp in clusters:
372
+ if written_vertical:
373
+ # sort top-to-bottom by y
374
+ grp.sort(key=lambda i: centers[i][1])
375
+ else:
376
+ # sort left-to-right by x
377
+ grp.sort(key=lambda i: centers[i][0])
378
+ final_indices.extend(grp)
379
+
380
+ # 9) Crop and return
381
+ crops = []
382
+ for i in final_indices:
383
+ x1, y1, x2, y2 = map(int, bboxes[i])
384
+ crops.append(img.crop((x1, y1, x2, y2)))
385
+
386
+ return crops
387
+
388
+
389
+ def move_slider(threshhold_slider, input_img):
390
+ pil_img = convert_to_pil(input_img)
391
+ # FIX #2: Add null check for pil_img
392
+ if pil_img is None:
393
+ return None
394
+ with torch.inference_mode():
395
+ results = detection_model(source=pil_img, conf=threshhold_slider)
396
+ draw_img = draw_bboxes(pil_img, results)
397
+ return draw_img
398
+
399
+ def select_image(evt: gr.SelectData):
400
+ selected_index = evt.index
401
+ return example_images[selected_index]
402
+
403
+ def translate_text(threshhold_slider, is_vertical, input_img):
404
+ # convert input to pil image
405
+ pil_img = convert_to_pil(input_img)
406
+ # FIX #2: Add null check for pil_img
407
+ if pil_img is None:
408
+ return "", "", ""
409
+
410
+ with torch.inference_mode():
411
+ results = detection_model(source=pil_img, conf=threshhold_slider)
412
+ sorted_cropped_images = crop_bboxes_by_reading_order(img=pil_img, bboxes=results[0].boxes.xyxy, written_vertical=is_vertical)
413
+ chinese_text = ""
414
+ for crop in sorted_cropped_images:
415
+ crop = test_transforms(crop)
416
+ crop = crop.to(user_device) # Move tensor to device
417
+ crop_batch = crop.unsqueeze(dim=0) # Add batch dimension
418
+
419
+ classifier.to(user_device)
420
+
421
+ logits = classifier(crop_batch)
422
+ class_idx = logits.argmax(dim=1)
423
+
424
+ class_idx_cpu = class_idx.cpu().item() # Convert to CPU integer
425
+ chinese_charcter = classes_dict[class_idx_cpu] # Use integer index for list
426
+ chinese_text += chinese_charcter
427
+
428
+ pinyin_text = pinyin(chinese_text)
429
+ pinyin_sentence = " "
430
+ for pin in pinyin_text:
431
+ pinyin_sentence += f"{pin[0]} "
432
+
433
+
434
+ prompt = f""" You are a professional Chinese to English translator.
435
+ 1. Translate the following Chinese text to natural, fluent English:
436
+ "{chinese_text}"
437
+ 2. Respond only with the translated English text.
438
+ English translation: """
439
+
440
+ output = llm(
441
+ prompt,
442
+ max_tokens=min(len(chinese_text)*2, 512), # More reasonable token limit with safety cap
443
+ stop=["</s>", "\n\n", "Chinese:", "Chinese text:"], # Enhanced stop tokens
444
+ echo=False,
445
+ temperature=0.3, # Slightly higher temperature for more natural language
446
+ frequency_penalty=.5, # Penalize repetition of tokens - higher values discourage repetition
447
+ presence_penalty=.5, # Penalize tokens that have appeared at all - prevents topic repetition
448
+ top_p=0.9, # Nucleus sampling - restricts to top 90% most likely tokens for more coherent output
449
+ stream=False # Explicitly disable streaming to get a dict response
450
+ )
451
+
452
+ # Extract the translation
453
+ translation = output["choices"][0]["text"].strip()
454
+ try:
455
+ start = translation.index('"') + 1
456
+ # find index of last double-quote
457
+ end = translation.rindex('"')
458
+ translation = translation[start:end]
459
+ except:
460
+ pass
461
+
462
+ return chinese_text, pinyin_sentence, translation
463
+
464
+
465
+ css = """
466
+ .centered-examples {
467
+ margin: 0 auto !important;
468
+ justify-content: center !important;
469
+ gap: 8px !important;
470
+ min-height: 150px !important; /* Added minimum height */
471
+ }
472
+ .centered-examples .thumb {
473
+ height: 100px !important;
474
+ width: 100px !important;
475
+ object-fit: cover !important;
476
+ margin: 5px !important; /* Added margin between thumbs */
477
+ }
478
+ /* 1) Global override: remove any forced sizing on .fixed-height anywhere */
479
+ .fixed-height.svelte-842rpi.svelte-842rpi {
480
+ min-height: 0 !important; /* cancel the global min-height */
481
+ max-height: none !important; /* cancel the global max-height */
482
+ height: auto !important; /* allow auto height */
483
+ }
484
+ /* 2) Same-query override: mirror Gradio's media query exactly */
485
+ @media (min-width: 1280px) {
486
+ /* target the exact same class chain inside the breakpoint */
487
+ .fixed-height.svelte-842rpi.svelte-842rpi {
488
+ min-height: 0 !important; /* zero-out the 55vh/min-height there */
489
+ max-height: none !important; /* remove the viewport-height cap */
490
+ height: auto !important; /* let content dictate height */
491
+ }
492
+ }
493
+ """
494
+
495
+
496
+ """gradio app"""
497
+ example_dir = "examples"
498
+ example_images = [os.path.join(example_dir, f) for f in os.listdir(example_dir)
499
+ if f.lower().endswith(('.png', '.jpg', '.jpeg', '.webp'))]
500
+ with gr.Blocks(css=css) as program:
501
+ gr.Markdown("## DeepTranslate: Chinese OCR with translation to english")
502
+ gr.Markdown("Upload or select an image and move the slider to detect characters.")
503
+ gr.Markdown("Make sure that the input image is high resolution and not rotated in any way!")
504
+ #inputs
505
+ with gr.Column(scale=1):
506
+ gallery = gr.Gallery(value=example_images,
507
+ label="Example Images (Click to Select)",
508
+ columns=6,
509
+ allow_preview=False,
510
+ elem_id="my_media_gallery",
511
+ elem_classes=["centered-examples"])
512
+ with gr.Row(scale=2):
513
+ input_img = gr.Image(label="Input Image ")
514
+ detection_img = gr.Image(label="Detection Image", interactive= False)
515
+
516
+ # slider and button
517
+ with gr.Column(scale=1):
518
+ with gr.Row(scale=3):
519
+ threshhold_slider = gr.Slider(value=0.25, minimum=0, maximum=0.75, label="Detection Threshhold", step = 0.01)
520
+ translate_button = gr.Button("Translate To English", variant="primary")
521
+ is_vertical = gr.Checkbox(value=False, label="Vertiacl Chinese Text?", interactive=True)
522
+ # outputs
523
+ with gr.Column(scale=1):
524
+ with gr.Row(scale=3):
525
+ chinese_text = gr.TextArea(label="Chinese Text", max_lines= 1000, interactive=False)
526
+ pinyin_text = gr.TextArea(label="Chinese Pinyin", max_lines= 1000, interactive=False)
527
+ english_text = gr.TextArea(label="English Text", max_lines= 1000, interactive=False)
528
+ #function, calling
529
+ threshhold_slider.change(fn= move_slider, inputs=[threshhold_slider, input_img], outputs=[detection_img])
530
+ translate_button.click(fn=translate_text, inputs= [threshhold_slider, is_vertical, input_img], outputs=[chinese_text, pinyin_text, english_text])
531
+ gallery.select(fn=select_image, inputs=None, outputs=input_img)
532
+
533
+ if __name__ == "__main__":
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
534
  program.launch(share=False)