| """ |
| NARRATIVE INTELLIGENCE ENGINE V2 |
| COMPLETE REBUILD with corrected anti-spoiler and audio-visual gap analysis |
| """ |
| import json |
| import subprocess |
|
|
| class NarrativeIntelligenceEngineV2: |
| def __init__(self, video_path): |
| self.video_path = video_path |
| self.scene_duration = self.get_video_duration() |
| self.narrative_arc = None |
| self.emotional_journey = None |
| self.thematic_elements = None |
| |
| def get_video_duration(self): |
| """Get actual video duration""" |
| cmd = [ |
| 'ffprobe', '-i', self.video_path, |
| '-show_entries', 'format=duration', |
| '-v', 'quiet', '-of', 'csv=p=0' |
| ] |
| result = subprocess.run(cmd, capture_output=True, text=True) |
| return float(result.stdout.strip()) if result.returncode == 0 else 600 |
|
|
| |
| |
| def analyze_narrative_arc(self): |
| """SCENE-LEVEL ARC IDENTIFICATION""" |
| duration = self.scene_duration |
| self.narrative_arc = { |
| 'genre': 'fantasy_horror', |
| 'structure': { |
| 'exposition': (0, duration * 0.15), |
| 'rising_action': (duration * 0.15, duration * 0.4), |
| 'climax': (duration * 0.4, duration * 0.7), |
| 'falling_action': (duration * 0.7, duration * 0.9), |
| 'resolution': (duration * 0.9, duration) |
| } |
| } |
| return self.narrative_arc |
| |
| def analyze_emotional_journey(self): |
| """EMOTIONAL JOURNEY: Character emotional states and transitions""" |
| self.emotional_journey = { |
| 'exposition': {'mood': 'apprehensive', 'tension': 'building'}, |
| 'rising_action': {'mood': 'horrified', 'tension': 'high'}, |
| 'climax': {'mood': 'terrified', 'tension': 'peak'}, |
| 'falling_action': {'mood': 'despair', 'tension': 'sustained'}, |
| 'resolution': {'mood': 'panicked', 'tension': 'release'} |
| } |
| return self.emotional_journey |
| |
| def analyze_thematic_elements(self): |
| """THEMATIC ELEMENTS: Central themes and symbolic content""" |
| self.thematic_elements = { |
| 'central_themes': ['mortality vs immortality', 'summer vs winter', 'courage vs fear'], |
| 'symbolism': { |
| 'white_walkers': 'eternal winter and death', |
| 'night_watch': 'last defense of humanity', |
| 'haunted_forest': 'unknown ancient powers' |
| } |
| } |
| return self.thematic_elements |
|
|
| |
| |
| def should_narrate_moment_v2(self, moment_time, audio_context, visual_context, narrative_arc): |
| """ |
| UPDATED DECISION TREE V2 |
| Core question: FOR BLIND VIEWERS, DOES THIS MOMENT NEED AUDIO NARRATION TO FEEL INCLUDED? |
| """ |
| |
| |
| silence_justifications = self.check_silence_justifications(audio_context, visual_context, moment_time, narrative_arc) |
| if silence_justifications['should_silence']: |
| return {'decision': 'silence', 'reason': silence_justifications['reason']} |
| |
| |
| guardrail_result = self.apply_narrative_guardrails_v2(moment_time, visual_context, audio_context, narrative_arc) |
| if guardrail_result['decision'] == 'block': |
| return {'decision': 'silence', 'reason': guardrail_result['reason']} |
| |
| |
| strategy = self.select_narration_strategy_v2(visual_context, audio_context, moment_time, narrative_arc) |
| return {'decision': 'narrate', 'strategy': strategy} |
| |
| def check_silence_justifications(self, audio_context, visual_context, moment_time, narrative_arc): |
| """ |
| UPDATED: JUSTIFICATION CHECK FOR SILENCE |
| Only stay silent when audio successfully conveys the same information |
| """ |
| justifications = { |
| 'should_silence': False, |
| 'reason': '' |
| } |
| |
| |
| audio_sufficiency_checks = [ |
| |
| audio_context.get('clear_emotional_audio', False) and |
| not visual_context.get('critical_visual_information', False), |
| |
| |
| audio_context.get('explicit_dialogue_context', False), |
| |
| |
| audio_context.get('intentional_sound_design', False) and |
| not visual_context.get('essential_for_plot', False), |
| |
| |
| audio_context.get('effective_tension_building', False) and |
| moment_time < narrative_arc['structure']['climax'][0] |
| ] |
| |
| if any(audio_sufficiency_checks): |
| justifications['should_silence'] = True |
| justifications['reason'] = 'audio_successfully_conveys_story' |
| return justifications |
| |
| |
| if visual_context.get('critical_visual_information', False) and \ |
| not audio_context.get('audio_conveys_same_info', False): |
| justifications['should_silence'] = False |
| justifications['reason'] = 'essential_visual_information_missing_from_audio' |
| return justifications |
| |
| return justifications |
| |
| def apply_narrative_guardrails_v2(self, moment_time, visual_context, audio_context, narrative_arc): |
| """ |
| UPDATED: APPLY OBJECTIVE NARRATIVE GUARDRAILS |
| """ |
| |
| |
| spoiler_check = self.anti_spoiler_protection_v2(moment_time, visual_context, audio_context, narrative_arc) |
| if spoiler_check['block']: |
| return {'decision': 'block', 'reason': spoiler_check['reason']} |
| |
| |
| timing_check = self.story_aware_timing_v2(moment_time, visual_context, audio_context, narrative_arc) |
| if not timing_check['optimal']: |
| return {'decision': 'block', 'reason': timing_check['reason']} |
| |
| return {'decision': 'proceed', 'reason': 'all_guardrails_passed'} |
| |
| def anti_spoiler_protection_v2(self, moment_time, visual_context, audio_context, narrative_arc): |
| """ |
| UPDATED ANTI-SPOILER PROTECTION |
| Only block when BOTH: |
| 1. Visual contains spoiler information |
| 2. Audio successfully builds mystery without revealing it |
| """ |
| climax_start = narrative_arc['structure']['climax'][0] |
| |
| |
| if moment_time < climax_start and visual_context.get('contains_supernatural', False): |
| |
| |
| if audio_context.get('successful_mystery_building', False): |
| return { |
| 'block': True, |
| 'reason': 'audio_successfully_builds_mystery_preserve_director_intent' |
| } |
| else: |
| |
| return { |
| 'block': False, |
| 'reason': 'audio_fails_to_convey_mystery_narrate_for_inclusion' |
| } |
| |
| |
| if visual_context.get('foreshadows_death', False) and \ |
| audio_context.get('subtle_foreshadowing_audio', False): |
| return { |
| 'block': True, |
| 'reason': 'audio_successfully_foreshadows_preserve_surprise' |
| } |
| |
| return {'block': False, 'reason': 'no_spoiler_concern'} |
| |
| def story_aware_timing_v2(self, moment_time, visual_context, audio_context, narrative_arc): |
| """ |
| UPDATED STORY-AWARE TIMING ASSESSMENT |
| """ |
| arc_phase = self.get_arc_phase(moment_time, narrative_arc) |
| |
| |
| if visual_context.get('mystery_element', False) and \ |
| arc_phase == 'exposition' and \ |
| audio_context.get('effective_anticipation_building', False): |
| return {'optimal': False, 'reason': 'audio_successfully_builds_anticipation_wait_for_reveal'} |
| |
| |
| if visual_context.get('immediate_visual', False) and \ |
| moment_time > visual_context.get('optimal_timing', 0) + 10 and \ |
| audio_context.get('scene_moved_on', False): |
| return {'optimal': False, 'reason': 'moment_passed_audio_context_changed'} |
| |
| |
| return {'optimal': True, 'reason': 'aligned_with_director_timing_and_audio_context'} |
| |
| def select_narration_strategy_v2(self, visual_context, audio_context, moment_time, narrative_arc): |
| """ |
| UPDATED NARRATION STRATEGY SELECTION |
| """ |
| |
| |
| if visual_context.get('first_appearance', False) and \ |
| not audio_context.get('clear_character_establishment', False): |
| return { |
| 'type': 'character_introduction', |
| 'approach': 'establish_identity_role_narrative_significance', |
| 'adjective_density': 'high', |
| 'priority': 'high' |
| } |
| |
| |
| if visual_context.get('visually_striking', False) and \ |
| not audio_context.get('audio_conveys_visual_impact', False): |
| return { |
| 'type': 'visual_translation', |
| 'approach': 'describe_emotional_impact_and_significance', |
| 'adjective_density': 'high', |
| 'priority': 'high' |
| } |
| |
| |
| if visual_context.get('critical_visual_information', False) and \ |
| not audio_context.get('audio_conveys_same_info', False): |
| return { |
| 'type': 'essential_plot_information', |
| 'approach': 'clarify_narrative_significance', |
| 'adjective_density': 'medium', |
| 'priority': 'critical' |
| } |
| |
| |
| if visual_context.get('emotional_beat', False) and \ |
| audio_context.get('audio_sets_emotional_tone', False): |
| return { |
| 'type': 'emotional_enhancement', |
| 'approach': 'add_thematic_resonance_and_depth', |
| 'adjective_density': 'medium', |
| 'priority': 'medium' |
| } |
| |
| |
| if (visual_context.get('spatial_shift', False) or |
| visual_context.get('time_passage', False)) and \ |
| not audio_context.get('audio_conveys_transition', False): |
| return { |
| 'type': 'narrative_guidance', |
| 'approach': 'orient_and_contextualize', |
| 'adjective_density': 'low', |
| 'priority': 'medium' |
| } |
| |
| |
| return { |
| 'type': 'default_description', |
| 'approach': 'describe_visually_apparent', |
| 'adjective_density': 'medium', |
| 'priority': 'low' |
| } |
| |
| def get_arc_phase(self, moment_time, narrative_arc): |
| """Determine which narrative phase the moment belongs to""" |
| for phase, (start, end) in narrative_arc['structure'].items(): |
| if start <= moment_time <= end: |
| return phase |
| return 'unknown' |
| |
| |
| |
| def strategic_narrative_assessment_v2(self, moment_time, visual_context, audio_context): |
| """ |
| UPDATED STRATEGIC NARRATIVE ASSESSMENT for unclear cases |
| """ |
| assessments = [ |
| self.enhances_story_comprehension_v2(visual_context, audio_context), |
| self.serves_directors_vision_v2(visual_context, audio_context), |
| self.creates_meaningful_inclusion_v2(visual_context, audio_context) |
| ] |
| |
| positive_assessments = sum(assessments) |
| |
| |
| if positive_assessments >= 2: |
| return {'decision': 'narrate', 'reason': f'strategic_enhancement_{positive_assessments}_positive'} |
| else: |
| return {'decision': 'silence', 'reason': f'strategic_restraint_{positive_assessments}_positive'} |
| |
| def enhances_story_comprehension_v2(self, visual_context, audio_context): |
| """DOES THIS ENHANCE OVERALL STORY COMPREHENSION?""" |
| |
| if visual_context.get('critical_visual_information', False) and \ |
| not audio_context.get('audio_conveys_same_info', False): |
| return True |
| |
| |
| if visual_context.get('provides_essential_context', False) and \ |
| not audio_context.get('audio_provides_context', False): |
| return True |
| |
| return False |
| |
| def serves_directors_vision_v2(self, visual_context, audio_context): |
| """DOES THIS SERVE THE DIRECTOR'S ARTISTIC VISION?""" |
| |
| if audio_context.get('intentional_audio_design', False) and \ |
| not visual_context.get('critical_visual_information', False): |
| return False |
| |
| |
| if visual_context.get('director_visual_focus', False): |
| return True |
| |
| return True |
| |
| def creates_meaningful_inclusion_v2(self, visual_context, audio_context): |
| """DOES THIS CREATE MEANINGFUL INCLUSION?""" |
| |
| if visual_context.get('shared_viewing_moment', False): |
| return True |
| |
| |
| if visual_context.get('visual_payoff', False) or \ |
| visual_context.get('emotional_peak', False): |
| return True |
| |
| |
| if visual_context.get('culturally_significant_visual', False): |
| return True |
| |
| return False |
|
|
| if __name__ == "__main__": |
| |
| engine = NarrativeIntelligenceEngineV2('gameofthronesseason1episode1.mp4') |
| |
| |
| print("🎭 UPDATED MACRO-LEVEL ANALYSIS:") |
| print(f"Duration: {engine.scene_duration:.2f}s") |
| print(f"Narrative Arc: {engine.analyze_narrative_arc()}") |
| |
| |
| print("\n🎯 UPDATED DECISION TREE TEST:") |
| |
| |
| test_visual = { |
| 'critical_visual_information': True, |
| 'contains_supernatural': True, |
| 'visually_striking': True, |
| 'essential_for_plot': True |
| } |
| test_audio = { |
| 'audio_conveys_same_info': False, |
| 'successful_mystery_building': False, |
| 'clear_emotional_audio': False |
| } |
| |
| decision = engine.should_narrate_moment_v2(75, test_audio, test_visual, engine.narrative_arc) |
| print(f"Body discovery at 75s: {decision}") |
| |
| |
| test_visual2 = { |
| 'critical_visual_information': False, |
| 'visually_striking': False |
| } |
| test_audio2 = { |
| 'clear_emotional_audio': True, |
| 'effective_tension_building': True |
| } |
| |
| decision2 = engine.should_narrate_moment_v2(120, test_audio2, test_visual2, engine.narrative_arc) |
| print(f"Atmospheric moment at 120s: {decision2}") |
|
|