|
|
""" |
|
|
Enhanced emotion matching with better text analysis and facial expression detection |
|
|
""" |
|
|
|
|
|
import cv2 |
|
|
import numpy as np |
|
|
from typing import Dict, List, Tuple |
|
|
import re |
|
|
try: |
|
|
from textblob import TextBlob |
|
|
except ImportError: |
|
|
TextBlob = None |
|
|
print("⚠️ TextBlob not installed - using basic sentiment analysis") |
|
|
|
|
|
class EnhancedEmotionMatcher: |
|
|
"""Improved emotion matching between text and facial expressions""" |
|
|
|
|
|
def __init__(self): |
|
|
|
|
|
self.emotion_keywords = { |
|
|
'happy': { |
|
|
'keywords': ['happy', 'joy', 'laugh', 'smile', 'fun', 'excited', 'yay', 'wow', |
|
|
'great', 'amazing', 'wonderful', 'love', 'beautiful', 'awesome'], |
|
|
'emojis': ['😊', '😄', '😃', '😁', '🙂', '😍', '❤️', '💕', '✨'], |
|
|
'punctuation': ['!', '!!', '!!!'], |
|
|
'weight': 1.0 |
|
|
}, |
|
|
'sad': { |
|
|
'keywords': ['sad', 'cry', 'tear', 'sorry', 'miss', 'lonely', 'hurt', 'pain', |
|
|
'depressed', 'unhappy', 'disappointed', 'grief', 'sorrow'], |
|
|
'emojis': ['😢', '😭', '😔', '😞', '💔', '😿'], |
|
|
'punctuation': ['...'], |
|
|
'weight': 1.0 |
|
|
}, |
|
|
'angry': { |
|
|
'keywords': ['angry', 'mad', 'furious', 'hate', 'annoyed', 'frustrated', |
|
|
'rage', 'irritated', 'stupid', 'damn', 'hell'], |
|
|
'emojis': ['😠', '😡', '🤬', '😤', '💢'], |
|
|
'punctuation': ['!?', '?!'], |
|
|
'weight': 1.0 |
|
|
}, |
|
|
'surprised': { |
|
|
'keywords': ['surprised', 'shock', 'what', 'oh', 'wow', 'really', 'seriously', |
|
|
'unbelievable', 'impossible', 'amazing'], |
|
|
'emojis': ['😮', '😱', '😲', '🤯', '⚡'], |
|
|
'punctuation': ['?!', '!?', '???'], |
|
|
'weight': 0.9 |
|
|
}, |
|
|
'scared': { |
|
|
'keywords': ['scared', 'fear', 'afraid', 'terrified', 'frightened', 'horror', |
|
|
'panic', 'worry', 'nervous', 'anxious'], |
|
|
'emojis': ['😨', '😰', '😱', '👻', '💀'], |
|
|
'punctuation': ['!!!', '...!'], |
|
|
'weight': 0.9 |
|
|
}, |
|
|
'neutral': { |
|
|
'keywords': ['okay', 'fine', 'alright', 'yes', 'no', 'maybe', 'sure'], |
|
|
'emojis': ['😐', '😑', '🙄'], |
|
|
'punctuation': ['.'], |
|
|
'weight': 0.5 |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
self.intensifiers = ['very', 'so', 'really', 'extremely', 'super', 'totally'] |
|
|
self.negations = ['not', 'no', "n't", 'never', 'neither', 'nor'] |
|
|
|
|
|
def analyze_text_emotion(self, text: str) -> Dict[str, float]: |
|
|
""" |
|
|
Enhanced text emotion analysis |
|
|
|
|
|
Returns emotions with confidence scores |
|
|
""" |
|
|
text_lower = text.lower() |
|
|
emotions = {emotion: 0.0 for emotion in self.emotion_keywords} |
|
|
|
|
|
|
|
|
for emotion, data in self.emotion_keywords.items(): |
|
|
score = 0.0 |
|
|
|
|
|
|
|
|
for keyword in data['keywords']: |
|
|
if keyword in text_lower: |
|
|
|
|
|
if self._is_negated(text_lower, keyword): |
|
|
|
|
|
opposite = self._get_opposite_emotion(emotion) |
|
|
if opposite: |
|
|
emotions[opposite] += 0.3 |
|
|
else: |
|
|
score += 0.5 |
|
|
|
|
|
|
|
|
if self._has_intensifier(text_lower, keyword): |
|
|
score += 0.3 |
|
|
|
|
|
|
|
|
for punct in data['punctuation']: |
|
|
if punct in text: |
|
|
score += 0.2 |
|
|
|
|
|
|
|
|
emotions[emotion] = min(score * data['weight'], 1.0) |
|
|
|
|
|
|
|
|
intensity = 0.5 |
|
|
if TextBlob: |
|
|
try: |
|
|
blob = TextBlob(text) |
|
|
polarity = blob.sentiment.polarity |
|
|
subjectivity = blob.sentiment.subjectivity |
|
|
|
|
|
|
|
|
if polarity > 0.3: |
|
|
emotions['happy'] += polarity * 0.5 |
|
|
elif polarity < -0.3: |
|
|
emotions['sad'] += abs(polarity) * 0.3 |
|
|
emotions['angry'] += abs(polarity) * 0.2 |
|
|
|
|
|
|
|
|
intensity = subjectivity * 0.5 |
|
|
|
|
|
except: |
|
|
intensity = 0.5 |
|
|
else: |
|
|
|
|
|
positive_words = ['good', 'great', 'love', 'happy', 'wonderful', 'amazing'] |
|
|
negative_words = ['bad', 'hate', 'sad', 'angry', 'terrible', 'awful'] |
|
|
|
|
|
pos_count = sum(1 for word in positive_words if word in text_lower) |
|
|
neg_count = sum(1 for word in negative_words if word in text_lower) |
|
|
|
|
|
if pos_count > neg_count: |
|
|
emotions['happy'] += 0.3 |
|
|
elif neg_count > pos_count: |
|
|
emotions['sad'] += 0.2 |
|
|
emotions['angry'] += 0.1 |
|
|
|
|
|
|
|
|
exclamation_count = text.count('!') |
|
|
if exclamation_count > 0: |
|
|
intensity = min(1.0, 0.5 + exclamation_count * 0.2) |
|
|
|
|
|
|
|
|
if '?' in text: |
|
|
emotions['surprised'] += 0.3 |
|
|
|
|
|
|
|
|
total = sum(emotions.values()) |
|
|
if total > 0: |
|
|
emotions = {k: v/total for k, v in emotions.items()} |
|
|
else: |
|
|
emotions['neutral'] = 1.0 |
|
|
|
|
|
|
|
|
emotions['intensity'] = intensity |
|
|
|
|
|
return emotions |
|
|
|
|
|
def _is_negated(self, text: str, keyword: str) -> bool: |
|
|
"""Check if keyword is negated""" |
|
|
|
|
|
keyword_pos = text.find(keyword) |
|
|
if keyword_pos > 0: |
|
|
before_text = text[:keyword_pos].split()[-3:] |
|
|
return any(neg in before_text for neg in self.negations) |
|
|
return False |
|
|
|
|
|
def _has_intensifier(self, text: str, keyword: str) -> bool: |
|
|
"""Check if keyword has intensifier""" |
|
|
keyword_pos = text.find(keyword) |
|
|
if keyword_pos > 0: |
|
|
before_text = text[:keyword_pos].split()[-2:] |
|
|
return any(intensifier in before_text for intensifier in self.intensifiers) |
|
|
return False |
|
|
|
|
|
def _get_opposite_emotion(self, emotion: str) -> str: |
|
|
"""Get opposite emotion""" |
|
|
opposites = { |
|
|
'happy': 'sad', |
|
|
'sad': 'happy', |
|
|
'angry': 'happy', |
|
|
'scared': 'confident', |
|
|
'confident': 'scared' |
|
|
} |
|
|
return opposites.get(emotion, 'neutral') |
|
|
|
|
|
def match_frames_to_emotions(self, frames: List[str], subtitles: List, |
|
|
eye_detector=None) -> List[Dict]: |
|
|
""" |
|
|
Match frames to subtitles based on emotions and eye state |
|
|
|
|
|
Returns list of matched panels with metadata |
|
|
""" |
|
|
from backend.emotion_aware_comic import FacialExpressionAnalyzer |
|
|
face_analyzer = FacialExpressionAnalyzer() |
|
|
|
|
|
matched_panels = [] |
|
|
|
|
|
for sub in subtitles: |
|
|
|
|
|
text_emotions = self.analyze_text_emotion(sub.content) |
|
|
|
|
|
|
|
|
start_frame = int(sub.index * len(frames) / len(subtitles)) |
|
|
end_frame = min(start_frame + 5, len(frames)) |
|
|
|
|
|
best_match = None |
|
|
best_score = -1 |
|
|
|
|
|
for i in range(start_frame, end_frame): |
|
|
if i >= len(frames): |
|
|
break |
|
|
|
|
|
frame_path = frames[i] |
|
|
|
|
|
|
|
|
eye_score = 1.0 |
|
|
if eye_detector: |
|
|
eye_state = eye_detector.check_eyes_state(frame_path) |
|
|
if eye_state['state'] == 'open': |
|
|
eye_score = 1.2 |
|
|
elif eye_state['state'] == 'half_closed': |
|
|
eye_score = 0.5 |
|
|
elif eye_state['state'] == 'closed': |
|
|
eye_score = 0.1 |
|
|
|
|
|
|
|
|
face_emotions = face_analyzer.analyze_expression(frame_path) |
|
|
|
|
|
|
|
|
emotion_score = self._calculate_match_score(text_emotions, face_emotions) |
|
|
total_score = emotion_score * eye_score |
|
|
|
|
|
if total_score > best_score: |
|
|
best_score = total_score |
|
|
best_match = { |
|
|
'frame': frame_path, |
|
|
'subtitle': sub, |
|
|
'text_emotions': text_emotions, |
|
|
'face_emotions': face_emotions, |
|
|
'match_score': total_score, |
|
|
'eye_score': eye_score |
|
|
} |
|
|
|
|
|
if best_match: |
|
|
matched_panels.append(best_match) |
|
|
print(f" ✅ Matched: '{sub.content[:30]}...' - Score: {best_score:.2f}") |
|
|
|
|
|
return matched_panels |
|
|
|
|
|
def _calculate_match_score(self, text_emotions: Dict, face_emotions: Dict) -> float: |
|
|
"""Calculate emotion match score with improved algorithm""" |
|
|
score = 0.0 |
|
|
|
|
|
|
|
|
text_top = sorted([(k, v) for k, v in text_emotions.items() if k != 'intensity'], |
|
|
key=lambda x: x[1], reverse=True)[:2] |
|
|
face_top = sorted([(k, v) for k, v in face_emotions.items() if k != 'intensity'], |
|
|
key=lambda x: x[1], reverse=True)[:2] |
|
|
|
|
|
|
|
|
if text_top and face_top: |
|
|
|
|
|
if text_top[0][0] == face_top[0][0]: |
|
|
score += 1.0 * min(text_top[0][1], face_top[0][1]) |
|
|
|
|
|
|
|
|
for t_emotion, t_score in text_top: |
|
|
for f_emotion, f_score in face_top: |
|
|
if t_emotion == f_emotion: |
|
|
score += 0.5 * min(t_score, f_score) |
|
|
|
|
|
|
|
|
if text_emotions.get('happy', 0) > 0.5 and face_emotions.get('sad', 0) > 0.5: |
|
|
score -= 0.5 |
|
|
if text_emotions.get('sad', 0) > 0.5 and face_emotions.get('happy', 0) > 0.5: |
|
|
score -= 0.5 |
|
|
|
|
|
|
|
|
text_intensity = text_emotions.get('intensity', 0.5) |
|
|
face_intensity = face_emotions.get('intensity', 0.5) |
|
|
intensity_diff = abs(text_intensity - face_intensity) |
|
|
score += (1 - intensity_diff) * 0.3 |
|
|
|
|
|
return max(0, score) |