ebhon commited on
Commit
5ff95a7
·
verified ·
1 Parent(s): 9d96218

Upload 9 files

Browse files
.streamlit/config.toml ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ [theme]
2
+ primaryColor = "#FF4B4B"
3
+ backgroundColor = "#FFFFFF"
4
+ secondaryBackgroundColor = "#F0F2F6"
5
+ textColor = "#262730"
6
+ font = "sans serif"
7
+
8
+ [server]
9
+ enableCORS = false
10
+ enableXsrfProtection = false
app.py ADDED
@@ -0,0 +1,251 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import os
3
+ from PIL import Image
4
+ import torch
5
+ from manga_translator.translator import MangaTextDetector, process_manga_pages
6
+ import tempfile
7
+ import cv2
8
+ import io
9
+
10
+ # Initialize session state
11
+ if 'processed_results' not in st.session_state:
12
+ st.session_state.processed_results = {}
13
+ if 'temp_dir' not in st.session_state:
14
+ st.session_state.temp_dir = tempfile.mkdtemp()
15
+
16
+ # Set page config for wider layout and title
17
+ st.set_page_config(
18
+ page_title="Manga Translator",
19
+ page_icon="📚",
20
+ layout="wide",
21
+ initial_sidebar_state="expanded"
22
+ )
23
+
24
+ # Custom CSS to improve the appearance
25
+ st.markdown("""
26
+ <style>
27
+ /* Reset container styles */
28
+ .block-container {
29
+ padding: 2rem 1rem !important;
30
+ max-width: none;
31
+ }
32
+
33
+ /* Main content area */
34
+ .main .block-container {
35
+ padding-left: calc(250px + 1rem) !important;
36
+ }
37
+
38
+ /* Sidebar styling */
39
+ section[data-testid="stSidebar"] {
40
+ width: 250px !important;
41
+ background-color: rgb(240, 242, 246) !important;
42
+ position: fixed !important;
43
+ left: 0 !important;
44
+ top: 0 !important;
45
+ height: 100vh !important;
46
+ }
47
+
48
+ /* Title styling */
49
+ .stTitle {
50
+ font-size: 3rem !important;
51
+ font-weight: 700 !important;
52
+ color: #1E1E1E !important;
53
+ padding-bottom: 2rem !important;
54
+ }
55
+
56
+ /* Subheader styling */
57
+ .stSubheader {
58
+ font-size: 1.5rem !important;
59
+ color: #4F4F4F !important;
60
+ padding-bottom: 1rem !important;
61
+ }
62
+
63
+ /* Upload section styling */
64
+ .uploadSection {
65
+ background-color: white;
66
+ padding: 2rem;
67
+ border-radius: 10px;
68
+ margin: 1rem 0;
69
+ border: 1px solid rgb(224, 224, 224);
70
+ }
71
+
72
+ /* Hide default menu text */
73
+ .css-17lntkn {
74
+ display: none;
75
+ }
76
+ .css-pkbazv {
77
+ display: none;
78
+ }
79
+ </style>
80
+ """, unsafe_allow_html=True)
81
+
82
+ # Main title and description
83
+ st.title("🎯 Manga Translator")
84
+ st.write("This app uses custom YOLO detection, OCR, and machine translation to automatically translate manga pages from Japanese to English.")
85
+
86
+ # Add warning note
87
+ st.warning("**Note:** Translation accuracy may vary due to the complexity of Japanese text and manga-specific expressions. We're continuously working to improve the system!")
88
+
89
+ # Guidelines section
90
+ st.markdown("📋 **Guidelines for Best Results**")
91
+
92
+ st.markdown("1. **Image Requirements:**")
93
+ st.markdown("""
94
+ - Clear, high-resolution manga page
95
+ - Japanese text should be clearly visible
96
+ - Text bubbles should not be cropped
97
+ - Supported formats: PNG, JPG, JPEG
98
+ """)
99
+
100
+ st.markdown("2. **For Best Results:**")
101
+ st.markdown("""
102
+ - Avoid pages with handwritten text
103
+ - Ensure text bubbles are not overlapping
104
+ - Image should be properly oriented
105
+ - Avoid heavily compressed images
106
+ """)
107
+
108
+ st.markdown("3. **Privacy & Copyright:**")
109
+ st.markdown("""
110
+ - Only upload content you have rights to use
111
+ - We don't store any uploaded images
112
+ - All processing is done in real-time
113
+ """)
114
+
115
+ # How it works section
116
+ st.markdown("🔍 **How It Works**")
117
+ st.markdown("""
118
+ 1. **Text Detection:** Custom YOLO model detects text bubbles
119
+ 2. **OCR Processing:** Extracts Japanese text
120
+ 3. **Translation:** Converts to English using DeepL API
121
+ 4. **Text Insertion:** Places translated text back into the image
122
+ """)
123
+
124
+ def process_image(uploaded_file):
125
+ """Process image and store results in session state."""
126
+ if uploaded_file.name not in st.session_state.processed_results:
127
+ try:
128
+ detector = MangaTextDetector('best.pt')
129
+
130
+ # Save temporary file
131
+ temp_path = os.path.join(st.session_state.temp_dir, uploaded_file.name)
132
+ with open(temp_path, "wb") as f:
133
+ f.write(uploaded_file.getbuffer())
134
+
135
+ # Process image
136
+ image, detections, result_image, processed_regions, translated_image = detector.process_image(temp_path)
137
+
138
+ # Store all results in session state
139
+ st.session_state.processed_results[uploaded_file.name] = {
140
+ 'image': image,
141
+ 'detections': detections,
142
+ 'result_image': result_image,
143
+ 'processed_regions': processed_regions,
144
+ 'translated_image': translated_image
145
+ }
146
+
147
+ return True
148
+ except Exception as e:
149
+ st.error(f"❌ Error: {str(e)}")
150
+ return False
151
+ return True
152
+
153
+ # File uploader section
154
+ with st.container():
155
+ st.markdown('<div class="uploadSection">', unsafe_allow_html=True)
156
+ uploaded_files = st.file_uploader(
157
+ "Choose manga pages",
158
+ type=['png', 'jpg', 'jpeg'],
159
+ accept_multiple_files=True,
160
+ help="Drag and drop your manga images here. Supported formats: PNG, JPG, JPEG"
161
+ )
162
+ st.markdown('</div>', unsafe_allow_html=True)
163
+
164
+ if uploaded_files:
165
+ # Create temporary directory for processing
166
+ with tempfile.TemporaryDirectory() as temp_dir:
167
+ # Save uploaded files to temp directory
168
+ for uploaded_file in uploaded_files:
169
+ file_path = os.path.join(temp_dir, uploaded_file.name)
170
+ with open(file_path, "wb") as f:
171
+ f.write(uploaded_file.getbuffer())
172
+
173
+ # Process the manga pages
174
+ with st.spinner("Processing your manga pages..."):
175
+ detector = MangaTextDetector('best.pt')
176
+
177
+ # Process each file
178
+ for uploaded_file in uploaded_files:
179
+ st.subheader(f"Processing: {uploaded_file.name}")
180
+
181
+ # Create columns for display
182
+ col1, col2 = st.columns(2)
183
+
184
+ # Load and display original image
185
+ image_path = os.path.join(temp_dir, uploaded_file.name)
186
+ original_image = Image.open(image_path)
187
+
188
+ with col1:
189
+ st.markdown("**Original Image**")
190
+ st.image(original_image, use_column_width=True)
191
+
192
+ # Process the image
193
+ try:
194
+ image, detections, result_image, processed_regions, translated_image = detector.process_image(image_path)
195
+
196
+ with col2:
197
+ if translated_image is not None:
198
+ st.markdown("**Translated Image**")
199
+ st.image(translated_image, use_column_width=True)
200
+ else:
201
+ st.error("No text was detected in this image.")
202
+
203
+ # Show detected text and translations if available
204
+ if processed_regions and processed_regions['text_regions']:
205
+ with st.expander("View Detected Text and Translations"):
206
+ for i, region in enumerate(processed_regions['text_regions'], 1):
207
+ st.markdown(f"**Region {i}**")
208
+ cols = st.columns(2)
209
+ with cols[0]:
210
+ st.markdown("Original Text:")
211
+ st.code(region['text'])
212
+ with cols[1]:
213
+ st.markdown("Translation:")
214
+ st.code(region['translation'])
215
+ st.markdown("---")
216
+
217
+ except Exception as e:
218
+ st.error(f"Error processing {uploaded_file.name}: {str(e)}")
219
+ continue
220
+
221
+ st.markdown("---")
222
+ else:
223
+ # Show instructions when no files are uploaded
224
+ st.info("👆 Upload a manga page to get started!")
225
+
226
+ # Footer
227
+ st.markdown("---")
228
+ st.markdown("""
229
+ <div style='text-align: center; color: #666666; padding: 1rem;'>
230
+ Made with ❤️ for manga fans
231
+ </div>
232
+ """, unsafe_allow_html=True)
233
+
234
+ # Add reset button to sidebar
235
+ if st.sidebar.button("🔄 Reset All"):
236
+ # Clear session state
237
+ for key in ['processed_results', 'temp_dir']:
238
+ if key in st.session_state:
239
+ del st.session_state[key]
240
+ st.experimental_rerun()
241
+
242
+ # Footer
243
+ st.markdown("---")
244
+ st.markdown("""
245
+ *Created by Ebhon*
246
+
247
+ This app translates manga text from Japanese to English using:
248
+ - YOLO for text detection
249
+ - Manga OCR for Japanese text recognition
250
+ - DeepL for translation
251
+ """)
manga_translator/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from .translator import process_manga_pages
2
+
3
+ __all__ = ['process_manga_pages']
manga_translator/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (275 Bytes). View file
 
manga_translator/__pycache__/translator.cpython-312.pyc ADDED
Binary file (38.5 kB). View file
 
manga_translator/font/CC Wild Words Bold Italic.ttf ADDED
Binary file (32.8 kB). View file
 
manga_translator/font/CC Wild Words Italic.ttf ADDED
Binary file (32.6 kB). View file
 
manga_translator/font/CC Wild Words Roman.ttf ADDED
Binary file (32.3 kB). View file
 
manga_translator/translator.py ADDED
@@ -0,0 +1,876 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from ultralytics import YOLO
2
+ import cv2
3
+ import os
4
+ from PIL import Image, ImageFile, ImageDraw, ImageFont
5
+ import numpy as np
6
+ import logging
7
+ import warnings
8
+ import transformers
9
+ import streamlit as st
10
+ import matplotlib.pyplot as plt
11
+ import re
12
+ from manga_ocr import MangaOcr
13
+ from difflib import SequenceMatcher
14
+ import deepl
15
+ from dotenv import load_dotenv
16
+ import textwrap
17
+
18
+ # Configure logging and warnings
19
+ transformers.logging.set_verbosity_error()
20
+ logging.getLogger("transformers").setLevel(logging.ERROR)
21
+ warnings.filterwarnings("ignore")
22
+ os.environ["TOKENIZERS_PARALLELISM"] = "false"
23
+
24
+ # Configure logging
25
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
26
+
27
+ # Configure image loading
28
+ ImageFile.LOAD_TRUNCATED_IMAGES = True
29
+
30
+ # Load environment variables
31
+ load_dotenv()
32
+
33
+ # Initialize DeepL translator with error handling
34
+ try:
35
+ deepl_api_key = os.getenv('DEEPL_API_KEY')
36
+ if not deepl_api_key:
37
+ raise ValueError("DeepL API key not found in environment variables")
38
+ translator_deepl = deepl.Translator(deepl_api_key)
39
+ except Exception as e:
40
+ logging.error(f"Failed to initialize DeepL translator: {e}")
41
+ # Fallback to a placeholder translator for testing
42
+ class PlaceholderTranslator:
43
+ def translate_text(self, text, source_lang, target_lang):
44
+ class Result:
45
+ def __init__(self, text):
46
+ self.text = f"[TRANSLATION: {text}]"
47
+ return Result(text)
48
+ translator_deepl = PlaceholderTranslator()
49
+
50
+ class MangaTextDetector:
51
+ def __init__(self, model_path="best.pt"):
52
+ """Initialize the detector with YOLO model"""
53
+ self.model = YOLO(model_path)
54
+ self.model.conf = 0.25
55
+ self.model.iou = 0.45
56
+ self.mocr = MangaOcr()
57
+ self.font_path = 'font/CC Wild Words Roman.ttf'
58
+
59
+ def reload_and_save_images(self, folder_path):
60
+ """
61
+ Reloads and resaves all images in a folder to ensure they are properly formatted.
62
+
63
+ Args:
64
+ folder_path (str): Path to the directory containing images
65
+ """
66
+ os.makedirs(folder_path, exist_ok=True)
67
+ for filename in os.listdir(folder_path):
68
+ if filename.lower().endswith(('.jpg', '.jpeg', '.png')):
69
+ path = os.path.join(folder_path, filename)
70
+ try:
71
+ img = Image.open(path)
72
+ img = img.convert("RGB")
73
+ img.save(path)
74
+ except Exception as e:
75
+ logging.error(f"Skipping {filename}: {e}")
76
+
77
+ def load_image(self, image_path):
78
+ """
79
+ Load an image from path
80
+
81
+ Args:
82
+ image_path (str): Path to image file
83
+
84
+ Returns:
85
+ numpy.ndarray: Loaded image in BGR format
86
+ """
87
+ image = cv2.imread(image_path)
88
+ if image is None:
89
+ raise ValueError(f"Could not load image: {image_path}")
90
+ return image
91
+
92
+ def detect_text(self, image):
93
+ """
94
+ Detect text regions in an image
95
+
96
+ Args:
97
+ image (numpy.ndarray): Input image
98
+
99
+ Returns:
100
+ list: List of detections (coordinates, class, confidence)
101
+ """
102
+ results = self.model(image)
103
+
104
+ if not results or len(results) == 0:
105
+ logging.warning("No text regions detected")
106
+ return []
107
+
108
+ detections = []
109
+ result = results[0] # Get first result
110
+
111
+ logging.info(f"Found {len(result.boxes)} potential text regions")
112
+
113
+ for box in result.boxes:
114
+ try:
115
+ coords = box.xyxy[0].cpu().numpy() # Get coordinates
116
+ cls = int(box.cls[0].item()) # Get class
117
+ conf = float(box.conf[0].item()) # Get confidence
118
+
119
+ box_coords = [int(c) for c in coords]
120
+
121
+ if conf > self.model.conf:
122
+ detections.append((box_coords, cls, conf))
123
+
124
+ except Exception as e:
125
+ logging.warning(f"Error processing detection: {str(e)}")
126
+ continue
127
+
128
+ return detections
129
+
130
+ def draw_detections(self, image, detections):
131
+ """
132
+ Draw detection boxes on image
133
+
134
+ Args:
135
+ image (numpy.ndarray): Input image
136
+ detections (list): List of detections
137
+
138
+ Returns:
139
+ numpy.ndarray: Image with drawn detections
140
+ """
141
+ display_img = image.copy()
142
+
143
+ colors = {
144
+ 0: (0, 255, 0), # Green for speech bubbles
145
+ 1: (255, 0, 0), # Blue for narration
146
+ 2: (0, 0, 255), # Red for other text
147
+ 3: (255, 255, 0), # Cyan for text
148
+ 4: (0, 255, 255) # Yellow for UI
149
+ }
150
+
151
+ class_names = {
152
+ 0: "Speech Bubble",
153
+ 1: "Narration",
154
+ 2: "Other Text",
155
+ 3: "Text",
156
+ 4: "UI Element"
157
+ }
158
+
159
+ for box_coords, cls, conf in detections:
160
+ x1, y1, x2, y2 = box_coords
161
+ color = colors.get(cls, (0, 255, 0))
162
+
163
+ # Draw rectangle
164
+ cv2.rectangle(display_img, (x1, y1), (x2, y2), color, 2)
165
+
166
+ # Add label
167
+ class_name = class_names.get(cls, "Unknown")
168
+ label = f"{class_name}: {conf:.2f}"
169
+
170
+ font = cv2.FONT_HERSHEY_SIMPLEX
171
+ font_scale = 0.5
172
+ thickness = 1
173
+
174
+ (text_width, text_height), _ = cv2.getTextSize(label, font, font_scale, thickness)
175
+ cv2.rectangle(display_img, (x1, y1-text_height-5), (x1+text_width, y1), color, -1)
176
+ cv2.putText(display_img, label, (x1, y1-5), font, font_scale, (255, 255, 255), thickness)
177
+
178
+ return display_img
179
+
180
+ def sort_bubbles(self, boxes):
181
+ """
182
+ Sorts text bubbles in reading order (top-to-bottom, right-to-left).
183
+
184
+ Args:
185
+ boxes (list or numpy.ndarray): List of bounding boxes in format [x1, y1, x2, y2]
186
+
187
+ Returns:
188
+ list: Sorted list of bounding boxes in reading order
189
+ """
190
+ return sorted(boxes, key=lambda b: (int(b[1] // 50), -int(b[0])))
191
+
192
+ def determine_region_type(self, box, image, class_id):
193
+ """
194
+ Determine region type based on the model's class prediction.
195
+ """
196
+ class_to_type = {
197
+ 0: "bubble", # Speech bubbles containing dialogue
198
+ 1: "narration", # Rectangular narration boxes
199
+ 2: "other", # Other manga elements
200
+ 3: "text", # Standalone text elements
201
+ 4: "ui" # User interface elements
202
+ }
203
+ return class_to_type.get(class_id, "unknown")
204
+
205
+ def enhance_text_region(self, image_region):
206
+ """
207
+ Enhance text clarity in an image region before OCR.
208
+ """
209
+ gray = cv2.cvtColor(image_region, cv2.COLOR_BGR2GRAY) if len(image_region.shape) == 3 else image_region
210
+ binary = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
211
+ cv2.THRESH_BINARY, 11, 2)
212
+ denoised = cv2.fastNlMeansDenoising(binary, None, 10, 7, 21)
213
+ clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))
214
+ enhanced = clahe.apply(gray)
215
+ return enhanced
216
+
217
+ def validate_ocr_result(self, text, image_region):
218
+ """
219
+ Validate OCR results to filter out hallucinations or low-confidence detections.
220
+ """
221
+ if not text or len(text.strip()) < 2:
222
+ return False
223
+
224
+ gray = cv2.cvtColor(image_region, cv2.COLOR_BGR2GRAY) if len(image_region.shape) == 3 else image_region
225
+ laplacian_var = cv2.Laplacian(gray, cv2.CV_64F).var()
226
+
227
+ if laplacian_var < 50:
228
+ return False
229
+
230
+ min_val, max_val, _, _ = cv2.minMaxLoc(gray)
231
+ contrast = max_val - min_val
232
+
233
+ if contrast < 30:
234
+ return False
235
+
236
+ text_density = np.count_nonzero(gray < 128) / gray.size
237
+
238
+ if text_density < 0.05:
239
+ return False
240
+
241
+ return True
242
+
243
+ def verify_japanese_text(self, text):
244
+ """
245
+ Verify if the detected text is likely valid Japanese.
246
+ """
247
+ japanese_chars = re.findall(r'[\u3000-\u303F\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FFF]', text)
248
+ if len(japanese_chars) < len(text) * 0.5:
249
+ return False
250
+
251
+ for char in set(text):
252
+ if text.count(char) > len(text) * 0.7:
253
+ return False
254
+
255
+ return True
256
+
257
+ def clean_ocr_text(self, text):
258
+ """
259
+ Clean OCR text by removing non-Japanese/non-English characters and normalizing spaces.
260
+
261
+ Args:
262
+ text (str): Raw OCR text to clean
263
+
264
+ Returns:
265
+ str: Cleaned and normalized text
266
+ """
267
+ # Remove non-Japanese/non-English characters
268
+ text = re.sub(r'[^\u3000-\u303F\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FFF\uFF00-\uFFEFa-zA-Z0-9\s.,!?\'\"-]', '', text)
269
+ # Merge separated kanji words
270
+ text = re.sub(r'(?<=[\u4E00-\u9FFF]) (?=[\u4E00-\u9FFF])', '', text)
271
+ # Normalize spaces
272
+ text = re.sub(r'\s+', ' ', text).strip()
273
+ return text
274
+
275
+ def split_japanese_sentences(self, text):
276
+ """
277
+ Split Japanese text into sentences based on punctuation.
278
+
279
+ Args:
280
+ text (str): Japanese text to split
281
+
282
+ Returns:
283
+ str: Text with newlines after each sentence
284
+ """
285
+ return re.sub(r'([。!?])', r'\1\n', text).strip()
286
+
287
+ def is_similar(self, a, b, threshold=0.8):
288
+ """
289
+ Check if two strings are similar based on sequence matching.
290
+
291
+ Args:
292
+ a (str): First string to compare
293
+ b (str): Second string to compare
294
+ threshold (float): Similarity threshold (0.0 to 1.0)
295
+
296
+ Returns:
297
+ bool: True if strings are similar enough to be considered duplicates
298
+ """
299
+ return SequenceMatcher(None, a, b).ratio() > threshold
300
+
301
+ def manga_style_formatting(self, text):
302
+ """Apply universal manga-specific formatting rules."""
303
+ manga_terms = {
304
+ 'sama': '-sama',
305
+ 'san': '-san',
306
+ 'kun': '-kun',
307
+ 'chan': '-chan',
308
+ 'sensei': '-sensei',
309
+ 'senpai': '-senpai',
310
+ 'kouhai': '-kouhai',
311
+ 'dono': '-dono',
312
+ 'shi': '-shi',
313
+ }
314
+
315
+ sfx_categories = {
316
+ 'ドドド|ゴゴゴ|ドンドン': '*RUMBLE*',
317
+ 'バキッ|バキバキ': '*CRACK*',
318
+ 'ガチャ|カチャ': '*CLICK*',
319
+ 'ザー|ザァ': '*WHOOSH*',
320
+ 'ドン|バン': '*BAM*',
321
+ 'シーン': '*SILENCE*',
322
+ 'キラキラ': '*SPARKLE*',
323
+ 'ニコ': '*SMILE*',
324
+ 'ハァハァ': '*PANT*',
325
+ 'ドキドキ': '*THUMP*'
326
+ }
327
+
328
+ character_names = {
329
+ 'カイドウ': 'Kaido',
330
+ 'モンキー・ロ・ルフィ': 'Monkey D. Luffy',
331
+ '海賊王': 'Pirate King'
332
+ }
333
+
334
+ formatted_text = text
335
+
336
+ for jp, en in character_names.items():
337
+ formatted_text = formatted_text.replace(jp, en)
338
+
339
+ formatted_text = formatted_text.replace('お前', 'you')
340
+ formatted_text = formatted_text.replace('おれ', 'I')
341
+
342
+ formatted_text = re.sub(r'(!+)', r'!\1', formatted_text)
343
+ formatted_text = formatted_text.replace('...', '…')
344
+ formatted_text = re.sub(r'\?+!+|\!+\?+', '?!', formatted_text)
345
+
346
+ if '!' in formatted_text:
347
+ formatted_text = formatted_text.upper()
348
+
349
+ return formatted_text
350
+
351
+ def clean_and_translate_text(self, text, context=None):
352
+ """Clean and translate text with universal manga context."""
353
+ if text.strip() in ['!', '。', '、', '...', '?']:
354
+ return ""
355
+
356
+ # Remove duplicate punctuation and lines
357
+ cleaned_text = text.strip()
358
+ cleaned_text = re.sub(r'([!。、?])\1+', r'\1', cleaned_text) # Remove duplicate punctuation
359
+ cleaned_text = re.sub(r'(.+?)(?:\n\1)+', r'\1', cleaned_text) # Remove duplicate lines
360
+
361
+ try:
362
+ translation = translator_deepl.translate_text(
363
+ cleaned_text,
364
+ source_lang='JA',
365
+ target_lang='EN-US',
366
+ preserve_formatting=True
367
+ ).text
368
+
369
+ translation = self.manga_style_formatting(translation)
370
+ translation = re.sub(r'\s+([!?.,])', r'\1', translation) # Fix spacing around punctuation
371
+ translation = re.sub(r'[\s\n]+', ' ', translation).strip() # Clean up whitespace
372
+
373
+ # Remove duplicate phrases in translation
374
+ translation_parts = translation.split()
375
+ unique_parts = []
376
+ for part in translation_parts:
377
+ if not unique_parts or part.upper() != unique_parts[-1].upper():
378
+ unique_parts.append(part)
379
+ translation = ' '.join(unique_parts)
380
+
381
+ logging.info(f"Translated: {cleaned_text} -> {translation}")
382
+ return translation
383
+ except Exception as e:
384
+ logging.error(f"Translation failed for {cleaned_text}: {e}")
385
+ return ""
386
+
387
+ def post_process_translation(self, translation, text_type=None):
388
+ """Apply final formatting based on text type."""
389
+ if text_type is None:
390
+ if bool(re.search(r'[ドゴバキガ]{2,}', translation)):
391
+ text_type = "sfx"
392
+ elif '!' in translation or '?' in translation:
393
+ text_type = "emphasis"
394
+
395
+ if text_type == "sfx":
396
+ return f"*{translation.upper()}*"
397
+ elif text_type == "emphasis":
398
+ if '!' in translation and '?' in translation:
399
+ return translation.upper() + "?!"
400
+ elif '!' in translation:
401
+ return translation.upper()
402
+ else:
403
+ return translation
404
+
405
+ return translation
406
+
407
+ def process_text_regions(self, detections, image):
408
+ """Process text regions from detections."""
409
+ text_regions = []
410
+ bubbles = []
411
+
412
+ for i, (box_coords, cls_id, conf) in enumerate(detections):
413
+ try:
414
+ x1, y1, x2, y2 = box_coords
415
+ region_type = self.determine_region_type(box_coords, image, cls_id)
416
+
417
+ if cls_id == 0: # Speech bubble
418
+ bubbles.append({
419
+ 'coords': (x1, y1, x2, y2),
420
+ 'type': region_type,
421
+ 'confidence': conf
422
+ })
423
+ continue
424
+
425
+ if cls_id != 3: # Only process text class
426
+ continue
427
+
428
+ cropped = image[y1:y2, x1:x2]
429
+ original_crop = cropped.copy()
430
+ enhanced_crop = self.enhance_text_region(cropped)
431
+
432
+ pil_crop = Image.fromarray(cv2.cvtColor(cropped, cv2.COLOR_BGR2RGB))
433
+
434
+ raw_text = self.mocr(pil_crop)
435
+ cleaned_text = self.clean_ocr_text(raw_text)
436
+ formatted_text = self.split_japanese_sentences(cleaned_text)
437
+
438
+ # Translate the text
439
+ translated_text = self.clean_and_translate_text(formatted_text)
440
+ final_translation = self.post_process_translation(translated_text)
441
+
442
+ is_valid = self.validate_ocr_result(cleaned_text, cropped) and self.verify_japanese_text(cleaned_text)
443
+
444
+ if not is_valid:
445
+ continue
446
+
447
+ text_regions.append({
448
+ 'text': formatted_text,
449
+ 'raw_text': raw_text,
450
+ 'translation': final_translation,
451
+ 'coords': (x1, y1, x2, y2),
452
+ 'type': region_type
453
+ })
454
+
455
+ except Exception as e:
456
+ logging.warning(f"Error processing region {i}: {str(e)}")
457
+ continue
458
+
459
+ # Deduplicate similar text regions
460
+ unique_regions = []
461
+ for region in text_regions:
462
+ is_duplicate = False
463
+ for existing in unique_regions:
464
+ if self.is_similar(region['text'], existing['text']):
465
+ is_duplicate = True
466
+ break
467
+ if not is_duplicate:
468
+ unique_regions.append(region)
469
+
470
+ return {
471
+ 'text_regions': unique_regions,
472
+ 'bubbles': bubbles
473
+ }
474
+
475
+ def insert_translation(self, image, box_coords, translated_text, font_size_multiplier=1.0):
476
+ """
477
+ Insert translated text into a text region with improved dynamic font sizing.
478
+ """
479
+ x1, y1, x2, y2 = map(int, box_coords)
480
+ region_width, region_height = x2-x1, y2-y1
481
+
482
+ # Extract the region
483
+ region = image[y1:y2, x1:x2].copy()
484
+
485
+ # Create a clean white background
486
+ clean_region = np.ones_like(region) * 255
487
+
488
+ # Create a PIL Image for text rendering
489
+ pil_region = Image.fromarray(cv2.cvtColor(clean_region, cv2.COLOR_BGR2RGB))
490
+ draw = ImageDraw.Draw(pil_region)
491
+
492
+ # Dynamic base font size calculation based on region dimensions and text length
493
+ area = region_width * region_height
494
+ text_length = len(translated_text)
495
+
496
+ # Calculate initial font size based on area and text length
497
+ initial_font_size = int(np.sqrt(area / (text_length + 1)) * 1.2)
498
+
499
+ # Adjust based on region shape
500
+ aspect_ratio = region_width / max(1, region_height)
501
+ if aspect_ratio > 2: # Wide region
502
+ initial_font_size = int(initial_font_size * 0.8)
503
+ elif aspect_ratio < 0.5: # Tall region
504
+ initial_font_size = int(initial_font_size * 0.9)
505
+
506
+ # Apply font size multiplier
507
+ initial_font_size = int(initial_font_size * font_size_multiplier)
508
+
509
+ # Set minimum and maximum font sizes based on region dimensions
510
+ min_font_size = max(12, int(min(region_width, region_height) * 0.1))
511
+ max_font_size = min(72, int(min(region_width, region_height) * 0.4))
512
+
513
+ # Clamp font size between min and max
514
+ font_size = max(min_font_size, min(initial_font_size, max_font_size))
515
+
516
+ try:
517
+ font = ImageFont.truetype(self.font_path, font_size)
518
+ except IOError:
519
+ logging.warning(f"Font {self.font_path} not found, using default font")
520
+ font = ImageFont.load_default()
521
+
522
+ # Calculate padding based on region size
523
+ padding_x = int(region_width * 0.05)
524
+ padding_y = int(region_height * 0.05)
525
+
526
+ # Calculate effective dimensions for text
527
+ effective_width = region_width - (2 * padding_x)
528
+ effective_height = region_height - (2 * padding_y)
529
+
530
+ # Calculate characters per line based on font metrics
531
+ test_text = "A" * 10 # Use a test string to measure character width
532
+ test_bbox = draw.textbbox((0, 0), test_text, font=font)
533
+ avg_char_width = (test_bbox[2] - test_bbox[0]) / 10
534
+ chars_per_line = max(1, int(effective_width / avg_char_width))
535
+
536
+ def smart_wrap(text, width):
537
+ """Improved text wrapping with better handling of long words and line breaks."""
538
+ # First try standard wrapping
539
+ wrapped = textwrap.fill(text, width=width)
540
+ lines = wrapped.split('\n')
541
+
542
+ # Check if any line is too long
543
+ max_line_length = max(len(line) for line in lines)
544
+ if max_line_length > width * 1.2:
545
+ # More aggressive wrapping for long lines
546
+ words = text.split()
547
+ lines = []
548
+ current_line = []
549
+ current_length = 0
550
+
551
+ for word in words:
552
+ word_length = len(word)
553
+ if current_length + word_length <= width:
554
+ current_line.append(word)
555
+ current_length += word_length + 1
556
+ else:
557
+ if word_length > width // 2:
558
+ # Break long words with hyphens
559
+ parts = [word[i:i+width//2] for i in range(0, len(word), width//2)]
560
+ current_line.append(parts[0] + "-")
561
+ lines.append(" ".join(current_line))
562
+ current_line = parts[1:]
563
+ current_length = sum(len(p) for p in current_line) + len(current_line)
564
+ else:
565
+ lines.append(" ".join(current_line))
566
+ current_line = [word]
567
+ current_length = word_length + 1
568
+
569
+ if current_line:
570
+ lines.append(" ".join(current_line))
571
+
572
+ wrapped = "\n".join(lines)
573
+
574
+ return wrapped
575
+
576
+ # Wrap text with improved algorithm
577
+ wrapped_text = smart_wrap(translated_text, width=chars_per_line)
578
+
579
+ # Calculate text dimensions
580
+ text_bbox = draw.textbbox((0, 0), wrapped_text, font=font)
581
+ text_width = text_bbox[2] - text_bbox[0]
582
+ text_height = text_bbox[3] - text_bbox[1]
583
+
584
+ # If text is too big, reduce font size iteratively
585
+ while (text_width > effective_width or text_height > effective_height) and font_size > min_font_size:
586
+ font_size = int(font_size * 0.9)
587
+ font = ImageFont.truetype(self.font_path, font_size)
588
+
589
+ # Recalculate chars per line and rewrap text
590
+ test_bbox = draw.textbbox((0, 0), test_text, font=font)
591
+ avg_char_width = (test_bbox[2] - test_bbox[0]) / 10
592
+ chars_per_line = max(1, int(effective_width / avg_char_width))
593
+ wrapped_text = smart_wrap(translated_text, width=chars_per_line)
594
+
595
+ # Update text dimensions
596
+ text_bbox = draw.textbbox((0, 0), wrapped_text, font=font)
597
+ text_width = text_bbox[2] - text_bbox[0]
598
+ text_height = text_bbox[3] - text_bbox[1]
599
+
600
+ # Center the text
601
+ text_x = (region_width - text_width) // 2
602
+ text_y = (region_height - text_height) // 2
603
+
604
+ # Draw text with slight shadow for better readability
605
+ shadow_offset = max(1, font_size // 20)
606
+ draw.text((text_x + shadow_offset, text_y + shadow_offset), wrapped_text, font=font, fill=(200, 200, 200))
607
+ draw.text((text_x, text_y), wrapped_text, font=font, fill=(0, 0, 0))
608
+
609
+ # Convert back to OpenCV format and update the image
610
+ result_region = cv2.cvtColor(np.array(pil_region), cv2.COLOR_RGB2BGR)
611
+ image[y1:y2, x1:x2] = result_region
612
+
613
+ return image
614
+
615
+ def check_and_fix_truncated_text(self, image, text_regions):
616
+ """Enhanced function to fix text issues with improved region analysis and font sizing."""
617
+ fixed_image = image.copy()
618
+
619
+ # First pass: analyze all text regions and their context
620
+ regions_to_process = []
621
+
622
+ # Get image dimensions for context
623
+ img_height, img_width = image.shape[:2]
624
+ total_image_area = img_width * img_height
625
+
626
+ for region in text_regions:
627
+ if not region.get('translation', '').strip():
628
+ continue
629
+
630
+ x1, y1, x2, y2 = region['coords']
631
+ translation = region['translation']
632
+
633
+ # Calculate region properties
634
+ region_width = x2 - x1
635
+ region_height = y2 - y1
636
+ region_area = region_width * region_height
637
+ text_length = len(translation)
638
+
639
+ # Calculate relative metrics
640
+ area_ratio = region_area / total_image_area
641
+ aspect_ratio = region_width / max(1, region_height)
642
+ text_density = text_length / max(1, region_area)
643
+
644
+ # Initialize font multiplier based on various factors
645
+ font_multiplier = 1.0
646
+ priority = 0
647
+
648
+ # Adjust for region size relative to image
649
+ if area_ratio < 0.02: # Very small region
650
+ font_multiplier *= 1.3
651
+ priority += 3
652
+ elif area_ratio < 0.05: # Small region
653
+ font_multiplier *= 1.2
654
+ priority += 2
655
+
656
+ # Adjust for aspect ratio
657
+ if aspect_ratio > 2.5: # Very wide region
658
+ font_multiplier *= 0.85
659
+ priority += 2
660
+ elif aspect_ratio > 1.5: # Wide region
661
+ font_multiplier *= 0.9
662
+ priority += 1
663
+ elif aspect_ratio < 0.4: # Very tall region
664
+ font_multiplier *= 0.9
665
+ priority += 2
666
+
667
+ # Adjust for text density
668
+ if text_density > 0.1: # Very dense text
669
+ font_multiplier *= 0.85
670
+ priority += 3
671
+ elif text_density > 0.05: # Dense text
672
+ font_multiplier *= 0.9
673
+ priority += 2
674
+
675
+ # Adjust for text content
676
+ if any(char in translation for char in '!?'): # Emphasis text
677
+ font_multiplier *= 1.1
678
+ priority += 1
679
+ if translation.isupper(): # All caps text
680
+ font_multiplier *= 0.9
681
+ priority += 1
682
+
683
+ # Position-based adjustments
684
+ center_y = (y1 + y2) / 2
685
+ if center_y < img_height * 0.2: # Top of page
686
+ font_multiplier *= 0.95
687
+ elif center_y > img_height * 0.8: # Bottom of page
688
+ font_multiplier *= 0.95
689
+
690
+ # Check for overlapping regions
691
+ overlaps = 0
692
+ for other in text_regions:
693
+ if other == region:
694
+ continue
695
+ ox1, oy1, ox2, oy2 = other['coords']
696
+ if (x1 < ox2 and x2 > ox1 and y1 < oy2 and y2 > oy1):
697
+ overlaps += 1
698
+
699
+ if overlaps > 0:
700
+ font_multiplier *= 0.9
701
+ priority += overlaps
702
+
703
+ # Store processed region info
704
+ regions_to_process.append({
705
+ 'region': region,
706
+ 'font_multiplier': max(0.7, min(1.5, font_multiplier)), # Clamp multiplier
707
+ 'priority': priority,
708
+ 'area': region_area,
709
+ 'text_density': text_density
710
+ })
711
+
712
+ # Sort regions by priority and area
713
+ regions_to_process.sort(key=lambda x: (-x['priority'], -x['area'], -x['text_density']))
714
+
715
+ # Second pass: process regions in order
716
+ for item in regions_to_process:
717
+ region = item['region']
718
+ font_multiplier = item['font_multiplier']
719
+
720
+ try:
721
+ fixed_image = self.insert_translation(
722
+ fixed_image,
723
+ region['coords'],
724
+ region['translation'],
725
+ font_size_multiplier=font_multiplier
726
+ )
727
+ except Exception as e:
728
+ logging.warning(f"Failed to process region: {str(e)}")
729
+ continue
730
+
731
+ return fixed_image
732
+
733
+ def process_image(self, image_path):
734
+ """Process a single image with text detection, OCR, and translation"""
735
+ image = self.load_image(image_path)
736
+ detections = self.detect_text(image)
737
+ result_image = self.draw_detections(image, detections) if detections else None
738
+
739
+ processed_regions = None
740
+ if detections:
741
+ processed_regions = self.process_text_regions(detections, image)
742
+
743
+ if processed_regions['text_regions']:
744
+ # Create translated image
745
+ translated_image = image.copy()
746
+ translated_image = self.check_and_fix_truncated_text(
747
+ translated_image,
748
+ processed_regions['text_regions']
749
+ )
750
+
751
+ # Save translated image
752
+ output_dir = "translated_images"
753
+ os.makedirs(output_dir, exist_ok=True)
754
+ output_path = os.path.join(output_dir, f"translated_{os.path.basename(image_path)}")
755
+ cv2.imwrite(output_path, translated_image)
756
+ logging.info(f"Saved translated image to: {output_path}")
757
+
758
+ return image, detections, result_image, processed_regions, translated_image
759
+
760
+ return image, detections, result_image, processed_regions, None
761
+
762
+ def process_manga_pages(image_folder, translated_dir, show_results=False):
763
+ """Process manga pages with text detection, OCR, and translation"""
764
+ os.makedirs(translated_dir, exist_ok=True)
765
+ detector = MangaTextDetector('best.pt')
766
+
767
+ # Get list of image files
768
+ image_files = [f for f in os.listdir(image_folder)
769
+ if f.lower().endswith(('.png', '.jpg', '.jpeg'))]
770
+
771
+ if not image_files:
772
+ if show_results and st:
773
+ st.warning("No image files found in the input folder")
774
+ return
775
+
776
+ if show_results and st:
777
+ progress_bar = st.progress(0)
778
+ status_text = st.empty()
779
+ total_steps = len(image_files)
780
+ current_step = 0
781
+
782
+ for filename in image_files:
783
+ try:
784
+ if show_results and st:
785
+ current_step += 1
786
+ progress = current_step / total_steps
787
+ progress_bar.progress(progress)
788
+ status_text.text(f"Processing image {current_step} of {total_steps}: {filename}")
789
+
790
+ input_path = os.path.join(image_folder, filename)
791
+
792
+ if show_results and st:
793
+ with st.spinner("Processing image..."):
794
+ image, detections, result_image, processed_regions, translated_image = detector.process_image(input_path)
795
+ else:
796
+ image, detections, result_image, processed_regions, translated_image = detector.process_image(input_path)
797
+
798
+ if show_results and st:
799
+ st.subheader(f"Results for {filename}")
800
+
801
+ # Show all three images side by side
802
+ col1, col2, col3 = st.columns(3)
803
+ with col1:
804
+ st.image(cv2.cvtColor(image, cv2.COLOR_BGR2RGB),
805
+ caption="Original Image")
806
+ with col2:
807
+ if result_image is not None:
808
+ st.image(cv2.cvtColor(result_image, cv2.COLOR_BGR2RGB),
809
+ caption="Detected Regions")
810
+ with col3:
811
+ if translated_image is not None:
812
+ st.image(cv2.cvtColor(translated_image, cv2.COLOR_BGR2RGB),
813
+ caption="Translated Image")
814
+
815
+ if processed_regions and processed_regions['text_regions']:
816
+ st.subheader("Detected Text Regions")
817
+ tabs = st.tabs([f"Region {i+1}" for i in range(len(processed_regions['text_regions']))])
818
+
819
+ for i, (tab, region) in enumerate(zip(tabs, processed_regions['text_regions'])):
820
+ with tab:
821
+ col1, col2 = st.columns(2)
822
+ with col1:
823
+ x1, y1, x2, y2 = region['coords']
824
+ region_img = image[y1:y2, x1:x2]
825
+ st.image(cv2.cvtColor(region_img, cv2.COLOR_BGR2RGB),
826
+ caption="Region Image")
827
+ with col2:
828
+ st.markdown("**Raw OCR Text:**")
829
+ st.code(region['raw_text'])
830
+ st.markdown("**Cleaned Text:**")
831
+ st.code(region['text'])
832
+ if 'translation' in region:
833
+ st.markdown("**English Translation:**")
834
+ st.code(region['translation'])
835
+
836
+ except Exception as e:
837
+ logging.error(f"Error processing {filename}: {str(e)}")
838
+ if show_results and st:
839
+ st.error(f"Error processing {filename}: {str(e)}")
840
+ continue
841
+
842
+ if show_results and st:
843
+ progress_bar.empty()
844
+ status_text.empty()
845
+ st.success(f"✅ Processing complete! Processed {len(image_files)} images.")
846
+
847
+ def main():
848
+ st.title("Manga Text Detection, OCR, and Translation")
849
+ st.write("Upload manga pages to detect, recognize, and translate text")
850
+
851
+ uploaded_files = st.file_uploader("Choose manga pages", type=['jpg', 'jpeg', 'png'], accept_multiple_files=True)
852
+
853
+ if uploaded_files:
854
+ temp_input_dir = "temp_input"
855
+ temp_output_dir = "temp_output"
856
+ os.makedirs(temp_input_dir, exist_ok=True)
857
+
858
+ try:
859
+ for uploaded_file in uploaded_files:
860
+ with open(os.path.join(temp_input_dir, uploaded_file.name), "wb") as f:
861
+ f.write(uploaded_file.getbuffer())
862
+
863
+ process_manga_pages(temp_input_dir, temp_output_dir, show_results=True)
864
+
865
+ except Exception as e:
866
+ st.error(f"Error: {str(e)}")
867
+
868
+ finally:
869
+ import shutil
870
+ if os.path.exists(temp_input_dir):
871
+ shutil.rmtree(temp_input_dir)
872
+ if os.path.exists(temp_output_dir):
873
+ shutil.rmtree(temp_output_dir)
874
+
875
+ if __name__ == "__main__":
876
+ main()