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(""" +
+

๐ŸŽฌ 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. +

+
+ """) + + # State management + current_session_id = gr.State(None) + + with gr.Tabs(): + # Main Writing Tab + with gr.Tab("โœ๏ธ Write Screenplay"): + with gr.Row(): + with gr.Column(scale=3): + query_input = gr.Textbox( + label="Screenplay Concept", + placeholder="""Describe your screenplay idea. For example: +- A detective with memory loss must solve their own attempted murder +- Two rival food truck owners forced to work together to save the city food festival +- A space station AI develops consciousness during a critical mission +- A family reunion turns into a murder mystery during a hurricane + +The more specific your concept, the better the screenplay will be tailored to your vision.""", + lines=6 + ) + + with gr.Column(scale=1): + screenplay_type = gr.Radio( + choices=list(SCREENPLAY_LENGTHS.keys()), + value="movie", + label="Screenplay Type", + info="Choose your format" + ) + + genre_select = gr.Dropdown( + choices=list(GENRE_TEMPLATES.keys()), + value="drama", + label="Primary Genre", + info="Select main genre" + ) + + language_select = gr.Radio( + choices=["English", "Korean"], + value="English", + label="Language" + ) + + with gr.Row(): + random_btn = gr.Button("๐ŸŽฒ Random Concept", scale=1) + clear_btn = gr.Button("๐Ÿ—‘๏ธ Clear", scale=1) + submit_btn = gr.Button("๐ŸŽฌ Start Writing", variant="primary", scale=2) + + status_text = gr.Textbox( + label="Status", + interactive=False, + value="Ready to create your screenplay" + ) + + # Session management + with gr.Group(): + gr.Markdown("### ๐Ÿ“ Saved Projects") + with gr.Row(): + session_dropdown = gr.Dropdown( + label="Active Sessions", + choices=[], + interactive=True, + scale=3 + ) + refresh_btn = gr.Button("๐Ÿ”„", scale=1) + resume_btn = gr.Button("๐Ÿ“‚ Load", scale=1) + + # Output displays + with gr.Row(): + with gr.Column(): + with gr.Tab("๐ŸŽญ Writing Progress"): + stages_display = gr.Markdown( + value="*Your screenplay journey will unfold here...*", + elem_id="stages-display" + ) + + with gr.Tab("๐Ÿ“„ Screenplay"): + screenplay_output = gr.Markdown( + value="*Your formatted screenplay will appear here...*", + elem_id="screenplay-output" + ) + + with gr.Row(): + format_select = gr.Radio( + choices=["PDF", "FDX", "FOUNTAIN", "TXT"], + value="PDF", + label="Export Format" + ) + download_btn = gr.Button("๐Ÿ“ฅ Download Screenplay", variant="secondary") + + download_file = gr.File( + label="Download", + visible=False + ) + + # Examples + gr.Examples( + examples=[ + ["A burned-out teacher discovers her students are being replaced by AI duplicates"], + ["Two funeral home employees accidentally release a ghost who helps them solve murders"], + ["A time-loop forces a wedding planner to relive the worst wedding until they find true love"], + ["An astronaut returns to Earth to find everyone has forgotten space exists"], + ["A support group for reformed villains must save the city when heroes disappear"], + ["A food critic loses their sense of taste and teams up with a street food vendor"] + ], + inputs=query_input, + label="๐Ÿ’ก Example Concepts" + ) + + # Screenplay Library Tab + with gr.Tab("๐Ÿ“š Concept Library"): + gr.Markdown(""" + ### ๐ŸŽฒ Random Screenplay Concepts + + Browse through AI-generated screenplay concepts. Each concept includes a title, logline, and brief setup. + """) + + library_display = gr.HTML( + value="

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