Download src/analysis/creative_analyzer.py from yalrashed/ScriptLLM: direct link, hf CLI and curl.
- Browser
- Download file 10.9 kB
-
https://huggingface.co/spaces/yalrashed/ScriptLLM/resolve/98799954df72b61fc16f46c912d55ddd986187f4/src/analysis/creative_analyzer.py
- Command line
-
hf download hf://spaces/yalrashed/ScriptLLM@98799954df72b61fc16f46c912d55ddd986187f4/src/analysis/creative_analyzer.py
-
curl -L -o creative_analyzer.py https://huggingface.co/spaces/yalrashed/ScriptLLM/resolve/98799954df72b61fc16f46c912d55ddd986187f4/src/analysis/creative_analyzer.py
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 |