Spaces:
Sleeping
Sleeping
| from __future__ import annotations | |
| import base64 | |
| import re | |
| import requests | |
| from langchain_core.tools import tool | |
| from app.services.vision.pipeline import ( | |
| process_image_hybrid, | |
| process_image_force_vision, | |
| ) | |
| from app.tools.scraper import _is_safe_url | |
| from app.utils.logger import get_logger | |
| logger = get_logger(__name__) | |
| MAX_IMAGE_DOWNLOAD = 20 * 1024 * 1024 | |
| def _decode_base64(data_url: str) -> bytes: | |
| match = re.match(r"data:image/[^;]+;base64,(.+)", data_url) | |
| if not match: | |
| raise ValueError("Invalid base64 image data URL") | |
| return base64.b64decode(match.group(1)) | |
| def _fetch_image(url: str) -> bytes: | |
| if not _is_safe_url(url): | |
| raise ValueError("Image URL is not allowed. Only public HTTP/HTTPS URLs are supported.") | |
| headers = { | |
| "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" | |
| } | |
| resp = requests.get(url, timeout=20, headers=headers, stream=True) | |
| resp.raise_for_status() | |
| content = b"" | |
| for chunk in resp.iter_content(chunk_size=8192): | |
| content += chunk | |
| if len(content) > MAX_IMAGE_DOWNLOAD: | |
| raise ValueError(f"Image too large (max {MAX_IMAGE_DOWNLOAD // 1024 // 1024}MB)") | |
| return content | |
| def analyze_image( | |
| image_source: str, | |
| query: str = None, | |
| force_vision: str = "false", | |
| ) -> str: | |
| """Analyze an image using OCR and optional vision model for complex scenes.""" | |
| try: | |
| if image_source.startswith("data:image"): | |
| image_bytes = _decode_base64(image_source) | |
| image_url = None | |
| elif image_source.startswith(("http://", "https://")): | |
| image_bytes = _fetch_image(image_source) | |
| image_url = image_source | |
| else: | |
| return "Error: image_source must be a valid URL or base64 data URL." | |
| force = force_vision.lower() == "true" | |
| if force: | |
| result = process_image_force_vision( | |
| image_bytes, | |
| image_url=image_url, | |
| custom_query=query, | |
| ) | |
| else: | |
| result = process_image_hybrid( | |
| image_bytes, | |
| image_url=image_url, | |
| custom_query=query, | |
| ) | |
| if result.complexity == "simple_text": | |
| return result.ocr_text if result.ocr_text else "No text found in image." | |
| else: | |
| output_parts = [] | |
| if result.vision_analysis and "failed" not in result.vision_analysis.lower(): | |
| output_parts.append(result.vision_analysis) | |
| if result.ocr_text and "cannot" not in result.ocr_text.lower(): | |
| output_parts.append(f"Text in image: {result.ocr_text}") | |
| return "\n\n".join(output_parts) if output_parts else "Unable to analyze image." | |
| except requests.RequestException: | |
| return "Failed to fetch image. The URL may be unreachable or the image too large." | |
| except ValueError: | |
| return "Image analysis failed. The image format or URL is not supported." | |
| except Exception: | |
| logger.exception("Image tool error") | |
| return "Image analysis failed. Please try again." | |