Note: Docling generates image descriptions during the conversion step when do_picture_description=True is enabled. The analyze_image_with_docling() method retrieves these pre-generated descriptions.
Vision Analyzer Service
Core Methods
Extract Images
Code
def extract_images_from_document( self, docling_doc: DoclingDocument) -> list[ExtractedImage]: """Extract all images from a DoclingDocument. Returns: List of ExtractedImage objects with: - image_id: Unique identifier - pil_image: PIL Image object - page_number: Page location - bbox: Bounding box coordinates - caption: Document caption (if any) - existing_description: Docling description (if any) """
Analyze with Vision Model
Code
async def analyze_image_with_vision_model( self, pil_image: Image.Image, prompt: Optional[str] = None) -> Optional[str]: """Analyze image using configured vision model. Process: 1. Convert PIL image to base64 data URL 2. Prepare OpenAI-compatible request 3. Call vision API 4. Return description or None on failure Timeout: 60 seconds """
Smart Analysis
Code
async def analyze_image( self, extracted_image: ExtractedImage, force_vision_model: bool = False, custom_prompt: Optional[str] = None) -> ImageAnalysisResult: """Analyze with automatic method selection. Priority: 1. Vision model (if configured and available) 2. Docling description (pre-generated during conversion) 3. Basic metadata (fallback) Returns: ImageAnalysisResult with description, method, and metadata """
Docling Fallback
Code
def analyze_image_with_docling( self, extracted_image: ExtractedImage) -> Optional[str]: """Return Docling's pre-generated image description from conversion. NOTE: This requires do_picture_description=True in the Docling pipeline options. If not enabled during conversion, this will always return None. The description is generated by Docling's built-in vision model (SmolDocling) during the document conversion step, not on-demand. """
Direct extraction: Via Docling's get_image() method
Base64 Encoding
Code
def _pil_to_data_url(pil_image: Image.Image) -> str: """Convert PIL image to data URL. - PNG format for RGBA images - JPEG format for RGB images - Automatic mode conversion Returns: data:image/{type};base64,{data} """
async with httpx.AsyncClient(timeout=60.0) as client: response = await client.post(...)
Adjust timeout based on:
Image size
Vision model speed
Network conditions
Custom Analysis Prompts
Default Prompt
Code
DEFAULT_PROMPT = """Analyze this image in detail. Describe what you see, including:1. Main objects, people, or elements visible2. Text visible in the image (if any)3. Charts, diagrams, or data visualizations (if any)4. Overall context and purpose of the image5. Any relevant details that would help understand the documentProvide a comprehensive description suitable for document retrieval and understanding."""
Custom Prompts by Use Case
Business Charts
Code
CHART_PROMPT = """Analyze this business chart. Provide:1. Chart type (bar, line, pie, scatter, etc.)2. Title and axis labels3. Key data trends and patterns4. Notable data points or anomalies5. Overall business insightsFormat for easy text search and retrieval."""
Technical Diagrams
Code
DIAGRAM_PROMPT = """Analyze this technical diagram. Include:1. Diagram type (flowchart, architecture, schema, etc.)2. Components and their relationships3. Data flow or process steps4. Technical specifications visible5. Purpose and contextDescribe for technical documentation retrieval."""
Text-Heavy Images
Code
TEXT_PROMPT = """Extract and analyze text from this image:1. All visible text (OCR)2. Document type and format3. Layout and structure4. Key information and data5. Context and purposeOptimize for text search and citation."""
Implementing Custom Prompts
Code
# In your coderesult = await analyzer.analyze_image( extracted_image, custom_prompt=CHART_PROMPT)
Storage Integration
Image Chunk Structure
Code
image_chunk = DocumentChunk( id=f"{doc_id}_image_{idx}", document_id=doc_id, content=f"[Image Analysis]\n{analysis.description}", embedding=embedded_vector, chunk_index=1000 + idx, # Separate from text chunks metadata={ "type": "image_analysis", "image_id": analysis.image_id, "analysis_method": analysis.analysis_method, "page_number": extracted_image.page_number, "bbox": extracted_image.bbox, "caption": extracted_image.caption })
def preprocess_image(pil_image: Image.Image) -> Image.Image: """Optimize image before vision API call.""" # Resize large images max_size = (1024, 1024) if pil_image.size > max_size: pil_image.thumbnail(max_size, Image.Resampling.LANCZOS) # Convert mode for compatibility if pil_image.mode == "RGBA": pil_image = pil_image.convert("RGB") return pil_image
Batch Processing
Code
# Images are processed concurrently, controlled by VISION_MAX_CONCURRENT (default 2).# The built-in analyze_all_images() uses asyncio.gather with a global semaphore.# Example of the same pattern for custom usage:async def analyze_images_batch( images: list[ExtractedImage], max_concurrent: int = 2) -> list[ImageAnalysisResult]: """Process images with controlled concurrency.""" semaphore = asyncio.Semaphore(max_concurrent) async def analyze_with_limit(img): async with semaphore: return await analyzer.analyze_image(img) return await asyncio.gather(*[ analyze_with_limit(img) for img in images ])
from sentry_sdk import capture_exceptiontry: result = await analyzer.analyze_image(img)except Exception as e: logger.error(f"Image analysis failed: {e}") capture_exception(e) # Fall back to basic metadata result = create_fallback_result(img)
Testing
Unit Tests
Code
import pytestfrom PIL import Image@pytest.mark.asyncioasync def test_analyze_image_with_vision_model(): analyzer = VisionAnalyzer() # Create test image test_image = Image.new('RGB', (100, 100), color='red') result = await analyzer.analyze_image_with_vision_model( test_image, prompt="What color is this image?" ) assert result is not None assert "red" in result.lower()
Integration Tests
Code
@pytest.mark.asyncioasync def test_document_processing_with_images(): processor = DocumentProcessor() # Process document with images doc_id = await processor.process_file( file_path="test_document.pdf", filename="test.pdf", file_size=1024 ) # Verify image chunks created chunks = neo4j.get_document_chunks(doc_id) image_chunks = [c for c in chunks if c.metadata.get('type') == 'image_analysis'] assert len(image_chunks) > 0
class MultiModelAnalyzer(VisionAnalyzer): """Use different models for different image types.""" async def analyze_image( self, extracted_image: ExtractedImage, **kwargs ) -> ImageAnalysisResult: # Detect image type image_type = self.classify_image(extracted_image.pil_image) # Select appropriate model if image_type == "chart": model = "gpt-4o" elif image_type == "text": model = "claude-3-5-sonnet" else: model = "llava" # Use selected model return await self.analyze_with_model( extracted_image, model=model, **kwargs )