|
|
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.""" |
|
|
|
|
|
|
|
|
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" |
|
|
|
|
|
|
|
|
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 |
|
|
} |
|
|
|
|
|
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'] |
|
|
|
|
|
|
|
|
try: |
|
|
|
|
|
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()) |
|
|
|
|
|
|
|
|
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 |
|
|
} |
|
|
|