๐ฌ AI Screenplay Generator
++ Transform your ideas into professional screenplays for films, TV shows, and streaming series. + Using industry-standard format and story structure to create compelling, producible scripts. +
+diff --git "a/app-backup.py" "b/app-backup.py" new file mode 100644--- /dev/null +++ "b/app-backup.py" @@ -0,0 +1,2929 @@ +import gradio as gr +import os +import json +import requests +from datetime import datetime +import time +from typing import List, Dict, Any, Generator, Tuple, Optional, Set +import logging +import re +import tempfile +from pathlib import Path +import sqlite3 +import hashlib +import threading +from contextlib import contextmanager +from dataclasses import dataclass, field, asdict +from collections import defaultdict +import random +from huggingface_hub import HfApi, upload_file, hf_hub_download + +# --- Logging setup --- +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') +logger = logging.getLogger(__name__) + +# --- Document export imports --- +try: + from docx import Document + from docx.shared import Inches, Pt, RGBColor, Mm + from docx.enum.text import WD_ALIGN_PARAGRAPH + from docx.enum.style import WD_STYLE_TYPE + from docx.oxml.ns import qn + from docx.oxml import OxmlElement + DOCX_AVAILABLE = True +except ImportError: + DOCX_AVAILABLE = False + logger.warning("python-docx not installed. DOCX export will be disabled.") + +# --- Environment variables and constants --- +FRIENDLI_TOKEN = os.getenv("FRIENDLI_TOKEN", "") +BRAVE_SEARCH_API_KEY = os.getenv("BRAVE_SEARCH_API_KEY", "") +API_URL = "https://api.friendli.ai/dedicated/v1/chat/completions" +MODEL_ID = "dep86pjolcjjnv8" +DB_PATH = "screenplay_sessions_v1.db" + +# Screenplay length settings +SCREENPLAY_LENGTHS = { + "movie": {"pages": 110, "description": "Feature Film (90-120 pages)"}, + "tv_drama": {"pages": 55, "description": "TV Drama Episode (50-60 pages)"}, + "ott_series": {"pages": 45, "description": "OTT Series Episode (30-60 pages)"}, + "short_film": {"pages": 15, "description": "Short Film (10-20 pages)"} +} + +# --- Environment validation --- +if not FRIENDLI_TOKEN: + logger.error("FRIENDLI_TOKEN not set. Application will not work properly.") + FRIENDLI_TOKEN = "dummy_token_for_testing" + +if not BRAVE_SEARCH_API_KEY: + logger.warning("BRAVE_SEARCH_API_KEY not set. Web search features will be disabled.") + +# --- Global variables --- +db_lock = threading.Lock() + +# Genre templates +GENRE_TEMPLATES = { + "action": { + "pacing": "fast", + "scene_length": "short", + "dialogue_ratio": 0.3, + "key_elements": ["set pieces", "physical conflict", "urgency", "stakes escalation"], + "structure_beats": ["explosive opening", "pursuit/chase", "confrontation", "climactic battle"] + }, + "thriller": { + "pacing": "fast", + "scene_length": "short", + "dialogue_ratio": 0.35, + "key_elements": ["suspense", "twists", "paranoia", "time pressure"], + "structure_beats": ["hook", "mystery deepens", "false victory", "revelation", "final confrontation"] + }, + "drama": { + "pacing": "moderate", + "scene_length": "medium", + "dialogue_ratio": 0.5, + "key_elements": ["character depth", "emotional truth", "relationships", "internal conflict"], + "structure_beats": ["status quo", "catalyst", "debate", "commitment", "complications", "crisis", "resolution"] + }, + "comedy": { + "pacing": "fast", + "scene_length": "short", + "dialogue_ratio": 0.6, + "key_elements": ["setup/payoff", "timing", "character comedy", "escalation"], + "structure_beats": ["funny opening", "complication", "misunderstandings multiply", "chaos peak", "resolution with callback"] + }, + "horror": { + "pacing": "variable", + "scene_length": "mixed", + "dialogue_ratio": 0.3, + "key_elements": ["atmosphere", "dread", "jump scares", "gore/psychological"], + "structure_beats": ["normal world", "first sign", "investigation", "first attack", "survival", "final girl/boy"] + }, + "sci-fi": { + "pacing": "moderate", + "scene_length": "medium", + "dialogue_ratio": 0.4, + "key_elements": ["world building", "technology", "concepts", "visual spectacle"], + "structure_beats": ["ordinary world", "discovery", "new world", "complications", "understanding", "choice", "new normal"] + }, + "romance": { + "pacing": "moderate", + "scene_length": "medium", + "dialogue_ratio": 0.55, + "key_elements": ["chemistry", "obstacles", "emotional moments", "intimacy"], + "structure_beats": ["meet cute", "attraction", "first conflict", "deepening", "crisis/breakup", "grand gesture", "together"] + } +} + +# Screenplay stages definition +SCREENPLAY_STAGES = [ + ("producer", "๐ฌ Producer: Concept Development & Market Analysis"), + ("story_developer", "๐ Story Developer: Synopsis & Three-Act Structure"), + ("character_designer", "๐ฅ Character Designer: Cast & Relationships"), + ("critic_structure", "๐ Structure Critic: Story & Character Review"), + ("scene_planner", "๐ฏ Scene Planner: Detailed Scene Breakdown"), + ("screenwriter", "โ๏ธ Screenwriter: Act 1 - Setup (25%)"), + ("script_doctor", "๐ง Script Doctor: Act 1 Review & Polish"), + ("screenwriter", "โ๏ธ Screenwriter: Act 2A - Rising Action (25%)"), + ("script_doctor", "๐ง Script Doctor: Act 2A Review & Polish"), + ("screenwriter", "โ๏ธ Screenwriter: Act 2B - Complications (25%)"), + ("script_doctor", "๐ง Script Doctor: Act 2B Review & Polish"), + ("screenwriter", "โ๏ธ Screenwriter: Act 3 - Resolution (25%)"), + ("final_reviewer", "๐ญ Final Review: Complete Screenplay Analysis"), +] + +# Save the Cat Beat Sheet +SAVE_THE_CAT_BEATS = { + 1: "Opening Image (0-1%)", + 2: "Setup (1-10%)", + 3: "Theme Stated (5%)", + 4: "Catalyst (10%)", + 5: "Debate (10-20%)", + 6: "Break into Two (20%)", + 7: "B Story (22%)", + 8: "Fun and Games (20-50%)", + 9: "Midpoint (50%)", + 10: "Bad Guys Close In (50-75%)", + 11: "All Is Lost (75%)", + 12: "Dark Night of the Soul (75-80%)", + 13: "Break into Three (80%)", + 14: "Finale (80-99%)", + 15: "Final Image (99-100%)" +} + +# --- Data classes --- +@dataclass +class ScreenplayBible: + """Screenplay bible for maintaining consistency""" + title: str = "" + logline: str = "" + genre: str = "" + subgenre: str = "" + tone: str = "" + themes: List[str] = field(default_factory=list) + + # Characters + protagonist: Dict[str, Any] = field(default_factory=dict) + antagonist: Dict[str, Any] = field(default_factory=dict) + supporting_cast: Dict[str, Dict[str, Any]] = field(default_factory=dict) + + # Structure + three_act_structure: Dict[str, str] = field(default_factory=dict) + save_the_cat_beats: Dict[int, str] = field(default_factory=dict) + + # World + time_period: str = "" + primary_locations: List[Dict[str, str]] = field(default_factory=list) + world_rules: List[str] = field(default_factory=list) + + # Visual style + visual_style: str = "" + key_imagery: List[str] = field(default_factory=list) + +@dataclass +class SceneBreakdown: + """Individual scene information""" + scene_number: int + act: int + location: str + time_of_day: str + characters: List[str] + purpose: str + conflict: str + page_count: float + beat: str = "" + transition: str = "CUT TO:" + +@dataclass +class CharacterProfile: + """Detailed character profile""" + name: str + age: int + role: str # protagonist, antagonist, supporting, etc. + archetype: str + want: str # External goal + need: str # Internal need + backstory: str + personality: List[str] + speech_pattern: str + character_arc: str + relationships: Dict[str, str] = field(default_factory=dict) + first_appearance: str = "" + +# --- Core logic classes --- +class ScreenplayTracker: + """Unified screenplay tracker""" + def __init__(self): + self.screenplay_bible = ScreenplayBible() + self.scenes: List[SceneBreakdown] = [] + self.characters: Dict[str, CharacterProfile] = {} + self.page_count = 0 + self.act_pages = {"1": 0, "2A": 0, "2B": 0, "3": 0} + self.dialogue_action_ratio = 0.0 + + def add_scene(self, scene: SceneBreakdown): + """Add scene to tracker""" + self.scenes.append(scene) + self.page_count += scene.page_count + + def add_character(self, character: CharacterProfile): + """Add character to tracker""" + self.characters[character.name] = character + # Update bible with main characters + if character.role == "protagonist": + self.screenplay_bible.protagonist = asdict(character) + elif character.role == "antagonist": + self.screenplay_bible.antagonist = asdict(character) + elif character.role == "supporting": + self.screenplay_bible.supporting_cast[character.name] = asdict(character) + + def update_bible(self, key: str, value: Any): + """Update screenplay bible""" + if hasattr(self.screenplay_bible, key): + setattr(self.screenplay_bible, key, value) + + def get_act_page_target(self, act: str, total_pages: int) -> int: + """Get target pages for each act""" + if act == "1": + return int(total_pages * 0.25) + elif act in ["2A", "2B"]: + return int(total_pages * 0.25) + elif act == "3": + return int(total_pages * 0.25) + return 0 + +class ScreenplayDatabase: + """Database management for screenplay sessions""" + @staticmethod + def init_db(): + with sqlite3.connect(DB_PATH) as conn: + conn.execute("PRAGMA journal_mode=WAL") + cursor = conn.cursor() + + # Main screenplay sessions table + cursor.execute(''' + CREATE TABLE IF NOT EXISTS screenplay_sessions ( + session_id TEXT PRIMARY KEY, + user_query TEXT NOT NULL, + screenplay_type TEXT NOT NULL, + genre TEXT NOT NULL, + subgenre TEXT, + target_pages INTEGER, + language TEXT NOT NULL, + title TEXT, + logline TEXT, + synopsis TEXT, + three_act_structure TEXT, + character_profiles TEXT, + scene_breakdown TEXT, + screenplay_bible TEXT, + final_screenplay TEXT, + pdf_path TEXT, + created_at TEXT DEFAULT (datetime('now')), + updated_at TEXT DEFAULT (datetime('now')), + status TEXT DEFAULT 'active', + current_stage INTEGER DEFAULT 0, + total_pages REAL DEFAULT 0 + ) + ''') + + # Stages table + cursor.execute(''' + CREATE TABLE IF NOT EXISTS screenplay_stages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id TEXT NOT NULL, + stage_number INTEGER NOT NULL, + stage_name TEXT NOT NULL, + role TEXT NOT NULL, + content TEXT, + page_count REAL DEFAULT 0, + status TEXT DEFAULT 'pending', + created_at TEXT DEFAULT (datetime('now')), + updated_at TEXT DEFAULT (datetime('now')), + FOREIGN KEY (session_id) REFERENCES screenplay_sessions(session_id), + UNIQUE(session_id, stage_number) + ) + ''') + + # Scenes table + cursor.execute(''' + CREATE TABLE IF NOT EXISTS scenes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id TEXT NOT NULL, + act_number INTEGER NOT NULL, + scene_number INTEGER NOT NULL, + location TEXT NOT NULL, + time_of_day TEXT NOT NULL, + characters TEXT, + purpose TEXT, + content TEXT, + page_count REAL, + created_at TEXT DEFAULT (datetime('now')), + FOREIGN KEY (session_id) REFERENCES screenplay_sessions(session_id) + ) + ''') + + # Characters table + cursor.execute(''' + CREATE TABLE IF NOT EXISTS characters ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id TEXT NOT NULL, + character_name TEXT NOT NULL, + character_data TEXT, + created_at TEXT DEFAULT (datetime('now')), + FOREIGN KEY (session_id) REFERENCES screenplay_sessions(session_id), + UNIQUE(session_id, character_name) + ) + ''') + + # Screenplay themes library + cursor.execute(''' + CREATE TABLE IF NOT EXISTS screenplay_themes_library ( + theme_id TEXT PRIMARY KEY, + theme_text TEXT NOT NULL, + screenplay_type TEXT NOT NULL, + genre TEXT NOT NULL, + language TEXT NOT NULL, + title TEXT, + logline TEXT, + protagonist_desc TEXT, + conflict_desc TEXT, + generated_at TEXT DEFAULT (datetime('now')), + view_count INTEGER DEFAULT 0, + used_count INTEGER DEFAULT 0, + tags TEXT, + metadata TEXT + ) + ''') + + conn.commit() + + @staticmethod + @contextmanager + def get_db(): + with db_lock: + conn = sqlite3.connect(DB_PATH, timeout=30.0) + conn.row_factory = sqlite3.Row + try: + yield conn + finally: + conn.close() + + @staticmethod + def create_session(user_query: str, screenplay_type: str, genre: str, language: str) -> str: + session_id = hashlib.md5(f"{user_query}{screenplay_type}{datetime.now()}".encode()).hexdigest() + target_pages = SCREENPLAY_LENGTHS[screenplay_type]["pages"] + + with ScreenplayDatabase.get_db() as conn: + conn.cursor().execute( + '''INSERT INTO screenplay_sessions + (session_id, user_query, screenplay_type, genre, target_pages, language) + VALUES (?, ?, ?, ?, ?, ?)''', + (session_id, user_query, screenplay_type, genre, target_pages, language) + ) + conn.commit() + return session_id + + @staticmethod + def save_stage(session_id: str, stage_number: int, stage_name: str, + role: str, content: str, status: str = 'complete'): + page_count = 0 + if role == "screenwriter" and content: + # Estimate pages based on screenplay format (rough estimate) + page_count = len(content.split('\n')) / 55 # ~55 lines per page + + with ScreenplayDatabase.get_db() as conn: + cursor = conn.cursor() + cursor.execute(''' + INSERT INTO screenplay_stages + (session_id, stage_number, stage_name, role, content, page_count, status) + VALUES (?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(session_id, stage_number) + DO UPDATE SET content=?, page_count=?, status=?, updated_at=datetime('now') + ''', (session_id, stage_number, stage_name, role, content, page_count, status, + content, page_count, status)) + + # Update session info + cursor.execute(''' + UPDATE screenplay_sessions + SET current_stage = ?, updated_at = datetime('now') + WHERE session_id = ? + ''', (stage_number, session_id)) + + conn.commit() + + @staticmethod + def save_screenplay_bible(session_id: str, bible: ScreenplayBible): + """Save screenplay bible""" + with ScreenplayDatabase.get_db() as conn: + bible_json = json.dumps(asdict(bible)) + conn.cursor().execute( + 'UPDATE screenplay_sessions SET screenplay_bible = ? WHERE session_id = ?', + (bible_json, session_id) + ) + conn.commit() + + @staticmethod + def save_character(session_id: str, character: CharacterProfile): + """Save character profile""" + with ScreenplayDatabase.get_db() as conn: + char_json = json.dumps(asdict(character)) + conn.cursor().execute( + '''INSERT INTO characters (session_id, character_name, character_data) + VALUES (?, ?, ?) + ON CONFLICT(session_id, character_name) + DO UPDATE SET character_data = ?''', + (session_id, character.name, char_json, char_json) + ) + conn.commit() + + @staticmethod + def save_scene(session_id: str, scene: SceneBreakdown): + """Save scene breakdown""" + with ScreenplayDatabase.get_db() as conn: + conn.cursor().execute( + '''INSERT INTO scenes + (session_id, act_number, scene_number, location, time_of_day, + characters, purpose, page_count) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)''', + (session_id, scene.act, scene.scene_number, scene.location, + scene.time_of_day, json.dumps(scene.characters), scene.purpose, + scene.page_count) + ) + conn.commit() + + @staticmethod + def get_screenplay_content(session_id: str) -> str: + """Get complete screenplay content""" + with ScreenplayDatabase.get_db() as conn: + rows = conn.cursor().execute(''' + SELECT content FROM screenplay_stages + WHERE session_id = ? AND role = 'screenwriter' + ORDER BY stage_number + ''', (session_id,)).fetchall() + + if rows: + return '\n\n'.join(row['content'] for row in rows if row['content']) + return "" + + @staticmethod + def update_final_screenplay(session_id: str, final_screenplay: str, title: str, logline: str): + """Update final screenplay""" + with ScreenplayDatabase.get_db() as conn: + total_pages = len(final_screenplay.split('\n')) / 55 + conn.cursor().execute( + '''UPDATE screenplay_sessions + SET final_screenplay = ?, title = ?, logline = ?, + total_pages = ?, status = 'complete', updated_at = datetime('now') + WHERE session_id = ?''', + (final_screenplay, title, logline, total_pages, session_id) + ) + conn.commit() + + @staticmethod + def get_session(session_id: str) -> Optional[Dict]: + with ScreenplayDatabase.get_db() as conn: + row = conn.cursor().execute( + 'SELECT * FROM screenplay_sessions WHERE session_id = ?', + (session_id,) + ).fetchone() + return dict(row) if row else None + + @staticmethod + def get_stages(session_id: str) -> List[Dict]: + """Get all stages for a session""" + with ScreenplayDatabase.get_db() as conn: + rows = conn.cursor().execute( + '''SELECT * FROM screenplay_stages + WHERE session_id = ? + ORDER BY stage_number''', + (session_id,) + ).fetchall() + return [dict(row) for row in rows] + + @staticmethod + def get_active_sessions() -> List[Dict]: + with ScreenplayDatabase.get_db() as conn: + rows = conn.cursor().execute( + '''SELECT session_id, title, user_query, screenplay_type, genre, + created_at, current_stage, total_pages + FROM screenplay_sessions + WHERE status = 'active' + ORDER BY updated_at DESC + LIMIT 10''' + ).fetchall() + return [dict(row) for row in rows] + + @staticmethod + def save_random_theme(theme_text: str, screenplay_type: str, genre: str, + language: str, metadata: Dict[str, Any]) -> str: + """Save randomly generated screenplay theme""" + theme_id = hashlib.md5(f"{theme_text}{datetime.now()}".encode()).hexdigest()[:12] + + title = metadata.get('title', '') + logline = metadata.get('logline', '') + protagonist_desc = metadata.get('protagonist', '') + conflict_desc = metadata.get('conflict', '') + tags = json.dumps(metadata.get('tags', [])) + + with ScreenplayDatabase.get_db() as conn: + conn.cursor().execute(''' + INSERT INTO screenplay_themes_library + (theme_id, theme_text, screenplay_type, genre, language, title, logline, + protagonist_desc, conflict_desc, tags, metadata) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ''', (theme_id, theme_text, screenplay_type, genre, language, title, logline, + protagonist_desc, conflict_desc, tags, json.dumps(metadata))) + conn.commit() + + return theme_id + + @staticmethod + def get_stages(session_id: str) -> List[Dict]: + """Get all stages for a session""" + with ScreenplayDatabase.get_db() as conn: + rows = conn.cursor().execute( + '''SELECT * FROM screenplay_stages + WHERE session_id = ? + ORDER BY stage_number''', + (session_id,) + ).fetchall() + return [dict(row) for row in rows] + +class WebSearchIntegration: + """Web search functionality for screenplay research""" + def __init__(self): + self.brave_api_key = BRAVE_SEARCH_API_KEY + self.search_url = "https://api.search.brave.com/res/v1/web/search" + self.enabled = bool(self.brave_api_key) + + def search(self, query: str, count: int = 3, language: str = "en") -> List[Dict]: + if not self.enabled: + return [] + headers = { + "Accept": "application/json", + "X-Subscription-Token": self.brave_api_key + } + params = { + "q": query, + "count": count, + "search_lang": "ko" if language == "Korean" else "en", + "text_decorations": False, + "safesearch": "moderate" + } + try: + response = requests.get(self.search_url, headers=headers, params=params, timeout=10) + response.raise_for_status() + results = response.json().get("web", {}).get("results", []) + return results + except requests.exceptions.RequestException as e: + logger.error(f"Web search API error: {e}") + return [] + + def extract_relevant_info(self, results: List[Dict], max_chars: int = 1500) -> str: + if not results: + return "" + extracted = [] + total_chars = 0 + for i, result in enumerate(results[:3], 1): + title = result.get("title", "") + description = result.get("description", "") + info = f"[{i}] {title}: {description}" + if total_chars + len(info) < max_chars: + extracted.append(info) + total_chars += len(info) + else: + break + return "\n".join(extracted) + +class ScreenplayGenerationSystem: + """Professional screenplay generation system""" + def __init__(self): + self.token = FRIENDLI_TOKEN + self.api_url = API_URL + self.model_id = MODEL_ID + self.screenplay_tracker = ScreenplayTracker() + self.web_search = WebSearchIntegration() + self.current_session_id = None + ScreenplayDatabase.init_db() + + def create_headers(self): + """Create headers for API request with proper token""" + if not self.token or self.token == "dummy_token_for_testing": + raise ValueError("Valid FRIENDLI_TOKEN is required") + + return { + "Authorization": f"Bearer {self.token}", + "Content-Type": "application/json" + } + + # --- Prompt generation functions --- + def create_producer_prompt(self, user_query: str, screenplay_type: str, + genre: str, language: str) -> str: + """Producer initial concept development""" + + # Web search for market trends if enabled + search_results = "" + if self.web_search.enabled: + queries = [ + f"box office success {genre} films 2024 2025", + f"popular {screenplay_type} {genre} trends", + f"audience demographics {genre} movies" + ] + for q in queries[:2]: + results = self.web_search.search(q, count=2, language=language) + if results: + search_results += self.web_search.extract_relevant_info(results) + "\n" + + lang_prompts = { + "Korean": f"""๋น์ ์ ํ ๋ฆฌ์ฐ๋ ํ๋ก๋์์ ๋๋ค. ์์ ์ ์ผ๋ก ์ฑ๊ณต ๊ฐ๋ฅํ {screenplay_type} ์ปจ์ ์ ๊ฐ๋ฐํ์ธ์. + +**์์ฒญ์ฌํญ:** {user_query} +**ํ์ :** {SCREENPLAY_LENGTHS[screenplay_type]['description']} +**์ฅ๋ฅด:** {genre} + +**์์ฅ ์กฐ์ฌ:** +{search_results if search_results else "N/A"} + +**ํ์ ์ ๊ณต ํญ๋ชฉ:** + +1. **์ ๋ชฉ (TITLE)** + - ๊ธฐ์ตํ๊ธฐ ์ฝ๊ณ ๋ง์ผํ ๊ฐ๋ฅํ ์ ๋ชฉ + - ์ฅ๋ฅด์ ํค์ ์์ํ๋ ์ ๋ชฉ + +2. **๋ก๊ทธ๋ผ์ธ (LOGLINE)** + - 25๋จ์ด ์ด๋ด ํ ๋ฌธ์ฅ + - ํ์: "[์ฌ๊ฑด]์ด ์ผ์ด๋ฌ์ ๋, [์ฃผ์ธ๊ณต]์ [๋ชฉํ]๋ฅผ ์ด๋ฃจ์ด์ผ ํ๋ค. ๊ทธ๋ ์ง ์์ผ๋ฉด [๊ฒฐ๊ณผ]" + - ๊ฐ๋ฑ๊ณผ stakes๊ฐ ๋ช ํํด์ผ ํจ + +3. **์ฅ๋ฅด ๋ถ์** + - ์ฃผ ์ฅ๋ฅด: {genre} + - ์๋ธ ์ฅ๋ฅด: + - ํค & ๋ถ์๊ธฐ: + +4. **ํ๊ฒ ๊ด๊ฐ** + - ์ฃผ์ ์ฐ๋ น๋: + - ์ฑ๋ณ ๋ถํฌ: + - ๊ด์ฌ์ฌ: + - ์ ์ฌ ์ํ ํฌ์ธต: + +5. **๋น๊ต ์ํ (COMPS)** + - 3๊ฐ์ ์ฑ๊ณตํ ์ ์ฌ ํ๋ก์ ํธ + - ๊ฐ๊ฐ์ ๋ฐ์ค์คํผ์ค/์์ฒญ๋ฅ ์ฑ๊ณผ + - ์ฐ๋ฆฌ ํ๋ก์ ํธ์์ ์ฐจ๋ณ์ + +6. **๊ณ ์ ํ๋งค ํฌ์ธํธ (USP)** + - ์ด ์ด์ผ๊ธฐ๋ง์ ๋ ํนํ ์ + - ํ์ฌ ์์ฅ์์์ ํ์์ฑ + - ์ ์ ๊ฐ๋ฅ์ฑ + +7. **๋น์ฃผ์ผ ์ปจ์ ** + - ํต์ฌ ๋น์ฃผ์ผ ์ด๋ฏธ์ง 3๊ฐ + - ์ ์ฒด์ ์ธ ๋ฃฉ & ํ + +๊ตฌ์ฒด์ ์ด๊ณ ์์ฅ์ฑ ์๋ ์ปจ์ ์ ์ ์ํ์ธ์.""", + + "English": f"""You are a Hollywood producer. Develop a commercially viable {screenplay_type} concept. + +**Request:** {user_query} +**Type:** {SCREENPLAY_LENGTHS[screenplay_type]['description']} +**Genre:** {genre} + +**Market Research:** +{search_results if search_results else "N/A"} + +**Required Elements:** + +1. **TITLE** + - Memorable and marketable + - Hints at genre and tone + +2. **LOGLINE** + - One sentence, 25 words max + - Format: "When [inciting incident], a [protagonist] must [objective] or else [stakes]" + - Clear conflict and stakes + +3. **GENRE ANALYSIS** + - Primary Genre: {genre} + - Sub-genre: + - Tone & Mood: + +4. **TARGET AUDIENCE** + - Primary Age Range: + - Gender Distribution: + - Interests: + - Similar Works Fanbase: + +5. **COMPARABLE FILMS (COMPS)** + - 3 successful similar projects + - Box office/viewership performance + - How ours differs + +6. **UNIQUE SELLING POINT (USP)** + - What makes this story unique + - Why now in the market + - Production feasibility + +7. **VISUAL CONCEPT** + - 3 key visual images + - Overall look & feel + +Provide specific, marketable concept.""" + } + + return lang_prompts.get(language, lang_prompts["English"]) + + def create_story_developer_prompt(self, producer_concept: str, user_query: str, + screenplay_type: str, genre: str, language: str) -> str: + """Story developer for synopsis and structure""" + + genre_template = GENRE_TEMPLATES.get(genre, GENRE_TEMPLATES["drama"]) + + lang_prompts = { + "Korean": f"""๋น์ ์ ์คํ ๋ฆฌ ๊ฐ๋ฐ์์ ๋๋ค. ํ๋ก๋์์ ์ปจ์ ์ ๋ฐํ์ผ๋ก {screenplay_type}์ ์๋์์ค์ 3๋ง ๊ตฌ์กฐ๋ฅผ ๊ฐ๋ฐํ์ธ์. + +**ํ๋ก๋์ ์ปจ์ :** +{producer_concept} + +**์ฅ๋ฅด ํน์ฑ:** {genre} +- ํ์ด์ฑ: {genre_template['pacing']} +- ํต์ฌ ์์: {', '.join(genre_template['key_elements'])} +- ๊ตฌ์กฐ ๋นํธ: {', '.join(genre_template['structure_beats'])} + +**ํ์ ์์ฑ ํญ๋ชฉ:** + +1. **์๋์์ค (SYNOPSIS)** + - 300-500๋จ์ด + - 3๋ง ๊ตฌ์กฐ๊ฐ ๋ช ํํ ๋๋ฌ๋๋๋ก + - ์ฃผ์ธ๊ณต์ ๋ณํ arc ํฌํจ + - ์ฃผ์ ์ ํ์ ๋ช ์ + - ๊ฒฐ๋ง ํฌํจ (์คํฌ์ผ๋ฌ OK) + +2. **3๋ง ๊ตฌ์กฐ (THREE-ACT STRUCTURE)** + + **์ 1๋ง - ์ค์ (Setup) [25%]** + - ์ผ์ ์ธ๊ณ (Ordinary World): + - ๊ทผ์น์๊ฐ ์ฌ๊ฑด (Inciting Incident): + - ์ฃผ์ธ๊ณต ์๊ฐ ๋ฐ ๊ฒฐํจ: + - 1๋ง ์ ํ์ (Plot Point 1): + + **์ 2๋งA - ์์น ์ก์ (Rising Action) [25%]** + - ์๋ก์ด ์ธ๊ณ ์ง์ : + - ์ฌ๋ฏธ์ ๊ฒ์ (Fun and Games): + - B ์คํ ๋ฆฌ (๊ด๊ณ/ํ ๋ง): + - ์ค๊ฐ์ (Midpoint) - ๊ฐ์ง ์น๋ฆฌ/ํจ๋ฐฐ: + + **์ 2๋งB - ๋ณต์กํ (Complications) [25%]** + - ์ ๋น์ ๋ฐ๊ฒฉ: + - ํ ํด์ฒด/์๊ธฐ: + - ๋ชจ๋ ๊ฒ์ ์์ (All Is Lost): + - ์ํผ์ ์ด๋ ๋ฐค: + + **์ 3๋ง - ํด๊ฒฐ (Resolution) [25%]** + - 2๋ง ์ ํ์ (Plot Point 2): + - ์ต์ข ์ ํฌ ์ค๋น: + - ํด๋ผ์ด๋งฅ์ค: + - ์๋ก์ด ์ผ์: + +3. **Save the Cat ๋นํธ ์ํธ** + 15๊ฐ ๋นํธ๋ฅผ {SCREENPLAY_LENGTHS[screenplay_type]['pages']}ํ์ด์ง์ ๋ง์ถฐ ๋ฐฐ์น + +4. **์ฃผ์ (THEME)** + - ์ค์ฌ ์ฃผ์ : + - ์ฃผ์ ๊ฐ ๋๋ฌ๋๋ ์๊ฐ: + - ์ฃผ์ ์ ์๊ฐ์ ํํ: + +5. **ํค & ์คํ์ผ** + - ์ ์ฒด์ ์ธ ํค: + - ์ ๋จธ ์ฌ์ฉ ์ฌ๋ถ: + - ๋น์ฃผ์ผ ์คํ์ผ: + +๊ตฌ์ฒด์ ์ด๊ณ ๊ฐ์ ์ ์ผ๋ก ๊ณต๊ฐ๊ฐ๋ ์คํ ๋ฆฌ๋ฅผ ๋ง๋์ธ์.""", + + "English": f"""You are a story developer. Based on the producer's concept, develop the synopsis and three-act structure for this {screenplay_type}. + +**Producer Concept:** +{producer_concept} + +**Genre Characteristics:** {genre} +- Pacing: {genre_template['pacing']} +- Key Elements: {', '.join(genre_template['key_elements'])} +- Structure Beats: {', '.join(genre_template['structure_beats'])} + +**Required Elements:** + +1. **SYNOPSIS** + - 300-500 words + - Clear three-act structure + - Protagonist's change arc + - Major turning points + - Include ending (spoilers OK) + +2. **THREE-ACT STRUCTURE** + + **ACT 1 - Setup [25%]** + - Ordinary World: + - Inciting Incident: + - Protagonist Introduction & Flaw: + - Plot Point 1: + + **ACT 2A - Rising Action [25%]** + - Entering New World: + - Fun and Games: + - B Story (Relationship/Theme): + - Midpoint - False Victory/Defeat: + + **ACT 2B - Complications [25%]** + - Bad Guys Close In: + - Team Breaks Down/Crisis: + - All Is Lost: + - Dark Night of the Soul: + + **ACT 3 - Resolution [25%]** + - Plot Point 2: + - Final Battle Preparation: + - Climax: + - New Normal: + +3. **SAVE THE CAT BEAT SHEET** + Place 15 beats across {SCREENPLAY_LENGTHS[screenplay_type]['pages']} pages + +4. **THEME** + - Central Theme: + - Theme Stated Moment: + - Visual Theme Expression: + +5. **TONE & STYLE** + - Overall Tone: + - Use of Humor: + - Visual Style: + +Create specific, emotionally resonant story.""" + } + + return lang_prompts.get(language, lang_prompts["English"]) + + def create_character_designer_prompt(self, producer_concept: str, story_structure: str, + genre: str, language: str) -> str: + """Character designer prompt""" + + lang_prompts = { + "Korean": f"""๋น์ ์ ์บ๋ฆญํฐ ๋์์ด๋์ ๋๋ค. ๋ค์ธต์ ์ด๊ณ ๋งค๋ ฅ์ ์ธ ์บ๋ฆญํฐ๋ค์ ์ฐฝ์กฐํ์ธ์. + +**ํ๋ก๋์ ์ปจ์ :** +{producer_concept} + +**์คํ ๋ฆฌ ๊ตฌ์กฐ:** +{story_structure} + +**ํ์ ์บ๋ฆญํฐ ํ๋กํ:** + +1. **์ฃผ์ธ๊ณต (PROTAGONIST)** + - ์ด๋ฆ & ๋์ด: + - ์ง์ /์ญํ : + - ์บ๋ฆญํฐ ์ํฌํ์ : + - WANT (์ธ์ ๋ชฉํ): + - NEED (๋ด์ ํ์): + - ์น๋ช ์ ๊ฒฐํจ (Fatal Flaw): + - ๋ฐฑ์คํ ๋ฆฌ (ํต์ฌ ์์ฒ): + - ์ฑ๊ฒฉ ํน์ฑ (3-5๊ฐ): + - ๋งํฌ & ์ธ์ด ํจํด: + - ์๊ฐ์ ํน์ง: + - ์บ๋ฆญํฐ ์ํฌ (AโB): + +2. **์ ๋์ (ANTAGONIST)** + - ์ด๋ฆ & ๋์ด: + - ์ง์ /์ญํ : + - ์ ์ญ ์ํฌํ์ : + - ๋ชฉํ & ๋๊ธฐ: + - ์ฃผ์ธ๊ณต๊ณผ์ ์ฐ๊ฒฐ์ : + - ์ ๋น์ฑ ์๋ ์ด์ : + - ์ฝ์ : + - ํน์ง์ ํ๋: + +3. **์กฐ๋ ฅ์๋ค (SUPPORTING CAST)** + ์ต์ 3๋ช , ๊ฐ๊ฐ: + - ์ด๋ฆ & ์ญํ : + - ์ฃผ์ธ๊ณต๊ณผ์ ๊ด๊ณ: + - ์คํ ๋ฆฌ ๊ธฐ๋ฅ: + - ๋ ํนํ ํน์ฑ: + - ๊ธฐ์ฌํ๋ ๋ฐ: + +4. **์บ๋ฆญํฐ ๊ด๊ณ๋** + - ์ฃผ์ ๊ด๊ณ ์ญํ: + - ๊ฐ๋ฑ ๊ตฌ์กฐ: + - ๊ฐ์ ์ ์ฐ๊ฒฐ: + - ํ์ ๋ค์ด๋๋ฏน: + +5. **์บ์คํ ์ ์** + - ๊ฐ ์ฃผ์ ์บ๋ฆญํฐ๋ณ ์ด์์ ์ธ ๋ฐฐ์ฐ ํ์ + - ์ฐ๋ น๋, ์ธ๋ชจ, ์ฐ๊ธฐ ์คํ์ผ + +6. **๋ํ ์ํ** + - ๊ฐ ์ฃผ์ ์บ๋ฆญํฐ์ ์๊ทธ๋์ฒ ๋์ฌ 2-3๊ฐ + - ์บ๋ฆญํฐ์ ๋ณธ์ง์ ๋๋ฌ๋ด๋ ๋ํ + +๊ฐ ์บ๋ฆญํฐ๊ฐ ํ ๋ง๋ฅผ ๊ตฌํํ๊ณ ์คํ ๋ฆฌ๋ฅผ ์ถ์งํ๋๋ก ๋์์ธํ์ธ์.""", + + "English": f"""You are a character designer. Create multi-dimensional, compelling characters. + +**Producer Concept:** +{producer_concept} + +**Story Structure:** +{story_structure} + +**Required Character Profiles:** + +1. **PROTAGONIST** + - Name & Age: + - Occupation/Role: + - Character Archetype: + - WANT (External Goal): + - NEED (Internal Need): + - Fatal Flaw: + - Backstory (Core Wound): + - Personality Traits (3-5): + - Speech Pattern: + - Visual Characteristics: + - Character Arc (AโB): + +2. **ANTAGONIST** + - Name & Age: + - Occupation/Role: + - Villain Archetype: + - Goal & Motivation: + - Connection to Protagonist: + - Justifiable Reason: + - Weakness: + - Signature Behaviors: + +3. **SUPPORTING CAST** + Minimum 3, each with: + - Name & Role: + - Relationship to Protagonist: + - Story Function: + - Unique Traits: + - What They Contribute: + +4. **CHARACTER RELATIONSHIPS** + - Key Relationship Dynamics: + - Conflict Structure: + - Emotional Connections: + - Power Dynamics: + +5. **CASTING SUGGESTIONS** + - Ideal actor type for each major character + - Age range, appearance, acting style + +6. **DIALOGUE SAMPLES** + - 2-3 signature lines per major character + - Dialogue revealing character essence + +Design each character to embody theme and drive story.""" + } + + return lang_prompts.get(language, lang_prompts["English"]) + + def create_scene_planner_prompt(self, story_structure: str, characters: str, + screenplay_type: str, genre: str, language: str) -> str: + """Scene breakdown planner""" + + total_pages = SCREENPLAY_LENGTHS[screenplay_type]['pages'] + + lang_prompts = { + "Korean": f"""๋น์ ์ ์ฌ ํ๋๋์ ๋๋ค. {total_pages}ํ์ด์ง {screenplay_type}์ ์์ธํ ์ฌ ๋ธ๋ ์ดํฌ๋ค์ด์ ์์ฑํ์ธ์. + +**์คํ ๋ฆฌ ๊ตฌ์กฐ:** +{story_structure} + +**์บ๋ฆญํฐ:** +{characters} + +**์ฌ ๋ธ๋ ์ดํฌ๋ค์ด ์๊ตฌ์ฌํญ:** + +๊ฐ ์ฌ๋ง๋ค ๋ค์ ์ ๋ณด ์ ๊ณต: +- ์ฌ ๋ฒํธ +- ์ฅ์ (INT./EXT. LOCATION) +- ์๊ฐ (DAY/NIGHT/DAWN/DUSK) +- ๋ฑ์ฅ์ธ๋ฌผ +- ์ฌ์ ๋ชฉ์ (์คํ ๋ฆฌ/์บ๋ฆญํฐ/ํ ๋ง) +- ํต์ฌ ๊ฐ๋ฑ +- ์์ ํ์ด์ง ์ +- Save the Cat ๋นํธ (ํด๋น์) + +**๋ง๋ณ ๋ฐฐ๋ถ:** +- 1๋ง: ~{int(total_pages * 0.25)}ํ์ด์ง (10-12์ฌ) +- 2๋งA: ~{int(total_pages * 0.25)}ํ์ด์ง (12-15์ฌ) +- 2๋งB: ~{int(total_pages * 0.25)}ํ์ด์ง (12-15์ฌ) +- 3๋ง: ~{int(total_pages * 0.25)}ํ์ด์ง (8-10์ฌ) + +**์ฅ๋ฅด๋ณ ๊ณ ๋ ค์ฌํญ:** {genre} +{self._get_genre_scene_guidelines(genre, "Korean")} + +**์ฌ ์ ํ ์คํ์ผ:** +- CUT TO: +- FADE IN/OUT: +- MATCH CUT: +- SMASH CUT: +- DISSOLVE TO: + +๊ฐ ์ฌ์ด ์คํ ๋ฆฌ๋ฅผ ์ ์ง์ํค๊ณ ์บ๋ฆญํฐ๋ฅผ ๋ฐ์ ์ํค๋๋ก ๊ณํํ์ธ์.""", + + "English": f"""You are a scene planner. Create detailed scene breakdown for {total_pages}-page {screenplay_type}. + +**Story Structure:** +{story_structure} + +**Characters:** +{characters} + +**Scene Breakdown Requirements:** + +For each scene provide: +- Scene Number +- Location (INT./EXT. LOCATION) +- Time (DAY/NIGHT/DAWN/DUSK) +- Characters Present +- Scene Purpose (Story/Character/Theme) +- Core Conflict +- Estimated Page Count +- Save the Cat Beat (if applicable) + +**Act Distribution:** +- Act 1: ~{int(total_pages * 0.25)} pages (10-12 scenes) +- Act 2A: ~{int(total_pages * 0.25)} pages (12-15 scenes) +- Act 2B: ~{int(total_pages * 0.25)} pages (12-15 scenes) +- Act 3: ~{int(total_pages * 0.25)} pages (8-10 scenes) + +**Genre Considerations:** {genre} +{self._get_genre_scene_guidelines(genre, "English")} + +**Scene Transitions:** +- CUT TO: +- FADE IN/OUT: +- MATCH CUT: +- SMASH CUT: +- DISSOLVE TO: + +Plan each scene to advance story and develop character.""" + } + + return lang_prompts.get(language, lang_prompts["English"]) + + def create_screenwriter_prompt(self, act: str, scene_breakdown: str, + characters: str, previous_acts: str, + screenplay_type: str, genre: str, language: str) -> str: + """Screenwriter prompt for actual screenplay pages""" + + act_pages = int(SCREENPLAY_LENGTHS[screenplay_type]['pages'] * 0.25) + + lang_prompts = { + "Korean": f"""๋น์ ์ ํ๋ก ์๋๋ฆฌ์ค ์๊ฐ์ ๋๋ค. {act}์ ํ์ค ์๋๋ฆฌ์ค ํฌ๋งท์ผ๋ก ์์ฑํ์ธ์. + +**ํ๊ฒ ๋ถ๋:** {act_pages}ํ์ด์ง + +**์ฌ ๋ธ๋ ์ดํฌ๋ค์ด:** +{self._extract_act_scenes(scene_breakdown, act)} + +**์บ๋ฆญํฐ ์ ๋ณด:** +{characters} + +**์ด์ ๋ด์ฉ:** +{previous_acts if previous_acts else "์ฒซ ๋ง์ ๋๋ค."} + +**์๋๋ฆฌ์ค ํฌ๋งท ๊ท์น:** + +1. **์ฌ ํค๋ฉ** + INT. ์ฅ์ - ์๊ฐ + EXT. ์ฅ์ - ์๊ฐ + +2. **์ก์ ๋ผ์ธ** + - ํ์ฌ ์์ ์ฌ์ฉ + - ์๊ฐ์ ์ผ๋ก ๋ณด์ด๋ ๊ฒ๋ง ๋ฌ์ฌ + - 4์ค ์ดํ๋ก ์ ์ง + - ๊ฐ์ ์ ํ๋์ผ๋ก ํํ + +3. **์บ๋ฆญํฐ ์๊ฐ** + ์ฒซ ๋ฑ์ฅ์: ์ด๋ฆ (๋์ด) ๊ฐ๋จํ ๋ฌ์ฌ + +4. **๋ํ** + ์บ๋ฆญํฐ๋ช + (์ง๋ฌธ) + ๋์ฌ + +5. **์ค์ ์์น** + - Show, don't tell + - ์๋ธํ ์คํธ ํ์ฉ + - ์์ฐ์ค๋ฌ์ด ๋ํ + - ์๊ฐ์ ์คํ ๋ฆฌํ ๋ง + - ํ์ด์ง๋น 1๋ถ ๊ท์น + +**{genre} ์ฅ๋ฅด ํน์ฑ:** +- ๋ํ ๋น์จ: {GENRE_TEMPLATES[genre]['dialogue_ratio']*100}% +- ์ฌ ๊ธธ์ด: {GENRE_TEMPLATES[genre]['scene_length']} +- ํต์ฌ ์์: {', '.join(GENRE_TEMPLATES[genre]['key_elements'][:2])} + +์ ํํ ํฌ๋งท๊ณผ ๋ชฐ์ ๊ฐ ์๋ ์คํ ๋ฆฌํ ๋ง์ผ๋ก ์์ฑํ์ธ์.""", + + "English": f"""You are a professional screenwriter. Write {act} in standard screenplay format. + +**Target Length:** {act_pages} pages + +**Scene Breakdown:** +{self._extract_act_scenes(scene_breakdown, act)} + +**Character Info:** +{characters} + +**Previous Content:** +{previous_acts if previous_acts else "This is the first act."} + +**Screenplay Format Rules:** + +1. **Scene Headings** + INT. LOCATION - TIME + EXT. LOCATION - TIME + +2. **Action Lines** + - Present tense + - Only what's visually seen + - Keep under 4 lines + - Emotions through actions + +3. **Character Intros** + First appearance: NAME (age) brief description + +4. **Dialogue** + CHARACTER NAME + (parenthetical) + Dialogue + +5. **Key Principles** + - Show, don't tell + - Use subtext + - Natural dialogue + - Visual storytelling + - One page = one minute + +**{genre} Genre Characteristics:** +- Dialogue Ratio: {GENRE_TEMPLATES[genre]['dialogue_ratio']*100}% +- Scene Length: {GENRE_TEMPLATES[genre]['scene_length']} +- Key Elements: {', '.join(GENRE_TEMPLATES[genre]['key_elements'][:2])} + +Write with proper format and engaging storytelling.""" + } + + return lang_prompts.get(language, lang_prompts["English"]) + + def create_script_doctor_prompt(self, act_content: str, act: str, + genre: str, language: str) -> str: + """Script doctor review and polish""" + + lang_prompts = { + "Korean": f"""๋น์ ์ ์คํฌ๋ฆฝํธ ๋ฅํฐ์ ๋๋ค. {act}๋ฅผ ๊ฒํ ํ๊ณ ๊ฐ์ ์ฌํญ์ ์ ์ํ์ธ์. + +**์์ฑ๋ ๋ด์ฉ:** +{act_content} + +**๊ฒํ ํญ๋ชฉ:** + +1. **ํฌ๋งท ์ ํ์ฑ** + - ์ฌ ํค๋ฉ ํ์ + - ์ก์ ๋ผ์ธ ๊ธธ์ด + - ๋ํ ํฌ๋งท + - ์ ํ ํ์ + +2. **์คํ ๋ฆฌํ ๋ง** + - ์๊ฐ์ ๋ช ํ์ฑ + - ํ์ด์ฑ + - ๊ธด์ฅ๊ฐ ๊ตฌ์ถ + - ์ฌ ๋ชฉ์ ๋ฌ์ฑ + +3. **๋ํ ํ์ง** + - ์์ฐ์ค๋ฌ์ + - ์บ๋ฆญํฐ ๊ณ ์ ์ฑ + - ์๋ธํ ์คํธ + - ๋ถํ์ํ ์ค๋ช ์ ๊ฑฐ + +4. **{genre} ์ฅ๋ฅด ์ ํฉ์ฑ** + - ์ฅ๋ฅด ๊ด์ต ์ค์ + - ํค ์ผ๊ด์ฑ + - ๊ธฐ๋ ์ถฉ์กฑ + +5. **๊ธฐ์ ์ ์ธก๋ฉด** + - ํ์ด์ง ์ ์ ์ ์ฑ + - ์ ์ ๊ฐ๋ฅ์ฑ + - ์์ฐ ๊ณ ๋ ค์ฌํญ + +**ํ์ ๊ฐ์ ์ฌํญ:** +๊ตฌ์ฒด์ ์ธ ์์ ์ ์๊ณผ ๊ฐ์ ๋ ์์๋ฅผ ์ ๊ณตํ์ธ์.""", + + "English": f"""You are a script doctor. Review and provide improvements for {act}. + +**Written Content:** +{act_content} + +**Review Areas:** + +1. **Format Accuracy** + - Scene heading format + - Action line length + - Dialogue format + - Transitions + +2. **Storytelling** + - Visual clarity + - Pacing + - Tension building + - Scene purpose achievement + +3. **Dialogue Quality** + - Naturalness + - Character uniqueness + - Subtext + - Remove exposition + +4. **{genre} Genre Fit** + - Genre conventions + - Tone consistency + - Meeting expectations + +5. **Technical Aspects** + - Page count appropriateness + - Production feasibility + - Budget considerations + +**Required Improvements:** +Provide specific revision suggestions with improved examples.""" + } + + return lang_prompts.get(language, lang_prompts["English"]) + + def create_final_reviewer_prompt(self, complete_screenplay: str, + screenplay_type: str, genre: str, language: str) -> str: + """Final comprehensive review""" + + lang_prompts = { + "Korean": f"""๋น์ ์ ์ต์ข ๋ฆฌ๋ทฐ์ด์ ๋๋ค. ์์ฑ๋ {screenplay_type} ์๋๋ฆฌ์ค๋ฅผ ์ข ํฉ ํ๊ฐํ์ธ์. + +**ํ๊ฐ ๊ธฐ์ค:** + +1. **์์ ์ฑ (25์ )** + - ์์ฅ์ฑ + - ํ๊ฒ ๊ด๊ฐ ์ดํ + - ์ ์ ๊ฐ๋ฅ์ฑ + - ๋ฐฐ๊ธ ์ ์ฌ๋ ฅ + +2. **์คํ ๋ฆฌ (25์ )** + - 3๋ง ๊ตฌ์กฐ ํจ๊ณผ์ฑ + - ์บ๋ฆญํฐ ์ํฌ + - ํ๋กฏ ์ผ๊ด์ฑ + - ํ ๋ง ์ ๋ฌ + +3. **๊ธฐ์ ์ ์์ฑ๋ (25์ )** + - ํฌ๋งท ์ ํ์ฑ + - ํ์ด์ง ์ ์ ์ ์ฑ + - ์ฌ ๊ตฌ์ฑ + - ์๊ฐ์ ์คํ ๋ฆฌํ ๋ง + +4. **๋ํ & ์บ๋ฆญํฐ (25์ )** + - ๋ํ ์์ฐ์ค๋ฌ์ + - ์บ๋ฆญํฐ ๊ณ ์ ์ฑ + - ๊ด๊ณ ์ญํ + - ๊ฐ์ ์ ์ง์ ์ฑ + +**์ข ํฉ ํ๊ฐ:** +- ๊ฐ์ (3-5๊ฐ) +- ๊ฐ์ ํ์ ์ฌํญ (3-5๊ฐ) +- ์์ฅ ์ ์ฌ๋ ฅ +- ์ถ์ฒ ์ฌํญ + +**๋ฑ๊ธ:** A+ ~ F + +๊ตฌ์ฒด์ ์ด๊ณ ๊ฑด์ค์ ์ธ ํผ๋๋ฐฑ์ ์ ๊ณตํ์ธ์.""", + + "English": f"""You are the final reviewer. Comprehensively evaluate the completed {screenplay_type} screenplay. + +**Evaluation Criteria:** + +1. **Commercial Viability (25 points)** + - Marketability + - Target audience appeal + - Production feasibility + - Distribution potential + +2. **Story (25 points)** + - Three-act structure effectiveness + - Character arcs + - Plot consistency + - Theme delivery + +3. **Technical Excellence (25 points)** + - Format accuracy + - Page count appropriateness + - Scene construction + - Visual storytelling + +4. **Dialogue & Character (25 points)** + - Dialogue naturalness + - Character uniqueness + - Relationship dynamics + - Emotional authenticity + +**Overall Assessment:** +- Strengths (3-5) +- Areas for Improvement (3-5) +- Market Potential +- Recommendations + +**Grade:** A+ to F + +Provide specific, constructive feedback.""" + } + + return lang_prompts.get(language, lang_prompts["English"]) + + def create_critic_structure_prompt(self, story_structure: str, characters: str, + screenplay_type: str, genre: str, language: str) -> str: + """Structure critic prompt""" + lang_prompts = { + "Korean": f"""๋น์ ์ ๊ตฌ์กฐ ๋นํ๊ฐ์ ๋๋ค. ์คํ ๋ฆฌ ๊ตฌ์กฐ์ ์บ๋ฆญํฐ ์ค์ ์ ์ฌ์ธต ๋ถ์ํ์ธ์. + +**์คํ ๋ฆฌ ๊ตฌ์กฐ:** +{story_structure} + +**์บ๋ฆญํฐ ์ค์ :** +{characters} + +**๋ถ์ ํญ๋ชฉ:** + +1. **3๋ง ๊ตฌ์กฐ ํจ๊ณผ์ฑ** + - ๊ฐ ๋ง์ ๊ท ํ + - ์ ํ์ ์ ๊ฐ๋ + - ํ๋กฏ ํฌ์ธํธ์ ๋ช ํ์ฑ + - ํด๋ผ์ด๋งฅ์ค ์์น + +2. **์บ๋ฆญํฐ ์ํฌ ํ๋น์ฑ** + - ๋ณํ์ ์ ๋น์ฑ + - ๋๊ธฐ์ ๋ช ํ์ฑ + - ๋ด์ /์ธ์ ๋ชฉํ ์ผ์น + - ๊ด๊ณ ์ญํ + +3. **ํ ๋ง ํตํฉ** + - ํ ๋ง์ ์ผ๊ด์ฑ + - ํ๋กฏ๊ณผ ํ ๋ง ์ฐ๊ฒฐ + - ์บ๋ฆญํฐ์ ํ ๋ง ์ฐ๊ฒฐ + - ์๊ฐ์ ํ ๋ง ํํ + +4. **์ฅ๋ฅด ๊ธฐ๋์น** + - {genre} ๊ด์ต ์ถฉ์กฑ + - ๋ ์ฐฝ์ฑ๊ณผ ์น์ํจ ๊ท ํ + - ํ๊ฒ ๊ด๊ฐ ๋ง์กฑ๋ + +5. **์ ์ ํ์ค์ฑ** + - ์์ฐ ๊ท๋ชจ ์ ์ ์ฑ + - ๋ก์ผ์ด์ ์คํ ๊ฐ๋ฅ์ฑ + - ํน์ํจ๊ณผ ์๊ตฌ์ฌํญ + +**ํ์ ๊ฐ์ ์ ์:** +๊ฐ ๋ฌธ์ ์ ์ ๋ํ ๊ตฌ์ฒด์ ํด๊ฒฐ์ฑ ์ ์ ์ํ์ธ์.""", + + "English": f"""You are a structure critic. Deeply analyze story structure and character setup. + +**Story Structure:** +{story_structure} + +**Character Setup:** +{characters} + +**Analysis Items:** + +1. **Three-Act Structure Effectiveness** + - Balance of each act + - Strength of transitions + - Clarity of plot points + - Climax positioning + +2. **Character Arc Validity** + - Credibility of change + - Clarity of motivation + - Internal/external goal alignment + - Relationship dynamics + +3. **Theme Integration** + - Theme consistency + - Plot-theme connection + - Character-theme connection + - Visual theme expression + +4. **Genre Expectations** + - Meeting {genre} conventions + - Balance of originality and familiarity + - Target audience satisfaction + +5. **Production Reality** + - Budget scale appropriateness + - Location feasibility + - Special effects requirements + +**Required Improvement Suggestions:** +Provide specific solutions for each issue.""" + } + + return lang_prompts.get(language, lang_prompts["English"]) + + def _get_genre_scene_guidelines(self, genre: str, language: str) -> str: + """Get genre-specific scene guidelines""" + guidelines = { + "action": { + "Korean": "- ์งง๊ณ ํ์น๊ฐ ์๋ ์ฌ\n- ์ก์ ์ํ์ค ์์ธ ๊ณํ\n- ๊ธด์ฅ๊ฐ ์ง์", + "English": "- Short, punchy scenes\n- Detailed action sequences\n- Maintain tension" + }, + "thriller": { + "Korean": "- ์์คํ์ค ๊ตฌ์ถ\n- ์ ๋ณด ์ ์ง์ ๊ณต๊ฐ\n- ๋ฐ์ ๋ฐฐ์น", + "English": "- Build suspense\n- Gradual information reveal\n- Place twists" + }, + "drama": { + "Korean": "- ๊ฐ์ ์ ๋นํธ ๊ฐ์กฐ\n- ์บ๋ฆญํฐ ์ค์ฌ ์ฌ\n- ๋ํ ๊ณต๊ฐ ํ๋ณด", + "English": "- Emphasize emotional beats\n- Character-driven scenes\n- Allow dialogue space" + }, + "comedy": { + "Korean": "- ์ ์ ๊ณผ ํ์ด์คํ\n- ์ฝ๋ฏน ํ์ด๋ฐ\n- ์๊ฐ์ ๊ฐ๊ทธ", + "English": "- Setup and payoff\n- Comic timing\n- Visual gags" + }, + "horror": { + "Korean": "- ๋ถ์๊ธฐ ์กฐ์ฑ\n- ์ ํ ์ค์ผ์ด ๋ฐฐ์น\n- ๊ธด์ฅ๊ณผ ์ด์", + "English": "- Atmosphere building\n- Jump scare placement\n- Tension and release" + }, + "sci-fi": { + "Korean": "- ์ธ๊ณ๊ด ์ค๋ช \n- ์๊ฐ์ ์คํํฐํด\n- ๊ฐ๋ ์๊ฐ", + "English": "- World building\n- Visual spectacle\n- Concept introduction" + }, + "romance": { + "Korean": "- ๊ฐ์ ์ ์น๋ฐ๊ฐ\n- ๊ด๊ณ ๋ฐ์ \n- ๋ก๋งจํฑ ๋นํธ", + "English": "- Emotional intimacy\n- Relationship progression\n- Romantic beats" + } + } + + return guidelines.get(genre, guidelines["drama"]).get(language, "") + + def _extract_act_scenes(self, scene_breakdown: str, act: str) -> str: + """Extract scenes for specific act""" + # This would parse the scene breakdown and return only scenes for the requested act + # For now, returning a placeholder + return f"Scenes for {act} from the breakdown" + + # --- LLM call functions --- + def call_llm_sync(self, messages: List[Dict[str, str]], role: str, language: str) -> str: + full_content = "" + for chunk in self.call_llm_streaming(messages, role, language): + full_content += chunk + if full_content.startswith("โ"): + raise Exception(f"LLM Call Failed: {full_content}") + return full_content + + + + def call_llm_streaming(self, messages: List[Dict[str, str]], role: str, + language: str) -> Generator[str, None, None]: + try: + # Debug logging + logger.info(f"Calling LLM for role: {role}, language: {language}") + + system_prompts = self.get_system_prompts(language) + system_content = system_prompts.get(role, "") + + if not system_content: + logger.warning(f"No system prompt found for role: {role}") + system_content = "You are a helpful assistant." + + full_messages = [ + {"role": "system", "content": system_content}, + *messages + ] + + max_tokens = 15000 if role == "screenwriter" else 8000 + + payload = { + "model": self.model_id, + "messages": full_messages, + "max_tokens": max_tokens, + "temperature": 0.7 if role in ["screenwriter", "script_doctor"] else 0.8, + "top_p": 0.9, + "presence_penalty": 0.3, + "frequency_penalty": 0.3, + "stream": True + } + + # Debug logging + logger.debug(f"API URL: {self.api_url}") + logger.debug(f"Model ID: {self.model_id}") + + try: + headers = self.create_headers() + except ValueError as e: + logger.error(f"Header creation failed: {e}") + yield f"โ API configuration error: {e}" + return + + response = requests.post( + self.api_url, + headers=headers, + json=payload, + stream=True, + timeout=180 + ) + + logger.info(f"API Response Status: {response.status_code}") + + if response.status_code != 200: + error_msg = f"API Error (Status Code: {response.status_code})" + try: + error_data = response.json() + logger.error(f"API Error Response: {error_data}") + if isinstance(error_data, dict): + if 'error' in error_data: + error_msg += f" - {error_data['error']}" + elif 'message' in error_data: + error_msg += f" - {error_data['message']}" + except Exception as e: + logger.error(f"Error parsing error response: {e}") + error_msg += f" - {response.text[:200]}" + + yield f"โ {error_msg}" + return + + buffer = "" + line_count = 0 + + for line in response.iter_lines(): + if not line: + continue + + line_count += 1 + + try: + line_str = line.decode('utf-8').strip() + + # Skip non-SSE lines + if not line_str.startswith("data: "): + logger.debug(f"Skipping non-SSE line: {line_str[:50]}") + continue + + data_str = line_str[6:] # Remove "data: " prefix + + if data_str == "[DONE]": + logger.info(f"Stream completed. Total lines: {line_count}") + break + + if not data_str: + continue + + # Parse JSON data + try: + data = json.loads(data_str) + except json.JSONDecodeError as e: + logger.warning(f"JSON decode error on line {line_count}: {e}") + logger.debug(f"Problematic data: {data_str[:100]}") + continue + + # Extract content from response + if isinstance(data, dict) and "choices" in data: + choices = data["choices"] + if isinstance(choices, list) and len(choices) > 0: + choice = choices[0] + if isinstance(choice, dict) and "delta" in choice: + delta = choice["delta"] + if isinstance(delta, dict) and "content" in delta: + content = delta["content"] + if content: + buffer += content + + # Yield when buffer is large enough + if len(buffer) >= 50 or '\n' in buffer: + yield buffer + buffer = "" + time.sleep(0.01) + + except Exception as e: + logger.error(f"Error processing line {line_count}: {str(e)}") + logger.debug(f"Problematic line: {line_str[:100] if 'line_str' in locals() else 'N/A'}") + continue + + # Yield any remaining buffer content + if buffer: + yield buffer + + # Check if we got any content + if line_count == 0: + logger.error("No lines received from API") + yield "โ No response from API" + + except requests.exceptions.Timeout: + logger.error("API request timed out") + yield "โ Request timed out. Please try again." + except requests.exceptions.ConnectionError as e: + logger.error(f"Connection error: {e}") + yield "โ Connection error. Please check your internet connection." + except requests.exceptions.RequestException as e: + logger.error(f"Request error: {type(e).__name__}: {str(e)}") + yield f"โ Network error: {str(e)}" + except Exception as e: + logger.error(f"Unexpected error in streaming: {type(e).__name__}: {str(e)}") + import traceback + logger.error(traceback.format_exc()) + yield f"โ Unexpected error: {str(e)}" + + + + def get_system_prompts(self, language: str) -> Dict[str, str]: + """Role-specific system prompts""" + + base_prompts = { + "Korean": { + "producer": """๋น์ ์ 20๋ ๊ฒฝ๋ ฅ์ ํ ๋ฆฌ์ฐ๋ ํ๋ก๋์์ ๋๋ค. +์์ ์ ์ฑ๊ณต๊ณผ ์์ ์ ๊ฐ์น๋ฅผ ๋ชจ๋ ์ถ๊ตฌํฉ๋๋ค. +์์ฅ ํธ๋ ๋์ ๊ด๊ฐ ์ฌ๋ฆฌ๋ฅผ ์ ํํ ํ์ ํฉ๋๋ค. +์คํ ๊ฐ๋ฅํ๊ณ ๋งค๋ ฅ์ ์ธ ํ๋ก์ ํธ๋ฅผ ๊ฐ๋ฐํฉ๋๋ค.""", + + "story_developer": """๋น์ ์ ์์ ๊ฒฝ๋ ฅ์ด ์๋ ์คํ ๋ฆฌ ๊ฐ๋ฐ์์ ๋๋ค. +๊ฐ์ ์ ์ผ๋ก ๊ณต๊ฐ๊ฐ๊ณ ๊ตฌ์กฐ์ ์ผ๋ก ํํํ ์ด์ผ๊ธฐ๋ฅผ ๋ง๋ญ๋๋ค. +์บ๋ฆญํฐ์ ๋ด์ ์ฌ์ ๊ณผ ์ธ์ ํ๋กฏ์ ์กฐํ๋กญ๊ฒ ์ฎ์ต๋๋ค. +๋ณดํธ์ ์ฃผ์ ๋ฅผ ๋ ํนํ ๋ฐฉ์์ผ๋ก ํ๊ตฌํฉ๋๋ค.""", + + "character_designer": """๋น์ ์ ์ฌ๋ฆฌํ์ ๊ณต๋ถํ ์บ๋ฆญํฐ ๋์์ด๋์ ๋๋ค. +์ง์ง ๊ฐ์ ์ธ๋ฌผ๋ค์ ์ฐฝ์กฐํ๋ ์ ๋ฌธ๊ฐ์ ๋๋ค. +๊ฐ ์บ๋ฆญํฐ์๊ฒ ๊ณ ์ ํ ๋ชฉ์๋ฆฌ์ ๊ด์ ์ ๋ถ์ฌํฉ๋๋ค. +๋ณต์กํ๊ณ ๋ชจ์์ ์ธ ์ธ๊ฐ์ฑ์ ํฌ์ฐฉํฉ๋๋ค.""", + + "scene_planner": """๋น์ ์ ์ ๋ฐํ ์ฌ ๊ตฌ์ฑ์ ๋๊ฐ์ ๋๋ค. +๊ฐ ์ฌ์ด ์คํ ๋ฆฌ์ ์บ๋ฆญํฐ๋ฅผ ์ ์ง์ํค๋๋ก ์ค๊ณํฉ๋๋ค. +๋ฆฌ๋ฌ๊ณผ ํ์ด์ฑ์ ์๋ฒฝํ๊ฒ ์กฐ์ ํฉ๋๋ค. +์๊ฐ์ ์คํ ๋ฆฌํ ๋ง์ ๊ทน๋ํํฉ๋๋ค.""", + + "screenwriter": """๋น์ ์ ๋ค์์ ์๋๋ฆฌ์ค ์๊ฐ์ ๋๋ค. +'๋ณด์ฌ์ฃผ๊ธฐ'์ ๋๊ฐ์ด๋ฉฐ ์๋ธํ ์คํธ๋ฅผ ๋ฅ์ํ๊ฒ ๋ค๋ฃน๋๋ค. +์์ํ๊ณ ์์ฐ์ค๋ฌ์ด ๋ํ๋ฅผ ์ฐ๋ ์ ๋ฌธ๊ฐ์ ๋๋ค. +์ ์ ํ์ค์ ๊ณ ๋ คํ๋ฉด์๋ ์ฐฝ์์ ์ธ ํด๊ฒฐ์ฑ ์ ์ฐพ์ต๋๋ค.""", + + "script_doctor": """๋น์ ์ ๊น๋ค๋ก์ด ์คํฌ๋ฆฝํธ ๋ฅํฐ์ ๋๋ค. +์์ ๋ํ ์ผ๋ ๋์น์ง ์๋ ์๋ฒฝ์ฃผ์์์ ๋๋ค. +์คํ ๋ฆฌ์ ์ ์ฌ๋ ฅ์ ์ต๋ํ ๋์ด๋ ๋๋ค. +๊ฑด์ค์ ์ด๊ณ ๊ตฌ์ฒด์ ์ธ ๊ฐ์ ์์ ์ ์ํฉ๋๋ค.""", + + "critic_structure": """๋น์ ์ ๊ตฌ์กฐ ๋ถ์ ์ ๋ฌธ๊ฐ์ ๋๋ค. +์คํ ๋ฆฌ์ ๋ผ๋์ ๊ทผ์ก์ ๊ฟฐ๋ซ์ด ๋ด ๋๋ค. +๋ ผ๋ฆฌ์ ํ์ ๊ณผ ๊ฐ์ ์ ๊ณต๋ฐฑ์ ์ฐพ์๋ ๋๋ค. +๋ ๋์ ๊ตฌ์กฐ๋ฅผ ์ํ ๊ตฌ์ฒด์ ์ ์์ ํฉ๋๋ค.""", + + "final_reviewer": """๋น์ ์ ์ ๊ณ ๋ฒ ํ ๋ ์ต์ข ๋ฆฌ๋ทฐ์ด์ ๋๋ค. +์์ ์ฑ๊ณผ ์์ ์ฑ์ ๊ท ํ์๊ฒ ํ๊ฐํฉ๋๋ค. +์ ์์ฌ, ๋ฐฐ์ฐ, ๊ด๊ฐ ๋ชจ๋ ๊ด์ ์ ๊ณ ๋ คํฉ๋๋ค. +๋์ ํ์ง๋ง ๊ฒฉ๋ คํ๋ ํผ๋๋ฐฑ์ ์ ๊ณตํฉ๋๋ค.""" + }, + "English": { + "producer": """You are a Hollywood producer with 20 years experience. +You pursue both commercial success and artistic value. +You accurately grasp market trends and audience psychology. +You develop feasible and attractive projects.""", + + "story_developer": """You are an award-winning story developer. +You create emotionally resonant and structurally sound stories. +You harmoniously weave internal journeys with external plots. +You explore universal themes in unique ways.""", + + "character_designer": """You are a character designer who studied psychology. +You're an expert at creating lifelike characters. +You give each character a unique voice and perspective. +You capture complex and contradictory humanity.""", + + "scene_planner": """You are a master of precise scene construction. +You design each scene to advance story and character. +You perfectly control rhythm and pacing. +You maximize visual storytelling.""", + + "screenwriter": """You are a prolific screenwriter. +You're a master of 'showing' and skilled with subtext. +You're an expert at writing vivid, natural dialogue. +You find creative solutions while considering production reality.""", + + "script_doctor": """You are a demanding script doctor. +You're a perfectionist who misses no small detail. +You maximize the story's potential. +You provide constructive and specific improvements.""", + + "critic_structure": """You are a structure analysis expert. +You see through the story's skeleton and muscles. +You find logical gaps and emotional voids. +You make specific suggestions for better structure.""", + + "final_reviewer": """You are an industry veteran final reviewer. +You evaluate commercial and artistic value in balance. +You consider all perspectives: producers, actors, audience. +You provide feedback that's critical yet encouraging.""" + } + } + + return base_prompts.get(language, base_prompts["English"]) + + + +# --- Main process --- + def process_screenplay_stream(self, query: str, screenplay_type: str, genre: str, + language: str, session_id: Optional[str] = None + ) -> Generator[Tuple[str, List[Dict[str, Any]], str], None, None]: + """Main screenplay generation process""" + try: + resume_from_stage = 0 + if session_id: + self.current_session_id = session_id + session = ScreenplayDatabase.get_session(session_id) + if session: + query = session['user_query'] + screenplay_type = session['screenplay_type'] + genre = session['genre'] + language = session['language'] + resume_from_stage = session['current_stage'] + 1 + else: + self.current_session_id = ScreenplayDatabase.create_session( + query, screenplay_type, genre, language + ) + logger.info(f"Created new screenplay session: {self.current_session_id}") + + stages = [] + if resume_from_stage > 0: + # Get existing stages from database + db_stages = ScreenplayDatabase.get_stages(self.current_session_id) + stages = [{ + "name": s['stage_name'], + "status": s['status'], + "content": s.get('content', ''), + "page_count": s.get('page_count', 0) + } for s in db_stages] + + for stage_idx in range(resume_from_stage, len(SCREENPLAY_STAGES)): + role, stage_name = SCREENPLAY_STAGES[stage_idx] + + if stage_idx >= len(stages): + stages.append({ + "name": stage_name, + "status": "active", + "content": "", + "page_count": 0 + }) + else: + stages[stage_idx]["status"] = "active" + + yield f"๐ Processing {stage_name}...", stages, self.current_session_id + + prompt = self.get_stage_prompt(stage_idx, role, query, screenplay_type, + genre, language, stages) + stage_content = "" + + for chunk in self.call_llm_streaming([{"role": "user", "content": prompt}], + role, language): + stage_content += chunk + stages[stage_idx]["content"] = stage_content + if role == "screenwriter": + stages[stage_idx]["page_count"] = len(stage_content.split('\n')) / 55 + yield f"๐ {stage_name} in progress...", stages, self.current_session_id + + # Process content based on role + if role == "producer": + self._process_producer_content(stage_content) + elif role == "story_developer": + self._process_story_content(stage_content) + elif role == "character_designer": + self._process_character_content(stage_content) + elif role == "scene_planner": + self._process_scene_content(stage_content) + + stages[stage_idx]["status"] = "complete" + ScreenplayDatabase.save_stage( + self.current_session_id, stage_idx, stage_name, role, + stage_content, "complete" + ) + + yield f"โ {stage_name} completed", stages, self.current_session_id + + # Final processing + final_screenplay = ScreenplayDatabase.get_screenplay_content(self.current_session_id) + title = self.screenplay_tracker.screenplay_bible.title + logline = self.screenplay_tracker.screenplay_bible.logline + + ScreenplayDatabase.update_final_screenplay( + self.current_session_id, final_screenplay, title, logline + ) + + yield f"โ Screenplay completed! {title}", stages, self.current_session_id + + except Exception as e: + logger.error(f"Screenplay generation error: {e}", exc_info=True) + yield f"โ Error occurred: {e}", stages if 'stages' in locals() else [], self.current_session_id + + def get_stage_prompt(self, stage_idx: int, role: str, query: str, + screenplay_type: str, genre: str, language: str, + stages: List[Dict]) -> str: + """Generate stage-specific prompt""" + if stage_idx == 0: # Producer + return self.create_producer_prompt(query, screenplay_type, genre, language) + + if stage_idx == 1: # Story Developer + return self.create_story_developer_prompt( + stages[0]["content"], query, screenplay_type, genre, language + ) + + if stage_idx == 2: # Character Designer + return self.create_character_designer_prompt( + stages[0]["content"], stages[1]["content"], genre, language + ) + + if stage_idx == 3: # Structure Critic + return self.create_critic_structure_prompt( + stages[1]["content"], stages[2]["content"], screenplay_type, genre, language + ) + + if stage_idx == 4: # Scene Planner + return self.create_scene_planner_prompt( + stages[1]["content"], stages[2]["content"], screenplay_type, genre, language + ) + + # Screenwriter acts + if role == "screenwriter": + act_mapping = {5: "Act 1", 7: "Act 2A", 9: "Act 2B", 11: "Act 3"} + if stage_idx in act_mapping: + act = act_mapping[stage_idx] + previous_acts = self._get_previous_acts(stages, stage_idx) + return self.create_screenwriter_prompt( + act, stages[4]["content"], stages[2]["content"], + previous_acts, screenplay_type, genre, language + ) + + # Script doctor reviews + if role == "script_doctor": + act_mapping = {6: "Act 1", 8: "Act 2A", 10: "Act 2B"} + if stage_idx in act_mapping: + act = act_mapping[stage_idx] + act_content = stages[stage_idx-1]["content"] + return self.create_script_doctor_prompt(act_content, act, genre, language) + + # Final reviewer + if role == "final_reviewer": + complete_screenplay = ScreenplayDatabase.get_screenplay_content(self.current_session_id) + return self.create_final_reviewer_prompt( + complete_screenplay, screenplay_type, genre, language + ) + + return "" + + def _get_previous_acts(self, stages: List[Dict], current_idx: int) -> str: + """Get previous acts content""" + previous = [] + act_indices = {5: [], 7: [5], 9: [5, 7], 11: [5, 7, 9]} + + if current_idx in act_indices: + for idx in act_indices[current_idx]: + if idx < len(stages) and stages[idx]["content"]: + previous.append(stages[idx]["content"]) + + return "\n\n---\n\n".join(previous) if previous else "" + + + def _parse_character_profile(self, content: str, role: str) -> CharacterProfile: + """Parse character profile from content""" + # Debug logging + logger.debug(f"Parsing character profile for role: {role}") + logger.debug(f"Content preview: {content[:200]}...") + + # Extract name first - handle various formats + name = f"Character_{role}" # default + name_patterns = [ + r'(?:์ด๋ฆ|Name)[:\s]*([^,\n]+?)(?:\s*\([^)]+\))?\s*,?\s*\d*์ธ?', + r'^\s*[-*โข]\s*([^,\n]+?)(?:\s*\([^)]+\))?\s*,?\s*\d*์ธ?', + r'^([^,\n]+?)(?:\s*\([^)]+\))?\s*,?\s*\d*์ธ?' + ] + + for pattern in name_patterns: + name_match = re.search(pattern, content, re.IGNORECASE | re.MULTILINE) + if name_match: + extracted_name = name_match.group(1).strip() + # Remove markdown and extra characters + extracted_name = re.sub(r'[*:\s]+$', '', extracted_name) + extracted_name = re.sub(r'^[*:\s]+', '', extracted_name) + if extracted_name and len(extracted_name) > 1: + name = extracted_name + break + + # Extract age with multiple patterns + age = 30 # default age + age_patterns = [ + r'(\d+)\s*์ธ', + r'(\d+)\s*์ด', + r',\s*(\d+)\s*[,\s]', + r'\((\d+)\)', + r'Age[:\s]*(\d+)', + r'๋์ด[:\s]*(\d+)' + ] + + for pattern in age_patterns: + age_match = re.search(pattern, content, re.IGNORECASE) + if age_match: + try: + extracted_age = int(age_match.group(1)) + if 10 <= extracted_age <= 100: # Reasonable age range + age = extracted_age + logger.debug(f"Extracted age: {age}") + break + except ValueError: + continue + + # Helper function to extract clean fields + def extract_clean_field(patterns): + if isinstance(patterns, str): + patterns = [patterns] + + for pattern in patterns: + match = re.search(rf'{pattern}[:\s]*([^\n*]+?)(?=\n|$)', content, re.IGNORECASE | re.DOTALL) + if match: + value = match.group(1).strip() + # Clean up the value + value = re.sub(r'^[-*โข:\s]+', '', value) + value = re.sub(r'[*]+', '', value) + value = re.sub(r'\s+', ' ', value) + if value: + return value + return "" + + # Extract all fields + profile = CharacterProfile( + name=name, + age=age, + role=role, + archetype=extract_clean_field([ + r"์บ๋ฆญํฐ ์ํฌํ์ ", + r"Character Archetype", + r"Archetype", + r"์ํฌํ์ " + ]), + want=extract_clean_field([ + r"WANT\s*\(์ธ์ ๋ชฉํ\)", + r"WANT", + r"์ธ์ ๋ชฉํ", + r"External Goal" + ]), + need=extract_clean_field([ + r"NEED\s*\(๋ด์ ํ์\)", + r"NEED", + r"๋ด์ ํ์", + r"Internal Need" + ]), + backstory=extract_clean_field([ + r"๋ฐฑ์คํ ๋ฆฌ", + r"Backstory", + r"ํต์ฌ ์์ฒ", + r"Core Wound" + ]), + personality=self._extract_personality_traits(content), + speech_pattern=extract_clean_field([ + r"๋งํฌ.*?ํจํด", + r"Speech Pattern", + r"์ธ์ด ํจํด", + r"๋งํฌ" + ]), + character_arc=extract_clean_field([ + r"์บ๋ฆญํฐ ์ํฌ", + r"Character Arc", + r"Arc", + r"๋ณํ" + ]) + ) + + logger.debug(f"Parsed character: {profile.name}, age: {profile.age}") + return profile + + def _extract_field(self, content: str, field_pattern: str) -> Optional[str]: + """Extract field value from content with improved parsing""" + # Use word boundary to prevent partial matches + pattern = rf'\b{field_pattern}\b[:\s]*([^\n]+?)(?=\n|$)' + match = re.search(pattern, content, re.IGNORECASE) + if match: + value = match.group(1).strip() + # Remove markdown formatting + value = re.sub(r'\*+', '', value) + # Remove list markers + value = re.sub(r'^\s*[-โข*]\s*', '', value) + # Remove trailing punctuation + value = re.sub(r'[,.:;]$', '', value) + return value.strip() + return None + + def _extract_personality_traits(self, content: str) -> List[str]: + """Extract personality traits from content""" + traits = [] + # Look for personality section + personality_section = self._extract_field(content, r"(?:Personality|์ฑ๊ฒฉ)[:\s]*") + if personality_section: + # Extract individual traits (usually listed) + trait_lines = personality_section.split('\n') + for line in trait_lines: + line = line.strip() + if line and not line.endswith(':'): + # Remove list markers + trait = re.sub(r'^\s*[-โข*]\s*', '', line) + if trait: + traits.append(trait) + return traits[:5] # Limit to 5 traits + + def _process_character_content(self, content: str): + """Process character designer output with better error handling""" + try: + # Extract protagonist + protagonist_section = self._extract_section(content, r"(?:PROTAGONIST|์ฃผ์ธ๊ณต)") + if protagonist_section: + protagonist = self._parse_character_profile(protagonist_section, "protagonist") + self.screenplay_tracker.add_character(protagonist) + ScreenplayDatabase.save_character(self.current_session_id, protagonist) + + # Extract antagonist + antagonist_section = self._extract_section(content, r"(?:ANTAGONIST|์ ๋์)") + if antagonist_section: + antagonist = self._parse_character_profile(antagonist_section, "antagonist") + self.screenplay_tracker.add_character(antagonist) + ScreenplayDatabase.save_character(self.current_session_id, antagonist) + + # Extract supporting characters + supporting_section = self._extract_section(content, r"(?:SUPPORTING CAST|์กฐ๋ ฅ์๋ค)") + if supporting_section: + # Parse multiple supporting characters + self._parse_supporting_characters(supporting_section) + + except Exception as e: + logger.error(f"Error processing character content: {e}") + # Continue with default values rather than failing + + def _parse_supporting_characters(self, content: str): + """Parse supporting characters from content""" + # Split by character markers (numbers or bullets) + char_sections = re.split(r'\n(?:\d+\.|[-โข*])\s*', content) + + for i, section in enumerate(char_sections[1:], 1): # Skip first empty split + if section.strip(): + try: + name = self._extract_field(section, r"(?:Name|์ด๋ฆ)[:\s]*") or f"Supporting_{i}" + role = self._extract_field(section, r"(?:Role|์ญํ )[:\s]*") or "supporting" + + character = CharacterProfile( + name=name, + age=30, # Default age for supporting characters + role="supporting", + archetype=role, + want="", + need="", + backstory=self._extract_field(section, r"(?:Backstory|๋ฐฑ์คํ ๋ฆฌ)[:\s]*") or "", + personality=[], + speech_pattern="", + character_arc="" + ) + + self.screenplay_tracker.add_character(character) + ScreenplayDatabase.save_character(self.current_session_id, character) + + except Exception as e: + logger.warning(f"Error parsing supporting character {i}: {e}") + continue + + def _extract_section(self, content: str, section_pattern: str) -> str: + """Extract section from content with improved pattern matching""" + # More flexible section extraction + pattern = rf'{section_pattern}[:\s]*\n?(.*?)(?=\n\n[A-Z๊ฐ-ํฃ]{{2,}}[:\s]|\n\n\d+\.|$)' + match = re.search(pattern, content, re.IGNORECASE | re.DOTALL) + if match: + return match.group(1).strip() + + # Try alternative pattern + pattern2 = rf'{section_pattern}.*?\n((?:.*\n)*?)(?=\n[A-Z๊ฐ-ํฃ]{{2,}}:|$)' + match2 = re.search(pattern2, content, re.IGNORECASE | re.DOTALL) + if match2: + return match2.group(1).strip() + + return "" + + def _process_producer_content(self, content: str): + """Process producer output with better extraction""" + try: + # Extract title with various formats + title_patterns = [ + r'(?:TITLE|์ ๋ชฉ)[:\s]*\*?\*?([^\n*]+)\*?\*?', + r'\*\*(?:TITLE|์ ๋ชฉ)\*\*[:\s]*([^\n]+)', + r'Title[:\s]*([^\n]+)' + ] + + for pattern in title_patterns: + title_match = re.search(pattern, content, re.IGNORECASE) + if title_match: + self.screenplay_tracker.screenplay_bible.title = title_match.group(1).strip() + break + + # Extract logline with various formats + logline_patterns = [ + r'(?:LOGLINE|๋ก๊ทธ๋ผ์ธ)[:\s]*\*?\*?([^\n]+)', + r'\*\*(?:LOGLINE|๋ก๊ทธ๋ผ์ธ)\*\*[:\s]*([^\n]+)', + r'Logline[:\s]*([^\n]+)' + ] + + for pattern in logline_patterns: + logline_match = re.search(pattern, content, re.IGNORECASE | re.DOTALL) + if logline_match: + # Get full logline (might be multi-line) + logline_text = logline_match.group(1).strip() + # Continue reading if it's incomplete + if not logline_text.endswith('.'): + next_lines = content[logline_match.end():].split('\n') + for line in next_lines[:3]: # Check next 3 lines + if line.strip() and not re.match(r'^[A-Z๊ฐ-ํฃ\d]', line.strip()): + logline_text += ' ' + line.strip() + else: + break + self.screenplay_tracker.screenplay_bible.logline = logline_text + break + + # Extract genre + genre_match = re.search(r'(?:Primary Genre|์ฃผ ์ฅ๋ฅด)[:\s]*([^\n]+)', content, re.IGNORECASE) + if genre_match: + self.screenplay_tracker.screenplay_bible.genre = genre_match.group(1).strip() + + # Save to database + ScreenplayDatabase.save_screenplay_bible(self.current_session_id, + self.screenplay_tracker.screenplay_bible) + + except Exception as e: + logger.error(f"Error processing producer content: {e}") + + + + + + def _process_story_content(self, content: str): + """Process story developer output""" + # Extract three-act structure + self.screenplay_tracker.screenplay_bible.three_act_structure = { + "act1": self._extract_section(content, "ACT 1|์ 1๋ง"), + "act2a": self._extract_section(content, "ACT 2A|์ 2๋งA"), + "act2b": self._extract_section(content, "ACT 2B|์ 2๋งB"), + "act3": self._extract_section(content, "ACT 3|์ 3๋ง") + } + + ScreenplayDatabase.save_screenplay_bible(self.current_session_id, + self.screenplay_tracker.screenplay_bible) + + def _process_character_content(self, content: str): + """Process character designer output""" + # Extract protagonist + protagonist_section = self._extract_section(content, "PROTAGONIST|์ฃผ์ธ๊ณต") + if protagonist_section: + protagonist = self._parse_character_profile(protagonist_section, "protagonist") + self.screenplay_tracker.add_character(protagonist) + ScreenplayDatabase.save_character(self.current_session_id, protagonist) + + # Extract antagonist + antagonist_section = self._extract_section(content, "ANTAGONIST|์ ๋์") + if antagonist_section: + antagonist = self._parse_character_profile(antagonist_section, "antagonist") + self.screenplay_tracker.add_character(antagonist) + ScreenplayDatabase.save_character(self.current_session_id, antagonist) + + def _process_scene_content(self, content: str): + """Process scene planner output""" + # Parse scene breakdown + scene_pattern = r'(?:Scene|์ฌ)\s*(\d+).*?(?:INT\.|EXT\.)\s*(.+?)\s*-\s*(\w+)' + scenes = re.finditer(scene_pattern, content, re.IGNORECASE | re.MULTILINE) + + for match in scenes: + scene_num = int(match.group(1)) + location = match.group(2).strip() + time_of_day = match.group(3).strip() + + # Determine act based on scene number + act = 1 if scene_num <= 12 else 2 if scene_num <= 35 else 3 + + scene = SceneBreakdown( + scene_number=scene_num, + act=act, + location=location, + time_of_day=time_of_day, + characters=[], # Would be extracted from content + purpose="", # Would be extracted from content + conflict="", # Would be extracted from content + page_count=1.5 # Default estimate + ) + + self.screenplay_tracker.add_scene(scene) + ScreenplayDatabase.save_scene(self.current_session_id, scene) + + def _extract_section(self, content: str, section_pattern: str) -> str: + """Extract section from content""" + pattern = rf'(?:{section_pattern})[:\s]*(.+?)(?=\n(?:[A-Z]{{2,}}|[๊ฐ-ํฃ]{{2,}}):|\Z)' + match = re.search(pattern, content, re.IGNORECASE | re.DOTALL) + return match.group(1).strip() if match else "" + + def _parse_character_profile(self, content: str, role: str) -> CharacterProfile: + """Parse character profile from content""" + # Extract character details using regex or string parsing + name = self._extract_field(content, "Name|์ด๋ฆ") or f"Character_{role}" + age = int(self._extract_field(content, "Age|๋์ด") or "30") + + return CharacterProfile( + name=name, + age=age, + role=role, + archetype=self._extract_field(content, "Archetype|์ํฌํ์ ") or "", + want=self._extract_field(content, "WANT|์ธ์ ๋ชฉํ") or "", + need=self._extract_field(content, "NEED|๋ด์ ํ์") or "", + backstory=self._extract_field(content, "Backstory|๋ฐฑ์คํ ๋ฆฌ") or "", + personality=[], # Would be parsed from content + speech_pattern=self._extract_field(content, "Speech|๋งํฌ") or "", + character_arc=self._extract_field(content, "Arc|์ํฌ") or "" + ) + + def _extract_field(self, content: str, field_pattern: str) -> Optional[str]: + """Extract field value from content with improved parsing""" + # More flexible pattern that handles various formats + pattern = rf'{field_pattern}(.+?)(?=\n[A-Z๊ฐ-ํฃ]|$)' + match = re.search(pattern, content, re.IGNORECASE | re.DOTALL) + if match: + value = match.group(1).strip() + # Remove markdown formatting if present + value = re.sub(r'\*\*', '', value) + value = re.sub(r'^\s*[-โข]\s*', '', value) + # Remove trailing punctuation + value = re.sub(r'[,.:;]$', '', value) + return value.strip() + return None + +# --- Utility functions --- +def generate_random_screenplay_theme(screenplay_type: str, genre: str, language: str) -> str: + """Generate random screenplay theme""" + try: + # Log the attempt + logger.info(f"Generating random theme - Type: {screenplay_type}, Genre: {genre}, Language: {language}") + + # Load themes data + themes_data = load_screenplay_themes_data() + + # Select random elements + import secrets + situations = themes_data['situations'].get(genre, themes_data['situations']['drama']) + protagonists = themes_data['protagonists'].get(genre, themes_data['protagonists']['drama']) + conflicts = themes_data['conflicts'].get(genre, themes_data['conflicts']['drama']) + + if not situations or not protagonists or not conflicts: + logger.error(f"No theme data available for genre {genre}") + return f"Error: No theme data available for genre {genre}" + + situation = secrets.choice(situations) + protagonist = secrets.choice(protagonists) + conflict = secrets.choice(conflicts) + + logger.info(f"Selected elements - Situation: {situation}, Protagonist: {protagonist}, Conflict: {conflict}") + + # Check if API token is valid + if not FRIENDLI_TOKEN or FRIENDLI_TOKEN == "dummy_token_for_testing": + logger.warning("No valid API token, returning fallback theme") + return get_fallback_theme(screenplay_type, genre, language, situation, protagonist, conflict) + + # Generate theme using LLM + system = ScreenplayGenerationSystem() + + if language == "Korean": + prompt = f"""๋ค์ ์์๋ค๋ก {screenplay_type}์ฉ ๋งค๋ ฅ์ ์ธ ์ปจ์ ์ ์์ฑํ์ธ์: + +์ํฉ: {situation} +์ฃผ์ธ๊ณต: {protagonist} +๊ฐ๋ฑ: {conflict} +์ฅ๋ฅด: {genre} + +๋ค์ ํ์์ผ๋ก ์์ฑ: + +**์ ๋ชฉ:** [๋งค๋ ฅ์ ์ธ ์ ๋ชฉ] + +**๋ก๊ทธ๋ผ์ธ:** [25๋จ์ด ์ด๋ด ํ ๋ฌธ์ฅ] + +**์ปจ์ :** [์ฃผ์ธ๊ณต]์ด(๊ฐ) [์ํฉ]์์ [๊ฐ๋ฑ]์ ๊ฒช์ผ๋ฉฐ [๋ชฉํ]๋ฅผ ์ถ๊ตฌํ๋ ์ด์ผ๊ธฐ. + +**๋ ํนํ ์์:** [์ด ์ด์ผ๊ธฐ๋ง์ ํน๋ณํ ์ ]""" + else: + prompt = f"""Generate an attractive concept for {screenplay_type} using these elements: + +Situation: {situation} +Protagonist: {protagonist} +Conflict: {conflict} +Genre: {genre} + +Format as: + +**Title:** [Compelling title] + +**Logline:** [One sentence, 25 words max] + +**Concept:** A story about [protagonist] who faces [conflict] in [situation] while pursuing [goal]. + +**Unique Element:** [What makes this story special]""" + + messages = [{"role": "user", "content": prompt}] + + # Call LLM with error handling + logger.info("Calling LLM for theme generation...") + + generated_theme = "" + error_occurred = False + + # Use streaming to get the response + for chunk in system.call_llm_streaming(messages, "producer", language): + if chunk.startswith("โ"): + logger.error(f"LLM streaming error: {chunk}") + error_occurred = True + break + generated_theme += chunk + + # If error occurred or no content generated, use fallback + if error_occurred or not generated_theme.strip(): + logger.warning("LLM call failed or empty response, using fallback theme") + return get_fallback_theme(screenplay_type, genre, language, situation, protagonist, conflict) + + logger.info(f"Successfully generated theme of length: {len(generated_theme)}") + + # Extract metadata + metadata = { + 'title': extract_title_from_theme(generated_theme), + 'logline': extract_logline_from_theme(generated_theme), + 'protagonist': protagonist, + 'conflict': conflict, + 'situation': situation, + 'tags': [genre, screenplay_type] + } + + # Save to database + try: + theme_id = ScreenplayDatabase.save_random_theme( + generated_theme, screenplay_type, genre, language, metadata + ) + logger.info(f"Saved theme with ID: {theme_id}") + except Exception as e: + logger.error(f"Failed to save theme to database: {e}") + + return generated_theme + + except Exception as e: + logger.error(f"Theme generation error: {str(e)}") + import traceback + logger.error(traceback.format_exc()) + return f"Error generating theme: {str(e)}" + +def get_fallback_theme(screenplay_type: str, genre: str, language: str, + situation: str, protagonist: str, conflict: str) -> str: + """Generate fallback theme without LLM""" + if language == "Korean": + return f"""**์ ๋ชฉ:** {protagonist}์ ์ ํ + +**๋ก๊ทธ๋ผ์ธ:** {situation}์ ๊ฐํ {protagonist}๊ฐ {conflict}์ ๋ง์๋ฉฐ ์์กด์ ์ํด ์ธ์ด๋ค. + +**์ปจ์ :** {protagonist}๊ฐ {situation}์์ {conflict}์ ๊ฒช์ผ๋ฉฐ ์์ ์ ํ๊ณ๋ฅผ ๊ทน๋ณตํ๋ ์ด์ผ๊ธฐ. + +**๋ ํนํ ์์:** {genre} ์ฅ๋ฅด์ ์ ํต์ ์์๋ฅผ ํ๋์ ์ผ๋ก ์ฌํด์ํ ์ํ.""" + else: + return f"""**Title:** The {protagonist.title()}'s Choice + +**Logline:** When trapped in {situation}, a {protagonist} must face {conflict} to survive. + +**Concept:** A story about a {protagonist} who faces {conflict} in {situation} while discovering their true strength. + +**Unique Element:** A fresh take on {genre} genre conventions with contemporary relevance.""" + +def load_screenplay_themes_data() -> Dict: + """Load screenplay themes data""" + return { + 'situations': { + 'action': ['hostage crisis', 'heist gone wrong', 'revenge mission', 'race against time'], + 'thriller': ['false accusation', 'witness protection', 'conspiracy uncovered', 'identity theft'], + 'drama': ['family reunion', 'terminal diagnosis', 'divorce proceedings', 'career crossroads'], + 'comedy': ['mistaken identity', 'wedding disaster', 'workplace chaos', 'odd couple roommates'], + 'horror': ['isolated location', 'ancient curse', 'home invasion', 'supernatural investigation'], + 'sci-fi': ['first contact', 'time loop', 'AI awakening', 'space colony crisis'], + 'romance': ['second chance', 'enemies to lovers', 'long distance', 'forbidden love'] + }, + 'protagonists': { + 'action': ['ex-soldier', 'undercover cop', 'skilled thief', 'reluctant hero'], + 'thriller': ['investigative journalist', 'wrongly accused person', 'FBI agent', 'whistleblower'], + 'drama': ['single parent', 'recovering addict', 'immigrant', 'caregiver'], + 'comedy': ['uptight professional', 'slacker', 'fish out of water', 'eccentric artist'], + 'horror': ['skeptical scientist', 'final girl', 'paranormal investigator', 'grieving parent'], + 'sci-fi': ['astronaut', 'AI researcher', 'time traveler', 'colony leader'], + 'romance': ['workaholic', 'hopeless romantic', 'cynical divorce lawyer', 'small town newcomer'] + }, + 'conflicts': { + 'action': ['stop the villain', 'save the hostages', 'prevent disaster', 'survive pursuit'], + 'thriller': ['prove innocence', 'expose truth', 'stay alive', 'protect loved ones'], + 'drama': ['reconcile past', 'find purpose', 'heal relationships', 'accept change'], + 'comedy': ['save the business', 'win the competition', 'fool everyone', 'find love'], + 'horror': ['survive the night', 'break the curse', 'escape the monster', 'save the town'], + 'sci-fi': ['save humanity', 'prevent paradox', 'stop the invasion', 'preserve identity'], + 'romance': ['overcome differences', 'choose between options', 'trust again', 'follow heart'] + } + } + +def extract_title_from_theme(theme_text: str) -> str: + """Extract title from generated theme""" + match = re.search(r'\*\*(?:Title|์ ๋ชฉ):\*\*\s*(.+)', theme_text, re.IGNORECASE) + return match.group(1).strip() if match else "" + +def extract_logline_from_theme(theme_text: str) -> str: + """Extract logline from generated theme""" + match = re.search(r'\*\*(?:Logline|๋ก๊ทธ๋ผ์ธ):\*\*\s*(.+)', theme_text, re.IGNORECASE) + return match.group(1).strip() if match else "" + +def format_screenplay_display(screenplay_text: str) -> str: + """Format screenplay for display""" + if not screenplay_text: + return "No screenplay content yet." + + formatted = "# ๐ฌ Screenplay\n\n" + + # Format scene headings + formatted_text = re.sub( + r'^(INT\.|EXT\.)(.*?)$', + r'**\1\2**', + screenplay_text, + flags=re.MULTILINE + ) + + # Format character names (all caps on their own line) + formatted_text = re.sub( + r'^([A-Z][A-Z\s]+)$', + r'**\1**', + formatted_text, + flags=re.MULTILINE + ) + + # Add spacing for readability + lines = formatted_text.split('\n') + formatted_lines = [] + + for i, line in enumerate(lines): + formatted_lines.append(line) + # Add extra space after scene headings + if line.startswith('**INT.') or line.startswith('**EXT.'): + formatted_lines.append('') + + formatted += '\n'.join(formatted_lines) + + # Add page count + page_count = len(screenplay_text.split('\n')) / 55 + formatted = f"**Total Pages: {page_count:.1f}**\n\n" + formatted + + return formatted + +def format_stages_display(stages: List[Dict]) -> str: + """Format stages display for screenplay""" + markdown = "## ๐ฌ Production Progress\n\n" + + # Progress summary + completed = sum(1 for s in stages if s.get('status') == 'complete') + total = len(stages) + markdown += f"**Progress: {completed}/{total} stages complete**\n\n" + + # Page count if available + total_pages = sum(s.get('page_count', 0) for s in stages if s.get('page_count')) + if total_pages > 0: + markdown += f"**Current Page Count: {total_pages:.1f} pages**\n\n" + + markdown += "---\n\n" + + # Stage details + current_act = None + for i, stage in enumerate(stages): + status_icon = "โ " if stage['status'] == 'complete' else "๐" if stage['status'] == 'active' else "โณ" + + # Group by acts + if 'Act' in stage.get('name', ''): + act_match = re.search(r'Act (\w+)', stage['name']) + if act_match and act_match.group(1) != current_act: + current_act = act_match.group(1) + markdown += f"\n### ๐ Act {current_act}\n\n" + + markdown += f"{status_icon} **{stage['name']}**" + + if stage.get('page_count', 0) > 0: + markdown += f" ({stage['page_count']:.1f} pages)" + + markdown += "\n" + + if stage['content'] and stage['status'] == 'complete': + preview_length = 200 + preview = stage['content'][:preview_length] + "..." if len(stage['content']) > preview_length else stage['content'] + markdown += f"> {preview}\n\n" + elif stage['status'] == 'active': + markdown += "> *In progress...*\n\n" + + return markdown + +def process_query(query: str, screenplay_type: str, genre: str, language: str, + session_id: Optional[str] = None) -> Generator[Tuple[str, str, str, str], None, None]: + """Main query processing function""" + if not query.strip(): + yield "", "", "โ Please enter a screenplay concept.", session_id + return + + system = ScreenplayGenerationSystem() + stages_markdown = "" + screenplay_display = "" + + for status, stages, current_session_id in system.process_screenplay_stream( + query, screenplay_type, genre, language, session_id + ): + stages_markdown = format_stages_display(stages) + + # Get screenplay content when available + if stages and all(s.get("status") == "complete" for s in stages[-4:]): + screenplay_text = ScreenplayDatabase.get_screenplay_content(current_session_id) + screenplay_display = format_screenplay_display(screenplay_text) + + yield stages_markdown, screenplay_display, status or "๐ Processing...", current_session_id + +def get_active_sessions() -> List[str]: + """Get active screenplay sessions""" + sessions = ScreenplayDatabase.get_active_sessions() + return [ + f"{s['session_id'][:8]}... - {s.get('title', s['user_query'][:30])}... " + f"({s['screenplay_type']}/{s['genre']}) [{s['total_pages']:.1f} pages]" + for s in sessions + ] + +def export_screenplay_pdf(screenplay_text: str, title: str, session_id: str) -> str: + """Export screenplay to PDF format""" + # This would use a library like reportlab to create industry-standard PDF + # For now, returning a placeholder + pdf_path = f"screenplay_{session_id[:8]}.pdf" + # PDF generation logic would go here + return pdf_path + +def export_screenplay_fdx(screenplay_text: str, title: str, session_id: str) -> str: + """Export to Final Draft format""" + # This would create .fdx XML format + fdx_path = f"screenplay_{session_id[:8]}.fdx" + # FDX generation logic would go here + return fdx_path + +def download_screenplay(screenplay_text: str, format_type: str, title: str, + session_id: str) -> Optional[str]: + """Generate screenplay download file""" + if not screenplay_text or not session_id: + return None + + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + + try: + if format_type == "PDF": + return export_screenplay_pdf(screenplay_text, title, session_id) + elif format_type == "FDX": + return export_screenplay_fdx(screenplay_text, title, session_id) + elif format_type == "FOUNTAIN": + filepath = f"screenplay_{session_id[:8]}_{timestamp}.fountain" + with open(filepath, 'w', encoding='utf-8') as f: + f.write(screenplay_text) + return filepath + else: # TXT + filepath = f"screenplay_{session_id[:8]}_{timestamp}.txt" + with open(filepath, 'w', encoding='utf-8') as f: + f.write(f"Title: {title}\n") + f.write("=" * 50 + "\n\n") + f.write(screenplay_text) + return filepath + except Exception as e: + logger.error(f"Download generation failed: {e}") + return None + +# Create Gradio interface +def create_interface(): + """Create Gradio interface for screenplay generation""" + + css = """ + .main-header { + text-align: center; + margin-bottom: 2rem; + padding: 2rem; + background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); + border-radius: 10px; + color: white; + } + + .header-title { + font-size: 3rem; + margin-bottom: 1rem; + background: linear-gradient(45deg, #f39c12, #e74c3c); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + } + + .header-description { + font-size: 1.1rem; + opacity: 0.9; + line-height: 1.6; + } + + .type-selector { + display: flex; + gap: 1rem; + margin: 1rem 0; + } + + .type-card { + flex: 1; + padding: 1rem; + border: 2px solid #ddd; + border-radius: 8px; + cursor: pointer; + transition: all 0.3s; + } + + .type-card:hover { + border-color: #f39c12; + transform: translateY(-2px); + } + + .type-card.selected { + border-color: #e74c3c; + background: #fff5f5; + } + + #stages-display { + max-height: 600px; + overflow-y: auto; + padding: 1rem; + background: #f8f9fa; + border-radius: 8px; + } + + #screenplay-output { + font-family: 'Courier New', monospace; + white-space: pre-wrap; + background: white; + padding: 2rem; + border: 1px solid #ddd; + border-radius: 8px; + max-height: 800px; + overflow-y: auto; + } + + .genre-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 0.5rem; + margin: 1rem 0; + } + + .genre-btn { + padding: 0.75rem; + border: 2px solid #e0e0e0; + background: white; + border-radius: 8px; + cursor: pointer; + transition: all 0.3s; + text-align: center; + } + + .genre-btn:hover { + border-color: #f39c12; + background: #fffbf0; + } + + .genre-btn.selected { + border-color: #e74c3c; + background: #fff5f5; + font-weight: bold; + } + """ + + with gr.Blocks(theme=gr.themes.Soft(), css=css, title="Screenplay Generator") as interface: + gr.HTML(""" +
+ Transform your ideas into professional screenplays for films, TV shows, and streaming series. + Using industry-standard format and story structure to create compelling, producible scripts. +
+Library feature coming soon...
" + ) + + # Event handlers + def handle_submit(query, s_type, genre, lang, session_id): + if not query: + yield "", "", "โ Please enter a concept", session_id + return + + yield from process_query(query, s_type, genre, lang, session_id) + + def handle_random(s_type, genre, lang): + return generate_random_screenplay_theme(s_type, genre, lang) + + def handle_download(screenplay_text, format_type, session_id): + if not screenplay_text or not session_id: + return gr.update(visible=False) + + # Get title from database + session = ScreenplayDatabase.get_session(session_id) + title = session.get('title', 'Untitled') if session else 'Untitled' + + file_path = download_screenplay(screenplay_text, format_type, title, session_id) + if file_path and os.path.exists(file_path): + return gr.update(value=file_path, visible=True) + return gr.update(visible=False) + + # Connect events + submit_btn.click( + fn=handle_submit, + inputs=[query_input, screenplay_type, genre_select, language_select, current_session_id], + outputs=[stages_display, screenplay_output, status_text, current_session_id] + ) + + random_btn.click( + fn=handle_random, + inputs=[screenplay_type, genre_select, language_select], + outputs=[query_input] + ) + + clear_btn.click( + fn=lambda: ("", "", "Ready to create your screenplay", None), + outputs=[stages_display, screenplay_output, status_text, current_session_id] + ) + + refresh_btn.click( + fn=get_active_sessions, + outputs=[session_dropdown] + ) + + download_btn.click( + fn=handle_download, + inputs=[screenplay_output, format_select, current_session_id], + outputs=[download_file] + ) + + # Load sessions on start + interface.load( + fn=get_active_sessions, + outputs=[session_dropdown] + ) + + return interface + +# Main function +if __name__ == "__main__": + logger.info("Screenplay Generator Starting...") + logger.info("=" * 60) + + # Environment check + logger.info(f"API Endpoint: {API_URL}") + logger.info("Screenplay Types Available:") + for s_type, info in SCREENPLAY_LENGTHS.items(): + logger.info(f" - {s_type}: {info['description']}") + logger.info(f"Genres: {', '.join(GENRE_TEMPLATES.keys())}") + + if BRAVE_SEARCH_API_KEY: + logger.info("Web search enabled for market research.") + else: + logger.warning("Web search disabled.") + + logger.info("=" * 60) + + # Initialize database + logger.info("Initializing database...") + ScreenplayDatabase.init_db() + logger.info("Database initialization complete.") + + # Create and launch interface + interface = create_interface() + + interface.launch( + server_name="0.0.0.0", + server_port=7860, + share=False, + debug=True + ) \ No newline at end of file