manishw7 commited on
Commit
adb49d0
·
1 Parent(s): 99ff973

Feature: Integrated CNN model and optimized TrOCR with Beam Search

Browse files
Files changed (4) hide show
  1. app.py +62 -25
  2. cnn_model.py +226 -0
  3. devanagari-cnn-classifier.pt +3 -0
  4. requirements.txt +3 -0
app.py CHANGED
@@ -1,21 +1,24 @@
1
  import os
2
  import gradio as gr
3
  import torch
 
4
  from PIL import Image
5
  from peft import PeftModel
6
  from transformers import AutoTokenizer, TrOCRProcessor, ViTImageProcessor, VisionEncoderDecoderModel
 
7
 
8
  # --- CONFIGURATION ---
9
  BASE_MODEL_ID = "paudelanil/trocr-devanagari-2"
10
- ADAPTER_ID = "manishw10/devgen-trocr-devanagari-lora"
 
11
 
12
  # Detect environment
13
  IS_SPACE = "SPACE_ID" in os.environ
14
-
15
- print(f"System: Loading model... (Env: {'Hugging Face Space' if IS_SPACE else 'Local'})")
16
  device = "cuda" if torch.cuda.is_available() else "cpu"
17
 
18
- # Load Processor
 
 
19
  try:
20
  processor = TrOCRProcessor.from_pretrained(BASE_MODEL_ID)
21
  except Exception:
@@ -23,48 +26,82 @@ except Exception:
23
  tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL_ID)
24
  processor = TrOCRProcessor(image_processor=image_processor, tokenizer=tokenizer)
25
 
26
- # Load Model
27
  base_model = VisionEncoderDecoderModel.from_pretrained(BASE_MODEL_ID)
28
- # The PeftModel wrapper injects weights into base_model
29
  model = PeftModel.from_pretrained(base_model, ADAPTER_ID)
30
  model.to(device)
31
  model.eval()
32
- print(f"System: Model loaded successfully on {device}")
33
 
34
- def predict(image):
 
 
 
 
 
35
  if image is None:
36
  return "Error: No image uploaded"
37
  try:
38
  image = image.convert("RGB")
39
  pixel_values = processor(image, return_tensors="pt").pixel_values.to(device)
40
 
41
- # --- THE ROBUST FIX ---
42
- # We call .base_model.generate() directly.
43
- # This bypasses the buggy PEFT wrapper while still using the LoRA weights.
44
- # We also add max_new_tokens for a better result.
45
  with torch.no_grad():
46
  generated_ids = model.base_model.generate(
47
  pixel_values=pixel_values,
48
- max_new_tokens=64
 
 
 
 
49
  )
50
  generated_text = processor.batch_decode(generated_ids, skip_special_tokens=True)[0]
51
  return generated_text
52
  except Exception as e:
53
  import traceback
54
- print(traceback.format_exc()) # Log the full error to the Space logs
55
- return f"Error during inference: {str(e)}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
 
57
- # Interface setup
58
- demo = gr.Interface(
59
- fn=predict,
60
- inputs=gr.Image(type="pil", label="Upload Handwritten Devanagari Word"),
61
- outputs=gr.Textbox(label="Recognized Text"),
62
- title="DevGen Devanagari OCR",
63
- description="Recognize handwritten Devanagari words using TrOCR and LoRA adaptation.",
64
- allow_flagging="never"
65
- )
66
 
67
  if __name__ == "__main__":
68
- # If running on HF Spaces, use 0.0.0.0. If local, use default localhost.
69
  server_name = "0.0.0.0" if IS_SPACE else "127.0.0.1"
 
70
  demo.launch(server_name=server_name)
 
1
  import os
2
  import gradio as gr
3
  import torch
4
+ import numpy as np
5
  from PIL import Image
6
  from peft import PeftModel
7
  from transformers import AutoTokenizer, TrOCRProcessor, ViTImageProcessor, VisionEncoderDecoderModel
8
+ from cnn_model import CharacterClassifier # Importing your CNN logic
9
 
10
  # --- CONFIGURATION ---
11
  BASE_MODEL_ID = "paudelanil/trocr-devanagari-2"
12
+ ADAPTER_ID = "manishw10/devgen-trocr-devanagari-lora"
13
+ CNN_MODEL_PATH = "devanagari-cnn-classifier.pt"
14
 
15
  # Detect environment
16
  IS_SPACE = "SPACE_ID" in os.environ
 
 
17
  device = "cuda" if torch.cuda.is_available() else "cpu"
18
 
19
+ print(f"System: Initializing Models (Env: {'Hugging Face Space' if IS_SPACE else 'Local'})")
20
+
21
+ # 1. Load TrOCR Model & Processor
22
  try:
23
  processor = TrOCRProcessor.from_pretrained(BASE_MODEL_ID)
24
  except Exception:
 
26
  tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL_ID)
27
  processor = TrOCRProcessor(image_processor=image_processor, tokenizer=tokenizer)
28
 
 
29
  base_model = VisionEncoderDecoderModel.from_pretrained(BASE_MODEL_ID)
 
30
  model = PeftModel.from_pretrained(base_model, ADAPTER_ID)
31
  model.to(device)
32
  model.eval()
 
33
 
34
+ # 2. Load CNN Classifier
35
+ cnn_engine = CharacterClassifier(model_path=CNN_MODEL_PATH, device=device)
36
+
37
+ print(f"System: Models loaded successfully on {device}")
38
+
39
+ def predict_trocr(image):
40
  if image is None:
41
  return "Error: No image uploaded"
42
  try:
43
  image = image.convert("RGB")
44
  pixel_values = processor(image, return_tensors="pt").pixel_values.to(device)
45
 
46
+ # --- HIGH-QUALITY GENERATION ---
47
+ # Added num_beams and length_penalty to fix the "rubbish" output.
48
+ # This makes TrOCR use Beam Search instead of Greedy Search.
 
49
  with torch.no_grad():
50
  generated_ids = model.base_model.generate(
51
  pixel_values=pixel_values,
52
+ num_beams=4,
53
+ length_penalty=1.0,
54
+ max_new_tokens=64,
55
+ early_stopping=True,
56
+ decoder_start_token_id=model.config.decoder_start_token_id
57
  )
58
  generated_text = processor.batch_decode(generated_ids, skip_special_tokens=True)[0]
59
  return generated_text
60
  except Exception as e:
61
  import traceback
62
+ print(traceback.format_exc())
63
+ return f"TrOCR Error: {str(e)}"
64
+
65
+ def predict_cnn(image):
66
+ if image is None:
67
+ return "Error: No image uploaded"
68
+ try:
69
+ image = image.convert("RGB")
70
+ result = cnn_engine.predict(image)
71
+ if "error" in result:
72
+ return result["error"]
73
+ return f"Character: {result['text']} (Confidence: {result['confidence']:.2%})"
74
+ except Exception as e:
75
+ return f"CNN Error: {str(e)}"
76
+
77
+ # --- CUSTOM GRADIO INTERFACE ---
78
+ with gr.Blocks(title="DevGen OCR Suite") as demo:
79
+ gr.Markdown("# 🕉️ DevGen Devanagari OCR Suite")
80
+ gr.Markdown("Switch between TrOCR (for words/sentences) and CNN (for single characters).")
81
+
82
+ with gr.Tabs():
83
+ with gr.TabItem("TrOCR (Word/Sentence Recognition)"):
84
+ with gr.Row():
85
+ with gr.Column():
86
+ img_input = gr.Image(type="pil", label="Upload Handwritten Word")
87
+ btn_trocr = gr.Button("Recognize Word", variant="primary")
88
+ with gr.Column():
89
+ text_output = gr.Textbox(label="Recognized Text")
90
+ btn_trocr.click(fn=predict_trocr, inputs=img_input, outputs=text_output)
91
+
92
+ with gr.TabItem("CNN (Single Character Recognition)"):
93
+ with gr.Row():
94
+ with gr.Column():
95
+ char_input = gr.Image(type="pil", label="Upload Single Character")
96
+ btn_cnn = gr.Button("Classify Character", variant="primary")
97
+ with gr.Column():
98
+ char_output = gr.Textbox(label="Classification Result")
99
+ btn_cnn.click(fn=predict_cnn, inputs=char_input, outputs=char_output)
100
 
101
+ gr.Markdown("---")
102
+ gr.Markdown("Built with ❤️ by DevGen Team. Using TrOCR + LoRA and custom 3-layer CNN.")
 
 
 
 
 
 
 
103
 
104
  if __name__ == "__main__":
 
105
  server_name = "0.0.0.0" if IS_SPACE else "127.0.0.1"
106
+ # Note: We don't use monkey-patching here, the base_model.generate handles it.
107
  demo.launch(server_name=server_name)
cnn_model.py ADDED
@@ -0,0 +1,226 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ DevGen Framework — CNN Devanagari Character Classifier
3
+
4
+ A lightweight CNN for classifying individual handwritten Devanagari
5
+ characters (vowels, consonants, digits) — 46 classes total.
6
+
7
+ This model complements TrOCR (which handles words) by handling single
8
+ characters that TrOCR hallucinates on.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import os
14
+ import time
15
+ from pathlib import Path
16
+ from typing import Optional
17
+
18
+ import torch
19
+ import torch.nn as nn
20
+ import torch.nn.functional as F
21
+ from PIL import Image
22
+ from torchvision import transforms
23
+
24
+ # ── 46-class label map ──────────────────────────────────────────────────────
25
+ # Standard DHCD ordering: 36 consonants/vowels + 10 digits
26
+ DEVANAGARI_CLASSES = [
27
+ # Consonants (ka to gya)
28
+ "क", "ख", "ग", "घ", "ङ",
29
+ "च", "छ", "ज", "झ", "ञ",
30
+ "ट", "ठ", "ड", "ढ", "ण",
31
+ "त", "थ", "द", "ध", "न",
32
+ "प", "फ", "ब", "भ", "म",
33
+ "य", "र", "ल", "व",
34
+ "श", "ष", "स", "ह",
35
+ "क्ष", "त्र", "ज्ञ",
36
+ # Digits (0-9)
37
+ "०", "१", "२", "३", "४", "५", "६", "७", "८", "९",
38
+ ]
39
+
40
+ # Reverse map: character → index
41
+ CHAR_TO_INDEX = {ch: i for i, ch in enumerate(DEVANAGARI_CLASSES)}
42
+ NUM_CLASSES = len(DEVANAGARI_CLASSES)
43
+
44
+ # Default model path
45
+ DEFAULT_CNN_MODEL_PATH = "devanagari-cnn-classifier.pt"
46
+
47
+
48
+ class DevanagariCNN(nn.Module):
49
+ """
50
+ 3-layer CNN for 32×32 grayscale character images.
51
+ ~500K parameters — fast inference even on CPU.
52
+ """
53
+
54
+ def __init__(self, num_classes: int = NUM_CLASSES):
55
+ super().__init__()
56
+ self.features = nn.Sequential(
57
+ # Block 1: 32×32 → 16×16
58
+ nn.Conv2d(1, 32, kernel_size=3, padding=1),
59
+ nn.BatchNorm2d(32),
60
+ nn.ReLU(inplace=True),
61
+ nn.Conv2d(32, 32, kernel_size=3, padding=1),
62
+ nn.BatchNorm2d(32),
63
+ nn.ReLU(inplace=True),
64
+ nn.MaxPool2d(2),
65
+ nn.Dropout2d(0.25),
66
+
67
+ # Block 2: 16×16 → 8×8
68
+ nn.Conv2d(32, 64, kernel_size=3, padding=1),
69
+ nn.BatchNorm2d(64),
70
+ nn.ReLU(inplace=True),
71
+ nn.Conv2d(64, 64, kernel_size=3, padding=1),
72
+ nn.BatchNorm2d(64),
73
+ nn.ReLU(inplace=True),
74
+ nn.MaxPool2d(2),
75
+ nn.Dropout2d(0.25),
76
+
77
+ # Block 3: 8×8 → 4×4
78
+ nn.Conv2d(64, 128, kernel_size=3, padding=1),
79
+ nn.BatchNorm2d(128),
80
+ nn.ReLU(inplace=True),
81
+ nn.AdaptiveAvgPool2d(4),
82
+ nn.Dropout2d(0.25),
83
+ )
84
+
85
+ self.classifier = nn.Sequential(
86
+ nn.Flatten(),
87
+ nn.Linear(128 * 4 * 4, 256),
88
+ nn.ReLU(inplace=True),
89
+ nn.Dropout(0.5),
90
+ nn.Linear(256, num_classes),
91
+ )
92
+
93
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
94
+ x = self.features(x)
95
+ x = self.classifier(x)
96
+ return x
97
+
98
+
99
+ # ── Inference transform ─────────────────────────────────────────────────────
100
+ # Matches training: resize to 32×32, grayscale, normalize
101
+ INFERENCE_TRANSFORM = transforms.Compose([
102
+ transforms.Grayscale(num_output_channels=1),
103
+ transforms.Resize((32, 32)),
104
+ transforms.ToTensor(),
105
+ transforms.Normalize(mean=[0.5], std=[0.5]),
106
+ ])
107
+
108
+
109
+ class CharacterClassifier:
110
+ """
111
+ Wrapper for loading and running the trained CNN model.
112
+ Used by the smart router in TrOCREngine.
113
+ """
114
+
115
+ def __init__(
116
+ self,
117
+ model_path: Optional[str] = None,
118
+ device: Optional[str] = None,
119
+ ):
120
+ self.device = device or ("mps" if torch.backends.mps.is_available() else "cpu")
121
+
122
+ # Find model file
123
+ if model_path is None:
124
+ project_root = Path(__file__).resolve().parent.parent
125
+ model_path = str(project_root / DEFAULT_CNN_MODEL_PATH)
126
+
127
+ self.model_path = model_path
128
+ self.model: Optional[DevanagariCNN] = None
129
+ self.available = False
130
+
131
+ if os.path.exists(model_path):
132
+ self._load_model()
133
+ else:
134
+ print(f"[CNN Classifier] Model not found at {model_path} — single character recognition disabled")
135
+
136
+ def _load_model(self):
137
+ """Load the trained CNN weights."""
138
+ try:
139
+ self.model = DevanagariCNN(NUM_CLASSES)
140
+ state_dict = torch.load(self.model_path, map_location=self.device, weights_only=True)
141
+ self.model.load_state_dict(state_dict)
142
+ self.model.to(self.device)
143
+ self.model.eval()
144
+ self.available = True
145
+ size_mb = os.path.getsize(self.model_path) / 1e6
146
+ print(f"[CNN Classifier] Loaded ({size_mb:.1f} MB) on {self.device} — {NUM_CLASSES} classes")
147
+ except Exception as exc:
148
+ print(f"[CNN Classifier] Failed to load model: {exc}")
149
+ self.model = None
150
+ self.available = False
151
+
152
+ def predict(self, image: Image.Image) -> dict:
153
+ """
154
+ Classify a single character image.
155
+
156
+ Returns:
157
+ dict with text, confidence, class_index, model_used
158
+ """
159
+ if not self.available or self.model is None:
160
+ return {"text": "", "confidence": 0.0, "error": "CNN model not loaded"}
161
+
162
+ started_at = time.perf_counter()
163
+
164
+ # Preprocess using DHCD style
165
+ tensor = self._preprocess_dhcd_style(image).unsqueeze(0).to(self.device)
166
+
167
+ with torch.inference_mode():
168
+ logits = self.model(tensor)
169
+ probs = F.softmax(logits, dim=1)
170
+ confidence, pred_idx = probs.max(dim=1)
171
+
172
+ predicted_char = DEVANAGARI_CLASSES[pred_idx.item()]
173
+ conf_value = round(confidence.item(), 4)
174
+ inference_ms = round((time.perf_counter() - started_at) * 1000, 2)
175
+
176
+ # Top-3 predictions for debugging
177
+ top3_probs, top3_indices = probs.topk(3, dim=1)
178
+ top3 = [
179
+ {"char": DEVANAGARI_CLASSES[idx.item()], "confidence": round(prob.item(), 4)}
180
+ for idx, prob in zip(top3_indices[0], top3_probs[0])
181
+ ]
182
+
183
+ return {
184
+ "text": predicted_char,
185
+ "confidence": conf_value,
186
+ "class_index": pred_idx.item(),
187
+ "top3": top3,
188
+ "inference_ms": inference_ms,
189
+ "model_used": "cnn_classifier",
190
+ }
191
+
192
+ def _preprocess_dhcd_style(self, image: Image.Image) -> torch.Tensor:
193
+ """Preprocesses a character image to match DHCD dataset (inverted, tightly cropped, padded)."""
194
+ import cv2
195
+ import numpy as np
196
+
197
+ # Convert PIL to CV2 grayscale
198
+ img = np.array(image.convert("L"))
199
+
200
+ # Binarize and invert (DHCD is white ink on black background)
201
+ _, binary = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
202
+
203
+ # Crop to bounding box
204
+ contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
205
+ if contours:
206
+ c = max(contours, key=cv2.contourArea)
207
+ x, y, w, h = cv2.boundingRect(c)
208
+ cropped = binary[y:y+h, x:x+w]
209
+ else:
210
+ cropped = binary
211
+ h, w = cropped.shape
212
+
213
+ # Pad to square and add 16px border (helps CNN focus on center)
214
+ side = max(w, h)
215
+ padded = np.zeros((side + 16, side + 16), dtype=np.uint8)
216
+ y_off = (side + 16 - h) // 2
217
+ x_off = (side + 16 - w) // 2
218
+ padded[y_off:y_off+h, x_off:x_off+w] = cropped
219
+
220
+ # Resize to 32x32
221
+ resized = cv2.resize(padded, (32, 32), interpolation=cv2.INTER_AREA)
222
+
223
+ # Convert to tensor and normalize to [-1, 1]
224
+ tensor = torch.tensor(resized, dtype=torch.float32).unsqueeze(0)
225
+ tensor = (tensor / 255.0 - 0.5) / 0.5
226
+ return tensor
devanagari-cnn-classifier.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9b78f582dabc34056303acf582b5b97a47ac32fa847bfeb33227c1a41681f760
3
+ size 2719832
requirements.txt CHANGED
@@ -7,3 +7,6 @@ safetensors
7
  sentencepiece
8
  fastapi<0.113.0
9
  uvicorn
 
 
 
 
7
  sentencepiece
8
  fastapi<0.113.0
9
  uvicorn
10
+ opencv-python-headless
11
+ torchvision
12
+ numpy