ScriptLLM / src /analysis /creative_analyzer.py
yalrashed's picture
Upload creative_analyzer.py
edb392c verified
raw
history blame
10.9 kB
import os
from pathlib import Path
import logging
from tqdm import tqdm
import requests
from src.analysis.analysis_cleaner import AnalysisCleaner
logger = logging.getLogger(__name__)
class CreativeAnalyzer:
def __init__(self):
# Initialize Claude
self.api_key = os.getenv("ANTHROPIC_API_KEY")
if not self.api_key:
raise ValueError("ANTHROPIC_API_KEY not found")
self.api_url = "https://api.anthropic.com/v1/messages"
self.model = "claude-3-sonnet-20240229"
self.headers = {
"x-api-key": self.api_key,
"anthropic-version": "2023-06-01",
"content-type": "application/json"
}
# Set chunk size
self.chunk_size = 6000 # Claude handles larger chunks well
def query_claude(self, prompt: str) -> str:
"""Send request to Claude API with proper response handling"""
try:
payload = {
"model": self.model,
"max_tokens": 4096,
"messages": [{
"role": "user",
"content": prompt
}]
}
response = requests.post(self.api_url, headers=self.headers, json=payload)
if response.status_code == 200:
response_json = response.json()
# Get the message content from Claude's response
if ('content' in response_json and
isinstance(response_json['content'], list) and
len(response_json['content']) > 0 and
'text' in response_json['content'][0]):
return response_json['content'][0]['text']
else:
logger.error("Invalid response structure")
logger.error(f"Response: {response_json}")
return None
else:
logger.error(f"API Error: {response.status_code}")
logger.error(f"Response: {response.text}")
return None
except Exception as e:
logger.error(f"Error making API request: {str(e)}")
logger.error("Full error details:", exc_info=True)
return None
def count_tokens(self, text: str) -> int:
"""Estimate token count using simple word-based estimation"""
words = text.split()
return int(len(words) * 1.3)
def chunk_screenplay(self, text: str) -> list:
"""Split screenplay into chunks with overlap for context"""
logger.info("Chunking screenplay...")
scenes = text.split("\n\n")
chunks = []
current_chunk = []
current_size = 0
overlap_scenes = 2
for scene in scenes:
scene_size = self.count_tokens(scene)
if current_size + scene_size > self.chunk_size and current_chunk:
overlap = current_chunk[-overlap_scenes:] if len(current_chunk) > overlap_scenes else current_chunk
chunks.append("\n\n".join(current_chunk))
current_chunk = overlap + [scene]
current_size = sum(self.count_tokens(s) for s in current_chunk)
else:
current_chunk.append(scene)
current_size += scene_size
if current_chunk:
chunks.append("\n\n".join(current_chunk))
logger.info(f"Split screenplay into {len(chunks)} chunks with {overlap_scenes} scene overlap")
return chunks
def analyze_plot_development(self, chunk: str, previous_plot_points: str = "") -> str:
prompt = f"""You are a professional screenplay analyst. Building on this previous analysis:
{previous_plot_points}
Continue analyzing the story's progression. Tell me what happens next, focusing on new developments and changes. Reference specific moments from this section but don't repeat what we've covered.
Consider:
- How events build on what came before
- Their impact on story direction
- Changes to the narrative
Use flowing paragraphs and support with specific examples.
Screenplay section to analyze:
{chunk}"""
return self.query_claude(prompt)
def analyze_character_arcs(self, chunk: str, plot_context: str, previous_character_dev: str = "") -> str:
prompt = f"""You are a professional screenplay analyst. Based on these plot developments:
{plot_context}
And previous character analysis:
{previous_character_dev}
Continue analyzing how the characters evolve. Focus on their growth, changes, and key moments from this section. Build on, don't repeat, previous analysis.
Consider:
- Character choices and consequences
- Relationship dynamics
- Internal conflicts and growth
Use flowing paragraphs with specific examples.
Screenplay section to analyze:
{chunk}"""
return self.query_claude(prompt)
def analyze_dialogue_progression(self, chunk: str, character_context: str, previous_dialogue: str = "") -> str:
prompt = f"""You are a professional screenplay analyst. Understanding the character context:
{character_context}
And previous dialogue analysis:
{previous_dialogue}
Analyze the dialogue in this section from a screenwriting perspective. What makes it effective or distinctive?
Consider:
- How dialogue reveals character
- Subtext and meaning
- Character voices and patterns
- Impact on relationships
Use specific dialogue examples in flowing paragraphs.
Screenplay section to analyze:
{chunk}"""
return self.query_claude(prompt)
def analyze_themes(self, chunk: str, plot_context: str, character_context: str) -> str:
prompt = f"""You are a professional screenplay analyst. Based on these plot developments:
{plot_context}
And character journeys:
{character_context}
Analyze how themes develop in this section. What deeper meanings emerge? How do they connect to previous themes?
Consider:
- Core ideas and messages
- Symbolic elements
- How themes connect to character arcs
- Social or philosophical implications
Support with specific examples in flowing paragraphs.
Screenplay section to analyze:
{chunk}"""
return self.query_claude(prompt)
def analyze_screenplay(self, screenplay_path: Path) -> bool:
"""Main method to generate creative analysis"""
logger.info("Starting creative analysis")
try:
# Read screenplay
with open(screenplay_path, 'r', encoding='utf-8') as file:
screenplay_text = file.read()
# Split into chunks
chunks = self.chunk_screenplay(screenplay_text)
# Initialize analyses
plot_analysis = []
character_analysis = []
dialogue_analysis = []
theme_analysis = []
# First Pass: Plot Development
logger.info("First Pass: Analyzing plot development")
with tqdm(total=len(chunks), desc="Analyzing plot") as pbar:
for chunk in chunks:
result = self.analyze_plot_development(
chunk,
"\n\n".join(plot_analysis)
)
if result:
plot_analysis.append(result)
else:
logger.error("Failed to get plot analysis")
return False
pbar.update(1)
# Second Pass: Character Arcs
logger.info("Second Pass: Analyzing character arcs")
with tqdm(total=len(chunks), desc="Analyzing characters") as pbar:
for chunk in chunks:
result = self.analyze_character_arcs(
chunk,
"\n\n".join(plot_analysis),
"\n\n".join(character_analysis)
)
if result:
character_analysis.append(result)
else:
logger.error("Failed to get character analysis")
return False
pbar.update(1)
# Third Pass: Dialogue Progression
logger.info("Third Pass: Analyzing dialogue")
with tqdm(total=len(chunks), desc="Analyzing dialogue") as pbar:
for chunk in chunks:
result = self.analyze_dialogue_progression(
chunk,
"\n\n".join(character_analysis),
"\n\n".join(dialogue_analysis)
)
if result:
dialogue_analysis.append(result)
else:
logger.error("Failed to get dialogue analysis")
return False
pbar.update(1)
# Fourth Pass: Thematic Development
logger.info("Fourth Pass: Analyzing themes")
with tqdm(total=len(chunks), desc="Analyzing themes") as pbar:
for chunk in chunks:
result = self.analyze_themes(
chunk,
"\n\n".join(plot_analysis),
"\n\n".join(character_analysis)
)
if result:
theme_analysis.append(result)
else:
logger.error("Failed to get theme analysis")
return False
pbar.update(1)
# Clean up analyses
cleaner = AnalysisCleaner()
cleaned_analyses = {
'plot': cleaner.clean_analysis("\n\n".join(plot_analysis)),
'character': cleaner.clean_analysis("\n\n".join(character_analysis)),
'dialogue': cleaner.clean_analysis("\n\n".join(dialogue_analysis)),
'theme': cleaner.clean_analysis("\n\n".join(theme_analysis))
}
# Save Analysis
output_path = screenplay_path.parent / "creative_analysis.txt"
with open(output_path, 'w', encoding='utf-8') as f:
f.write("SCREENPLAY CREATIVE ANALYSIS\n\n")
sections = [
("PLOT PROGRESSION", cleaned_analyses['plot']),
("CHARACTER ARCS", cleaned_analyses['character']),
("DIALOGUE PROGRESSION", cleaned_analyses['dialogue']),
("THEMATIC DEVELOPMENT", cleaned_analyses['theme'])
]
for title, content in sections:
f.write(f"### {title} ###\n\n")
f.write(content)
f.write("\n\n")
logger.info(f"Analysis saved to: {output_path}")
return True
except Exception as e:
logger.error(f"Error in creative analysis: {str(e)}")
logger.error("Full error details:", exc_info=True)
return False