diff --git a/claude_ocr.py b/claude_ocr.py index 08a9a7a..bca2029 100644 --- a/claude_ocr.py +++ b/claude_ocr.py @@ -18,8 +18,23 @@ OUTPUT_DIR = Path(__file__).parent / "output" def read_article_image(client, img_path, model="claude-sonnet-4-6"): """Send article image to Claude and get Telugu text back.""" - with open(img_path, "rb") as f: - img_data = base64.standard_b64encode(f.read()).decode("utf-8") + from PIL import Image + import io + + with Image.open(img_path) as img: + # Resize if too large to fit in 10MB limit (usually > 4000x4000) + max_dim = 2800 + if max(img.width, img.height) > max_dim: + ratio = max_dim / max(img.width, img.height) + new_w = int(img.width * ratio) + new_h = int(img.height * ratio) + print(f" [Claude OCR] Resizing {img.width}x{img.height} to {new_w}x{new_h} to fit API limits") + img = img.resize((new_w, new_h), Image.LANCZOS) + + # Save to bytes + buffer = io.BytesIO() + img.save(buffer, format="PNG", optimize=True) + img_data = base64.standard_b64encode(buffer.getvalue()).decode("utf-8") response = client.messages.create( model=model, diff --git a/extractor.py b/extractor.py index 991e83e..4f6070f 100644 --- a/extractor.py +++ b/extractor.py @@ -334,103 +334,22 @@ def crop_full_articles(page_png_path, selected_ids, articles, out_dir, page_num) # --------------------------------------------------------------------------- # Stage 5 — OCR each cropped article (Telugu) # --------------------------------------------------------------------------- -_paddle_ocr_instance = None - -def _ocr_with_paddleocr(img_path): - """Use PaddleOCR for better reading-order-aware Telugu OCR.""" - global _paddle_ocr_instance - temp_path = None - try: - from PIL import Image - with Image.open(img_path) as im: - px = im.width * im.height - if px > 4000000: - # Instead of skipping, resize the article image to prevent PaddleOCR deadlock - ratio = (3800000 / px) ** 0.5 - new_w = int(im.width * ratio) - new_h = int(im.height * ratio) - print(f"Article image {img_path.name} is too large ({im.width}x{im.height} = {px}px). Resizing to {new_w}x{new_h} for safe PaddleOCR.") - resized_im = im.resize((new_w, new_h), Image.LANCZOS) - temp_path = img_path.parent / f"temp_resized_{img_path.name}" - resized_im.save(temp_path) - - import paddleocr - if _paddle_ocr_instance is None: - _paddle_ocr_instance = paddleocr.PaddleOCR(lang='te') - - path_to_ocr = str(temp_path) if temp_path else str(img_path) - result = _paddle_ocr_instance.predict(path_to_ocr) - - lines = [] - if isinstance(result, list) and len(result) > 0: - det_result = result[0] - # PaddleOCR v3.5 returns DetResult with 'rec_texts' or nested structure - rec_texts = None - if hasattr(det_result, 'get'): - rec_texts = det_result.get('rec_texts', None) - if rec_texts: - lines = list(rec_texts) - else: - # Try to extract from boxes/text pairs - boxes = det_result.get('dt_polys', []) if hasattr(det_result, 'get') else [] - texts = det_result.get('rec_texts', []) if hasattr(det_result, 'get') else [] - if texts: - # Sort by y-coordinate (top to bottom), then x (left to right) - # to get proper reading order - pairs = [] - for i, txt in enumerate(texts): - if i < len(boxes): - box = boxes[i] - if hasattr(box, 'tolist'): - box = box.tolist() - # Get top-left y coordinate for sorting - if isinstance(box, (list, tuple)) and len(box) >= 4: - y = min(box[j] for j in range(1, len(box), 2)) if len(box) >= 8 else box[1] - x = min(box[j] for j in range(0, len(box), 2)) if len(box) >= 8 else box[0] - else: - y, x = 0, 0 - pairs.append((y, x, txt)) - else: - pairs.append((0, 0, txt)) - - # Sort: primarily by y (row), then x (column within row) - # Group into rows based on y proximity - if pairs: - pairs.sort(key=lambda p: (p[0], p[1])) - row_threshold = 20 # pixels - rows = [] - current_row = [pairs[0]] - for p in pairs[1:]: - if abs(p[0] - current_row[0][0]) < row_threshold: - current_row.append(p) - else: - current_row.sort(key=lambda p: p[1]) # sort by x within row - rows.append(current_row) - current_row = [p] - current_row.sort(key=lambda p: p[1]) - rows.append(current_row) - - for row in rows: - lines.append(" ".join(p[2] for p in row)) - else: - # Last resort: try str representation - try: - s = str(det_result) - if len(s) > 10: - return s - except: - pass - - return "\n".join(lines) if lines else None - except Exception as e: - print(f"PaddleOCR text extraction failed: {e}") +def _ocr_with_claude(img_path): + """Use Claude Vision API for Telugu OCR as a fallback since PaddleOCR is missing.""" + import os + api_key = os.environ.get("ANTHROPIC_API_KEY") + if not api_key: + print("ANTHROPIC_API_KEY not set. Skipping OCR.") + return None + + try: + import anthropic + import claude_ocr + client = anthropic.Anthropic(api_key=api_key) + return claude_ocr.read_article_image(client, str(img_path)) + except Exception as e: + print(f"Claude text extraction failed: {e}") return None - finally: - if temp_path and temp_path.exists(): - try: - temp_path.unlink() - except: - pass def ocr_headlines(headline_records): @@ -439,7 +358,7 @@ def ocr_headlines(headline_records): if not img_path.exists(): rec["headline_text"] = "" continue - text = _ocr_with_paddleocr(img_path) + text = _ocr_with_claude(img_path) rec["headline_text"] = (text or "").strip() (img_path.parent / "headline.txt").write_text(rec["headline_text"], encoding="utf-8") @@ -450,12 +369,14 @@ def ocr_articles(article_records): if not img_path.exists(): continue - text = _ocr_with_paddleocr(img_path) + text = _ocr_with_claude(img_path) (art_dir / "article.txt").write_text(text or "", encoding="utf-8") info_path = art_dir / "info.json" if info_path.exists(): + import json info = json.loads(info_path.read_text(encoding="utf-8")) + info["ocr_method"] = "claude_vision" info_path.write_text(json.dumps(info, indent=2, ensure_ascii=False), encoding="utf-8") diff --git a/requirements.txt b/requirements.txt index ec25bf3..105e286 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,3 +8,5 @@ pytesseract>=0.3.10 anthropic>=0.40 surya-ocr==0.4.15 fpdf2>=2.7.0 +gunicorn>=21.0.0 +opencv-python>=4.8.0