chore: add gunicorn and opencv-python to requirements
This commit is contained in:
parent
8f69c0fddc
commit
65c1501d89
|
|
@ -18,8 +18,23 @@ OUTPUT_DIR = Path(__file__).parent / "output"
|
||||||
|
|
||||||
def read_article_image(client, img_path, model="claude-sonnet-4-6"):
|
def read_article_image(client, img_path, model="claude-sonnet-4-6"):
|
||||||
"""Send article image to Claude and get Telugu text back."""
|
"""Send article image to Claude and get Telugu text back."""
|
||||||
with open(img_path, "rb") as f:
|
from PIL import Image
|
||||||
img_data = base64.standard_b64encode(f.read()).decode("utf-8")
|
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(
|
response = client.messages.create(
|
||||||
model=model,
|
model=model,
|
||||||
|
|
|
||||||
117
extractor.py
117
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)
|
# Stage 5 — OCR each cropped article (Telugu)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
_paddle_ocr_instance = None
|
def _ocr_with_claude(img_path):
|
||||||
|
"""Use Claude Vision API for Telugu OCR as a fallback since PaddleOCR is missing."""
|
||||||
def _ocr_with_paddleocr(img_path):
|
import os
|
||||||
"""Use PaddleOCR for better reading-order-aware Telugu OCR."""
|
api_key = os.environ.get("ANTHROPIC_API_KEY")
|
||||||
global _paddle_ocr_instance
|
if not api_key:
|
||||||
temp_path = None
|
print("ANTHROPIC_API_KEY not set. Skipping OCR.")
|
||||||
try:
|
return None
|
||||||
from PIL import Image
|
|
||||||
with Image.open(img_path) as im:
|
try:
|
||||||
px = im.width * im.height
|
import anthropic
|
||||||
if px > 4000000:
|
import claude_ocr
|
||||||
# Instead of skipping, resize the article image to prevent PaddleOCR deadlock
|
client = anthropic.Anthropic(api_key=api_key)
|
||||||
ratio = (3800000 / px) ** 0.5
|
return claude_ocr.read_article_image(client, str(img_path))
|
||||||
new_w = int(im.width * ratio)
|
except Exception as e:
|
||||||
new_h = int(im.height * ratio)
|
print(f"Claude text extraction failed: {e}")
|
||||||
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}")
|
|
||||||
return None
|
return None
|
||||||
finally:
|
|
||||||
if temp_path and temp_path.exists():
|
|
||||||
try:
|
|
||||||
temp_path.unlink()
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def ocr_headlines(headline_records):
|
def ocr_headlines(headline_records):
|
||||||
|
|
@ -439,7 +358,7 @@ def ocr_headlines(headline_records):
|
||||||
if not img_path.exists():
|
if not img_path.exists():
|
||||||
rec["headline_text"] = ""
|
rec["headline_text"] = ""
|
||||||
continue
|
continue
|
||||||
text = _ocr_with_paddleocr(img_path)
|
text = _ocr_with_claude(img_path)
|
||||||
rec["headline_text"] = (text or "").strip()
|
rec["headline_text"] = (text or "").strip()
|
||||||
(img_path.parent / "headline.txt").write_text(rec["headline_text"], encoding="utf-8")
|
(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():
|
if not img_path.exists():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
text = _ocr_with_paddleocr(img_path)
|
text = _ocr_with_claude(img_path)
|
||||||
(art_dir / "article.txt").write_text(text or "", encoding="utf-8")
|
(art_dir / "article.txt").write_text(text or "", encoding="utf-8")
|
||||||
|
|
||||||
info_path = art_dir / "info.json"
|
info_path = art_dir / "info.json"
|
||||||
if info_path.exists():
|
if info_path.exists():
|
||||||
|
import json
|
||||||
info = json.loads(info_path.read_text(encoding="utf-8"))
|
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")
|
info_path.write_text(json.dumps(info, indent=2, ensure_ascii=False), encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,3 +8,5 @@ pytesseract>=0.3.10
|
||||||
anthropic>=0.40
|
anthropic>=0.40
|
||||||
surya-ocr==0.4.15
|
surya-ocr==0.4.15
|
||||||
fpdf2>=2.7.0
|
fpdf2>=2.7.0
|
||||||
|
gunicorn>=21.0.0
|
||||||
|
opencv-python>=4.8.0
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue