Dragneel commited on
Commit
456449d
·
verified ·
1 Parent(s): 36dcc97

Upload folder using huggingface_hub

Browse files
Files changed (3) hide show
  1. .github/workflows/update_space.yml +28 -0
  2. README.md +2 -8
  3. gradio_test.py +1010 -0
.github/workflows/update_space.yml ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Run Python script
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+
8
+ jobs:
9
+ build:
10
+ runs-on: ubuntu-latest
11
+
12
+ steps:
13
+ - name: Checkout
14
+ uses: actions/checkout@v2
15
+
16
+ - name: Set up Python
17
+ uses: actions/setup-python@v2
18
+ with:
19
+ python-version: '3.9'
20
+
21
+ - name: Install Gradio
22
+ run: python -m pip install gradio
23
+
24
+ - name: Log in to Hugging Face
25
+ run: python -c 'import huggingface_hub; huggingface_hub.login(token="${{ secrets.hf_token }}")'
26
+
27
+ - name: Deploy to Spaces
28
+ run: gradio deploy
README.md CHANGED
@@ -1,12 +1,6 @@
1
  ---
2
- title: Test Test
3
- emoji: 🐢
4
- colorFrom: green
5
- colorTo: red
6
  sdk: gradio
7
  sdk_version: 5.23.3
8
- app_file: app.py
9
- pinned: false
10
  ---
11
-
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: test_test
3
+ app_file: gradio_test.py
 
 
4
  sdk: gradio
5
  sdk_version: 5.23.3
 
 
6
  ---
 
 
gradio_test.py ADDED
@@ -0,0 +1,1010 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import io
3
+ import uuid
4
+ import json
5
+ import numpy as np
6
+ import requests
7
+ import gradio as gr
8
+ from bs4 import BeautifulSoup
9
+ from PIL import Image, ImageDraw, ImageFont
10
+ import easyocr
11
+ from deep_translator import GoogleTranslator, MyMemoryTranslator, LingueeTranslator, DeeplTranslator
12
+ from typing import List, Dict, Any, Optional, Tuple, Generator
13
+ import math # Add this import at the top
14
+
15
+ # --- Configuration ---
16
+ STATIC_DIR = "static"
17
+ TRANSLATED_IMAGE_DIR = os.path.join(STATIC_DIR, "translated")
18
+ FONT_PATH = "font/Movistar Text Regular.ttf"
19
+ USE_POLLINATIONS = True # Toggle to use Pollinations.ai for translation
20
+ # Select which free translator to use as default
21
+ DEFAULT_FREE_TRANSLATOR = "google" # Options: "google", "mymemory", "linguee"
22
+ GROUPING_VERTICAL_THRESHOLD = 25 # Pixels - Adjust as needed
23
+ GROUPING_HORIZONTAL_OVERLAP_THRESHOLD = 0.1 # Minimum % overlap to consider horizontal grouping
24
+
25
+ # --- Create necessary directories ---
26
+ os.makedirs(TRANSLATED_IMAGE_DIR, exist_ok=True)
27
+
28
+ # --- Helper Functions ---
29
+ def calculate_font_size(text: str, max_width: float, max_height: float, font_path: str = FONT_PATH) -> int:
30
+ """Calculates a suitable font size to fit text within a bounding box."""
31
+ font_size = int(max_height * 0.8) # Start with a reasonable guess
32
+ if font_size <= 0:
33
+ return 1 # Minimum font size
34
+
35
+ try:
36
+ font = ImageFont.truetype(font_path, font_size)
37
+ text_bbox = font.getbbox(text)
38
+ text_width = text_bbox[2] - text_bbox[0]
39
+ text_height = text_bbox[3] - text_bbox[1]
40
+
41
+ # Reduce font size until it fits (simplified approach)
42
+ while (text_width > max_width or text_height > max_height) and font_size > 5:
43
+ font_size -= 1
44
+ font = ImageFont.truetype(font_path, font_size)
45
+ text_bbox = font.getbbox(text)
46
+ text_width = text_bbox[2] - text_bbox[0]
47
+ text_height = text_bbox[3] - text_bbox[1]
48
+
49
+ return max(font_size, 5) # Ensure a minimum size
50
+ except IOError:
51
+ print(f"Warning: Font file not found at {font_path}. Using default PIL font.")
52
+ # Fallback logic if font file is not found
53
+ return int(max_height * 0.5) # Simplified fallback
54
+ except Exception as e:
55
+ print(f"Error calculating font size: {e}")
56
+ return int(max_height * 0.5) # Simplified fallback
57
+
58
+ def scrape_comic_images(url: str) -> List[str]:
59
+ """Scrape all comic images from the provided URL."""
60
+ headers = {
61
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3"
62
+ }
63
+ try:
64
+ response = requests.get(url, headers=headers, timeout=15)
65
+ response.raise_for_status() # Raise an exception for bad status codes
66
+
67
+ soup = BeautifulSoup(response.content, "html.parser")
68
+ images = []
69
+ image_urls = set() # Use a set to avoid duplicate URLs
70
+
71
+ # Common selectors for manhwa/manhua sites
72
+ selectors = [
73
+ ".chapter-content img",
74
+ ".comic-container img",
75
+ ".reading-content img",
76
+ "#readerarea img",
77
+ ".viewer-container img",
78
+ "img.comic-panel"
79
+ ]
80
+
81
+ for selector in selectors:
82
+ for img in soup.select(selector):
83
+ src = img.get("src") or img.get("data-src") or img.get("data-original")
84
+ if src:
85
+ # Resolve relative URLs
86
+ src = requests.compat.urljoin(url, src.strip())
87
+ if src not in image_urls:
88
+ images.append(src)
89
+ image_urls.add(src)
90
+
91
+ if not images:
92
+ # Fallback: Find all images if specific selectors fail
93
+ print("Warning: Specific selectors failed, trying to find all images.")
94
+ for img in soup.find_all("img"):
95
+ src = img.get("src") or img.get("data-src") or img.get("data-original")
96
+ if src:
97
+ src = requests.compat.urljoin(url, src.strip())
98
+ if src not in image_urls:
99
+ images.append(src)
100
+ image_urls.add(src)
101
+
102
+ if not images:
103
+ raise ValueError("Could not find any images on the page using common selectors.")
104
+
105
+ return images
106
+
107
+ except Exception as e:
108
+ print(f"Error scraping comic images: {e}")
109
+ return []
110
+
111
+ def detect_text(image_content: bytes, language: str) -> List[dict]:
112
+ """Detect text regions in the image using OCR."""
113
+ try:
114
+ image = Image.open(io.BytesIO(image_content)).convert("RGB")
115
+ image_np = np.array(image)
116
+
117
+ # Initialize OCR reader
118
+ # Use 'ch_sim' for Simplified Chinese instead of 'zh'
119
+ lang_list = [lang.strip() for lang in language.split(',')] if language != "auto" else ['ko', 'ch_sim', 'ja', 'en']
120
+ print(f"Initializing EasyOCR with languages: {lang_list}")
121
+ reader = easyocr.Reader(lang_list, gpu=False) # Specify gpu=False if no GPU or CUDA issues
122
+
123
+ # Detect text
124
+ results = reader.readtext(image_np, detail=1, paragraph=False) # Process line by line
125
+
126
+ # Process results
127
+ text_regions = []
128
+ for bbox, text, conf in results:
129
+ # bbox is [[x1,y1],[x2,y1],[x2,y2],[x1,y2]]
130
+ # Ensure bbox coordinates are standard Python numbers
131
+ bbox_float = [[float(p[0]), float(p[1])] for p in bbox]
132
+ if conf > 0.3: # Confidence threshold (adjust as needed)
133
+ text_regions.append({
134
+ "bbox": bbox_float,
135
+ "text": text,
136
+ "confidence": float(conf)
137
+ })
138
+ print(f"Detected {len(text_regions)} text regions.")
139
+ return text_regions
140
+ except Exception as e:
141
+ print(f"Error during OCR detection: {e}")
142
+ return []
143
+
144
+ def _calculate_group_bbox(regions: List[Dict]) -> List[float]:
145
+ """Calculates the encompassing bounding box for a list of regions."""
146
+ if not regions:
147
+ return [0, 0, 0, 0]
148
+
149
+ min_x = min(r['bbox'][0][0] for r in regions)
150
+ min_y = min(r['bbox'][0][1] for r in regions)
151
+ max_x = max(r['bbox'][1][0] for r in regions) # Using top-right x for max_x
152
+ max_y = max(r['bbox'][2][1] for r in regions) # Using bottom-right y for max_y
153
+
154
+ # Ensure coordinates are valid
155
+ min_x = max(0, min_x)
156
+ min_y = max(0, min_y)
157
+ # Assuming max_x and max_y will be derived from valid regions
158
+
159
+ # Return as [x1, y1, x2, y2] format for consistency
160
+ return [min_x, min_y, max_x, max_y]
161
+
162
+ # Define utility functions for rectangle operations
163
+ def rect_distance(rect1, rect2):
164
+ """Calculate the distance between two rectangles (bounding boxes)"""
165
+ # Convert from [[x1,y1],[x2,y1],[x2,y2],[x1,y2]] format to [x1,y1,x2,y2]
166
+ r1 = [rect1[0][0], rect1[0][1], rect1[2][0], rect1[2][1]]
167
+ r2 = [rect2[0][0], rect2[0][1], rect2[2][0], rect2[2][1]]
168
+
169
+ # Check for overlap
170
+ if (r1[0] <= r2[2] and r2[0] <= r1[2] and r1[1] <= r2[3] and r2[1] <= r1[3]):
171
+ return 0 # Rectangles overlap
172
+
173
+ # Calculate distances
174
+ dx = max(0, max(r1[0], r2[0]) - min(r1[2], r2[2]))
175
+ dy = max(0, max(r1[1], r2[1]) - min(r1[3], r2[3]))
176
+
177
+ # Return Euclidean distance
178
+ return math.sqrt(dx*dx + dy*dy)
179
+
180
+ def rect_center(rect):
181
+ """Calculate the center point of a rectangle"""
182
+ # Convert from [[x1,y1],[x2,y1],[x2,y2],[x1,y2]] format
183
+ x1, y1 = rect[0]
184
+ x2, y2 = rect[2]
185
+ return [(x1 + x2) / 2, (y1 + y2) / 2]
186
+
187
+ def rect_contains_point(rect, point):
188
+ """Check if a rectangle contains a point"""
189
+ # Convert from [[x1,y1],[x2,y1],[x2,y2],[x1,y2]] format
190
+ x1, y1 = rect[0]
191
+ x2, y2 = rect[2]
192
+ px, py = point
193
+ return x1 <= px <= x2 and y1 <= py <= y2
194
+
195
+ def expand_rect(rect1, rect2):
196
+ """Create a new rectangle that encompasses both input rectangles"""
197
+ # Convert from [[x1,y1],[x2,y1],[x2,y2],[x1,y2]] format
198
+ x1_1, y1_1 = rect1[0]
199
+ x2_1, y2_1 = rect1[2]
200
+ x1_2, y1_2 = rect2[0]
201
+ x2_2, y2_2 = rect2[2]
202
+
203
+ # Find the min/max coordinates
204
+ x1 = min(x1_1, x1_2)
205
+ y1 = min(y1_1, y1_2)
206
+ x2 = max(x2_1, x2_2)
207
+ y2 = max(y2_1, y2_2)
208
+
209
+ # Return in the format [[x1,y1],[x2,y1],[x2,y2],[x1,y2]]
210
+ return [[x1, y1], [x2, y1], [x2, y2], [x1, y2]]
211
+
212
+ def is_valid_rect(rect):
213
+ """Validate rectangle properties"""
214
+ # Convert from [[x1,y1],[x2,y1],[x2,y2],[x1,y2]] format
215
+ x1, y1 = rect[0]
216
+ x2, y2 = rect[2]
217
+
218
+ # Check if width and height are positive
219
+ return x2 > x1 and y2 > y1
220
+
221
+ def group_text_regions(regions: List[Dict],
222
+ proximity_threshold: float = 100.0) -> List[Dict]:
223
+ """Groups text regions based on proximity and overlap to identify speech bubbles."""
224
+ if not regions:
225
+ return []
226
+
227
+ # Extract bounding boxes from regions
228
+ bboxes = [region['bbox'] for region in regions]
229
+
230
+ # Dictionary to track which rectangles have been grouped
231
+ grouped = [False] * len(bboxes)
232
+
233
+ # List to store the grouped rectangles
234
+ grouped_boxes = []
235
+
236
+ # Group rectangles based on proximity and overlap
237
+ for i in range(len(bboxes)):
238
+ if grouped[i]:
239
+ continue # Skip if already grouped
240
+
241
+ # Start a new group with this rectangle
242
+ current_group = bboxes[i]
243
+ grouped[i] = True
244
+
245
+ # Flag to check if we made any changes in this pass
246
+ made_changes = True
247
+
248
+ # Keep expanding the group until no more changes
249
+ while made_changes:
250
+ made_changes = False
251
+
252
+ for j in range(len(bboxes)):
253
+ if grouped[j]:
254
+ continue # Skip if already grouped
255
+
256
+ # Check if this rectangle should be added to the current group
257
+ if rect_distance(current_group, bboxes[j]) < proximity_threshold:
258
+ # Expand the current group to include this rectangle
259
+ current_group = expand_rect(current_group, bboxes[j])
260
+ grouped[j] = True
261
+ made_changes = True
262
+
263
+ # Add the final group to our list if it's valid
264
+ if is_valid_rect(current_group):
265
+ grouped_boxes.append(current_group)
266
+
267
+ # Now combine text from all regions within each group
268
+ result_groups = []
269
+
270
+ for group_bbox in grouped_boxes:
271
+ # Find all regions whose center is within this group
272
+ group_regions = []
273
+ group_text = ""
274
+
275
+ for region in regions:
276
+ center = rect_center(region['bbox'])
277
+ if rect_contains_point(group_bbox, center):
278
+ group_regions.append(region)
279
+ # Add space between text fragments
280
+ if group_text:
281
+ group_text += " "
282
+ group_text += region['text']
283
+
284
+ # Create the grouped region
285
+ if group_regions:
286
+ result_groups.append({
287
+ "text": group_text,
288
+ "bbox": group_bbox,
289
+ "original_regions": group_regions,
290
+ "is_group": True
291
+ })
292
+
293
+ # Debug output
294
+ print(f"Proximity-based grouping: {len(regions)} individual regions into {len(result_groups)} speech bubbles.")
295
+
296
+ return result_groups
297
+
298
+ def translate_with_pollinations(texts: List[str], src_lang: str, target_lang: str) -> List[Dict[str, str]]:
299
+ """Translate texts using Pollinations.ai API."""
300
+ if not texts:
301
+ return []
302
+
303
+ try:
304
+ # Convert language codes to what Pollinations expects
305
+ lang_map = {
306
+ "zh": "zh-CN",
307
+ "ko": "ko",
308
+ "ja": "ja",
309
+ "en": "en",
310
+ "auto": "auto"
311
+ }
312
+
313
+ # Map our language codes to Pollinations expected codes
314
+ src_lang_mapped = lang_map.get(src_lang, src_lang)
315
+ target_lang_mapped = lang_map.get(target_lang, target_lang)
316
+
317
+ # Preparing batch of at least 10 texts for translation
318
+ batch_texts = texts.copy()
319
+ while len(batch_texts) < 10:
320
+ batch_texts.extend(texts[:min(len(texts), 10-len(batch_texts))])
321
+
322
+ # Prepare the system prompt for the translation task
323
+ system_prompt = f"You are a professional translator. Translate the following texts from {src_lang_mapped} to {target_lang_mapped}. Preserve the meaning, tone, and style of the original text. Return the results in JSON format with 'original' and 'translated' keys for each text."
324
+
325
+ # Create the user prompt with the texts to translate
326
+ user_prompt = "Translate these texts and return a JSON array with objects containing 'original' and 'translated' properties:\n"
327
+ for i, text in enumerate(batch_texts):
328
+ user_prompt += f"{i+1}. {text}\n"
329
+
330
+ # Prepare the API request to Pollinations.ai
331
+ api_url = "https://api.pollinations.ai/v2/generate/text"
332
+ headers = {
333
+ "Content-Type": "application/json"
334
+ }
335
+
336
+ payload = {
337
+ "model": "openai", # Using OpenAI model as it's good for translation
338
+ "prompt": user_prompt,
339
+ "system": system_prompt,
340
+ "jsonMode": True, # Request JSON output
341
+ "reasoning_effort": "high", # Higher quality translations
342
+ "private": True,
343
+ "referrer": "manga_ocr_translator"
344
+ }
345
+
346
+ print(f"Sending batch of {len(batch_texts)} texts to Pollinations.ai for translation")
347
+ response = requests.post(api_url, headers=headers, json=payload, timeout=60)
348
+ response.raise_for_status()
349
+
350
+ # Parse the response
351
+ result = response.json()
352
+ translated_text = result.get("response", "")
353
+
354
+ # The response should be a JSON string that we need to parse
355
+ try:
356
+ translated_data = json.loads(translated_text)
357
+
358
+ # Map the translation results back to the original texts
359
+ # Create a mapping of original text to its translation
360
+ translation_map = {}
361
+ for item in translated_data:
362
+ if isinstance(item, dict) and "original" in item and "translated" in item:
363
+ translation_map[item["original"]] = item["translated"]
364
+
365
+ # Apply translations to our original texts list
366
+ translated_results = []
367
+ for text in texts:
368
+ translated_results.append({
369
+ "original": text,
370
+ "translated": translation_map.get(text, text) # Default to original if not found
371
+ })
372
+ print(f"Pollinations translation: '{text}' -> '{translation_map.get(text, text)}'")
373
+
374
+ return translated_results
375
+
376
+ except json.JSONDecodeError as e:
377
+ print(f"Error parsing translation response as JSON: {e}")
378
+ print(f"Raw response: {translated_text}")
379
+ # Fallback: Return original texts
380
+ return [{"original": text, "translated": text} for text in texts]
381
+
382
+ except Exception as e:
383
+ print(f"Error with Pollinations.ai translation: {e}")
384
+ # Return original texts as fallback
385
+ return [{"original": text, "translated": text} for text in texts]
386
+
387
+ def translate_with_free_translator(texts: List[str], src_lang: str, target_lang: str,
388
+ translator_type: str = DEFAULT_FREE_TRANSLATOR) -> List[Dict[str, str]]:
389
+ """Translate texts using available free translation APIs."""
390
+ if not texts:
391
+ return []
392
+
393
+ # Debug info
394
+ print(f"Translating {len(texts)} texts using {translator_type} translator")
395
+ print(f"Source language: {src_lang}, Target language: {target_lang}")
396
+
397
+ # Standardize language codes for different services
398
+ lang_map = {
399
+ # ISO-639 language code mapping for various services
400
+ "auto": "auto",
401
+ "en": "en",
402
+ "zh": "zh-CN",
403
+ "ja": "ja",
404
+ "ko": "ko",
405
+ "es": "es",
406
+ "fr": "fr",
407
+ "de": "de",
408
+ "it": "it",
409
+ "pt": "pt",
410
+ "ru": "ru"
411
+ }
412
+
413
+ # Map to standardized language codes if available, otherwise use as-is
414
+ std_src_lang = lang_map.get(src_lang, src_lang)
415
+ std_target_lang = lang_map.get(target_lang, target_lang)
416
+
417
+ translated_results = []
418
+
419
+ try:
420
+ # Select translator based on specified type
421
+ if translator_type == "google":
422
+ # Google Translate (free tier without API key)
423
+ translator = GoogleTranslator(source=std_src_lang if std_src_lang != "auto" else "auto",
424
+ target=std_target_lang)
425
+
426
+ for text in texts:
427
+ if not text or len(text.strip()) < 2:
428
+ translated_results.append({"original": text, "translated": text})
429
+ continue
430
+
431
+ try:
432
+ translated = translator.translate(text)
433
+ translated_results.append({
434
+ "original": text,
435
+ "translated": translated or text # Fallback to original if None
436
+ })
437
+ print(f"Translated: '{text}' -> '{translated}'")
438
+ except Exception as e:
439
+ print(f"Error translating text '{text}': {e}")
440
+ translated_results.append({"original": text, "translated": text})
441
+
442
+ elif translator_type == "mymemory":
443
+ # MyMemory (free with limits)
444
+ translator = MyMemoryTranslator(source=std_src_lang if std_src_lang != "auto" else "auto",
445
+ target=std_target_lang)
446
+
447
+ for text in texts:
448
+ if not text or len(text.strip()) < 2:
449
+ translated_results.append({"original": text, "translated": text})
450
+ continue
451
+
452
+ try:
453
+ translated = translator.translate(text)
454
+ translated_results.append({
455
+ "original": text,
456
+ "translated": translated or text
457
+ })
458
+ print(f"Translated: '{text}' -> '{translated}'")
459
+ except Exception as e:
460
+ print(f"Error translating text '{text}': {e}")
461
+ translated_results.append({"original": text, "translated": text})
462
+
463
+ elif translator_type == "linguee":
464
+ # Linguee (free)
465
+ # Note: Linguee has limited language support
466
+ try:
467
+ translator = LingueeTranslator(source=std_src_lang, target=std_target_lang)
468
+
469
+ for text in texts:
470
+ if not text or len(text.strip()) < 2:
471
+ translated_results.append({"original": text, "translated": text})
472
+ continue
473
+
474
+ try:
475
+ translated = translator.translate(text)
476
+ translated_results.append({
477
+ "original": text,
478
+ "translated": translated or text
479
+ })
480
+ print(f"Translated: '{text}' -> '{translated}'")
481
+ except Exception as e:
482
+ print(f"Error translating text '{text}': {e}")
483
+ translated_results.append({"original": text, "translated": text})
484
+ except Exception as e:
485
+ print(f"Linguee translator error: {e}. Falling back to Google Translate.")
486
+ # Fallback to Google Translate
487
+ return translate_with_free_translator(texts, src_lang, target_lang, "google")
488
+
489
+ else:
490
+ # Default fallback to Google
491
+ print(f"Unknown translator type '{translator_type}', using Google Translate as fallback")
492
+ return translate_with_free_translator(texts, src_lang, target_lang, "google")
493
+
494
+ except Exception as e:
495
+ print(f"Error setting up translator: {e}")
496
+ # Return original texts if translation fails
497
+ for text in texts:
498
+ translated_results.append({"original": text, "translated": text})
499
+
500
+ return translated_results
501
+
502
+ def translate_grouped_regions(grouped_regions: List[Dict], src_lang: str, target_lang: str, use_pollinations: bool = False,
503
+ free_translator: str = DEFAULT_FREE_TRANSLATOR) -> List[Dict]:
504
+ """Translate text within grouped regions."""
505
+ if not grouped_regions:
506
+ return []
507
+
508
+ # Add translated_text to all regions with original text as a fallback
509
+ for region in grouped_regions:
510
+ region["translated_text"] = region["text"] # Default fallback for the group
511
+
512
+ # Extract all texts (already grouped) for translation
513
+ texts_to_translate = [region["text"] for region in grouped_regions if region["text"] and len(region["text"].strip()) >= 2]
514
+
515
+ if not texts_to_translate:
516
+ print("No valid grouped texts to translate")
517
+ return grouped_regions # Return groups with original text as fallback
518
+
519
+ try:
520
+ print(f"Translating {len(texts_to_translate)} grouped texts from '{src_lang}' to '{target_lang}'...")
521
+
522
+ translation_results = []
523
+ # Use Pollinations.ai for translation if enabled
524
+ if use_pollinations and USE_POLLINATIONS:
525
+ print("Using Pollinations.ai for translation")
526
+ translation_results = translate_with_pollinations(texts_to_translate, src_lang, target_lang)
527
+
528
+ # Otherwise, use selected free translator
529
+ else:
530
+ print(f"Using free translator: {free_translator}")
531
+ translation_results = translate_with_free_translator(
532
+ texts_to_translate,
533
+ src_lang,
534
+ target_lang,
535
+ free_translator
536
+ )
537
+
538
+ # Create a dictionary mapping original grouped text to translated text
539
+ # Ensure the results match the input order
540
+ translations_dict = {item["original"]: item["translated"] for item in translation_results}
541
+
542
+ # Apply translations back to the grouped regions
543
+ for region in grouped_regions:
544
+ original_text = region["text"]
545
+ if original_text in translations_dict:
546
+ region["translated_text"] = translations_dict[original_text]
547
+ print(f" Applied translation to group: '{original_text}' -> '{region['translated_text']}'")
548
+ else:
549
+ print(f" Warning: Translation not found for group text: '{original_text}'") # Should not happen if results map correctly
550
+
551
+ return grouped_regions
552
+
553
+ except Exception as e:
554
+ print(f"Error during grouped translation setup: {e}")
555
+ # Fallback already handled by setting original text
556
+ return grouped_regions
557
+
558
+ def overlay_grouped_text(image_content: bytes, translated_grouped_regions: List[dict]) -> Image.Image:
559
+ """Overlay translated grouped text onto the original image and return the PIL Image."""
560
+ try:
561
+ image = Image.open(io.BytesIO(image_content)).convert("RGBA")
562
+ draw = ImageDraw.Draw(image)
563
+
564
+ # Sort regions by area (smallest to largest) to ensure smaller bubbles are processed later
565
+ # This helps with overlapping bubbles, as smaller ones often appear on top
566
+ sorted_regions = sorted(translated_grouped_regions,
567
+ key=lambda r: (r.get("bbox")[2][0] - r.get("bbox")[0][0]) *
568
+ (r.get("bbox")[2][1] - r.get("bbox")[0][1])
569
+ if r.get("bbox") else 0)
570
+
571
+ for group in sorted_regions:
572
+ if "translated_text" not in group or not group.get("is_group", False):
573
+ print("Skipping non-group or untranslated region in overlay.")
574
+ continue
575
+
576
+ group_bbox_corners = group["bbox"] # This is the combined bbox for the group
577
+ translated_text = group["translated_text"]
578
+
579
+ # Extract combined bounding box coordinates [x1, y1, x2, y2]
580
+ x1, y1 = group_bbox_corners[0] # Top-left
581
+ x2, y2 = group_bbox_corners[2] # Bottom-right
582
+
583
+ # Basic validation
584
+ if x1 >= x2 or y1 >= y2:
585
+ print(f"Warning: Degenerate group bbox found: {group_bbox_corners}. Skipping group.")
586
+ continue
587
+
588
+ width, height = x2 - x1, y2 - y1
589
+ if width <= 0 or height <= 0:
590
+ print(f"Warning: Non-positive dimensions for group bbox: {group_bbox_corners}. Skipping group.")
591
+ continue
592
+
593
+ # --- Background Clearing ---
594
+ # Apply a more generous padding to ensure no text from other bubbles bleeds in
595
+ padding = max(10, int(min(width, height) * 0.1)) # Increased padding for better erasure
596
+
597
+ # For more complete text removal, we'll clear both the group bounding box and each original region
598
+
599
+ # 1. First clear the entire group bounding box with padding
600
+ for px in range(int(x1 - padding), int(x2 + padding + 1)):
601
+ for py in range(int(y1 - padding), int(y2 + padding + 1)):
602
+ if 0 <= px < image.width and 0 <= py < image.height:
603
+ image.putpixel((px, py), (255, 255, 255, 255)) # White background
604
+
605
+ # 2. For more thorough clearing, also clear each original region with its own padding
606
+ # This helps ensure we catch text that might be outside the main group bbox
607
+ if "original_regions" in group:
608
+ for orig_region in group["original_regions"]:
609
+ orig_bbox = orig_region["bbox"]
610
+ orig_x1, orig_y1 = orig_bbox[0]
611
+ orig_x2, orig_y2 = orig_bbox[2]
612
+ # Add extra padding specifically for original regions
613
+ region_padding = max(8, int(min(orig_x2 - orig_x1, orig_y2 - orig_y1) * 0.15))
614
+
615
+ # Clear each original region with its own padding
616
+ for px in range(int(orig_x1 - region_padding), int(orig_x2 + region_padding + 1)):
617
+ for py in range(int(orig_y1 - region_padding), int(orig_y2 + region_padding + 1)):
618
+ if 0 <= px < image.width and 0 <= py < image.height:
619
+ image.putpixel((px, py), (255, 255, 255, 255)) # White background
620
+
621
+ print(f"Cleared background for text region and {len(group.get('original_regions', []))} original regions")
622
+
623
+ # --- Font Calculation with Wrapping Logic ---
624
+ # Get an initial font size estimate
625
+ initial_font_size = calculate_font_size(translated_text, width, height, FONT_PATH)
626
+ try:
627
+ font = ImageFont.truetype(FONT_PATH, initial_font_size)
628
+ except Exception as e:
629
+ print(f"Error loading font size {initial_font_size}: {e}. Using default.")
630
+ try:
631
+ font = ImageFont.load_default()
632
+ except Exception as font_e:
633
+ print(f"Error loading default font: {font_e}. Cannot draw text.")
634
+ continue
635
+
636
+ # Calculate effective drawing area (with reduced width for better aesthetics)
637
+ effective_width = width * 0.9 # Reduce slightly to avoid text touching edges
638
+ effective_height = height * 0.9
639
+
640
+ # Determine if text needs wrapping
641
+ text_lines = []
642
+ words = translated_text.split()
643
+ current_line = words[0] if words else ""
644
+
645
+ # Simple word wrapping algorithm
646
+ for word in words[1:]:
647
+ test_line = current_line + " " + word
648
+ # Use getbbox for more accurate width calculation during wrapping check
649
+ line_bbox_wrap = font.getbbox(test_line)
650
+ line_width_wrap = line_bbox_wrap[2] - line_bbox_wrap[0]
651
+
652
+ if line_width_wrap <= effective_width:
653
+ current_line = test_line
654
+ else:
655
+ text_lines.append(current_line)
656
+ current_line = word
657
+
658
+ # Add the last line
659
+ if current_line:
660
+ text_lines.append(current_line)
661
+
662
+ # If no lines were created (empty text), skip
663
+ if not text_lines:
664
+ continue
665
+
666
+ # --- Font Calculation & Line Height (Robust Spacing) ---
667
+ # Use getbbox for line height calculation based on a reference string
668
+ line_bbox_ref = font.getbbox("Tg")
669
+ line_height_metric = line_bbox_ref[3] - line_bbox_ref[1] # Height of the bbox
670
+ # Increase spacing significantly - force separation
671
+ line_spacing_factor = 2.0
672
+ line_height = line_height_metric * line_spacing_factor
673
+ print(f"Using bbox height for metric: {line_height_metric:.2f}, Aggressive Line Height ({line_spacing_factor}x): {line_height:.2f}")
674
+
675
+ # Approximate total height for resizing check
676
+ total_text_height_check = line_height * len(text_lines)
677
+
678
+ # If wrapped text is too tall, recalculate font size
679
+ if total_text_height_check > effective_height:
680
+ print(f"Resizing font: Estimated wrapped height ({total_text_height_check:.1f}) > effective height ({effective_height:.1f})")
681
+ scale_factor = effective_height / total_text_height_check
682
+ new_font_size = max(6, int(initial_font_size * scale_factor)) # Min size 6pt
683
+ print(f"Original font size: {initial_font_size}, New font size: {new_font_size}")
684
+ try:
685
+ font = ImageFont.truetype(FONT_PATH, new_font_size)
686
+ # Recalculate line height metric and line height with new font
687
+ line_bbox_ref = font.getbbox("Tg")
688
+ line_height_metric = line_bbox_ref[3] - line_bbox_ref[1]
689
+ line_height = line_height_metric * line_spacing_factor # Apply same spacing factor
690
+ print(f"Recalculated Aggressive Line Height after resize: {line_height:.2f}")
691
+ except Exception as e:
692
+ print(f"Error loading adjusted font: {e}")
693
+
694
+ # Final font decided. Get its metrics if needed elsewhere, but height is set.
695
+ print(f"Final line height for drawing: {line_height:.2f}")
696
+
697
+ # --- Draw Text (Robust Top-Left Stacking) ---
698
+ try:
699
+ # Calculate vertical starting position for the *top* of the first line
700
+ total_drawn_height = line_height * len(text_lines) # Total height including full spacing for all lines
701
+ start_y_top = y1 + (height - total_drawn_height) / 2
702
+ print(f"Drawing text block: Total Height={total_drawn_height:.1f}, Start Top Y={start_y_top:.1f}")
703
+
704
+ # Draw each line using top-left anchor and explicit vertical step
705
+ for i, line in enumerate(text_lines):
706
+ # Use getlength for precise width if possible
707
+ try:
708
+ line_width = font.getlength(line)
709
+ except AttributeError:
710
+ line_bbox_draw = font.getbbox(line, anchor="lt") # Use top-left anchor for bbox width
711
+ line_width = line_bbox_draw[2] - line_bbox_draw[0]
712
+
713
+ draw_x = x1 + (width - line_width) / 2
714
+ # Position the *top* of the current line
715
+ draw_y_top = start_y_top + (i * line_height)
716
+
717
+ print(f" Drawing line {i+1}/{len(text_lines)}: '{line}' at Top-Left ({draw_x:.1f}, {draw_y_top:.1f}) Width={line_width:.1f}")
718
+
719
+ # Basic bounds check for top-left corner
720
+ draw_x = max(padding, min(image.width - padding - line_width, draw_x))
721
+ draw_y_top = max(padding, min(image.height - padding - line_height_metric, draw_y_top)) # Check against metric height
722
+
723
+ # Draw using Pillow's stroke feature with top-left anchor
724
+ stroke_width = max(1, int(initial_font_size * 0.08))
725
+ draw.text(
726
+ (draw_x, draw_y_top),
727
+ line,
728
+ font=font,
729
+ fill="black",
730
+ anchor="lt", # Use top-left anchor
731
+ stroke_width=stroke_width,
732
+ stroke_fill="white"
733
+ )
734
+
735
+ print(f"Drew wrapped text ({len(text_lines)} lines) in bbox [{x1:.0f},{y1:.0f} - {x2:.0f},{y2:.0f}]")
736
+
737
+ except Exception as draw_e:
738
+ print(f"Error drawing text: {draw_e}")
739
+
740
+ # Debug statement to confirm processing is complete
741
+ print(f"Overlay complete. Processed {len(sorted_regions)} regions.")
742
+ return image
743
+
744
+ except Exception as e:
745
+ print(f"Error during image overlay: {e}")
746
+ import traceback
747
+ traceback.print_exc()
748
+ # Return original image in case of error
749
+ return Image.open(io.BytesIO(image_content))
750
+
751
+ # --- Gradio Functions ---
752
+
753
+ def translate_manga_page(url: str, source_language: str, target_language: str,
754
+ use_pollinations: bool = False, free_translator: str = DEFAULT_FREE_TRANSLATOR) -> Generator[Tuple[Optional[str], Optional[Image.Image]], None, None]:
755
+ """Process manga URL, yielding status and translated images one by one."""
756
+ processed_count = 0
757
+ total_images = 0
758
+ try:
759
+ # Scrape image URLs
760
+ print(f"Scraping images from: {url}")
761
+ yield f"Scraping images from URL...", None
762
+ image_urls = scrape_comic_images(url)
763
+ total_images = len(image_urls)
764
+ if not image_urls:
765
+ print("No images found at the URL.")
766
+ yield "No images found at the URL.", None
767
+ return
768
+
769
+ yield f"Found {total_images} images. Starting processing...", None
770
+ print(f"Found {total_images} images.")
771
+
772
+ if len(image_urls) > 5:
773
+ print("Limiting to the first 5 images for translation.")
774
+ image_urls_to_process = image_urls[0:5]
775
+ else:
776
+ image_urls_to_process = image_urls
777
+ for i, image_url in enumerate(image_urls_to_process):
778
+ current_status = f"Processing image {i+1}/{len(image_urls_to_process)}: {os.path.basename(image_url)}"
779
+ print(current_status)
780
+ yield current_status, None # Yield status update
781
+
782
+ try:
783
+ img_response = requests.get(image_url, timeout=15)
784
+ img_response.raise_for_status()
785
+ image_content = img_response.content
786
+
787
+ # 1. Detect text regions
788
+ text_regions = detect_text(image_content, source_language)
789
+
790
+ if not text_regions:
791
+ print("No text detected in this image.")
792
+ continue # Skip this image if no text found
793
+
794
+ # 2. Group detected regions
795
+ grouped_regions = group_text_regions(text_regions)
796
+ if not grouped_regions:
797
+ print("No groups formed from detected text.")
798
+ continue # Skip if grouping fails
799
+
800
+ # 3. Translate grouped text
801
+ translated_grouped_regions = translate_grouped_regions(
802
+ grouped_regions,
803
+ source_language,
804
+ target_language,
805
+ use_pollinations,
806
+ free_translator
807
+ )
808
+
809
+ # 4. Overlay translated grouped text
810
+ translated_image = overlay_grouped_text(image_content, translated_grouped_regions)
811
+
812
+ # 5. Yield the translated image
813
+ yield None, translated_image
814
+ processed_count += 1
815
+ print(f"Successfully processed and yielded image {i+1}.")
816
+
817
+ except requests.exceptions.RequestException as req_e:
818
+ print(f"Failed to download image {image_url}: {req_e}")
819
+ yield f"Failed to download image {i+1}", None
820
+ except Exception as e:
821
+ print(f"Error processing image {image_url}: {e}")
822
+ yield f"Error processing image {i+1}", None
823
+ continue
824
+
825
+ yield f"Finished processing. Successfully translated {processed_count}/{len(image_urls_to_process)} images.", None
826
+
827
+ except Exception as e:
828
+ print(f"Error in translate_manga_page: {e}")
829
+ yield f"An error occurred during the process: {str(e)}", None
830
+ finally:
831
+ print(f"Translation process ended. Processed {processed_count} images.")
832
+
833
+ def batch_translate_text(texts: str, source_language: str, target_language: str) -> str:
834
+ """Translate batch of texts using Pollinations.ai."""
835
+ # Split text into lines and filter out empty ones
836
+ text_list = [line.strip() for line in texts.split('\n') if line.strip()]
837
+
838
+ if not text_list:
839
+ return "No text provided for translation."
840
+
841
+ try:
842
+ # Translate the texts
843
+ translated_results = translate_with_pollinations(text_list, source_language, target_language)
844
+
845
+ # Format results
846
+ output = "Translation Results:\n\n"
847
+ for item in translated_results:
848
+ output += f"Original: {item['original']}\nTranslated: {item['translated']}\n\n"
849
+
850
+ return output
851
+
852
+ except Exception as e:
853
+ print(f"Error in batch_translate_text: {e}")
854
+ return f"An error occurred during translation: {str(e)}"
855
+
856
+ # --- Gradio Interface ---
857
+
858
+ # CSS for vertical scrolling in the image column
859
+ css = """
860
+ #image-column {
861
+ max-height: 80vh; /* Adjust height as needed */
862
+ overflow-y: auto;
863
+ }
864
+ """
865
+
866
+ with gr.Blocks(title="Manga Translator", theme=gr.themes.Soft(), css=css) as app:
867
+ gr.Markdown("# Manga Translator with AI 🌐")
868
+
869
+ with gr.Tab("Translate Manga Pages"):
870
+ with gr.Row():
871
+ with gr.Column():
872
+ url_input = gr.Textbox(label="Manga URL", placeholder="Enter manga chapter URL")
873
+ source_language = gr.Dropdown(
874
+ choices=["auto", "ja", "ko", "zh"],
875
+ value="auto",
876
+ label="Source Language"
877
+ )
878
+ target_language = gr.Dropdown(
879
+ choices=["en", "es", "fr", "de", "it", "pt", "ru"],
880
+ value="en",
881
+ label="Target Language"
882
+ )
883
+ translator_option = gr.Radio(
884
+ choices=["pollinations", "google", "mymemory", "linguee"],
885
+ value="pollinations",
886
+ label="Translation Engine"
887
+ )
888
+ translate_button = gr.Button("Translate Manga")
889
+
890
+ with gr.Row():
891
+ # Use Textbox for status updates
892
+ translation_status = gr.Textbox(label="Status", value="Idle", interactive=False)
893
+
894
+ # Create an output gallery instead of a column for better image display
895
+ output_gallery = gr.Gallery(
896
+ label="Translation Results",
897
+ columns=1,
898
+ rows=None,
899
+ object_fit="contain",
900
+ height="auto",
901
+ preview=True
902
+ )
903
+
904
+ with gr.Tab("Direct Text Translation"):
905
+ with gr.Row():
906
+ with gr.Column():
907
+ batch_text = gr.Textbox(
908
+ label="Enter Text (one per line)",
909
+ placeholder="Enter text to translate (one segment per line)",
910
+ lines=10
911
+ )
912
+ with gr.Row():
913
+ with gr.Column():
914
+ batch_source_language = gr.Dropdown(
915
+ choices=["auto", "ja", "ko", "zh", "en"],
916
+ value="auto",
917
+ label="Source Language"
918
+ )
919
+ with gr.Column():
920
+ batch_target_language = gr.Dropdown(
921
+ choices=["en", "es", "fr", "de", "it", "pt", "ru", "ja", "ko", "zh"],
922
+ value="en",
923
+ label="Target Language"
924
+ )
925
+ batch_translator_option = gr.Radio(
926
+ choices=["pollinations", "google", "mymemory", "linguee"],
927
+ value="pollinations",
928
+ label="Translation Engine"
929
+ )
930
+ batch_translate_btn = gr.Button("Translate Text")
931
+
932
+ batch_result = gr.Textbox(
933
+ label="Translation Results",
934
+ lines=15,
935
+ interactive=False
936
+ )
937
+
938
+ # Event handlers
939
+ def process_manga_stream(url, src_lang, tgt_lang, translator):
940
+ """Handles the streaming process for manga translation, yielding images to a gallery."""
941
+ if not url or url.strip() == "":
942
+ yield "Please enter a valid URL", []
943
+ return
944
+
945
+ # Determine which translator to use
946
+ use_pollinations = (translator == "pollinations")
947
+ free_translator = translator if not use_pollinations else DEFAULT_FREE_TRANSLATOR
948
+
949
+ translated_images = [] # Use a list to accumulate images
950
+ final_status = "Process started..."
951
+
952
+ # Iterate through the generator
953
+ for status_update, image_result in translate_manga_page(url, src_lang, tgt_lang, use_pollinations, free_translator):
954
+
955
+ if status_update:
956
+ final_status = status_update # Update status message
957
+
958
+ if image_result:
959
+ # Convert PIL image to NumPy array
960
+ if image_result.mode == 'RGBA':
961
+ image_result = image_result.convert('RGB')
962
+ numpy_image = np.array(image_result)
963
+ translated_images.append(numpy_image)
964
+
965
+ # Debug print - helps track if images are being processed
966
+ print(f"Added new image to gallery. Total images: {len(translated_images)}")
967
+
968
+ # Yield the current status and updated list of images
969
+ yield final_status, translated_images
970
+
971
+ # Final yield after the loop finishes
972
+ yield final_status, translated_images
973
+
974
+
975
+ def translate_batch_text(texts, src_lang, tgt_lang, translator):
976
+ """Translate batch of texts using selected translation engine."""
977
+ # ... (batch translation logic remains the same) ...
978
+ text_list = [line.strip() for line in texts.split('\n') if line.strip()]
979
+ if not text_list: return "No text provided for translation."
980
+ try:
981
+ if translator == "pollinations":
982
+ results = translate_with_pollinations(text_list, src_lang, tgt_lang)
983
+ else:
984
+ results = translate_with_free_translator(text_list, src_lang, tgt_lang, translator)
985
+ output = f"Translation Results (using {translator}):\n\n"
986
+ for item in results:
987
+ output += f"Original: {item['original']}\nTranslated: {item['translated']}\n\n"
988
+ return output
989
+ except Exception as e:
990
+ print(f"Error in batch_translate_text: {e}")
991
+ return f"An error occurred: {str(e)}"
992
+
993
+ translate_button.click(
994
+ process_manga_stream,
995
+ inputs=[url_input, source_language, target_language, translator_option],
996
+ outputs=[translation_status, output_gallery]
997
+ )
998
+
999
+ batch_translate_btn.click(
1000
+ translate_batch_text,
1001
+ inputs=[batch_text, batch_source_language, batch_target_language, batch_translator_option],
1002
+ outputs=batch_result
1003
+ )
1004
+
1005
+ # Launch the app
1006
+ if __name__ == "__main__":
1007
+ print("Starting Gradio app for Manga Translation...")
1008
+ print(f"Default free translator: {DEFAULT_FREE_TRANSLATOR}")
1009
+ app.launch(share=False)
1010
+ # Use app.launch(share=True) if you want to create a shareable link