fixmyneighborhood-app / core /image_analysis.py
tan-en-yao's picture
feat: add security, observability layers and update documentation
6062397
Raw
History Blame Contribute Delete
3.51 kB
"""Image analysis using Claude Vision.
Provides infrastructure image analysis for the FixMyNeighborhood app.
"""
import base64
from typing import Optional
import anthropic
from config import ANTHROPIC_API_KEY
# Initialize Claude client for image analysis
_claude_client: Optional[anthropic.Anthropic] = None
def get_claude_client() -> Optional[anthropic.Anthropic]:
"""Get or create the Claude client for image analysis."""
global _claude_client
if _claude_client is None and ANTHROPIC_API_KEY:
try:
_claude_client = anthropic.Anthropic(api_key=ANTHROPIC_API_KEY)
print("Claude client initialized for image analysis")
except Exception as e:
print(f"Claude client error: {e}")
return _claude_client
class ImageAnalyzer:
"""
Analyzes infrastructure images using Claude Vision.
Provides concise analysis of:
- Issue type (pothole, streetlight, drain, etc.)
- Severity assessment
- Safety hazard evaluation
"""
ANALYSIS_PROMPT = (
"Describe this NYC infrastructure issue. What type of issue is it? "
"How severe does it appear? Is it a safety hazard? Be concise."
)
def __init__(self, client: anthropic.Anthropic = None):
self.client = client or get_claude_client()
def analyze(self, image_path: str) -> Optional[str]:
"""
Analyze an infrastructure image.
Args:
image_path: Path to the uploaded image
Returns:
Analysis text or None if failed
"""
if not self.client or not image_path:
return None
try:
with open(image_path, "rb") as f:
data = base64.standard_b64encode(f.read()).decode("utf-8")
media_type = self._get_media_type(image_path)
response = self.client.messages.create(
model="claude-haiku-4-5-20251001", # Cost-optimized
max_tokens=500,
messages=[{
"role": "user",
"content": [
{
"type": "image",
"source": {
"type": "base64",
"media_type": media_type,
"data": data
}
},
{
"type": "text",
"text": self.ANALYSIS_PROMPT
}
]
}]
)
return response.content[0].text
except Exception as e:
print(f"Vision analysis error: {e}")
return None
def _get_media_type(self, image_path: str) -> str:
"""Determine media type from file extension."""
path_lower = image_path.lower()
if path_lower.endswith(".png"):
return "image/png"
elif path_lower.endswith(".gif"):
return "image/gif"
elif path_lower.endswith(".webp"):
return "image/webp"
return "image/jpeg"
# Convenience function for backwards compatibility
def analyze_image(image_path: str) -> Optional[str]:
"""
Analyze an infrastructure image using Claude Vision.
Args:
image_path: Path to the uploaded image
Returns:
Analysis text or None if failed
"""
analyzer = ImageAnalyzer()
return analyzer.analyze(image_path)