From dd1cf54f86d6b9adf19268cab60beabee7dfca2c Mon Sep 17 00:00:00 2001 From: Guillem Hernandez Sola Date: Wed, 15 Apr 2026 21:12:41 +0200 Subject: [PATCH] First beta --- README.md | 53 +++ fonts/ComicNeue-Bold.ttf | Bin 0 -> 53376 bytes fonts/ComicRelief-Bold.ttf | Bin 0 -> 93304 bytes manga-renderer.py | 801 ++++++++++++++++--------------------- manga-translator.py | 679 +++++++++++-------------------- requirements | 79 ++++ requirements.txt | 29 +- 7 files changed, 736 insertions(+), 905 deletions(-) create mode 100755 fonts/ComicNeue-Bold.ttf create mode 100755 fonts/ComicRelief-Bold.ttf create mode 100644 requirements diff --git a/README.md b/README.md index e69de29..61328c9 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,53 @@ +# Manga Translator OCR Pipeline + +A robust manga/comic OCR + translation pipeline with: + +- EasyOCR (default, reliable on macOS M1) +- Optional PaddleOCR (auto-fallback if unavailable) +- Bubble clustering and line-level boxes +- Robust reread pass (multi-preprocessing + slight rotation) +- Translation export + debug overlays + +--- + +## โœจ Features + +- OCR from raw manga pages +- Noise filtering (`BOX` debug artifacts, tiny garbage tokens, symbols) +- Speech bubble grouping +- Reading order estimation (`ltr` / `rtl`) +- Translation output (`output.txt`) +- Structured bubble metadata (`bubbles.json`) +- Visual debug output (`debug_clusters.png`) + +--- + +## ๐Ÿงฐ Requirements + +- macOS (Apple Silicon supported) +- Python **3.11** recommended +- Homebrew (for Python install) + +--- + +## ๐Ÿš€ Setup (Python 3.11 venv) + +```bash +cd /path/to/manga-translator + +# 1) Create venv with 3.11 +/opt/homebrew/bin/python3.11 -m venv venv + +# 2) Activate +source venv/bin/activate + +# 3) Verify interpreter +python -V +# expected: Python 3.11.x + +# 4) Install dependencies +python -m pip install --upgrade pip setuptools wheel +python -m pip install -r requirements.txt + +# Optional Paddle runtime +python -m pip install paddlepaddle || true diff --git a/fonts/ComicNeue-Bold.ttf b/fonts/ComicNeue-Bold.ttf new file mode 100755 index 0000000000000000000000000000000000000000..91d871e1ee7bee7e5b7525e46d4d0f6d2a3e329e GIT binary patch literal 53376 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 Optional[ImageFont.FreeTypeFont]: + """Try every face index in a .ttc collection. Validate with getbbox.""" + indices = range(4) if path.lower().endswith(".ttc") else [0] + for idx in indices: + try: + font = ImageFont.truetype(path, size, index=idx) + font.getbbox("A") # raises if face metrics are broken + return font + except Exception: + continue + return None + + +def resolve_font() -> Tuple[str, ImageFont.FreeTypeFont]: + """Return (path, font) for the first working candidate.""" + for candidate in FONT_CANDIDATES: + font = load_font(candidate, FONT_SIZE) + if font is not None: + print(f" โœ… Font: {candidate}") + return candidate, font + print(" โš ๏ธ No TrueType font found โ€” using Pillow bitmap fallback") + return "", ImageFont.load_default() + + +# ============================================================ +# PARSE output.txt โ†’ {bid: translated_string} +# ============================================================ +def parse_translations(filepath: str) -> Dict[int, str]: + """ + Reads output.txt and returns {bubble_id: translated_text}. + Lines look like: #2|1|vision-base|ORIGINAL|TRANSLATED|FLAGS + """ translations = {} - originals = {} - flags_map = {} - - with open(translations_file, "r", encoding="utf-8") as f: + with open(filepath, "r", encoding="utf-8") as f: for line in f: line = line.strip() if not line.startswith("#"): continue - parts = line.split("|") + if len(parts) < 5: + continue try: - bubble_id = int(parts[0].lstrip("#")) - except Exception: + bid = int(parts[0].lstrip("#")) + translated = parts[4].strip() + if translated and translated != "-": + translations[bid] = translated + except ValueError: continue - - if len(parts) >= 5: - original = parts[2].strip() - translated = parts[3].strip() - flags = parts[4].strip() - elif len(parts) >= 4: - original = parts[2].strip() - translated = parts[3].strip() - flags = "-" - elif len(parts) >= 3: - original = "" - translated = parts[2].strip() - flags = "-" - else: - continue - - if translated.startswith("["): - continue - - translations[bubble_id] = translated - originals[bubble_id] = original - flags_map[bubble_id] = flags - - return translations, originals, flags_map + return translations -def parse_bubbles(bubbles_file): - with open(bubbles_file, "r", encoding="utf-8") as f: - raw = json.load(f) - return {int(k): v for k, v in raw.items()} - - -# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -# HELPERS -# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -def clamp(v, lo, hi): - return max(lo, min(hi, v)) - - -def xywh_to_xyxy(box): - if not box: - return None - x = int(box.get("x", 0)) - y = int(box.get("y", 0)) - w = int(box.get("w", 0)) - h = int(box.get("h", 0)) - return (x, y, x + w, y + h) - - -def union_xyxy(boxes): - boxes = [b for b in boxes if b is not None] - if not boxes: - return None - x1 = min(b[0] for b in boxes) - y1 = min(b[1] for b in boxes) - x2 = max(b[2] for b in boxes) - y2 = max(b[3] for b in boxes) - if x2 <= x1 or y2 <= y1: - return None - return (x1, y1, x2, y2) - - -def bbox_from_mask(mask): - ys, xs = np.where(mask > 0) - if len(xs) == 0: - return None - return (int(xs.min()), int(ys.min()), int(xs.max()) + 1, int(ys.max()) + 1) - - -def normalize_text(s): - t = s.upper().strip() - t = re.sub(r"[^\w]+", "", t) - return t - - -def is_sfx_like(text): - t = normalize_text(text) - return bool(len(t) <= 8 and re.fullmatch(r"(SHA+|BIP+|BEEP+|HN+|AH+|OH+)", t)) - - -# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -# FONT -# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -def load_font_from_candidates(candidates, size): - for path in candidates: - if path and os.path.exists(path): - try: - return ImageFont.truetype(path, size), path - except Exception: - continue - return ImageFont.load_default(), "PIL_DEFAULT" - - -def measure_text(draw, text, font): - bb = draw.textbbox((0, 0), text, font=font) - return bb[2] - bb[0], bb[3] - bb[1] - - -def wrap_text(draw, text, font, max_width): - words = text.split() - lines = [] - cur = "" - - for w in words: - test = (cur + " " + w).strip() - tw, _ = measure_text(draw, test, font) - if tw <= max_width or not cur: - cur = test - else: - lines.append(cur) - cur = w - if cur: - lines.append(cur) - - if not lines: - return [""], 0, 0 - - widths = [] - heights = [] - for ln in lines: - lw, lh = measure_text(draw, ln, font) - widths.append(lw) - heights.append(lh) - - gap = max(2, heights[0] // 5) - total_h = sum(heights) + gap * (len(lines) - 1) - return lines, total_h, max(widths) - - -def fit_font(draw, text, font_candidates, safe_w, safe_h): - for size in range(MAX_FONT_SIZE, MIN_FONT_SIZE - 1, -1): - font, _ = load_font_from_candidates(font_candidates, size) - lines, total_h, max_w = wrap_text(draw, text, font, safe_w) - if total_h <= safe_h and max_w <= safe_w: - return font, lines, total_h - - font, _ = load_font_from_candidates(font_candidates, MIN_FONT_SIZE) - lines, total_h, _ = wrap_text(draw, text, font, safe_w) - return font, lines, total_h - - -def draw_text_with_stroke(draw, pos, text, font, fill, stroke_fill): - x, y = pos - _, h = measure_text(draw, text, font) - sw = 2 if h <= 11 else 1 - - for dx in range(-sw, sw + 1): - for dy in range(-sw, sw + 1): - if dx == 0 and dy == 0: - continue - draw.text((x + dx, y + dy), text, font=font, fill=stroke_fill) - - draw.text((x, y), text, font=font, fill=fill) - - -# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -# MASK BUILDERS -# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -def build_yellow_mask(bubble_data, img_h, img_w): +# ============================================================ +# PARSE bubbles.json โ†’ bubble_boxes, quads_per_bubble +# ============================================================ +def parse_bubbles(filepath: str): """ - HARD GUARANTEE: - Returned mask always covers all yellow squares (line_bboxes). + Returns: + bubble_boxes : {bid: (x1, y1, x2, y2)} + quads_per_bubble : {bid: [ [[x,y],[x,y],[x,y],[x,y]], ... ]} """ - mask = np.zeros((img_h, img_w), dtype=np.uint8) + with open(filepath, "r", encoding="utf-8") as f: + data = json.load(f) - # Preferred: exact line boxes - line_boxes = bubble_data.get("line_bboxes", []) - for lb in line_boxes: - b = xywh_to_xyxy(lb) - if not b: + bubble_boxes = {} + quads_per_bubble = {} + + for key, val in data.items(): + bid = int(key) + + x1 = val["x"]; y1 = val["y"] + x2 = x1 + val["w"]; y2 = y1 + val["h"] + bubble_boxes[bid] = (x1, y1, x2, y2) + + quads_per_bubble[bid] = val.get("quads", []) + + return bubble_boxes, quads_per_bubble + + +# ============================================================ +# ERASE โ€” white-fill every OCR quad (with small padding) +# ============================================================ +def erase_quads( + image_bgr, + quads_per_bubble: Dict[int, List], + translations: Dict[int, str], # โ† NEW: only erase what we'll render + skip_ids: Set[int], + pad: int = QUAD_PAD +): + """ + White-fills OCR quads ONLY for bubbles that: + - have a translation in output.txt AND + - are NOT in SKIP_BUBBLE_IDS + Everything else is left completely untouched. + """ + ih, iw = image_bgr.shape[:2] + result = image_bgr.copy() + + erased_count = 0 + skipped_count = 0 + + for bid, quads in quads_per_bubble.items(): + + # ignore if explicitly skipped + if bid in skip_ids: + skipped_count += 1 continue - x1, y1, x2, y2 = b - x1 -= YELLOW_BOX_PAD_X - y1 -= YELLOW_BOX_PAD_Y - x2 += YELLOW_BOX_PAD_X - y2 += YELLOW_BOX_PAD_Y - x1 = clamp(x1, 0, img_w - 1) - y1 = clamp(y1, 0, img_h - 1) - x2 = clamp(x2, 1, img_w) - y2 = clamp(y2, 1, img_h) - if x2 > x1 and y2 > y1: - cv2.rectangle(mask, (x1, y1), (x2, y2), 255, -1) - # If no line boxes available, use line_union fallback - if np.count_nonzero(mask) == 0: - ub = xywh_to_xyxy(bubble_data.get("line_union_bbox")) - if ub: - x1, y1, x2, y2 = ub - x1 -= YELLOW_UNION_PAD_X - y1 -= YELLOW_UNION_PAD_Y - x2 += YELLOW_UNION_PAD_X - y2 += YELLOW_UNION_PAD_Y - x1 = clamp(x1, 0, img_w - 1) - y1 = clamp(y1, 0, img_h - 1) - x2 = clamp(x2, 1, img_w) - y2 = clamp(y2, 1, img_h) - if x2 > x1 and y2 > y1: - cv2.rectangle(mask, (x1, y1), (x2, y2), 255, -1) + # ignore if no translation exists (deleted from output.txt) + if bid not in translations: + skipped_count += 1 + continue - # Last fallback: text_bbox - if np.count_nonzero(mask) == 0: - tb = xywh_to_xyxy(bubble_data.get("text_bbox")) - if tb: - x1, y1, x2, y2 = tb - x1 -= YELLOW_UNION_PAD_X - y1 -= YELLOW_UNION_PAD_Y - x2 += YELLOW_UNION_PAD_X - y2 += YELLOW_UNION_PAD_Y - x1 = clamp(x1, 0, img_w - 1) - y1 = clamp(y1, 0, img_h - 1) - x2 = clamp(x2, 1, img_w) - y2 = clamp(y2, 1, img_h) - if x2 > x1 and y2 > y1: - cv2.rectangle(mask, (x1, y1), (x2, y2), 255, -1) + for quad in quads: + pts = np.array(quad, dtype=np.int32) + cv2.fillPoly(result, [pts], (255, 255, 255)) - return mask + xs = [p[0] for p in quad]; ys = [p[1] for p in quad] + x1 = max(0, min(xs) - pad) + y1 = max(0, min(ys) - pad) + x2 = min(iw - 1, max(xs) + pad) + y2 = min(ih - 1, max(ys) + pad) + cv2.rectangle(result, (x1, y1), (x2, y2), (255, 255, 255), -1) + + erased_count += 1 + + print(f" Erased : {erased_count} bubbles") + print(f" Ignored: {skipped_count} bubbles (no translation or in skip list)") + return result -def bubble_interior_mask(img_bgr, bubble_data): +# ============================================================ +# FONT SIZING + TEXT WRAP +# ============================================================ +def fit_text( + text: str, + box_w: int, + box_h: int, + font_path: str, + max_size: int = FONT_SIZE, + min_size: int = MIN_FONT_SIZE +) -> Tuple[int, ImageFont.FreeTypeFont, List[str]]: """ - Optional helper to expand clean region safely; never used to shrink yellow coverage. + Returns (fitted_size, font, wrapped_lines) โ€” largest size where + the text block fits inside box_w ร— box_h. """ - h, w = img_bgr.shape[:2] + for size in range(max_size, min_size - 1, -1): + font = load_font(font_path, size) if font_path else None + if font is None: + return min_size, ImageFont.load_default(), [text] - panel = xywh_to_xyxy(bubble_data.get("panel_bbox")) - if panel is None: - panel = (0, 0, w, h) - px1, py1, px2, py2 = panel + chars_per_line = max(1, int(box_w / (size * 0.62))) + wrapped = textwrap.fill(text, width=chars_per_line) + lines = wrapped.split("\n") + total_h = (size + 8) * len(lines) - seed = bubble_data.get("seed_point", {}) - sx = int(seed.get("x", bubble_data.get("x", 0) + bubble_data.get("w", 1) // 2)) - sy = int(seed.get("y", bubble_data.get("y", 0) + bubble_data.get("h", 1) // 2)) - sx = clamp(sx, 1, w - 2) - sy = clamp(sy, 1, h - 2) + if total_h <= box_h - 8: + return size, font, lines - gray = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2GRAY) - _, binary = cv2.threshold(gray, 200, 255, cv2.THRESH_BINARY) + # Nothing fit โ€” use minimum size + font = load_font(font_path, min_size) if font_path else None + if font is None: + font = ImageFont.load_default() + chars_per_line = max(1, int(box_w / (min_size * 0.62))) + lines = textwrap.fill(text, width=chars_per_line).split("\n") + return min_size, font, lines - panel_bin = np.zeros_like(binary) - panel_bin[py1:py2, px1:px2] = binary[py1:py2, px1:px2] - # if seed on dark pixel, search nearby white - if gray[sy, sx] < 150: - found = False - search_r = max(2, min(bubble_data.get("w", 20), bubble_data.get("h", 20)) // 3) - for r in range(1, search_r + 1): - for dy in range(-r, r + 1): - for dx in range(-r, r + 1): - nx, ny = sx + dx, sy + dy - if px1 <= nx < px2 and py1 <= ny < py2 and gray[ny, nx] >= 200: - sx, sy = nx, ny - found = True - break - if found: - break - if found: - break - - if not found: - m = np.zeros((h, w), dtype=np.uint8) - bx = bubble_data.get("x", 0) - by = bubble_data.get("y", 0) - bw = bubble_data.get("w", 20) - bh = bubble_data.get("h", 20) - cv2.ellipse(m, (bx + bw // 2, by + bh // 2), (max(4, bw // 2), max(4, bh // 2)), 0, 0, 360, 255, -1) - return m - - ff_mask = np.zeros((h + 2, w + 2), dtype=np.uint8) - flood = panel_bin.copy() - cv2.floodFill( - flood, ff_mask, (sx, sy), 255, - loDiff=FLOOD_TOL, upDiff=FLOOD_TOL, - flags=cv2.FLOODFILL_FIXED_RANGE +# ============================================================ +# COLOR HELPERS +# ============================================================ +def sample_bg_color( + image_bgr, + x1: int, y1: int, + x2: int, y2: int +) -> Tuple[int, int, int]: + """Sample four corners of a bubble to estimate background color (R, G, B).""" + ih, iw = image_bgr.shape[:2] + samples = [] + for sx, sy in [(x1+4, y1+4), (x2-4, y1+4), (x1+4, y2-4), (x2-4, y2-4)]: + sx = max(0, min(iw-1, sx)); sy = max(0, min(ih-1, sy)) + b, g, r = image_bgr[sy, sx] + samples.append((int(r), int(g), int(b))) + return ( + int(np.median([s[0] for s in samples])), + int(np.median([s[1] for s in samples])), + int(np.median([s[2] for s in samples])), ) - m = (ff_mask[1:-1, 1:-1] * 255).astype(np.uint8) - m = cv2.morphologyEx(m, cv2.MORPH_CLOSE, np.ones((3, 3), np.uint8), iterations=1) - return m + +def pick_fg_color(bg: Tuple[int, int, int]) -> Tuple[int, int, int]: + lum = 0.299 * bg[0] + 0.587 * bg[1] + 0.114 * bg[2] + return (0, 0, 0) if lum > 128 else (255, 255, 255) -def build_clean_mask(img_bgr, bubble_data): - """ - FINAL RULE: - clean_mask MUST cover yellow_mask completely. - """ - h, w = img_bgr.shape[:2] - yellow = build_yellow_mask(bubble_data, h, w) - - # start with guaranteed yellow - clean = yellow.copy() - - if ENABLE_EXTRA_CLEAN: - bubble_m = bubble_interior_mask(img_bgr, bubble_data) - extra = cv2.dilate(yellow, np.ones((3, 3), np.uint8), iterations=EXTRA_DILATE_ITERS) - extra = cv2.morphologyEx(extra, cv2.MORPH_CLOSE, np.ones((3, 3), np.uint8), iterations=EXTRA_CLOSE_ITERS) - extra = cv2.bitwise_and(extra, bubble_m) - - # IMPORTANT: union with yellow (never subtract yellow) - clean = cv2.bitwise_or(yellow, extra) - - # final guarantee (defensive) - clean = cv2.bitwise_or(clean, yellow) - - return clean, yellow +def safe_textbbox( + draw, pos, text, font +) -> Tuple[int, int, int, int]: + try: + return draw.textbbox(pos, text, font=font) + except Exception: + size = getattr(font, "size", 12) + return ( + pos[0], pos[1], + pos[0] + int(len(text) * size * 0.6), + pos[1] + int(size * 1.2) + ) -# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -# DRAW BUBBLE -# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -def draw_bubble( - pil_img, - img_bgr_ref, - bubble_data, - original_text, - translated_text, - font_candidates, - font_color, - stroke_color -): - if original_text and translated_text: - if normalize_text(original_text) == normalize_text(translated_text) and is_sfx_like(original_text): - return "skip_sfx" - - rgb = np.array(pil_img) - h, w = rgb.shape[:2] - - clean_mask, yellow_mask = build_clean_mask(img_bgr_ref, bubble_data) - if np.count_nonzero(clean_mask) == 0: - return "skip_no_area" - - # 1) FORCE white fill on clean mask (includes full yellow by guarantee) - rgb[clean_mask == 255] = [255, 255, 255] - - # 2) Optional edge restore, but NEVER overwrite yellow coverage - if ENABLE_EDGE_RESTORE: - bubble_m = bubble_interior_mask(img_bgr_ref, bubble_data) - edge = cv2.morphologyEx(bubble_m, cv2.MORPH_GRADIENT, np.ones((3, 3), np.uint8)) - edge = cv2.dilate(edge, np.ones((3, 3), np.uint8), iterations=EDGE_RESTORE_DILATE) - - # Don't restore where yellow exists (hard guarantee) - edge[yellow_mask == 255] = 0 - - orig_rgb = cv2.cvtColor(img_bgr_ref, cv2.COLOR_BGR2RGB) - rgb[edge == 255] = orig_rgb[edge == 255] - - pil_img.paste(Image.fromarray(rgb)) - - if not translated_text: - return "clean_only" - - # text region based on yellow area (exact requirement) - text_bbox = bbox_from_mask(yellow_mask) - if text_bbox is None: - text_bbox = bbox_from_mask(clean_mask) - if text_bbox is None: - return "skip_no_area" - - x1, y1, x2, y2 = text_bbox - - draw = ImageDraw.Draw(pil_img) - text_cx = int((x1 + x2) / 2) - text_cy = int((y1 + y2) / 2) - safe_w = max(16, int((x2 - x1) * TEXT_INSET)) - safe_h = max(16, int((y2 - y1) * TEXT_INSET)) - - font, lines, total_h = fit_font(draw, translated_text, font_candidates, safe_w, safe_h) - - y_cursor = int(round(text_cy - total_h / 2.0)) - for line in lines: - lw, lh = measure_text(draw, line, font) - x = text_cx - lw // 2 - draw_text_with_stroke(draw, (x, y_cursor), line, font, fill=font_color, stroke_fill=stroke_color) - y_cursor += lh + max(lh // 5, 2) - - return "rendered" - - -# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -# MAIN -# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +# ============================================================ +# RENDER +# ============================================================ def render_translations( - input_image, - output_image, - translations_file, - bubbles_file, - font_candidates=DEFAULT_FONT_CANDIDATES, - font_color=DEFAULT_FONT_COLOR, - stroke_color=DEFAULT_STROKE_COLOR + image_bgr, + bubble_boxes: Dict[int, Tuple], + translations: Dict[int, str], + skip_ids: Set[int], + font_path: str, + font_size: int = FONT_SIZE, + bold_outline: bool = True, + auto_color: bool = True, + output_path: str = OUTPUT_PATH ): - img_bgr = cv2.imread(input_image) - if img_bgr is None: - raise FileNotFoundError(f"Cannot load image: {input_image}") + image_rgb = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB) + pil_img = Image.fromarray(image_rgb) + draw = ImageDraw.Draw(pil_img) - img_pil = Image.fromarray(cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)) + rendered = 0 + skipped = 0 + missing = 0 - translations, originals, flags_map = parse_translations(translations_file) - bubbles = parse_bubbles(bubbles_file) + for bid, (x1, y1, x2, y2) in sorted(bubble_boxes.items()): - rendered, skipped = 0, 0 - - def sort_key(item): - bid, _ = item - b = bubbles.get(bid, {}) - return int(b.get("reading_order", bid)) - - for bubble_id, translated_text in sorted(translations.items(), key=sort_key): - if bubble_id not in bubbles: + # โ”€โ”€ skip list check โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + if bid in skip_ids: + print(f" โญ๏ธ Bubble #{bid:<3} โ€” skipped (in SKIP_BUBBLE_IDS)") skipped += 1 continue - bubble_data = bubbles[bubble_id] - original_text = originals.get(bubble_id, "") + text = translations.get(bid, "").strip() + if not text: + print(f" โš ๏ธ Bubble #{bid:<3} โ€” no translation found, left blank") + missing += 1 + continue - status = draw_bubble( - pil_img=img_pil, - img_bgr_ref=img_bgr, - bubble_data=bubble_data, - original_text=original_text, - translated_text=translated_text, - font_candidates=font_candidates, - font_color=font_color, - stroke_color=stroke_color + box_w = x2 - x1 + box_h = y2 - y1 + if box_w < 10 or box_h < 10: + continue + + # โ”€โ”€ fit font + wrap โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + size, font, lines = fit_text( + text, box_w, box_h, font_path, max_size=font_size ) - if status.startswith("skip"): - skipped += 1 + # โ”€โ”€ colors โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + if auto_color: + bg = sample_bg_color(image_bgr, x1, y1, x2, y2) + fg = pick_fg_color(bg) + ol = (255, 255, 255) if fg == (0, 0, 0) else (0, 0, 0) else: - rendered += 1 + fg, ol = (0, 0, 0), (255, 255, 255) - out_bgr = cv2.cvtColor(np.array(img_pil), cv2.COLOR_RGB2BGR) - cv2.imwrite(output_image, out_bgr) + # โ”€โ”€ vertical center โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + line_h = size + 8 + total_h = line_h * len(lines) + y_cur = y1 + max(4, (box_h - total_h) // 2) - print(f"โœ… Done โ€” {rendered} rendered, {skipped} skipped.") - print(f"๐Ÿ“„ Output โ†’ {output_image}") - print("Guarantee: full yellow-square area is always white-cleaned before drawing text.") + for line in lines: + bb = safe_textbbox(draw, (0, 0), line, font) + line_w = bb[2] - bb[0] + x_cur = x1 + max(2, (box_w - line_w) // 2) + + if bold_outline: + for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]: + try: + draw.text((x_cur + dx, y_cur + dy), line, font=font, fill=ol) + except Exception: + pass + + try: + draw.text((x_cur, y_cur), line, font=font, fill=fg) + except Exception as e: + print(f" โŒ Draw error bubble #{bid}: {e}") + + y_cur += line_h + + print(f" โœ… Bubble #{bid:<3} โ€” rendered ({len(lines)} lines, size {size}px)") + rendered += 1 + + pil_img.save(output_path) + + print() + print(f"{'โ”€'*50}") + print(f" Rendered : {rendered}") + print(f" Skipped : {skipped} (SKIP_BUBBLE_IDS)") + print(f" No text : {missing} (not in output.txt)") + print(f"{'โ”€'*50}") + print(f"โœ… Saved โ†’ {output_path}") + + return pil_img + + +# ============================================================ +# MAIN +# ============================================================ +def main(): + print(f"๐Ÿ“– Loading image : {IMAGE_PATH}") + image = cv2.imread(IMAGE_PATH) + if image is None: + print(f"โŒ Cannot load: {IMAGE_PATH}"); return + + print(f"๐Ÿ“ฆ Loading bubbles : {BUBBLES_PATH}") + bubble_boxes, quads_per_bubble = parse_bubbles(BUBBLES_PATH) + print(f" {len(bubble_boxes)} bubbles | " + f"{sum(len(v) for v in quads_per_bubble.values())} quads total") + + print(f"๐ŸŒ Loading translations : {TRANSLATIONS_PATH}") + translations = parse_translations(TRANSLATIONS_PATH) + print(f" {len(translations)} translations found") + + if SKIP_BUBBLE_IDS: + print(f"โญ๏ธ Skip list : bubbles {sorted(SKIP_BUBBLE_IDS)}") + else: + print(f"โญ๏ธ Skip list : (empty โ€” all bubbles will be rendered)") + + print("๐Ÿ”ค Resolving font...") + font_path, _ = resolve_font() + + print(f"๐Ÿงน Erasing original text (quad fill + pad={QUAD_PAD}px)...") + clean_image = erase_quads( + image, + quads_per_bubble, + translations = translations, # โ† pass translations here + skip_ids = SKIP_BUBBLE_IDS, + pad = QUAD_PAD + ) + + print("โœ๏ธ Rendering translated text...") + render_translations( + image_bgr = clean_image, + bubble_boxes = bubble_boxes, + translations = translations, + skip_ids = SKIP_BUBBLE_IDS, + font_path = font_path, + font_size = FONT_SIZE, + bold_outline = True, + auto_color = True, + output_path = OUTPUT_PATH + ) if __name__ == "__main__": - render_translations( - input_image="001-page.png", - output_image="page_translated.png", - translations_file="output.txt", - bubbles_file="bubbles.json", - font_candidates=DEFAULT_FONT_CANDIDATES, - font_color=DEFAULT_FONT_COLOR, - stroke_color=DEFAULT_STROKE_COLOR - ) + main() diff --git a/manga-translator.py b/manga-translator.py index 7a95073..8aee6dc 100644 --- a/manga-translator.py +++ b/manga-translator.py @@ -6,13 +6,17 @@ import re import json import cv2 import numpy as np +import warnings +from typing import List, Tuple, Dict, Any, Optional from deep_translator import GoogleTranslator -# OCR engines -import easyocr -from paddleocr import PaddleOCR +# macOS Native Vision imports +import Vision +import Quartz +from Foundation import NSData +warnings.filterwarnings("ignore", category=UserWarning) # ============================================================ # CONFIG @@ -26,7 +30,7 @@ GLOSSARY = { } SOUND_EFFECT_PATTERNS = [ - r"^b+i+p+$", r"^sha+$", r"^ha+$", r"^ah+$", r"^oh+$", + r"^b+i+p+$", r"^sha+$", r"^ha+$", r"^ah+$", r"^ugh+$", r"^bam+$", r"^pow+$", r"^boom+$", r"^bang+$", r"^crash+$", r"^thud+$", r"^zip+$", r"^swoosh+$", r"^chirp+$" ] @@ -47,13 +51,13 @@ TOP_BAND_RATIO = 0.08 # ============================================================ -# TEXT HELPERS +# HELPERS # ============================================================ def normalize_text(text: str) -> str: t = (text or "").strip().upper() - t = t.replace("โ€œ", "\"").replace("โ€", "\"") - t = t.replace("โ€™", "'").replace("โ€˜", "'") - t = t.replace("โ€ฆ", "...") + t = t.replace("\u201c", "\"").replace("\u201d", "\"") + t = t.replace("\u2018", "'").replace("\u2019", "'") + t = t.replace("\u2026", "...") t = re.sub(r"\s+", " ", t) t = re.sub(r"\s+([,.;:!?])", r"\1", t) t = re.sub(r"([ยกยฟ])\s+", r"\1", t) @@ -88,24 +92,35 @@ def is_title_text(text: str) -> bool: return any(re.fullmatch(p, t, re.IGNORECASE) for p in TITLE_PATTERNS) +def looks_like_box_tag(t: str) -> bool: + s = re.sub(r"[^A-Z0-9#]", "", (t or "").upper()) + if re.fullmatch(r"[BEF]?[O0D]X#?\d{0,3}", s): + return True + if re.fullmatch(r"B[O0D]X\d{0,3}", s): + return True + return False + + def is_noise_text(text: str) -> bool: t = (text or "").strip() + + # Explicitly allow standalone punctuation like ? or ! + if re.fullmatch(r"[\?\!]+", t): + return False + if any(re.fullmatch(p, t) for p in NOISE_PATTERNS): return True - - if len(t) <= 2 and not re.search(r"[A-Z0-9]", t): + if looks_like_box_tag(t): + return True + if len(t) <= 2 and not re.search(r"[A-Z0-9\?\!]", t): return True symbol_ratio = sum(1 for c in t if not c.isalnum() and not c.isspace()) / max(1, len(t)) if len(t) <= 6 and symbol_ratio > 0.60: return True - return False -# ============================================================ -# GEOMETRY HELPERS -# ============================================================ def quad_bbox(quad): xs = [p[0] for p in quad] ys = [p[1] for p in quad] @@ -150,9 +165,6 @@ def overlap_or_near(a, b, gap=0): return gap_x <= gap and gap_y <= gap -# ============================================================ -# QUALITY -# ============================================================ def ocr_candidate_score(text: str) -> float: if not text: return 0.0 @@ -179,204 +191,98 @@ def ocr_candidate_score(text: str) -> float: # ============================================================ -# OCR ENGINE WRAPPER (PADDLE + EASYOCR HYBRID) +# OCR ENGINES (Apple Native Vision) # ============================================================ -class HybridOCR: - def __init__(self, source_lang="en", use_gpu=False): - self.source_lang = source_lang +class MacVisionDetector: + def __init__(self, source_lang="en"): + lang_map = {"en": "en-US", "es": "es-ES", "ca": "ca-ES", "fr": "fr-FR", "ja": "ja-JP"} + apple_lang = lang_map.get(source_lang, "en-US") + self.langs = [apple_lang] + print(f"โšก Using Apple Vision OCR (Language: {self.langs})") - # Paddle language choice (single lang for Paddle) - # For manga EN/ES pages, latin model is robust. - if source_lang in ("en", "es", "ca", "fr", "de", "it", "pt"): - paddle_lang = "latin" - elif source_lang in ("ja",): - paddle_lang = "japan" - elif source_lang in ("ko",): - paddle_lang = "korean" - elif source_lang in ("ch", "zh", "zh-cn", "zh-tw"): - paddle_lang = "ch" + def read(self, image_path_or_array): + if isinstance(image_path_or_array, str): + img = cv2.imread(image_path_or_array) else: - paddle_lang = "latin" + img = image_path_or_array - # EasyOCR language list - if source_lang == "ca": - easy_langs = ["es", "en"] - elif source_lang == "en": - easy_langs = ["en", "es"] - elif source_lang == "es": - easy_langs = ["es", "en"] - else: - easy_langs = [source_lang] + if img is None or img.size == 0: + return [] - self.paddle = PaddleOCR( - use_angle_cls=True, - lang=paddle_lang, - use_gpu=use_gpu, - show_log=False - ) - self.easy = easyocr.Reader(easy_langs, gpu=use_gpu) + ih, iw = img.shape[:2] - @staticmethod - def _paddle_to_std(result): - """ - Convert Paddle result to Easy-like: - [ (quad, text, conf), ... ] - """ - out = [] - # paddle.ocr(...) returns list per image - # each item line: [ [ [x,y],...4pts ], (text, conf) ] - if not result: - return out - # result can be [None] or nested list - blocks = result if isinstance(result, list) else [result] - for blk in blocks: - if blk is None: - continue - if len(blk) == 0: - continue - # some versions wrap once more - if isinstance(blk[0], list) and len(blk[0]) > 0 and isinstance(blk[0][0], (list, tuple)) and len(blk[0]) == 2: - lines = blk - elif isinstance(blk[0], (list, tuple)) and len(blk[0]) >= 2: - lines = blk - else: - # maybe nested once more - if len(blk) == 1 and isinstance(blk[0], list): - lines = blk[0] - else: - lines = [] + success, buffer = cv2.imencode('.png', img) + if not success: + return [] - for ln in lines: - try: - pts, rec = ln - txt, conf = rec[0], float(rec[1]) - quad = [[float(p[0]), float(p[1])] for p in pts] - out.append((quad, txt, conf)) - except Exception: - continue - return out + ns_data = NSData.dataWithBytes_length_(buffer.tobytes(), len(buffer.tobytes())) + handler = Vision.VNImageRequestHandler.alloc().initWithData_options_(ns_data, None) + results = [] - def read_full_image(self, image_path): - """ - Primary: Paddle - Fallback merge: EasyOCR - Returns merged standardized detections. - """ - # Paddle - pr = self.paddle.ocr(image_path, cls=True) - paddle_det = self._paddle_to_std(pr) + def completion_handler(request, error): + if error: + print(f"Vision API Error: {error}") + return - # Easy - easy_det = self.easy.readtext(image_path, paragraph=False) + for observation in request.results(): + candidate = observation.topCandidates_(1)[0] + text = candidate.string() + confidence = candidate.confidence() - # Merge by IOU/text proximity - merged = list(paddle_det) - for eb in easy_det: - eq, et, ec = eb - ebox = quad_bbox(eq) - keep = True - for pb in paddle_det: - pq, pt, pc = pb - pbox = quad_bbox(pq) + bbox = observation.boundingBox() + x = bbox.origin.x * iw + y_bottom_left = bbox.origin.y * ih + w = bbox.size.width * iw + h = bbox.size.height * ih - ix1 = max(ebox[0], pbox[0]); iy1 = max(ebox[1], pbox[1]) - ix2 = min(ebox[2], pbox[2]); iy2 = min(ebox[3], pbox[3]) - inter = max(0, ix2 - ix1) * max(0, iy2 - iy1) - a1 = max(1, (ebox[2] - ebox[0]) * (ebox[3] - ebox[1])) - a2 = max(1, (pbox[2] - pbox[0]) * (pbox[3] - pbox[1])) - iou = inter / float(a1 + a2 - inter) if (a1 + a2 - inter) > 0 else 0.0 + y = ih - y_bottom_left - h - if iou > 0.55: - # if overlapped and paddle exists, keep paddle unless easy much higher conf - if float(ec) > float(pc) + 0.20: - # replace paddle with easy-like entry - try: - merged.remove(pb) - except Exception: - pass - merged.append((eq, et, float(ec))) - keep = False - break + quad = [ + [int(x), int(y)], + [int(x + w), int(y)], + [int(x + w), int(y + h)], + [int(x), int(y + h)] + ] - if keep: - merged.append((eq, et, float(ec))) + results.append((quad, text, confidence)) - return merged + request = Vision.VNRecognizeTextRequest.alloc().initWithCompletionHandler_(completion_handler) + request.setRecognitionLevel_(Vision.VNRequestTextRecognitionLevelAccurate) + request.setUsesLanguageCorrection_(True) + request.setRecognitionLanguages_(self.langs) - def read_array_with_both(self, arr_gray_or_bgr): - """ - OCR from array (used in robust reread pass). - Returns merged detections in standardized format. - """ - tmp = "_tmp_ocr_hybrid.png" - cv2.imwrite(tmp, arr_gray_or_bgr) - try: - pr = self.paddle.ocr(tmp, cls=True) - paddle_det = self._paddle_to_std(pr) - easy_det = self.easy.readtext(tmp, paragraph=False) + handler.performRequests_error_([request], None) - merged = list(paddle_det) - - for eb in easy_det: - eq, et, ec = eb - ebox = quad_bbox(eq) - keep = True - for pb in paddle_det: - pq, pt, pc = pb - pbox = quad_bbox(pq) - - ix1 = max(ebox[0], pbox[0]); iy1 = max(ebox[1], pbox[1]) - ix2 = min(ebox[2], pbox[2]); iy2 = min(ebox[3], pbox[3]) - inter = max(0, ix2 - ix1) * max(0, iy2 - iy1) - a1 = max(1, (ebox[2] - ebox[0]) * (ebox[3] - ebox[1])) - a2 = max(1, (pbox[2] - pbox[0]) * (pbox[3] - pbox[1])) - iou = inter / float(a1 + a2 - inter) if (a1 + a2 - inter) > 0 else 0.0 - - if iou > 0.55: - if float(ec) > float(pc) + 0.20: - try: - merged.remove(pb) - except Exception: - pass - merged.append((eq, et, float(ec))) - keep = False - break - - if keep: - merged.append((eq, et, float(ec))) - - return merged - finally: - if os.path.exists(tmp): - os.remove(tmp) + return results # ============================================================ -# PREPROCESS + ROBUST REREAD +# PREPROCESS # ============================================================ def preprocess_variant(crop_bgr, mode): gray = cv2.cvtColor(crop_bgr, cv2.COLOR_BGR2GRAY) if mode == "raw": return gray - if mode == "clahe": return cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)).apply(gray) - if mode == "adaptive": den = cv2.GaussianBlur(gray, (3, 3), 0) - return cv2.adaptiveThreshold( - den, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, - cv2.THRESH_BINARY, 35, 11 - ) - + return cv2.adaptiveThreshold(den, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 35, 11) if mode == "otsu": den = cv2.GaussianBlur(gray, (3, 3), 0) _, th = cv2.threshold(den, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) return th - if mode == "invert": return 255 - gray + if mode == "bilateral": + den = cv2.bilateralFilter(gray, 7, 60, 60) + _, th = cv2.threshold(den, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) + return th + if mode == "morph_open": + _, th = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) + k = np.ones((2, 2), np.uint8) + return cv2.morphologyEx(th, cv2.MORPH_OPEN, k) return gray @@ -389,22 +295,18 @@ def rotate_image_keep_bounds(img, angle_deg): new_w = int((h * sin) + (w * cos)) new_h = int((h * cos) + (w * sin)) - M[0, 2] += (new_w / 2) - c[0] M[1, 2] += (new_h / 2) - c[1] return cv2.warpAffine(img, M, (new_w, new_h), flags=cv2.INTER_CUBIC, borderValue=255) -def rebuild_text_from_ocr_result(res): +def rebuild_text_from_vision_result(res): if not res: return "" norm = [] - for item in res: - if len(item) != 3: - continue - bbox, txt, conf = item + for bbox, txt, conf in res: if not txt or not txt.strip(): continue b = quad_bbox(bbox) @@ -419,7 +321,7 @@ def rebuild_text_from_ocr_result(res): med_h = float(np.median([x[5] for x in norm])) row_tol = max(6.0, med_h * 0.75) - norm.sort(key=lambda z: z[4]) # y + norm.sort(key=lambda z: z[4]) rows = [] for it in norm: placed = False @@ -435,7 +337,7 @@ def rebuild_text_from_ocr_result(res): rows.sort(key=lambda r: r["yc"]) lines = [] for r in rows: - mem = sorted(r["m"], key=lambda z: z[3]) # x + mem = sorted(r["m"], key=lambda z: z[3]) line = normalize_text(" ".join(x[1] for x in mem)) if line: lines.append(line) @@ -443,57 +345,51 @@ def rebuild_text_from_ocr_result(res): return normalize_text(" ".join(lines)) -def reread_crop_robust(image, bbox, hybrid_ocr: HybridOCR, upscale=3.0, pad=24): - ih, iw = image.shape[:2] - x1, y1, x2, y2 = bbox - x1 = max(0, int(x1 - pad)) - y1 = max(0, int(y1 - pad)) - x2 = min(iw, int(x2 + pad)) - y2 = min(ih, int(y2 + pad)) - crop = image[y1:y2, x1:x2] +def reread_bubble_with_vision( + image_bgr, + bbox_xyxy, + vision_detector: MacVisionDetector, + upscale=3.0, + pad=24 +): + ih, iw = image_bgr.shape[:2] + x1, y1, x2, y2 = bbox_xyxy + x1 = max(0, int(x1 - pad)); y1 = max(0, int(y1 - pad)) + x2 = min(iw, int(x2 + pad)); y2 = min(ih, int(y2 + pad)) + + crop = image_bgr[y1:y2, x1:x2] if crop.size == 0: - return None, 0.0 + return None, 0.0, "none" - up = cv2.resize( - crop, - (int(crop.shape[1] * upscale), int(crop.shape[0] * upscale)), - interpolation=cv2.INTER_CUBIC - ) - - modes = ["raw", "clahe", "adaptive", "otsu", "invert"] + modes = ["raw", "clahe", "adaptive", "otsu", "invert", "bilateral", "morph_open"] angles = [0.0, 1.5, -1.5] - best_text, best_score = "", 0.0 + best_v_txt, best_v_sc = "", 0.0 + up0 = cv2.resize(crop, (int(crop.shape[1]*upscale), int(crop.shape[0]*upscale)), interpolation=cv2.INTER_CUBIC) for mode in modes: - proc = preprocess_variant(up, mode) - - if len(proc.shape) == 2: - proc3 = cv2.cvtColor(proc, cv2.COLOR_GRAY2BGR) - else: - proc3 = proc - + proc = preprocess_variant(up0, mode) + proc3 = cv2.cvtColor(proc, cv2.COLOR_GRAY2BGR) if len(proc.shape) == 2 else proc for a in angles: rot = rotate_image_keep_bounds(proc3, a) - res = hybrid_ocr.read_array_with_both(rot) - txt = rebuild_text_from_ocr_result(res) + res = vision_detector.read(rot) + txt = rebuild_text_from_vision_result(res) sc = ocr_candidate_score(txt) + if sc > best_v_sc: + best_v_txt, best_v_sc = txt, sc - if sc > best_score: - best_text, best_score = txt, sc + if best_v_txt: + return best_v_txt, best_v_sc, "vision-reread" - if not best_text: - return None, 0.0 - return best_text, best_score + return None, 0.0, "none" # ============================================================ -# LINE REBUILD + YELLOW BOXES +# LINES + BUBBLES # ============================================================ def build_lines_from_indices(indices, ocr): if not indices: return [] - items = [] for i in indices: b = quad_bbox(ocr[i][0]) @@ -526,7 +422,6 @@ def build_lines_from_indices(indices, ocr): txt = normalize_text(" ".join(ocr[i][1] for i, _, _, _ in mem)) if txt and not is_noise_text(txt): lines.append(txt) - return lines @@ -540,16 +435,10 @@ def build_line_boxes_from_indices(indices, ocr, image_shape=None): txt = normalize_text(ocr[i][1]) if is_noise_text(txt): continue - xc = (b[0] + b[2]) / 2.0 yc = (b[1] + b[3]) / 2.0 - w = max(1.0, b[2] - b[0]) h = max(1.0, b[3] - b[1]) - - items.append({ - "i": i, "b": b, "txt": txt, - "xc": xc, "yc": yc, "w": w, "h": h - }) + items.append({"i": i, "b": b, "txt": txt, "xc": xc, "yc": yc, "h": h}) if not items: return [] @@ -559,16 +448,8 @@ def build_line_boxes_from_indices(indices, ocr, image_shape=None): gap_x_tol = max(8.0, med_h * 1.25) pad = max(3, int(round(med_h * 0.22))) - def is_punct_like(t): - raw = (t or "").strip() - if raw == "": - return True - punct_ratio = sum(1 for c in raw if not c.isalnum()) / max(1, len(raw)) - return punct_ratio >= 0.5 or len(raw) <= 2 - - items_sorted = sorted(items, key=lambda x: x["yc"]) rows = [] - for it in items_sorted: + for it in sorted(items, key=lambda x: x["yc"]): placed = False for r in rows: if abs(it["yc"] - r["yc"]) <= row_tol: @@ -584,16 +465,12 @@ def build_line_boxes_from_indices(indices, ocr, image_shape=None): for r in rows: mem = sorted(r["m"], key=lambda z: z["xc"]) - normal = [t for t in mem if not is_punct_like(t["txt"])] - punct = [t for t in mem if is_punct_like(t["txt"])] - - if not normal: - normal = mem - punct = [] + if not mem: + continue chunks = [] - cur = [normal[0]] - for t in normal[1:]: + cur = [mem[0]] + for t in mem[1:]: prev = cur[-1]["b"] b = t["b"] gap = b[0] - prev[2] @@ -604,106 +481,26 @@ def build_line_boxes_from_indices(indices, ocr, image_shape=None): cur = [t] chunks.append(cur) - for p in punct: - pb = p["b"] - pxc, pyc = p["xc"], p["yc"] - best_k = -1 - best_score = 1e18 - - for k, ch in enumerate(chunks): - ub = boxes_union_xyxy([x["b"] for x in ch]) - cx = (ub[0] + ub[2]) / 2.0 - cy = (ub[1] + ub[3]) / 2.0 - dx = abs(pxc - cx) - dy = abs(pyc - cy) - score = dx + 1.8 * dy - - near = overlap_or_near(pb, ub, gap=int(med_h * 1.25)) - if near: - score -= med_h * 2.0 - - if score < best_score: - best_score = score - best_k = k - - if best_k >= 0: - chunks[best_k].append(p) - else: - chunks.append([p]) - for ch in chunks: ub = boxes_union_xyxy([x["b"] for x in ch]) if ub: x1, y1, x2, y2 = ub - pad_x = pad - pad_top = int(round(pad * 1.35)) - pad_bot = int(round(pad * 0.95)) - out_boxes.append((x1 - pad_x, y1 - pad_top, x2 + pad_x, y2 + pad_bot)) - - token_boxes = [it["b"] for it in items] - - def inside(tb, lb): - return tb[0] >= lb[0] and tb[1] >= lb[1] and tb[2] <= lb[2] and tb[3] <= lb[3] - - for tb in token_boxes: - if not any(inside(tb, lb) for lb in out_boxes): - x1, y1, x2, y2 = tb - pad_x = pad - pad_top = int(round(pad * 1.35)) - pad_bot = int(round(pad * 0.95)) - out_boxes.append((x1 - pad_x, y1 - pad_top, x2 + pad_x, y2 + pad_bot)) - - merged = [] - for b in out_boxes: - merged_into = False - for i, m in enumerate(merged): - ix1 = max(b[0], m[0]); iy1 = max(b[1], m[1]) - ix2 = min(b[2], m[2]); iy2 = min(b[3], m[3]) - inter = max(0, ix2 - ix1) * max(0, iy2 - iy1) - a1 = max(1, (b[2] - b[0]) * (b[3] - b[1])) - a2 = max(1, (m[2] - m[0]) * (m[3] - m[1])) - iou = inter / float(a1 + a2 - inter) if (a1 + a2 - inter) > 0 else 0.0 - if iou > 0.72: - merged[i] = boxes_union_xyxy([b, m]) - merged_into = True - break - if not merged_into: - merged.append(b) - - safe = [] - for (x1, y1, x2, y2) in merged: - w = x2 - x1 - h = y2 - y1 - if w < 28: - d = (28 - w) // 2 + 2 - x1 -= d; x2 += d - if h < 18: - d = (18 - h) // 2 + 2 - y1 -= d; y2 += d - safe.append((x1, y1, x2, y2)) - merged = safe + out_boxes.append((x1 - pad, y1 - int(round(pad*1.35)), x2 + pad, y2 + int(round(pad*0.95)))) if image_shape is not None: ih, iw = image_shape[:2] clamped = [] - for b in merged: - x1 = max(0, int(b[0])) - y1 = max(0, int(b[1])) - x2 = min(iw - 1, int(b[2])) - y2 = min(ih - 1, int(b[3])) + for b in out_boxes: + x1 = max(0, int(b[0])); y1 = max(0, int(b[1])) + x2 = min(iw - 1, int(b[2])); y2 = min(ih - 1, int(b[3])) if x2 > x1 and y2 > y1: clamped.append((x1, y1, x2, y2)) - merged = clamped - else: - merged = [(int(b[0]), int(b[1]), int(b[2]), int(b[3])) for b in merged] + out_boxes = clamped - merged.sort(key=lambda z: (z[1], z[0])) - return merged + out_boxes.sort(key=lambda z: (z[1], z[0])) + return out_boxes -# ============================================================ -# GROUPING -# ============================================================ def auto_gap(image_path, base=18, ref_w=750): img = cv2.imread(image_path) if img is None: @@ -750,21 +547,14 @@ def group_tokens(ocr, image_shape, gap_px=18, bbox_padding=3): sorted_groups = sorted( groups.values(), - key=lambda idxs: ( - min(boxes[i][1] for i in idxs), - min(boxes[i][0] for i in idxs) - ) + key=lambda idxs: (min(boxes[i][1] for i in idxs), min(boxes[i][0] for i in idxs)) ) - bubbles = {} - bubble_boxes = {} - bubble_quads = {} - bubble_indices = {} - + bubbles, bubble_boxes, bubble_quads, bubble_indices = {}, {}, {}, {} ih, iw = image_shape[:2] + for bid, idxs in enumerate(sorted_groups, start=1): idxs = sorted(idxs, key=lambda k: boxes[k][1]) - lines = build_lines_from_indices(idxs, ocr) quads = [ocr[k][0] for k in idxs] ub = boxes_union_xyxy([quad_bbox(q) for q in quads]) @@ -772,10 +562,8 @@ def group_tokens(ocr, image_shape, gap_px=18, bbox_padding=3): continue x1, y1, x2, y2 = ub - x1 = max(0, x1 - bbox_padding) - y1 = max(0, y1 - bbox_padding) - x2 = min(iw - 1, x2 + bbox_padding) - y2 = min(ih - 1, y2 + bbox_padding) + x1 = max(0, x1 - bbox_padding); y1 = max(0, y1 - bbox_padding) + x2 = min(iw - 1, x2 + bbox_padding); y2 = min(ih - 1, y2 + bbox_padding) bubbles[bid] = lines bubble_boxes[bid] = (x1, y1, x2, y2) @@ -786,37 +574,63 @@ def group_tokens(ocr, image_shape, gap_px=18, bbox_padding=3): # ============================================================ -# DEBUG +# DEBUG / EXPORT # ============================================================ -def save_debug_clusters(image_path, ocr, bubble_boxes, bubble_indices, out_path="debug_clusters.png"): +def save_debug_clusters( + image_path, + ocr, + bubble_boxes, + bubble_indices, + clean_lines=None, + out_path="debug_clusters.png" +): img = cv2.imread(image_path) if img is None: return + # โ”€โ”€ FIX 1: white-fill each OCR quad before drawing its outline โ”€โ”€ for bbox, txt, conf in ocr: pts = np.array(bbox, dtype=np.int32) - cv2.polylines(img, [pts], True, (180, 180, 180), 1) + cv2.fillPoly(img, [pts], (255, 255, 255)) # โ† white background + cv2.polylines(img, [pts], True, (180, 180, 180), 1) # โ† grey outline for bid, bb in bubble_boxes.items(): x1, y1, x2, y2 = bb - cv2.rectangle(img, (x1, y1), (x2, y2), (0, 220, 0), 2) - cv2.putText( - img, f"BOX#{bid}", (x1 + 2, max(15, y1 + 16)), - cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 220, 0), 2 - ) - idxs = bubble_indices.get(bid, []) - line_boxes = build_line_boxes_from_indices(idxs, ocr, image_shape=img.shape) - for lb in line_boxes: - lx1, ly1, lx2, ly2 = lb - cv2.rectangle(img, (lx1, ly1), (lx2, ly2), (0, 255, 255), 3) + # Draw green bubble bounding box + ID label + cv2.rectangle(img, (x1, y1), (x2, y2), (0, 220, 0), 2) + cv2.putText(img, f"BOX#{bid}", (x1 + 2, max(15, y1 + 16)), + cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 220, 0), 2) + + # โ”€โ”€ FIX 2: yellow line-box drawing loop removed entirely โ”€โ”€โ”€โ”€ + + # Draw translated text overlay below each bubble box + if clean_lines and bid in clean_lines: + text = clean_lines[bid] + words = text.split() + lines = [] + current_line = "" + + for word in words: + if len(current_line) + len(word) < 25: + current_line += word + " " + else: + lines.append(current_line.strip()) + current_line = word + " " + if current_line: + lines.append(current_line.strip()) + + y_text = y2 + 18 + for line in lines: + cv2.putText(img, line, (x1, y_text), + cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 3) + cv2.putText(img, line, (x1, y_text), + cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 0, 0), 1) + y_text += 18 cv2.imwrite(out_path, img) -# ============================================================ -# EXPORT -# ============================================================ def estimate_reading_order(bbox_dict, mode="ltr"): items = [] for bid, (x1, y1, x2, y2) in bbox_dict.items(): @@ -826,8 +640,7 @@ def estimate_reading_order(bbox_dict, mode="ltr"): items.sort(key=lambda t: t[2]) - rows = [] - tol = 90 + rows, tol = [], 90 for it in items: placed = False for r in rows: @@ -850,7 +663,6 @@ def estimate_reading_order(bbox_dict, mode="ltr"): def export_bubbles(filepath, bbox_dict, quads_dict, indices_dict, ocr, reading_map, image_shape): out = {} - for bid, bb in bbox_dict.items(): x1, y1, x2, y2 = bb quads = quads_dict.get(bid, []) @@ -870,9 +682,7 @@ def export_bubbles(filepath, bbox_dict, quads_dict, indices_dict, ocr, reading_m {"x": int(b[0]), "y": int(b[1]), "w": int(b[2] - b[0]), "h": int(b[3] - b[1])} for b in qboxes ], - "quads": [ - [[int(p[0]), int(p[1])] for p in q] for q in quads - ], + "quads": [[[int(p[0]), int(p[1])] for p in q] for q in quads], "text_bbox": xyxy_to_xywh(text_union), "line_bboxes": [xyxy_to_xywh(lb) for lb in line_boxes_xyxy], "line_union_bbox": xyxy_to_xywh(line_union_xyxy) if line_union_xyxy else None, @@ -884,10 +694,10 @@ def export_bubbles(filepath, bbox_dict, quads_dict, indices_dict, ocr, reading_m # ============================================================ -# MAIN PIPELINE +# PIPELINE # ============================================================ def translate_manga_text( - image_path, + image_path="001-page.png", source_lang="en", target_lang="ca", confidence_threshold=0.12, @@ -898,8 +708,7 @@ def translate_manga_text( export_to_file="output.txt", export_bubbles_to="bubbles.json", reading_mode="ltr", - debug=True, - use_gpu=False + debug=True ): image = cv2.imread(image_path) if image is None: @@ -908,12 +717,12 @@ def translate_manga_text( resolved_gap = auto_gap(image_path) if gap_px == "auto" else float(gap_px) - print("Loading Hybrid OCR (Paddle + EasyOCR)...") - hybrid = HybridOCR(source_lang=source_lang, use_gpu=use_gpu) + print("Loading OCR engines...") + detector = MacVisionDetector(source_lang=source_lang) - print("Running OCR...") - raw = hybrid.read_full_image(image_path) - print(f"Raw detections (merged): {len(raw)}") + print("Running detection OCR (Apple Vision)...") + raw = detector.read(image_path) + print(f"Raw detections: {len(raw)}") filtered = [] skipped = 0 @@ -924,25 +733,18 @@ def translate_manga_text( qb = quad_bbox(bbox) if conf < confidence_threshold: - skipped += 1 - continue + skipped += 1; continue if len(t) < min_text_length: - skipped += 1 - continue + skipped += 1; continue if is_noise_text(t): - skipped += 1 - continue + skipped += 1; continue if filter_sound_effects and is_sound_effect(t): - skipped += 1 - continue + skipped += 1; continue if is_title_text(t): - skipped += 1 - continue - + skipped += 1; continue if qb[1] < int(ih * TOP_BAND_RATIO): if conf < 0.70 and len(t) >= 5: - skipped += 1 - continue + skipped += 1; continue filtered.append((bbox, t, conf)) @@ -955,75 +757,80 @@ def translate_manga_text( filtered, image.shape, gap_px=resolved_gap, bbox_padding=3 ) + translator = GoogleTranslator(source=source_lang, target=target_lang) + + clean_lines: Dict[int, str] = {} + sources_used: Dict[int, str] = {} + + for bid, lines in bubbles.items(): + base_txt = normalize_text(" ".join(lines)) + base_sc = ocr_candidate_score(base_txt) + + txt = base_txt + src_used = "vision-base" + + if base_sc < quality_threshold: + rr_txt, rr_sc, rr_src = reread_bubble_with_vision( + image_bgr=image, + bbox_xyxy=bubble_boxes[bid], + vision_detector=detector, + upscale=3.0, + pad=24 + ) + if rr_txt and rr_sc > base_sc + 0.04: + txt = rr_txt + src_used = rr_src + + txt = txt.replace(" BOMPORTA", " IMPORTA") + txt = txt.replace(" TESTO ", " ESTO ") + txt = txt.replace(" MIVERDAD", " MI VERDAD") + + clean_lines[bid] = apply_glossary(normalize_text(txt)) + sources_used[bid] = src_used + + reading_map = estimate_reading_order(bubble_boxes, mode=reading_mode) + if debug: save_debug_clusters( image_path=image_path, ocr=filtered, bubble_boxes=bubble_boxes, bubble_indices=bubble_indices, + clean_lines=clean_lines, out_path="debug_clusters.png" ) - translator = GoogleTranslator(source=source_lang, target=target_lang) - - clean_lines = {} - for bid, lines in bubbles.items(): - base_txt = normalize_text(" ".join(lines)) - base_sc = ocr_candidate_score(base_txt) - - if base_sc < quality_threshold: - rr_txt, rr_sc = reread_crop_robust( - image, - bubble_boxes[bid], - hybrid, - upscale=3.0, - pad=24 - ) - if rr_txt and rr_sc > base_sc + 0.06: - txt = rr_txt - else: - txt = base_txt - else: - txt = base_txt - - txt = txt.replace(" BOMPORTA", " IMPORTA") - txt = txt.replace(" TESTO ", " ESTO ") - txt = txt.replace(" MIVERDAD", " MI VERDAD") - - clean_lines[bid] = apply_glossary(normalize_text(txt)) - - reading_map = estimate_reading_order(bubble_boxes, mode=reading_mode) - divider = "โ”€" * 120 - out_lines = ["BUBBLE|ORDER|ORIGINAL|TRANSLATED|FLAGS", divider] + out_lines = ["BUBBLE|ORDER|OCR_SOURCE|ORIGINAL|TRANSLATED|FLAGS", divider] print(divider) - print(f"{'BUBBLE':<8} {'ORDER':<6} {'ORIGINAL':<50} {'TRANSLATED':<50} FLAGS") + print(f"{'BUBBLE':<8} {'ORDER':<6} {'SOURCE':<12} {'ORIGINAL':<40} {'TRANSLATED':<40} FLAGS") print(divider) translated_count = 0 for bid in sorted(clean_lines.keys(), key=lambda x: reading_map.get(x, x)): - src = clean_lines[bid].strip() - if not src: + src_txt = clean_lines[bid].strip() + if not src_txt: continue flags = [] try: - tgt = translator.translate(src) or "" + tgt = translator.translate(src_txt) or "" except Exception as e: tgt = f"[Translation error: {e}]" flags.append("TRANSLATION_ERROR") tgt = apply_glossary(postprocess_translation_general(tgt)).upper() - src_u = src.upper() + src_u = src_txt.upper() + src_engine = sources_used.get(bid, "unknown") out_lines.append( - f"#{bid}|{reading_map.get(bid,bid)}|{src_u}|{tgt}|{','.join(flags) if flags else '-'}" + f"#{bid}|{reading_map.get(bid,bid)}|{src_engine}|{src_u}|{tgt}|{','.join(flags) if flags else '-'}" ) print( - f"#{bid:<7} {reading_map.get(bid,bid):<6} " - f"{src_u[:50]:<50} {tgt[:50]:<50} {','.join(flags) if flags else '-'}" + f"#{bid:<7} {reading_map.get(bid,bid):<6} {src_engine:<12} " + f"{src_u[:40]:<40} {tgt[:40]:<40} {','.join(flags) if flags else '-'}" ) translated_count += 1 @@ -1050,22 +857,18 @@ def translate_manga_text( print("Saved: debug_clusters.png") -# ============================================================ -# ENTRYPOINT -# ============================================================ if __name__ == "__main__": translate_manga_text( - image_path="001-page.png", - source_lang="it", + image_path="003.jpg", + source_lang="es", target_lang="ca", confidence_threshold=0.12, - min_text_length=1, + min_text_length=2, gap_px="auto", filter_sound_effects=True, quality_threshold=0.62, export_to_file="output.txt", export_bubbles_to="bubbles.json", reading_mode="ltr", - debug=True, - use_gpu=False + debug=True ) diff --git a/requirements b/requirements new file mode 100644 index 0000000..492c48d --- /dev/null +++ b/requirements @@ -0,0 +1,79 @@ +aistudio-sdk==0.3.8 +annotated-doc==0.0.4 +annotated-types==0.7.0 +anyio==4.13.0 +bce-python-sdk==0.9.70 +beautifulsoup4==4.14.3 +certifi==2026.2.25 +chardet==7.4.3 +charset-normalizer==3.4.7 +click==8.3.2 +colorlog==6.10.1 +crc32c==2.8 +deep-translator==1.11.4 +easyocr==1.7.2 +filelock==3.28.0 +fsspec==2026.3.0 +future==1.0.0 +h11==0.16.0 +hf-xet==1.4.3 +httpcore==1.0.9 +httpx==0.28.1 +huggingface_hub==1.10.2 +idna==3.11 +ImageIO==2.37.3 +imagesize==2.0.0 +Jinja2==3.1.6 +lazy-loader==0.5 +markdown-it-py==4.0.0 +MarkupSafe==3.0.3 +mdurl==0.1.2 +modelscope==1.35.4 +mpmath==1.3.0 +networkx==3.6.1 +ninja==1.13.0 +numpy==1.26.4 +opencv-contrib-python==4.10.0.84 +opencv-python==4.11.0.86 +opencv-python-headless==4.11.0.86 +opt-einsum==3.3.0 +packaging==26.1 +paddleocr==3.4.1 +paddlepaddle==3.3.1 +paddlex==3.4.3 +pandas==3.0.2 +pillow==12.2.0 +prettytable==3.17.0 +protobuf==7.34.1 +psutil==7.2.2 +py-cpuinfo==9.0.0 +pyclipper==1.4.0 +pycryptodome==3.23.0 +pydantic==2.13.1 +pydantic_core==2.46.1 +Pygments==2.20.0 +pypdfium2==5.7.0 +python-bidi==0.6.7 +python-dateutil==2.9.0.post0 +PyYAML==6.0.2 +requests==2.33.1 +rich==15.0.0 +ruamel.yaml==0.19.1 +safetensors==0.7.0 +scikit-image==0.26.0 +scipy==1.17.1 +shapely==2.1.2 +shellingham==1.5.4 +six==1.17.0 +soupsieve==2.8.3 +sympy==1.14.0 +tifffile==2026.3.3 +torch==2.11.0 +torchvision==0.26.0 +tqdm==4.67.3 +typer==0.24.1 +typing-inspection==0.4.2 +typing_extensions==4.15.0 +ujson==5.12.0 +urllib3==2.6.3 +wcwidth==0.6.0 diff --git a/requirements.txt b/requirements.txt index eefc6d0..62d7a7d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,19 +1,12 @@ -# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -# manga-translator + manga-renderer -# Python >= 3.9 recommended -# โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - -# Computer vision + image processing -opencv-python>=4.8.0 -numpy>=1.24.0 -Pillow>=10.0.0 - -# OCR engine (manga-translator) -manga-ocr>=0.1.8 - -# Translation (manga-translator) -deep-translator>=1.11.0 - -# HTTP / file handling used internally by manga-ocr -requests>=2.31.0 +numpy<2.0 +opencv-python>=4.8 +easyocr>=1.7.1 +deep-translator>=1.11.4 +manga-ocr>=0.1.14 +torch +torchvision +Pillow +transformers +fugashi +unidic-lite