|
|
|
|
|
|
|
|
| import torch
|
| from PIL import Image
|
| import io
|
| import base64
|
| import requests
|
| import re
|
|
|
|
|
|
|
|
|
| try:
|
| from transformers import BlipProcessor, BlipForConditionalGeneration
|
| BLIP_AVAILABLE = True
|
| except ImportError:
|
| BLIP_AVAILABLE = False
|
| print("Warning: BLIP not available. Install with: pip install transformers")
|
|
|
|
|
| try:
|
| from transformers import AutoProcessor, AutoModelForCausalLM
|
| FLORENCE_AVAILABLE = True
|
| except ImportError:
|
| FLORENCE_AVAILABLE = False
|
|
|
|
|
| try:
|
| from transformers import OFATokenizer, OFAModel
|
| OFA_AVAILABLE = True
|
| except ImportError:
|
| OFA_AVAILABLE = False
|
|
|
|
|
| try:
|
| from transformers import GitProcessor, GitForCausalLM
|
| GIT_AVAILABLE = True
|
| except ImportError:
|
| GIT_AVAILABLE = False
|
|
|
|
|
| class VisionModel:
|
| """
|
| Multi-model vision processor with automatic fallback.
|
| Tries models in order: BLIP -> Florence-2 -> GIT -> Fallback text analysis
|
| """
|
|
|
| def __init__(self):
|
| self.device = "cuda" if torch.cuda.is_available() else "cpu"
|
| print(f"🖼️ Initializing Vision Model on {self.device}...")
|
|
|
| self.models = {}
|
| self.current_model = None
|
|
|
|
|
| if BLIP_AVAILABLE:
|
| try:
|
| print(" Loading BLIP model...")
|
| self.models['blip'] = {
|
| 'processor': BlipProcessor.from_pretrained("Salesforce/blip-image-captioning-base"),
|
| 'model': BlipForConditionalGeneration.from_pretrained(
|
| "Salesforce/blip-image-captioning-base",
|
| torch_dtype=torch.float16 if self.device == "cuda" else torch.float32
|
| ).to(self.device),
|
| 'name': 'BLIP'
|
| }
|
| self.models['blip']['model'].eval()
|
| print(" ✓ BLIP model loaded")
|
| self.current_model = 'blip'
|
| except Exception as e:
|
| print(f" ✗ Failed to load BLIP: {e}")
|
|
|
|
|
| if FLORENCE_AVAILABLE and not self.current_model:
|
| try:
|
| print(" Loading Florence-2 model...")
|
| self.models['florence'] = {
|
| 'processor': AutoProcessor.from_pretrained("microsoft/florence-2-base", trust_remote_code=True),
|
| 'model': AutoModelForCausalLM.from_pretrained(
|
| "microsoft/florence-2-base",
|
| trust_remote_code=True,
|
| torch_dtype=torch.float16 if self.device == "cuda" else torch.float32
|
| ).to(self.device),
|
| 'name': 'Florence-2'
|
| }
|
| self.models['florence']['model'].eval()
|
| print(" ✓ Florence-2 model loaded")
|
| self.current_model = 'florence'
|
| except Exception as e:
|
| print(f" ✗ Failed to load Florence-2: {e}")
|
|
|
|
|
| if GIT_AVAILABLE and not self.current_model:
|
| try:
|
| print(" Loading GIT model...")
|
| self.models['git'] = {
|
| 'processor': GitProcessor.from_pretrained("microsoft/git-base"),
|
| 'model': GitForCausalLM.from_pretrained(
|
| "microsoft/git-base",
|
| torch_dtype=torch.float16 if self.device == "cuda" else torch.float32
|
| ).to(self.device),
|
| 'name': 'GIT'
|
| }
|
| self.models['git']['model'].eval()
|
| print(" ✓ GIT model loaded")
|
| self.current_model = 'git'
|
| except Exception as e:
|
| print(f" ✗ Failed to load GIT: {e}")
|
|
|
| if not self.current_model:
|
| print("⚠️ No vision model loaded. Using fallback analysis.")
|
| self.current_model = None
|
|
|
| def get_vision_caption(self, image_bytes, max_length=100):
|
| """
|
| Generate a natural description of the image content.
|
| Returns a clean description without metadata.
|
| """
|
| if not self.current_model:
|
| return None
|
|
|
| try:
|
|
|
| image = Image.open(io.BytesIO(image_bytes)).convert('RGB')
|
|
|
|
|
| if self.current_model == 'blip':
|
| return self._caption_with_blip(image, max_length)
|
| elif self.current_model == 'florence':
|
| return self._caption_with_florence(image, max_length)
|
| elif self.current_model == 'git':
|
| return self._caption_with_git(image, max_length)
|
| else:
|
| return None
|
|
|
| except Exception as e:
|
| print(f"Error generating vision caption with {self.current_model}: {e}")
|
|
|
| return self._try_fallback_model(image_bytes, max_length)
|
|
|
| def _caption_with_blip(self, image, max_length):
|
| """Generate caption using BLIP"""
|
| processor = self.models['blip']['processor']
|
| model = self.models['blip']['model']
|
|
|
| inputs = processor(images=image, return_tensors="pt").to(self.device)
|
|
|
| with torch.no_grad():
|
| out = model.generate(
|
| **inputs,
|
| max_length=max_length,
|
| num_beams=3,
|
| temperature=0.7,
|
| do_sample=True
|
| )
|
|
|
| caption = processor.decode(out[0], skip_special_tokens=True)
|
| return self._clean_caption(caption)
|
|
|
| def _caption_with_florence(self, image, max_length):
|
| """Generate detailed caption using Florence-2"""
|
| processor = self.models['florence']['processor']
|
| model = self.models['florence']['model']
|
|
|
| prompt = "<MORE_DETAILED_CAPTION>"
|
| inputs = processor(text=prompt, images=image, return_tensors="pt").to(self.device)
|
|
|
| with torch.no_grad():
|
| generated_ids = model.generate(
|
| **inputs,
|
| max_new_tokens=max_length,
|
| do_sample=False,
|
| num_beams=3
|
| )
|
|
|
| generated_text = processor.batch_decode(generated_ids, skip_special_tokens=True)[0]
|
|
|
| generated_text = generated_text.replace(prompt, "").strip()
|
| return self._clean_caption(generated_text)
|
|
|
| def _caption_with_git(self, image, max_length):
|
| """Generate caption using GIT"""
|
| processor = self.models['git']['processor']
|
| model = self.models['git']['model']
|
|
|
| inputs = processor(images=image, return_tensors="pt").to(self.device)
|
|
|
| with torch.no_grad():
|
| generated_ids = model.generate(
|
| pixel_values=inputs.pixel_values,
|
| max_length=max_length,
|
| num_beams=3,
|
| temperature=0.7
|
| )
|
|
|
| caption = processor.batch_decode(generated_ids, skip_special_tokens=True)[0]
|
| return self._clean_caption(caption)
|
|
|
| def _try_fallback_model(self, image_bytes, max_length):
|
| """Try to use a different model if the current one fails"""
|
| original_model = self.current_model
|
| available_models = list(self.models.keys())
|
|
|
| for model_name in available_models:
|
| if model_name != original_model:
|
| print(f" Trying fallback model: {model_name}")
|
| self.current_model = model_name
|
| try:
|
| result = self.get_vision_caption(image_bytes, max_length)
|
| if result:
|
| print(f" ✓ Fallback to {model_name} successful")
|
| return result
|
| except Exception as e:
|
| print(f" ✗ Fallback to {model_name} failed: {e}")
|
| continue
|
|
|
|
|
| self.current_model = original_model
|
| return None
|
|
|
| def _clean_caption(self, caption):
|
| """Clean the caption by removing metadata and markdown"""
|
| if not caption:
|
| return None
|
|
|
|
|
| patterns_to_remove = [
|
| r'Photo by\s+\w+',
|
| r'©\s+\d{4}\s+\w+',
|
| r'Image courtesy of\s+\w+',
|
| r'Sourced from\s+\w+',
|
| r'Image from\s+\w+',
|
| r'Source:\s*\w+',
|
| r'\(Photo credit:.*?\)',
|
| r'\[.*?\]',
|
| r'^\w+:\s*',
|
| r'\*\*|\*|__|_',
|
| ]
|
|
|
| cleaned = caption
|
| for pattern in patterns_to_remove:
|
| cleaned = re.sub(pattern, '', cleaned, flags=re.IGNORECASE)
|
|
|
|
|
| cleaned = re.sub(r'\s+', ' ', cleaned)
|
|
|
|
|
| if cleaned and len(cleaned) > 0:
|
| cleaned = cleaned[0].upper() + cleaned[1:] if cleaned[1:] else cleaned
|
|
|
|
|
| cleaned = re.sub(r'\s*[|;:]\s*$', '', cleaned)
|
|
|
| return cleaned.strip()
|
|
|
| def analyze_image(self, image_bytes):
|
| """
|
| Generate a comprehensive, clean analysis of the image.
|
| Returns only the image content description, no metadata.
|
| """
|
| caption = self.get_vision_caption(image_bytes, max_length=120)
|
|
|
| if caption and len(caption) > 10:
|
|
|
|
|
| caption = re.sub(r'^(a|an)\s+(photo|picture|image)\s+of\s+', '', caption, flags=re.IGNORECASE)
|
| return caption
|
|
|
| return None
|
|
|
|
|
|
|
| _vision_model = None
|
|
|
| def get_vision_model():
|
| """Get or create the global vision model instance"""
|
| global _vision_model
|
| if _vision_model is None:
|
| _vision_model = VisionModel()
|
| return _vision_model |