| 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): |
| |
| 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" |
| } |
|
|
| |
| self.chunk_size = 6000 |
|
|
| 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() |
| |
| 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: |
| |
| with open(screenplay_path, 'r', encoding='utf-8') as file: |
| screenplay_text = file.read() |
|
|
| |
| chunks = self.chunk_screenplay(screenplay_text) |
|
|
| |
| plot_analysis = [] |
| character_analysis = [] |
| dialogue_analysis = [] |
| theme_analysis = [] |
|
|
| |
| 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) |
|
|
| |
| 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) |
|
|
| |
| 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) |
|
|
| |
| 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) |
|
|
| |
| 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)) |
| } |
|
|
| |
| 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 |