smartlearn / src /core /multimodal.py
Teja Chowdary
Fix all problematic imports: fitz, PIL, cv2 - use conditional imports and mock functions for cloud compatibility
11aefa0
Raw
History Blame Contribute Delete
25.3 kB
"""
Multimodal integration for SmartLearn AI.
Handles image, audio, and video processing and generation.
"""
import os
import json
import base64
from typing import List, Dict, Any, Optional, Tuple, Union
from dataclasses import dataclass
from pathlib import Path
import numpy as np
from datetime import datetime
# Conditional imports for cloud compatibility
try:
from PIL import Image, ImageDraw, ImageFont
PIL_AVAILABLE = True
except ImportError:
PIL_AVAILABLE = False
print("⚠️ PIL not available - image processing will be limited")
try:
import cv2
CV2_AVAILABLE = True
except ImportError:
CV2_AVAILABLE = False
print("⚠️ OpenCV not available - image processing will be limited")
import torch
from transformers import (
VisionEncoderDecoderModel, ViTImageProcessor, AutoTokenizer,
AutoProcessor, AutoModelForVision2Seq,
pipeline
)
# Optional imports with fallbacks
try:
import whisper
WHISPER_AVAILABLE = True
except ImportError:
WHISPER_AVAILABLE = False
print("⚠️ Whisper not available - audio transcription will be limited")
try:
from moviepy.editor import VideoFileClip, AudioFileClip
MOVIEPY_AVAILABLE = True
except ImportError:
MOVIEPY_AVAILABLE = False
print("⚠️ MoviePy not available - video processing will be limited")
try:
import librosa
LIBROSA_AVAILABLE = True
except ImportError:
LIBROSA_AVAILABLE = False
print("⚠️ Librosa not available - audio analysis will be limited")
try:
import matplotlib.pyplot as plt
import seaborn as sns
PLOTTING_AVAILABLE = True
except ImportError:
PLOTTING_AVAILABLE = False
print("⚠️ Matplotlib/Seaborn not available - plotting will be limited")
@dataclass
class MultimodalContent:
"""Represents multimodal content with metadata."""
content_type: str # "image", "audio", "video", "text"
content: Any
metadata: Dict[str, Any]
source: str
timestamp: str
class ImageProcessor:
"""Handles image processing and analysis."""
def __init__(self):
self.image_processor = None
self.image_model = None
self.caption_model = None
self.caption_tokenizer = None
self._load_models()
def _load_models(self):
"""Load image processing models."""
try:
# Load image captioning model
model_name = "nlpconnect/vit-gpt2-image-captioning"
self.caption_model = VisionEncoderDecoderModel.from_pretrained(model_name)
self.caption_tokenizer = AutoTokenizer.from_pretrained(model_name)
self.image_processor = ViTImageProcessor.from_pretrained(model_name)
print("✅ Image models loaded successfully")
except Exception as e:
print(f"⚠️ Warning: Could not load image models: {e}")
print("Image processing will be limited")
def process_image(self, image_path: str) -> MultimodalContent:
"""Process an image and extract information."""
if not PIL_AVAILABLE:
print(f"⚠️ PIL not available - cannot process image: {image_path}")
return MultimodalContent(
content_type="image",
content=None,
metadata={"error": "PIL not available"},
source=image_path,
timestamp=str(datetime.now())
)
try:
# Load image
image = Image.open(image_path)
# Generate caption
caption = self._generate_caption(image)
# Extract text using OCR (if available)
extracted_text = self._extract_text_from_image(image)
# Analyze image content
analysis = self._analyze_image_content(image)
metadata = {
"caption": caption,
"extracted_text": extracted_text,
"analysis": analysis,
"size": image.size,
"mode": image.mode,
"format": image.format
}
return MultimodalContent(
content_type="image",
content=image,
metadata=metadata,
source=image_path,
timestamp=self._get_timestamp()
)
except Exception as e:
print(f"❌ Error processing image {image_path}: {e}")
return None
def _generate_caption(self, image: Image.Image) -> str:
"""Generate a caption for the image."""
if not self.caption_model:
return "Image processing not available"
try:
# Preprocess image
pixel_values = self.image_processor(image, return_tensors="pt").pixel_values
# Generate caption
with torch.no_grad():
output_ids = self.caption_model.generate(
pixel_values,
max_length=50,
num_beams=4,
return_dict_in_generate=True
).sequences
caption = self.caption_tokenizer.batch_decode(output_ids, skip_special_tokens=True)[0]
return caption.strip()
except Exception as e:
print(f"Error generating caption: {e}")
return "Could not generate caption"
def _extract_text_from_image(self, image: Image.Image) -> str:
"""Extract text from image using OCR."""
try:
import pytesseract
text = pytesseract.image_to_string(image)
return text.strip()
except Exception as e:
return ""
def _analyze_image_content(self, image: Image.Image) -> Dict[str, Any]:
"""Analyze image content and characteristics."""
analysis = {}
# Convert to numpy array for analysis
img_array = np.array(image)
# Basic statistics
analysis["dimensions"] = img_array.shape
analysis["data_type"] = str(img_array.dtype)
# Color analysis
if len(img_array.shape) == 3: # Color image
analysis["channels"] = img_array.shape[2]
analysis["color_space"] = "RGB" if img_array.shape[2] == 3 else "RGBA"
# Calculate average colors
avg_colors = np.mean(img_array, axis=(0, 1))
analysis["average_colors"] = avg_colors.tolist()
# Brightness analysis
if len(img_array.shape) == 3:
gray = np.mean(img_array, axis=2)
else:
gray = img_array
analysis["brightness"] = {
"mean": float(np.mean(gray)),
"std": float(np.std(gray)),
"min": float(np.min(gray)),
"max": float(np.max(gray))
}
return analysis
def create_educational_image(self, text: str, subject: str,
size: Tuple[int, int] = (800, 600)) -> Image.Image:
"""Create an educational image with text."""
# Create blank image
image = Image.new('RGB', size, color='white')
draw = ImageDraw.Draw(image)
# Try to load a font
try:
font = ImageFont.truetype("arial.ttf", 24)
except:
font = ImageFont.load_default()
# Add subject header
draw.text((20, 20), f"Subject: {subject}", fill='black', font=font)
# Add main text
draw.text((20, 80), text, fill='black', font=font)
return image
class AudioProcessor:
"""Handles audio processing and analysis."""
def __init__(self):
self.whisper_model = None
self._load_models()
def _load_models(self):
"""Load audio processing models."""
if not WHISPER_AVAILABLE:
print("⚠️ Whisper not available - audio processing will be limited")
return
try:
# Load Whisper model for speech recognition
self.whisper_model = whisper.load_model("base")
print("✅ Audio models loaded successfully")
except Exception as e:
print(f"⚠️ Warning: Could not load audio models: {e}")
print("Audio processing will be limited")
def process_audio(self, audio_path: str) -> MultimodalContent:
"""Process an audio file and extract information."""
if not LIBROSA_AVAILABLE:
return MultimodalContent(
content_type="audio",
content=None,
metadata={"error": "Librosa not available"},
source=audio_path,
timestamp=self._get_timestamp()
)
try:
# Load audio
audio, sr = librosa.load(audio_path)
# Transcribe speech
transcription = self._transcribe_audio(audio_path)
# Analyze audio characteristics
analysis = self._analyze_audio_content(audio, sr)
metadata = {
"transcription": transcription,
"analysis": analysis,
"sample_rate": sr,
"duration": len(audio) / sr
}
return MultimodalContent(
content_type="audio",
content=audio,
metadata=metadata,
source=audio_path,
timestamp=self._get_timestamp()
)
except Exception as e:
print(f"❌ Error processing audio {audio_path}: {e}")
return None
def _transcribe_audio(self, audio_path: str) -> str:
"""Transcribe audio to text using Whisper."""
if not self.whisper_model:
return "Audio transcription not available"
try:
result = self.whisper_model.transcribe(audio_path)
return result["text"]
except Exception as e:
print(f"Error transcribing audio: {e}")
return "Could not transcribe audio"
def _analyze_audio_content(self, audio: np.ndarray, sr: int) -> Dict[str, Any]:
"""Analyze audio content and characteristics."""
if not LIBROSA_AVAILABLE:
return {"error": "Librosa not available"}
analysis = {}
# Basic statistics
analysis["length"] = len(audio)
analysis["sample_rate"] = sr
analysis["duration"] = len(audio) / sr
# Amplitude analysis
analysis["amplitude"] = {
"mean": float(np.mean(audio)),
"std": float(np.std(audio)),
"min": float(np.min(audio)),
"max": float(np.max(audio))
}
# Spectral analysis
try:
# Calculate spectrogram
D = librosa.amplitude_to_db(np.abs(librosa.stft(audio)), ref=np.max)
analysis["spectral"] = {
"spectrogram_shape": D.shape,
"spectral_centroid": float(np.mean(librosa.feature.spectral_centroid(y=audio, sr=sr))),
"spectral_rolloff": float(np.mean(librosa.feature.spectral_rolloff(y=audio, sr=sr)))
}
except Exception as e:
analysis["spectral"] = {"error": str(e)}
return analysis
def create_audio_summary(self, audio_path: str, max_duration: float = 30.0) -> str:
"""Create a summary of audio content."""
if not LIBROSA_AVAILABLE:
return "Audio analysis not available - librosa not installed"
try:
audio, sr = librosa.load(audio_path)
duration = len(audio) / sr
if duration <= max_duration:
return f"Audio duration: {duration:.2f} seconds"
# Analyze key segments
segments = self._analyze_audio_segments(audio, sr, max_duration)
summary = f"Audio duration: {duration:.2f} seconds\n"
summary += f"Key segments analyzed: {len(segments)}\n"
for i, segment in enumerate(segments[:3]): # Top 3 segments
summary += f"Segment {i+1}: {segment['description']}\n"
return summary
except Exception as e:
return f"Could not analyze audio: {e}"
def _analyze_audio_segments(self, audio: np.ndarray, sr: int,
segment_duration: float) -> List[Dict[str, Any]]:
"""Analyze audio in segments."""
segment_length = int(segment_duration * sr)
segments = []
for i in range(0, len(audio), segment_length):
segment = audio[i:i + segment_length]
if len(segment) > 0:
# Calculate segment energy
energy = np.mean(segment ** 2)
# Determine if segment has speech (simplified)
has_speech = energy > np.mean(audio ** 2)
segments.append({
"start_time": i / sr,
"duration": len(segment) / sr,
"energy": float(energy),
"has_speech": has_speech,
"description": f"High energy segment" if has_speech else "Low energy segment"
})
return segments
class VideoProcessor:
"""Handles video processing and analysis."""
def __init__(self):
self.video_processor = None
self._load_models()
def _load_models(self):
"""Load video processing models."""
if not MOVIEPY_AVAILABLE:
print("⚠️ MoviePy not available - video processing will be limited")
return
try:
# Load video captioning model
model_name = "microsoft/git-base-coco"
self.video_processor = AutoProcessor.from_pretrained(model_name)
print("✅ Video models loaded successfully")
except Exception as e:
print(f"⚠️ Warning: Could not load video models: {e}")
print("Video processing will be limited")
def process_video(self, video_path: str) -> MultimodalContent:
"""Process a video file and extract information."""
if not MOVIEPY_AVAILABLE:
return MultimodalContent(
content_type="video",
content=None,
metadata={"error": "MoviePy not available"},
source=video_path,
timestamp=self._get_timestamp()
)
try:
# Load video
video = VideoFileClip(video_path)
# Extract frames for analysis
frames = self._extract_key_frames(video)
# Extract audio
audio = video.audio
audio_analysis = None
if audio and LIBROSA_AVAILABLE:
audio_processor = AudioProcessor()
audio_analysis = audio_processor._analyze_audio_content(
np.array(audio.to_soundarray()), audio.fps
)
# Analyze video content
analysis = self._analyze_video_content(video, frames)
metadata = {
"frames_analyzed": len(frames),
"analysis": analysis,
"audio_analysis": audio_analysis,
"duration": video.duration,
"fps": video.fps,
"size": video.size
}
return MultimodalContent(
content_type="video",
content=video,
metadata=metadata,
source=video_path,
timestamp=self._get_timestamp()
)
except Exception as e:
print(f"❌ Error processing video {video_path}: {e}")
return None
def _extract_key_frames(self, video, num_frames: int = 10) -> List[np.ndarray]:
"""Extract key frames from video for analysis."""
if not MOVIEPY_AVAILABLE:
return []
frames = []
duration = video.duration
for i in range(num_frames):
time = (i + 1) * duration / (num_frames + 1)
frame = video.get_frame(time)
frames.append(frame)
return frames
def _analyze_video_content(self, video, frames: List[np.ndarray]) -> Dict[str, Any]:
"""Analyze video content and characteristics."""
analysis = {}
# Basic video info
analysis["duration"] = video.duration
analysis["fps"] = video.fps
analysis["size"] = video.size
analysis["num_frames"] = len(frames)
# Frame analysis
if frames:
frame_analysis = []
for i, frame in enumerate(frames):
frame_info = {
"frame_index": i,
"brightness": float(np.mean(frame)),
"contrast": float(np.std(frame))
}
frame_analysis.append(frame_info)
analysis["frame_analysis"] = frame_analysis
return analysis
def create_video_summary(self, video_path: str) -> str:
"""Create a summary of video content."""
if not MOVIEPY_AVAILABLE:
return "Video analysis not available - moviepy not installed"
try:
video = VideoFileClip(video_path)
summary = f"Video Summary:\n"
summary += f"Duration: {video.duration:.2f} seconds\n"
summary += f"FPS: {video.fps}\n"
summary += f"Resolution: {video.size[0]}x{video.size[1]}\n"
if video.audio:
summary += f"Has audio: Yes\n"
if LIBROSA_AVAILABLE:
audio_processor = AudioProcessor()
audio_summary = audio_processor.create_audio_summary(video_path)
summary += f"Audio: {audio_summary}\n"
else:
summary += f"Audio: Analysis not available\n"
else:
summary += f"Has audio: No\n"
return summary
except Exception as e:
return f"Could not analyze video: {e}"
class MultimodalManager:
"""Manages all multimodal processing capabilities."""
def __init__(self):
self.image_processor = ImageProcessor()
self.audio_processor = AudioProcessor()
self.video_processor = VideoProcessor()
self.supported_formats = {
"image": [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff"],
"audio": [".mp3", ".wav", ".flac", ".m4a", ".aac"],
"video": [".mp4", ".avi", ".mov", ".mkv", ".wmv"]
}
def process_file(self, file_path: str) -> Optional[MultimodalContent]:
"""Process any supported file type."""
file_ext = Path(file_path).suffix.lower()
if file_ext in self.supported_formats["image"]:
return self.image_processor.process_image(file_path)
elif file_ext in self.supported_formats["audio"]:
return self.audio_processor.process_audio(file_path)
elif file_ext in self.supported_formats["video"]:
return self.video_processor.process_video(file_path)
else:
print(f"❌ Unsupported file format: {file_ext}")
return None
def create_multimodal_summary(self, file_paths: List[str]) -> str:
"""Create summary of multiple multimodal files."""
summaries = []
for file_path in file_paths:
content = self.process_file(file_path)
if content:
file_name = Path(file_path).name
summary = f"\n--- {file_name} ---\n"
summary += f"Type: {content.content_type}\n"
if content.content_type == "image":
summary += f"Caption: {content.metadata.get('caption', 'N/A')}\n"
if content.metadata.get('extracted_text'):
summary += f"Text: {content.metadata['extracted_text'][:100]}...\n"
elif content.content_type == "audio":
summary += f"Transcription: {content.metadata.get('transcription', 'N/A')[:100]}...\n"
summary += f"Duration: {content.metadata.get('duration', 0):.2f}s\n"
elif content.content_type == "video":
summary += f"Duration: {content.metadata.get('duration', 0):.2f}s\n"
summary += f"Resolution: {content.metadata.get('analysis', {}).get('size', 'N/A')}\n"
summaries.append(summary)
return "\n".join(summaries) if summaries else "No files processed"
def generate_educational_content(self, subject: str, content_type: str,
prompt: str) -> Optional[MultimodalContent]:
"""Generate educational content based on prompts."""
if content_type == "image":
# Create educational image
image = self.image_processor.create_educational_image(prompt, subject)
return MultimodalContent(
content_type="image",
content=image,
metadata={
"generated": True,
"subject": subject,
"prompt": prompt
},
source="generated",
timestamp=self._get_timestamp()
)
# Add more content generation types here
return None
def _get_timestamp(self) -> str:
"""Get current timestamp."""
from datetime import datetime
return datetime.now().isoformat()
class CrossModalAnalyzer:
"""Analyzes relationships between different modalities."""
def __init__(self):
self.multimodal_manager = MultimodalManager()
def analyze_content_relationships(self, file_paths: List[str]) -> Dict[str, Any]:
"""Analyze relationships between different content types."""
analysis = {}
# Process all files
contents = []
for file_path in file_paths:
content = self.multimodal_manager.process_file(file_path)
if content:
contents.append(content)
if not contents:
return {"error": "No content processed"}
# Group by content type
by_type = {}
for content in contents:
content_type = content.content_type
if content_type not in by_type:
by_type[content_type] = []
by_type[content_type].append(content)
analysis["content_distribution"] = {
content_type: len(contents) for content_type, contents in by_type.items()
}
# Analyze cross-modal relationships
analysis["cross_modal_insights"] = self._find_cross_modal_insights(contents)
return analysis
def _find_cross_modal_insights(self, contents: List[MultimodalContent]) -> List[str]:
"""Find insights across different modalities."""
insights = []
# Check for text consistency across modalities
text_contents = []
for content in contents:
if content.content_type == "image" and content.metadata.get("extracted_text"):
text_contents.append(content.metadata["extracted_text"])
elif content.content_type == "audio" and content.metadata.get("transcription"):
text_contents.append(content.metadata["transcription"])
if len(text_contents) > 1:
# Simple text similarity check
insights.append(f"Found {len(text_contents)} text sources across different modalities")
# Check for temporal relationships
video_contents = [c for c in contents if c.content_type == "video"]
audio_contents = [c for c in contents if c.content_type == "audio"]
if video_contents and audio_contents:
insights.append("Video and audio content detected - potential for synchronized analysis")
return insights
# Utility functions
def get_supported_formats() -> Dict[str, List[str]]:
"""Get list of supported file formats."""
return {
"image": [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff"],
"audio": [".mp3", ".wav", ".flac", ".m4a", ".aac"],
"video": [".mp4", ".avi", ".mov", ".mkv", ".wmv"]
}
def is_supported_format(file_path: str) -> bool:
"""Check if a file format is supported."""
file_ext = Path(file_path).suffix.lower()
all_formats = []
for formats in get_supported_formats().values():
all_formats.extend(formats)
return file_ext in all_formats