| | 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 |