IncreasingLoss commited on
Commit
c987d89
·
verified ·
1 Parent(s): 829c526

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +199 -149
app.py CHANGED
@@ -6,7 +6,6 @@ from torchvision import transforms
6
  import numpy as np
7
  from ultralytics import YOLO
8
  from huggingface_hub import hf_hub_download
9
- from llama_cpp import Llama
10
  import os
11
  import torch
12
  from pypinyin import pinyin
@@ -15,13 +14,68 @@ import json
15
  import os
16
  import torchvision.transforms.functional as F
17
  from statistics import mean
18
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
 
20
  """yolo model"""
 
21
  user_device = "cuda" if torch.cuda.is_available() else "cpu"
22
- detection_model = YOLO("model/yolo_chinese_m.pt").to(user_device).eval()
23
-
24
 
 
 
 
 
 
 
25
 
26
  """LW-Vit Classifier"""
27
 
@@ -195,14 +249,26 @@ class LW_ViT(nn.Module):
195
  x = self.classifier(x)
196
  return x
197
 
198
- classifier = LW_ViT()
199
- classifier.load_state_dict(torch.load("model/chinese_Character_Classification_handwritten_LW_ViT.pth", weights_only=True, map_location=user_device))
200
- classifier = classifier.to(user_device)
201
- classifier.eval()
 
 
 
 
 
 
202
 
203
  """load classes dict"""
204
- with open("model/chinese_classes.json", "r", encoding="utf-8") as f:
205
- classes_dict = json.load(f)
 
 
 
 
 
 
206
 
207
  """transforms"""
208
  class ToSquare(object):
@@ -230,7 +296,7 @@ class ToSquare(object):
230
  pad_w_extra = (max_dim - w) % 2
231
  pad_h_extra = (max_dim - h) % 2
232
 
233
- # Create padding list (left, top, right, bottom) - FIX #1: Use list instead of tuple
234
  padding = [pad_w, pad_h, pad_w + pad_w_extra, pad_h + pad_h_extra]
235
 
236
  # Create new padded image
@@ -262,27 +328,9 @@ test_transforms = transforms.Compose([
262
  transforms.Normalize(mean=[0.5], std=[0.5])
263
  ])
264
 
265
-
266
- """import llm from huggingface"""
267
- model_path = hf_hub_download(
268
- repo_id="IncreasingLoss/FineTunedTranslation_Yi-1.5-9B-Chat-Q6_K",
269
- filename="Yi-1.5-9B-Chat-Q6_K.gguf",
270
- local_dir="./models", # Optional: specify where to save
271
- local_dir_use_symlinks=False
272
- )
273
-
274
- print(f"Model downloaded to: {model_path}")
275
-
276
- llm = Llama(
277
- model_path=model_path,
278
- n_ctx=4096,
279
- n_gpu_layers=-1, # Use all GPU layers
280
- verbose=False
281
- )
282
-
283
  """app functions"""
284
  def convert_to_pil(inputimg):
285
- # convert input to pil image
286
  if inputimg is None:
287
  return None
288
  if isinstance(inputimg, np.ndarray):
@@ -304,8 +352,6 @@ def draw_bboxes(img, results):
304
  draw.rectangle([x1, y1, x2, y2], outline=(255, 0, 0), width=2)
305
  return draw_img
306
 
307
-
308
-
309
  def crop_bboxes_by_reading_order(img, bboxes, written_vertical=False):
310
  """
311
  Crop and return image regions in proper reading order.
@@ -396,10 +442,8 @@ def crop_bboxes_by_reading_order(img, bboxes, written_vertical=False):
396
 
397
  return crops
398
 
399
-
400
  def move_slider(threshhold_slider, input_img):
401
  pil_img = convert_to_pil(input_img)
402
- # FIX #2: Add null check for pil_img
403
  if pil_img is None:
404
  return None
405
  with torch.inference_mode():
@@ -411,163 +455,169 @@ def select_image(evt: gr.SelectData):
411
  selected_index = evt.index
412
  return example_images[selected_index]
413
 
414
- def generate_translation(prompt, max_new_tokens=512):
415
- """Generate translation using Hugging Face model"""
416
-
417
- # Tokenize input
418
- inputs = tokenizer(prompt, return_tensors="pt", truncation=True, max_length=4096-max_new_tokens)
419
-
420
- # Move to same device as model
421
- inputs = {k: v.to(hf_model.device) for k, v in inputs.items()}
422
-
423
- # Generate response
424
- with torch.no_grad():
425
- outputs = hf_model.generate(
426
- **inputs,
427
- max_new_tokens=max_new_tokens,
428
- temperature=0.3,
429
- do_sample=True,
430
- top_p=0.9,
431
- repetition_penalty=1.1,
432
- pad_token_id=tokenizer.eos_token_id,
433
- eos_token_id=tokenizer.eos_token_id,
434
- stop_strings=["</s>", "\n\n", "Chinese:", "Chinese text:"],
435
- )
436
-
437
- # Decode only the new tokens (exclude the input prompt)
438
- response = tokenizer.decode(outputs[0][inputs['input_ids'].shape[1]:], skip_special_tokens=True)
439
- return response.strip()
440
-
441
-
442
  def translate_text(threshhold_slider, is_vertical, input_img):
443
  # convert input to pil image
444
  pil_img = convert_to_pil(input_img)
445
- # FIX #2: Add null check for pil_img
446
  if pil_img is None:
447
- return "", "", ""
448
 
449
- with torch.inference_mode():
450
- results = detection_model(source=pil_img, conf=threshhold_slider)
451
- sorted_cropped_images = crop_bboxes_by_reading_order(img=pil_img, bboxes=results[0].boxes.xyxy, written_vertical=is_vertical)
452
- chinese_text = ""
453
- for crop in sorted_cropped_images:
454
- crop = test_transforms(crop)
455
- crop = crop.to(user_device) # Move tensor to device
456
- crop_batch = crop.unsqueeze(dim=0) # Add batch dimension
457
-
458
- classifier.to(user_device)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
459
 
460
- logits = classifier(crop_batch)
461
- class_idx = logits.argmax(dim=1)
462
 
463
- class_idx_cpu = class_idx.cpu().item() # Convert to CPU integer
464
- chinese_charcter = classes_dict[class_idx_cpu] # Use integer index for list
465
- chinese_text += chinese_charcter
466
-
467
- pinyin_text = pinyin(chinese_text)
468
- pinyin_sentence = " "
469
- for pin in pinyin_text:
470
- pinyin_sentence += f"{pin[0]} "
471
-
472
-
473
- prompt = f""" You are a professional Chinese to English translator.
474
- 1. Translate the following Chinese text to natural, fluent English:
475
- "{chinese_text}"
476
- 2. Respond only with the translated English text.
477
- English translation: """
478
-
479
- output = llm(
480
- prompt,
481
- max_tokens=min(len(chinese_text)*2, 512), # More reasonable token limit with safety cap
482
- stop=["</s>", "\n\n", "Chinese:", "Chinese text:"], # Enhanced stop tokens
483
- echo=False,
484
- temperature=0.3, # Slightly higher temperature for more natural language
485
- frequency_penalty=.5, # Penalize repetition of tokens - higher values discourage repetition
486
- presence_penalty=.5, # Penalize tokens that have appeared at all - prevents topic repetition
487
- top_p=0.9, # Nucleus sampling - restricts to top 90% most likely tokens for more coherent output
488
- stream=False # Explicitly disable streaming to get a dict response
489
- )
490
-
491
- # Extract the translation
492
- translation = output["choices"][0]["text"].strip()
493
- try:
494
- start = translation.index('"') + 1
495
- # find index of last double-quote
496
- end = translation.rindex('"')
497
- translation = translation[start:end]
498
- except:
499
- pass
500
-
501
- return chinese_text, pinyin_sentence, translation
502
-
503
 
504
  css = """
505
  .centered-examples {
506
  margin: 0 auto !important;
507
  justify-content: center !important;
508
  gap: 8px !important;
509
- min-height: 150px !important; /* Added minimum height */
510
  }
511
  .centered-examples .thumb {
512
  height: 100px !important;
513
  width: 100px !important;
514
  object-fit: cover !important;
515
- margin: 5px !important; /* Added margin between thumbs */
516
  }
517
- /* 1) Global override: remove any forced sizing on .fixed-height anywhere */
518
  .fixed-height.svelte-842rpi.svelte-842rpi {
519
- min-height: 0 !important; /* cancel the global min-height */
520
- max-height: none !important; /* cancel the global max-height */
521
- height: auto !important; /* allow auto height */
522
  }
523
- /* 2) Same-query override: mirror Gradio's media query exactly */
524
  @media (min-width: 1280px) {
525
- /* target the exact same class chain inside the breakpoint */
526
  .fixed-height.svelte-842rpi.svelte-842rpi {
527
- min-height: 0 !important; /* zero-out the 55vh/min-height there */
528
- max-height: none !important; /* remove the viewport-height cap */
529
- height: auto !important; /* let content dictate height */
530
  }
531
  }
532
  """
533
 
534
-
535
  """gradio app"""
 
 
 
536
  example_dir = "examples"
537
- example_images = [os.path.join(example_dir, f) for f in os.listdir(example_dir)
538
- if f.lower().endswith(('.png', '.jpg', '.jpeg', '.webp'))]
539
- with gr.Blocks(css=css) as program:
540
- gr.Markdown("## DeepTranslate: Chinese OCR with translation to english")
 
 
 
 
 
 
541
  gr.Markdown("Upload or select an image and move the slider to detect characters.")
542
  gr.Markdown("Make sure that the input image is high resolution and not rotated in any way!")
 
 
 
 
 
543
  #inputs
544
  with gr.Column(scale=1):
545
- gallery = gr.Gallery(value=example_images,
546
- label="Example Images (Click to Select)",
547
- columns=6,
548
- allow_preview=False,
549
- elem_id="my_media_gallery",
550
- elem_classes=["centered-examples"])
 
551
  with gr.Row(scale=2):
552
  input_img = gr.Image(label="Input Image ")
553
- detection_img = gr.Image(label="Detection Image", interactive= False)
554
 
555
  # slider and button
556
  with gr.Column(scale=1):
557
  with gr.Row(scale=3):
558
- threshhold_slider = gr.Slider(value=0.25, minimum=0, maximum=0.75, label="Detection Threshhold", step = 0.01)
559
  translate_button = gr.Button("Translate To English", variant="primary")
560
- is_vertical = gr.Checkbox(value=False, label="Vertiacl Chinese Text?", interactive=True)
 
561
  # outputs
562
  with gr.Column(scale=1):
563
  with gr.Row(scale=3):
564
- chinese_text = gr.TextArea(label="Chinese Text", max_lines= 1000, interactive=False)
565
- pinyin_text = gr.TextArea(label="Chinese Pinyin", max_lines= 1000, interactive=False)
566
- english_text = gr.TextArea(label="English Text", max_lines= 1000, interactive=False)
567
- #function, calling
568
- threshhold_slider.change(fn= move_slider, inputs=[threshhold_slider, input_img], outputs=[detection_img])
569
- translate_button.click(fn=translate_text, inputs= [threshhold_slider, is_vertical, input_img], outputs=[chinese_text, pinyin_text, english_text])
570
- gallery.select(fn=select_image, inputs=None, outputs=input_img)
 
 
 
 
 
571
 
572
  if __name__ == "__main__":
573
- program.launch(share=False)
 
6
  import numpy as np
7
  from ultralytics import YOLO
8
  from huggingface_hub import hf_hub_download
 
9
  import os
10
  import torch
11
  from pypinyin import pinyin
 
14
  import os
15
  import torchvision.transforms.functional as F
16
  from statistics import mean
17
+ import logging
18
+
19
+ # Set up logging
20
+ logging.basicConfig(level=logging.INFO)
21
+ logger = logging.getLogger(__name__)
22
+
23
+ # Initialize LLM as None - will be loaded lazily
24
+ llm = None
25
+
26
+ def load_llm():
27
+ """Lazy loading of LLM to avoid startup delays"""
28
+ global llm
29
+ if llm is None:
30
+ try:
31
+ logger.info("Loading LLM model...")
32
+ from llama_cpp import Llama
33
+
34
+ # Check if model exists locally first
35
+ model_filename = "Yi-1.5-9B-Chat-Q6_K.gguf"
36
+ local_model_path = os.path.join("./models", model_filename)
37
+
38
+ if not os.path.exists(local_model_path):
39
+ logger.info("Downloading LLM model from HuggingFace...")
40
+ model_path = hf_hub_download(
41
+ repo_id="IncreasingLoss/FineTunedTranslation_Yi-1.5-9B-Chat-Q6_K",
42
+ filename=model_filename,
43
+ local_dir="./models",
44
+ local_dir_use_symlinks=False
45
+ )
46
+ else:
47
+ model_path = local_model_path
48
+ logger.info(f"Using existing model at: {model_path}")
49
+
50
+ # Initialize with conservative settings for HF Spaces
51
+ llm = Llama(
52
+ model_path=model_path,
53
+ n_ctx=2048, # Reduced context size
54
+ n_gpu_layers=0, # CPU only for HF Spaces compatibility
55
+ verbose=False,
56
+ n_threads=2, # Limit threads for shared environment
57
+ use_mmap=True, # Memory mapping for efficiency
58
+ use_mlock=False # Don't lock memory
59
+ )
60
+ logger.info("LLM model loaded successfully!")
61
+
62
+ except Exception as e:
63
+ logger.error(f"Failed to load LLM: {e}")
64
+ llm = None
65
+
66
+ return llm
67
 
68
  """yolo model"""
69
+ logger.info("Loading YOLO model...")
70
  user_device = "cuda" if torch.cuda.is_available() else "cpu"
71
+ logger.info(f"Using device: {user_device}")
 
72
 
73
+ try:
74
+ detection_model = YOLO("model/yolo_chinese_m.pt").to(user_device).eval()
75
+ logger.info("YOLO model loaded successfully!")
76
+ except Exception as e:
77
+ logger.error(f"Failed to load YOLO model: {e}")
78
+ raise
79
 
80
  """LW-Vit Classifier"""
81
 
 
249
  x = self.classifier(x)
250
  return x
251
 
252
+ logger.info("Loading classifier model...")
253
+ try:
254
+ classifier = LW_ViT()
255
+ classifier.load_state_dict(torch.load("model/chinese_Character_Classification_handwritten_LW_ViT.pth", weights_only=True, map_location=user_device))
256
+ classifier = classifier.to(user_device)
257
+ classifier.eval()
258
+ logger.info("Classifier model loaded successfully!")
259
+ except Exception as e:
260
+ logger.error(f"Failed to load classifier: {e}")
261
+ raise
262
 
263
  """load classes dict"""
264
+ logger.info("Loading classes dictionary...")
265
+ try:
266
+ with open("model/chinese_classes.json", "r", encoding="utf-8") as f:
267
+ classes_dict = json.load(f)
268
+ logger.info(f"Loaded {len(classes_dict)} classes")
269
+ except Exception as e:
270
+ logger.error(f"Failed to load classes dictionary: {e}")
271
+ raise
272
 
273
  """transforms"""
274
  class ToSquare(object):
 
296
  pad_w_extra = (max_dim - w) % 2
297
  pad_h_extra = (max_dim - h) % 2
298
 
299
+ # Create padding list (left, top, right, bottom)
300
  padding = [pad_w, pad_h, pad_w + pad_w_extra, pad_h + pad_h_extra]
301
 
302
  # Create new padded image
 
328
  transforms.Normalize(mean=[0.5], std=[0.5])
329
  ])
330
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
331
  """app functions"""
332
  def convert_to_pil(inputimg):
333
+ # convert input to pil image
334
  if inputimg is None:
335
  return None
336
  if isinstance(inputimg, np.ndarray):
 
352
  draw.rectangle([x1, y1, x2, y2], outline=(255, 0, 0), width=2)
353
  return draw_img
354
 
 
 
355
  def crop_bboxes_by_reading_order(img, bboxes, written_vertical=False):
356
  """
357
  Crop and return image regions in proper reading order.
 
442
 
443
  return crops
444
 
 
445
  def move_slider(threshhold_slider, input_img):
446
  pil_img = convert_to_pil(input_img)
 
447
  if pil_img is None:
448
  return None
449
  with torch.inference_mode():
 
455
  selected_index = evt.index
456
  return example_images[selected_index]
457
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
458
  def translate_text(threshhold_slider, is_vertical, input_img):
459
  # convert input to pil image
460
  pil_img = convert_to_pil(input_img)
 
461
  if pil_img is None:
462
+ return "", "", "Please upload an image first."
463
 
464
+ try:
465
+ with torch.inference_mode():
466
+ results = detection_model(source=pil_img, conf=threshhold_slider)
467
+ sorted_cropped_images = crop_bboxes_by_reading_order(img=pil_img, bboxes=results[0].boxes.xyxy, written_vertical=is_vertical)
468
+ chinese_text = ""
469
+ for crop in sorted_cropped_images:
470
+ crop = test_transforms(crop)
471
+ crop = crop.to(user_device) # Move tensor to device
472
+ crop_batch = crop.unsqueeze(dim=0) # Add batch dimension
473
+
474
+ classifier.to(user_device)
475
+
476
+ logits = classifier(crop_batch)
477
+ class_idx = logits.argmax(dim=1)
478
+
479
+ class_idx_cpu = class_idx.cpu().item() # Convert to CPU integer
480
+ chinese_charcter = classes_dict[class_idx_cpu] # Use integer index for list
481
+ chinese_text += chinese_charcter
482
+
483
+ # Generate pinyin
484
+ pinyin_text = pinyin(chinese_text)
485
+ pinyin_sentence = " "
486
+ for pin in pinyin_text:
487
+ pinyin_sentence += f"{pin[0]} "
488
+
489
+ # Load LLM lazily only when needed for translation
490
+ current_llm = load_llm()
491
+
492
+ if current_llm is None:
493
+ return chinese_text, pinyin_sentence, "Translation service unavailable - LLM failed to load."
494
+
495
+ # Generate translation
496
+ prompt = f""" You are a professional Chinese to English translator.
497
+ 1. Translate the following Chinese text to natural, fluent English:
498
+ "{chinese_text}"
499
+ 2. Respond only with the translated English text.
500
+ English translation: """
501
+
502
+ try:
503
+ output = current_llm(
504
+ prompt,
505
+ max_tokens=min(len(chinese_text)*3, 256), # Conservative token limit
506
+ stop=["</s>", "\n\n", "Chinese:", "Chinese text:"],
507
+ echo=False,
508
+ temperature=0.3,
509
+ frequency_penalty=0.5,
510
+ presence_penalty=0.5,
511
+ top_p=0.9,
512
+ stream=False
513
+ )
514
 
515
+ # Extract the translation
516
+ translation = output["choices"][0]["text"].strip()
517
 
518
+ # Clean up translation if it contains quotes
519
+ try:
520
+ if '"' in translation:
521
+ start = translation.index('"') + 1
522
+ end = translation.rindex('"')
523
+ translation = translation[start:end]
524
+ except:
525
+ pass
526
+
527
+ return chinese_text, pinyin_sentence, translation
528
+
529
+ except Exception as e:
530
+ logger.error(f"Translation failed: {e}")
531
+ return chinese_text, pinyin_sentence, f"Translation failed: {str(e)}"
532
+
533
+ except Exception as e:
534
+ logger.error(f"OCR processing failed: {e}")
535
+ return "", "", f"OCR processing failed: {str(e)}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
536
 
537
  css = """
538
  .centered-examples {
539
  margin: 0 auto !important;
540
  justify-content: center !important;
541
  gap: 8px !important;
542
+ min-height: 150px !important;
543
  }
544
  .centered-examples .thumb {
545
  height: 100px !important;
546
  width: 100px !important;
547
  object-fit: cover !important;
548
+ margin: 5px !important;
549
  }
 
550
  .fixed-height.svelte-842rpi.svelte-842rpi {
551
+ min-height: 0 !important;
552
+ max-height: none !important;
553
+ height: auto !important;
554
  }
 
555
  @media (min-width: 1280px) {
 
556
  .fixed-height.svelte-842rpi.svelte-842rpi {
557
+ min-height: 0 !important;
558
+ max-height: none !important;
559
+ height: auto !important;
560
  }
561
  }
562
  """
563
 
 
564
  """gradio app"""
565
+ logger.info("Setting up Gradio interface...")
566
+
567
+ # Check if examples directory exists
568
  example_dir = "examples"
569
+ example_images = []
570
+ if os.path.exists(example_dir):
571
+ example_images = [os.path.join(example_dir, f) for f in os.listdir(example_dir)
572
+ if f.lower().endswith(('.png', '.jpg', '.jpeg', '.webp'))]
573
+ logger.info(f"Found {len(example_images)} example images")
574
+ else:
575
+ logger.warning(f"Examples directory '{example_dir}' not found")
576
+
577
+ with gr.Blocks(css=css, title="DeepTranslate: Chinese OCR") as program:
578
+ gr.Markdown("## DeepTranslate: Chinese OCR with translation to English")
579
  gr.Markdown("Upload or select an image and move the slider to detect characters.")
580
  gr.Markdown("Make sure that the input image is high resolution and not rotated in any way!")
581
+
582
+ # Show loading status
583
+ with gr.Row():
584
+ gr.Markdown("🟢 YOLO Model: Loaded | 🟢 Classifier: Loaded | 🟡 Translation: Loading on demand")
585
+
586
  #inputs
587
  with gr.Column(scale=1):
588
+ if example_images:
589
+ gallery = gr.Gallery(value=example_images,
590
+ label="Example Images (Click to Select)",
591
+ columns=6,
592
+ allow_preview=False,
593
+ elem_id="my_media_gallery",
594
+ elem_classes=["centered-examples"])
595
  with gr.Row(scale=2):
596
  input_img = gr.Image(label="Input Image ")
597
+ detection_img = gr.Image(label="Detection Image", interactive=False)
598
 
599
  # slider and button
600
  with gr.Column(scale=1):
601
  with gr.Row(scale=3):
602
+ threshhold_slider = gr.Slider(value=0.25, minimum=0, maximum=0.75, label="Detection Threshold", step=0.01)
603
  translate_button = gr.Button("Translate To English", variant="primary")
604
+ is_vertical = gr.Checkbox(value=False, label="Vertical Chinese Text?", interactive=True)
605
+
606
  # outputs
607
  with gr.Column(scale=1):
608
  with gr.Row(scale=3):
609
+ chinese_text = gr.TextArea(label="Chinese Text", max_lines=1000, interactive=False)
610
+ pinyin_text = gr.TextArea(label="Chinese Pinyin", max_lines=1000, interactive=False)
611
+ english_text = gr.TextArea(label="English Text", max_lines=1000, interactive=False)
612
+
613
+ # function calling
614
+ threshhold_slider.change(fn=move_slider, inputs=[threshhold_slider, input_img], outputs=[detection_img])
615
+ translate_button.click(fn=translate_text, inputs=[threshhold_slider, is_vertical, input_img], outputs=[chinese_text, pinyin_text, english_text])
616
+
617
+ if example_images:
618
+ gallery.select(fn=select_image, inputs=None, outputs=input_img)
619
+
620
+ logger.info("Gradio interface ready!")
621
 
622
  if __name__ == "__main__":
623
+ program.launch(share=False, server_name="0.0.0.0", show_error=True)