# MCP Vision OCR Module """ Vision and OCR tools for MCP integration. Features: - Text extraction from images - Table detection and extraction - Chart analysis and data extraction - Document structure recognition """ from typing import Dict, List, Optional, Any from dataclasses import dataclass import re import json @dataclass class ExtractedTable: """Extracted table from image""" headers: List[str] rows: List[List[str]] confidence: float location: Dict[str, int] # x, y, width, height @dataclass class ChartData: """Extracted data from chart""" chart_type: str title: Optional[str] x_axis_label: Optional[str] y_axis_label: Optional[str] data_points: List[Dict[str, Any]] legend: List[str] def extract_text_from_image( image_path: str, language: str = "en" ) -> Dict: """ Extract text from image using OCR. Args: image_path: Path to image file language: Language code for OCR Returns: Extracted text and metadata """ try: # Try using Gemini Vision for OCR from core.vision import analyze_image_with_gemini prompt = """Extract ALL text from this image. Output format: - Return ONLY the text content - Preserve the structure (headings, paragraphs, lists) - Include any numbers, dates, and special characters - If there are tables, format them as plain text with | separators""" result = analyze_image_with_gemini(image_path, prompt) if result and not result.startswith("⚠️"): return { "success": True, "text": result, "method": "gemini_vision", "language": language } # Fallback error return { "success": False, "error": result if result else "OCR extraction failed", "text": "" } except ImportError: return { "success": False, "error": "Vision module not available", "text": "" } except Exception as e: return { "success": False, "error": str(e), "text": "" } def extract_tables_from_image( image_path: str, output_format: str = "json" ) -> Dict: """ Extract tables from image. Args: image_path: Path to image file output_format: "json", "csv", or "markdown" Returns: Extracted tables """ try: from core.vision import analyze_image_with_gemini prompt = """Analyze this image and extract ALL tables. For each table found: 1. List the column headers 2. List all data rows 3. Preserve exact numbers and text Return as JSON array: [ { "headers": ["Col1", "Col2", ...], "rows": [ ["val1", "val2", ...], ... ] } ] If no tables found, return: []""" result = analyze_image_with_gemini(image_path, prompt) if not result or result.startswith("⚠️"): return { "success": False, "error": result if result else "Table extraction failed", "tables": [] } # Try to parse JSON from response tables = _parse_tables_from_response(result) # Convert to requested format if output_format == "csv": formatted_tables = [_table_to_csv(t) for t in tables] elif output_format == "markdown": formatted_tables = [_table_to_markdown(t) for t in tables] else: formatted_tables = tables return { "success": True, "tables": formatted_tables, "table_count": len(tables), "format": output_format } except Exception as e: return { "success": False, "error": str(e), "tables": [] } def _parse_tables_from_response(response: str) -> List[Dict]: """Parse table data from LLM response""" tables = [] # Try to find JSON array in response json_pattern = r'\[\s*\{.*?\}\s*\]' matches = re.findall(json_pattern, response, re.DOTALL) for match in matches: try: parsed = json.loads(match) if isinstance(parsed, list): tables.extend(parsed) else: tables.append(parsed) except json.JSONDecodeError: continue # If no JSON found, try to parse markdown tables if not tables: tables = _parse_markdown_tables(response) return tables def _parse_markdown_tables(text: str) -> List[Dict]: """Parse markdown-style tables from text""" tables = [] # Find table blocks (lines with |) lines = text.split('\n') current_table = [] for line in lines: if '|' in line: current_table.append(line) elif current_table: # End of table table = _parse_single_markdown_table(current_table) if table: tables.append(table) current_table = [] # Handle last table if current_table: table = _parse_single_markdown_table(current_table) if table: tables.append(table) return tables def _parse_single_markdown_table(lines: List[str]) -> Optional[Dict]: """Parse a single markdown table""" if len(lines) < 2: return None # Remove separator line (contains only |-:) content_lines = [l for l in lines if not re.match(r'^[\|\s\-:]+$', l)] if not content_lines: return None # Parse header headers = [cell.strip() for cell in content_lines[0].split('|') if cell.strip()] # Parse rows rows = [] for line in content_lines[1:]: cells = [cell.strip() for cell in line.split('|') if cell.strip()] if cells: rows.append(cells) return { "headers": headers, "rows": rows } def _table_to_csv(table: Dict) -> str: """Convert table dict to CSV string""" lines = [] headers = table.get("headers", []) rows = table.get("rows", []) if headers: lines.append(','.join(f'"{h}"' for h in headers)) for row in rows: lines.append(','.join(f'"{c}"' for c in row)) return '\n'.join(lines) def _table_to_markdown(table: Dict) -> str: """Convert table dict to markdown string""" lines = [] headers = table.get("headers", []) rows = table.get("rows", []) if headers: lines.append('| ' + ' | '.join(headers) + ' |') lines.append('| ' + ' | '.join(['---'] * len(headers)) + ' |') for row in rows: lines.append('| ' + ' | '.join(row) + ' |') return '\n'.join(lines) def analyze_chart( image_path: str, chart_type: Optional[str] = None ) -> Dict: """ Analyze chart image and extract data. Args: image_path: Path to chart image chart_type: Hint about chart type (bar, line, pie, etc.) Returns: Extracted chart data """ try: from core.vision import analyze_image_with_gemini type_hint = f"This appears to be a {chart_type} chart." if chart_type else "" prompt = f"""Analyze this chart/graph image and extract ALL data. {type_hint} Identify and extract: 1. **Chart Type**: (bar, line, pie, scatter, etc.) 2. **Title**: (if visible) 3. **X-Axis Label**: and categories/values 4. **Y-Axis Label**: and scale 5. **Data Points**: Extract EVERY visible data point with exact values 6. **Legend**: (if present) Return as JSON: {{ "chart_type": "...", "title": "...", "x_axis_label": "...", "y_axis_label": "...", "data_series": [ {{ "name": "Series Name", "data": [ {{"x": "Category1", "y": 100}}, {{"x": "Category2", "y": 200}} ] }} ], "legend": ["Series1", "Series2"] }} Be precise with all numbers. Do not estimate or approximate visible values.""" result = analyze_image_with_gemini(image_path, prompt) if not result or result.startswith("⚠️"): return { "success": False, "error": result if result else "Chart analysis failed", "chart_data": None } # Try to parse JSON chart_data = _parse_chart_data(result) return { "success": True, "chart_data": chart_data, "raw_analysis": result } except Exception as e: return { "success": False, "error": str(e), "chart_data": None } def _parse_chart_data(response: str) -> Dict: """Parse chart data from LLM response""" # Try to find JSON in response json_pattern = r'\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}' matches = re.findall(json_pattern, response, re.DOTALL) for match in matches: try: parsed = json.loads(match) if "chart_type" in parsed or "data_series" in parsed: return parsed except json.JSONDecodeError: continue # If no JSON, return structured text return { "raw_text": response, "parsed": False } def extract_document_structure(image_path: str) -> Dict: """ Extract document structure from image. Args: image_path: Path to document image Returns: Document structure (headings, sections, etc.) """ try: from core.vision import analyze_image_with_gemini prompt = """Analyze this document image and extract its structure. Identify: 1. **Headings/Titles**: With their hierarchy levels (H1, H2, H3) 2. **Paragraphs**: Brief summary of each 3. **Lists**: Bullet or numbered items 4. **Tables**: If present 5. **Images/Figures**: If present 6. **Key Data Points**: Important numbers, dates, names Return as JSON: { "title": "Document Title", "sections": [ { "heading": "Section Name", "level": 1, "content_summary": "Brief description", "has_table": false, "has_image": false } ], "key_data": [ {"type": "date", "value": "2024-01-15"}, {"type": "amount", "value": "$1,000"} ] }""" result = analyze_image_with_gemini(image_path, prompt) if not result or result.startswith("⚠️"): return { "success": False, "error": result if result else "Structure extraction failed", "structure": None } # Try to parse JSON structure = _parse_document_structure(result) return { "success": True, "structure": structure, "raw_analysis": result } except Exception as e: return { "success": False, "error": str(e), "structure": None } def _parse_document_structure(response: str) -> Dict: """Parse document structure from LLM response""" # Try to find JSON json_pattern = r'\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}' matches = re.findall(json_pattern, response, re.DOTALL) for match in matches: try: parsed = json.loads(match) if "sections" in parsed or "title" in parsed: return parsed except json.JSONDecodeError: continue return { "raw_text": response, "parsed": False } def vision_to_rag_context( image_path: str, question: str ) -> Dict: """ Process image and prepare context for RAG pipeline. Args: image_path: Path to image question: User's question about the image Returns: Structured context for RAG """ try: # Extract text text_result = extract_text_from_image(image_path) # Extract tables table_result = extract_tables_from_image(image_path, output_format="json") # Build context context_parts = [] if text_result.get("success") and text_result.get("text"): context_parts.append(f"## Extracted Text\n{text_result['text']}") if table_result.get("success") and table_result.get("tables"): context_parts.append("## Extracted Tables") for i, table in enumerate(table_result["tables"]): md_table = _table_to_markdown(table) context_parts.append(f"### Table {i+1}\n{md_table}") context = "\n\n".join(context_parts) if context_parts else "No content extracted from image." return { "success": True, "context": context, "has_text": bool(text_result.get("text")), "table_count": len(table_result.get("tables", [])), "ready_for_rag": bool(context_parts) } except Exception as e: return { "success": False, "error": str(e), "context": "" }