testtest123's picture
fix: install tesseract-ocr-eng in Dockerfile and add grayscale contrast preprocessing for image OCR
d61e4dd
Raw
History Blame Contribute Delete
6.39 kB
import json
import re
from pathlib import Path
from typing import List, Dict, Any
from docx import Document
from .ocr_service import ocr_service
from .vision_service import vision_service
import logging
logger = logging.getLogger(__name__)
async def parse_file(file_path: str, file_type: str, job_id: str, ws_manager: Any, user_id: str) -> List[Dict[str, Any]]:
"""
Dispatcher for all file types.
Returns: [{"text": str, "metadata": {"source", "page", "section"}}]
"""
match file_type.lower():
case "pdf":
return await _parse_pdf(file_path, job_id, ws_manager, user_id)
case "jpg" | "jpeg" | "png":
return await _parse_image(file_path, job_id, ws_manager, user_id)
case "docx":
return _parse_docx(file_path)
case "txt":
return _parse_txt(file_path)
case "md":
return _parse_markdown(file_path)
case "json":
return _parse_json(file_path)
case _:
logger.warning(f"Unsupported file type: {file_type}")
return []
async def _parse_pdf(file_path: str, job_id: str, ws_manager: Any, user_id: str):
try:
await ws_manager.emit(job_id, user_id, {"step": "OCR_START", "color": "#8B5CF6", "detail": "Sending to Mistral OCR (Primary)..."})
results = await ocr_service.perform_ocr(file_path)
return [{"text": results['plain_text'], "metadata": {"source": Path(file_path).name, "page": 1}}]
except Exception as e:
logger.warning(f"Mistral OCR failed, falling back to PyMuPDF: {e}")
await ws_manager.emit(job_id, user_id, {"step": "FALLBACK", "color": "#F59E0B", "detail": "Mistral failed. Falling back to PyMuPDF..."})
import fitz # PyMuPDF
doc = fitz.open(file_path)
text = ""
for page in doc:
text += page.get_text()
return [{"text": text, "metadata": {"source": Path(file_path).name, "page": 1}}]
async def _parse_image(file_path: str, job_id: str, ws_manager: Any, user_id: str):
filename = Path(file_path).name
extracted_content = []
# 1. Tesseract OCR (Fast, Reliable Local OCR with grayscale preprocessing)
ocr_text = ""
try:
import pytesseract
from PIL import Image, ImageOps
img = Image.open(file_path)
# Convert to grayscale and enhance contrast for reliable OCR
gray_img = ImageOps.grayscale(img)
if gray_img.width < 1000:
gray_img = gray_img.resize((gray_img.width * 2, gray_img.height * 2), Image.Resampling.BILINEAR)
ocr_text = pytesseract.image_to_string(gray_img).strip()
if not ocr_text:
ocr_text = pytesseract.image_to_string(img).strip()
if ocr_text:
logger.info(f"OCR extracted {len(ocr_text)} characters from {filename}")
extracted_content.append(f"Visual Text Content Extracted via OCR:\n{ocr_text}")
except Exception as e:
logger.warning(f"Tesseract OCR failed: {e}")
# 2. Vision Space Analysis (with non-blocking 4s timeout)
try:
await ws_manager.emit(job_id, user_id, {"step": "IMAGE_ANALYZE", "color": "#8B5CF6", "detail": "Analyzing visual image contents..."})
import asyncio
description = await asyncio.wait_for(
asyncio.to_thread(vision_service.understand_image, file_path),
timeout=4.0
)
if description and "failed to understand" not in description.lower():
extracted_content.append(f"Visual Scene Description:\n{description}")
except Exception as e:
logger.warning(f"Vision space analysis skipped/timed out: {e}")
# 3. Fallback to image metadata if no text or vision
if not extracted_content:
try:
from PIL import Image
img = Image.open(file_path)
extracted_content.append(f"Image Document: {filename}\nResolution: {img.width}x{img.height}\nFormat: {img.format}\nMode: {img.mode}\nStatus: Visual image document indexed in knowledge base.")
except Exception:
extracted_content.append(f"Image Document: {filename}\nStatus: Visual image document indexed in knowledge base.")
final_text = "\n\n".join(extracted_content)
return [{"text": final_text, "metadata": {"source": filename, "page": 1}}]
def _parse_docx(file_path: str):
doc = Document(file_path)
sections, current_heading, current_text = [], "General", []
for para in doc.paragraphs:
if para.style.name.startswith('Heading'):
if current_text:
sections.append({"text": "\n".join(current_text), "metadata": {"source": Path(file_path).name, "section": current_heading}})
current_heading, current_text = para.text, []
elif para.text.strip():
current_text.append(para.text)
if current_text:
sections.append({"text": "\n".join(current_text), "metadata": {"source": Path(file_path).name, "section": current_heading}})
return sections
def _parse_markdown(file_path: str):
text = Path(file_path).read_text(encoding="utf-8")
parts = re.split(r'\n(?=#+\s)', text)
docs = []
for p in parts:
if not p.strip(): continue
match = re.match(r'^#+\s+(.*)', p)
section = match.group(1) if match else "General"
docs.append({"text": p.strip(), "metadata": {"source": Path(file_path).name, "section": section}})
return docs
def _parse_txt(file_path: str):
text = Path(file_path).read_text(encoding="utf-8")
paragraphs = [p.strip() for p in text.split("\n\n") if p.strip()]
return [{"text": p, "metadata": {"source": Path(file_path).name}} for p in paragraphs]
def _parse_json(file_path: str):
data = json.loads(Path(file_path).read_text())
docs = []
# If it's a list, treat each item as a doc
if isinstance(data, list):
items = data
# If it's a dict, treat each top-level key-value pair as a doc
elif isinstance(data, dict):
items = [{"key": k, "value": v} for k, v in data.items()]
else:
items = [data]
for item in items:
if isinstance(item, (dict, list)):
text = json.dumps(item, indent=2)
else:
text = str(item)
if text.strip():
docs.append({"text": text, "metadata": {"source": Path(file_path).name}})
return docs