StoryLens / narrative_classifier.py
Marek4321's picture
Upload 13 files
6bdfadc verified
import requests
import json
from typing import List, Dict
from taxonomy import STORY_ARCS, STORY_DEFINITION, get_taxonomy_formatted
class NarrativeClassifier:
def __init__(self, api_key: str, group_id: str):
self.api_key = api_key
self.group_id = group_id
self.base_url = "https://api.minimaxi.chat/v1"
def _build_prompt(self, segments: List[Dict]) -> str:
"""Build classification prompt."""
# Format segments
segments_text = ""
for seg in segments:
segments_text += f"\n[{seg['start']:.1f}s - {seg['end']:.1f}s]"
segments_text += f"\nVisual: {seg['visual']}"
if seg['speech']:
segments_text += f"\nSpeech: \"{seg['speech']}\""
segments_text += "\n"
# Format story arcs
arcs_text = ""
for arc_name, arc_info in STORY_ARCS.items():
arcs_text += f"\n- {arc_name}: {' -> '.join(arc_info['sequence'])}"
prompt = f"""You are an expert in advertising narrative structure analysis.
Analyze this video advertisement segment by segment.
## SEGMENTS TO ANALYZE:
{segments_text}
## FUNCTIONAL ROLE TAXONOMY:
{get_taxonomy_formatted()}
## KNOWN STORY ARCS:
{arcs_text}
## STORY DEFINITION:
{STORY_DEFINITION}
## YOUR TASK:
1. For each segment, determine the PRIMARY functional role from the taxonomy
2. Determine if this ad contains a STORY (YES/NO)
3. Identify which STORY ARC best matches (or "Custom" if none match)
4. List any MISSING elements that could strengthen the ad
## RESPONSE FORMAT (use exactly this JSON structure):
```json
{{
"segments": [
{{
"timestamp": "0.0-2.0s",
"functional_role": "Hook",
"role_category": "OPENING",
"reasoning": "Opens with provocative question to grab attention"
}}
],
"has_story": true,
"story_explanation": "Brief explanation of why story is present/absent",
"story_arc": "Problem-Solution-Outcome",
"detected_sequence": ["Hook", "Problem Setup", "Solution Reveal", "Call-to-Action"],
"missing_elements": ["Social Proof", "Outcome"]
}}
```
Respond ONLY with valid JSON, no other text."""
return prompt
def classify(self, segments: List[Dict]) -> Dict:
"""
Classify each segment and detect overall story arc.
Returns:
{
"segments": [...],
"has_story": True/False,
"story_arc": "...",
"detected_sequence": [...],
"missing_elements": [...],
"raw_response": "..."
}
"""
url = f"{self.base_url}/text/chatcompletion_v2"
headers = {
'Authorization': f'Bearer {self.api_key}',
'Content-Type': 'application/json'
}
prompt = self._build_prompt(segments)
payload = {
"model": "MiniMax-Text-01",
"messages": [
{"role": "user", "content": prompt}
],
"temperature": 0.3 # Lower temperature for more consistent classification
}
response = requests.post(url, headers=headers, json=payload)
if response.status_code != 200:
print(f"Classification API error: {response.text}")
return self._fallback_result(segments)
result = response.json()
raw_response = result['choices'][0]['message']['content']
# Parse JSON from response
try:
# Extract JSON from response (may be wrapped in markdown code block)
json_str = raw_response
if "```json" in json_str:
json_str = json_str.split("```json")[1].split("```")[0]
elif "```" in json_str:
json_str = json_str.split("```")[1].split("```")[0]
parsed = json.loads(json_str.strip())
# Merge with original segment data
for i, seg_analysis in enumerate(parsed.get('segments', [])):
if i < len(segments):
segments[i]['functional_role'] = seg_analysis.get('functional_role', 'Unknown')
segments[i]['role_category'] = seg_analysis.get('role_category', 'OTHER')
segments[i]['reasoning'] = seg_analysis.get('reasoning', '')
return {
"segments": segments,
"has_story": parsed.get('has_story', False),
"story_explanation": parsed.get('story_explanation', ''),
"story_arc": parsed.get('story_arc', 'Unknown'),
"detected_sequence": parsed.get('detected_sequence', []),
"missing_elements": parsed.get('missing_elements', []),
"raw_response": raw_response
}
except json.JSONDecodeError as e:
print(f"JSON parse error: {e}")
print(f"Raw response: {raw_response}")
return self._fallback_result(segments, raw_response)
def _fallback_result(self, segments: List[Dict], raw_response: str = "") -> Dict:
"""Return fallback result when parsing fails."""
for seg in segments:
seg['functional_role'] = 'Unknown'
seg['role_category'] = 'OTHER'
seg['reasoning'] = 'Classification failed'
return {
"segments": segments,
"has_story": False,
"story_explanation": "Unable to determine",
"story_arc": "Unknown",
"detected_sequence": [],
"missing_elements": [],
"raw_response": raw_response
}