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 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 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: - 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: - 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 - 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 with 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 _extract_field(self, content: str, field_pattern: str) -> Optional[str]: """Extract field value from content with improved parsing""" pattern = rf'{field_pattern}[:\s]*([^\n]+?)(?=\n[A-Z๊ฐ€-ํžฃ]|$)' match = re.search(pattern, content, re.IGNORECASE | re.DOTALL) if match and match.group(1): value = match.group(1).strip() value = re.sub(r'\*\*', '', value) # **bold** ์ œ๊ฑฐ value = re.sub(r'^\s*[-โ€ข]\s*', '', value) # ๊ธ€๋จธ๋ฆฌํ‘œ ์ œ๊ฑฐ value = re.sub(r'[,.:;]+$', '', value) # ํ–‰ ๋ ๊ตฌ๋‘์  ์ œ๊ฑฐ return value.strip() if value else None return None def _parse_character_profile(self, content: str, role: str) -> CharacterProfile: """Parse character profile from content""" logger.debug(f"Parsing character profile for role: {role}") logger.debug(f"Content preview: {content[:200]}...") # 1) ์ด๋ฆ„ ์ถ”์ถœ โ”€ ํŒจํ„ด 3์ข… name = f"Character_{role}" # fallback name_patterns = [ r'(?:์ด๋ฆ„|Name)[:\s]*([^\n,(]+)', # "์ด๋ฆ„: ํ™๊ธธ๋™" r'^\s*[-*โ€ข]\s*([^\n,(]+)', # "- ํ™๊ธธ๋™" r'^([^\n,(]+)' # ๋ฌธ๋‹จ ์ฒซ ๋‹จ์–ด ] for pat in name_patterns: m = re.search(pat, content, re.IGNORECASE | re.MULTILINE) if m and m.group(1).strip(): name = re.sub(r'[\*:\s]+', '', m.group(1).strip()) # ๋ถˆํ•„์š” ๊ธฐํ˜ธ ์ œ๊ฑฐ break # 2) ํ•„๋“œ ์ถ”์ถœ์šฉ ํ—ฌํผ def extract_clean_field(pats): pats = [pats] if isinstance(pats, str) else pats for p in pats: m = re.search(rf'{p}[:\s]*([^\n*]+?)(?=\n|$)', content, re.IGNORECASE | re.DOTALL) if m and m.group(1).strip(): v = m.group(1).strip() v = re.sub(r'^[-*โ€ข:\s]+', '', v) # ๋ฆฌ์ŠคํŠธยท๊ธฐํ˜ธ ์ œ๊ฑฐ v = v.replace('*', '').strip() return v return "" # 3) Personality(์—ฌ๋Ÿฌ ์ค„) ๋”ฐ๋กœ ํŒŒ์‹ฑ def extract_traits(): section = re.search(r'(?:Personality|์„ฑ๊ฒฉ[^\n]*)\n((?:[-*โ€ข].+\n?)+)', content, re.IGNORECASE) if not section: return [] traits = [ re.sub(r'^[-*โ€ข]\s*', '', line.strip()) for line in section.group(1).splitlines() if line.strip() ] return traits[:5] # 4) CharacterProfile ์ƒ์„ฑ return CharacterProfile( name=name, 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=extract_traits(), speech_pattern=extract_clean_field( [r"๋งํˆฌ.*?ํŒจํ„ด", r"Speech Pattern", r"์–ธ์–ด ํŒจํ„ด", r"๋งํˆฌ"] ), character_arc=extract_clean_field( [r"์บ๋ฆญํ„ฐ ์•„ํฌ", r"Character Arc", r"Arc", r"๋ณ€ํ™”"] ), ) def _extract_personality_traits(self, content: str) -> List[str]: """Extract personality traits from content""" traits = [] # Look for personality section with multiple pattern options personality_patterns = [ r"(?:Personality|์„ฑ๊ฒฉ ํŠน์„ฑ|์„ฑ๊ฒฉ)[:\s]*([^\n]+(?:\n(?![\w๊ฐ€-ํžฃ]+:)[^\n]+)*)", r"์„ฑ๊ฒฉ[:\s]*(?:\n?[-โ€ข*]\s*[^\n]+)+" ] for pattern in personality_patterns: match = re.search(pattern, content, re.IGNORECASE | re.DOTALL) if match and match.group(1): personality_section = match.group(1) # 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) trait = re.sub(r'^\d+\.\s*', '', trait) # Remove numbered lists if trait and len(trait) > 2: # Skip very short entries traits.append(trait) if traits: # If we found traits, stop looking break 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: try: protagonist = self._parse_character_profile(protagonist_section, "protagonist") self.screenplay_tracker.add_character(protagonist) ScreenplayDatabase.save_character(self.current_session_id, protagonist) except Exception as e: logger.error(f"Error parsing protagonist: {e}") # Create a default protagonist to continue protagonist = CharacterProfile( name="Protagonist", role="protagonist", archetype="Hero", want="To achieve goal", need="To grow", backstory="Unknown", personality=["Determined"], speech_pattern="Normal", character_arc="Growth" ) self.screenplay_tracker.add_character(protagonist) # Extract antagonist antagonist_section = self._extract_section(content, r"(?:ANTAGONIST|์ ๋Œ€์ž)") if antagonist_section: try: antagonist = self._parse_character_profile(antagonist_section, "antagonist") self.screenplay_tracker.add_character(antagonist) ScreenplayDatabase.save_character(self.current_session_id, antagonist) except Exception as e: logger.error(f"Error parsing antagonist: {e}") # Create a default antagonist to continue antagonist = CharacterProfile( name="Antagonist", role="antagonist", archetype="Villain", want="To stop protagonist", need="Power", backstory="Unknown", personality=["Ruthless"], speech_pattern="Menacing", character_arc="Downfall" ) self.screenplay_tracker.add_character(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 _extract_section(self, content: str, section_pattern: str) -> str: """Extract section from content with improved pattern matching""" # More flexible section extraction patterns = [ # Pattern 1: Section header followed by content until next major section rf'{section_pattern}[:\s]*\n?(.*?)(?=\n\n[A-Z๊ฐ€-ํžฃ]{{2,}}[:\s]|\n\n\d+\.|$)', # Pattern 2: Section header with content until next section (alternative) rf'{section_pattern}.*?\n((?:.*\n)*?)(?=\n[A-Z๊ฐ€-ํžฃ]{{2,}}:|$)', # Pattern 3: More flexible pattern for Korean text rf'{section_pattern}[:\s]*\n?((?:[^\n]+\n?)*?)(?=\n\n|\Z)' ] for pattern in patterns: match = re.search(pattern, content, re.IGNORECASE | re.DOTALL) if match and match.group(1): section_content = match.group(1).strip() if section_content: # Only return if we got actual content return section_content return "" 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: # Try multiple name extraction patterns name = None name_patterns = [ r"(?:์ด๋ฆ„|Name)[:\s]*([^,\n]+)", r"^([^:\n]+?)(?:\s*[-โ€“]\s*|:\s*)", # Name at start before dash or colon r"^([๊ฐ€-ํžฃA-Za-z\s]+?)(?:\s*\(|$)" # Korean/English name before parenthesis ] for pattern in name_patterns: name_match = re.search(pattern, section.strip(), re.IGNORECASE) if name_match and name_match.group(1): name = name_match.group(1).strip() if name and len(name) > 1: break if not name: name = f"Supporting_{i}" role_desc = self._extract_field(section, r"(?:Role|์—ญํ• )[:\s]*") or "supporting" character = CharacterProfile( name=name, role="supporting", archetype=role_desc, 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 _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_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) # --- 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 "" import re def format_screenplay_display(screenplay_text: str) -> str: """Convert raw screenplay text to a nicely formatted Markdown preview.""" if not screenplay_text: return "No screenplay content yet." # 1) ์ œ๋ชฉ ์˜์—ญ formatted = "# ๐ŸŽฌ Screenplay\n\n" # 2) ์”ฌ ํ—ค๋”ฉ(INT./EXT. ๋ผ์ธ) ๋ณผ๋“œ ์ฒ˜๋ฆฌ # - ^ : ํ–‰์˜ ์‹œ์ž‘ # - .* : ํ–‰ ์ „์ฒด # - re.MULTILINE : ๊ฐ ์ค„๋งˆ๋‹ค ^ $๊ฐ€ ๋™์ž‘ formatted_text = re.sub( r'^(INT\.|EXT\.).*$', # ์บก์ฒ˜: INT. ๋˜๋Š” EXT.์œผ๋กœ ์‹œ์ž‘ํ•˜๋Š” ํ•œ ์ค„ r'**\g<0>**', # ์ „์ฒด ํ–‰์„ ๊ตต๊ฒŒ screenplay_text, flags=re.MULTILINE ) # 3) ๋Œ€๋ฌธ์ž ์ „์›(์ธ๋ฌผ ์ด๋ฆ„) ๋ณผ๋“œ ์ฒ˜๋ฆฌ # - [A-Z][A-Z\s]+$ : ALL-CAPS ๊ธ€์ž์™€ ๊ณต๋ฐฑ๋งŒ์œผ๋กœ ์ด๋ค„์ง„ ํ–‰ formatted_text = re.sub( r'^([A-Z][A-Z\s]+)$', r'**\g<0>**', formatted_text, flags=re.MULTILINE ) # 4) ๊ฐ€๋…์„ฑ์„ ์œ„ํ•ด INT./EXT. ๋’ค์— ๋นˆ ์ค„ ์‚ฝ์ž… lines = formatted_text.splitlines() pretty_lines = [] for line in lines: pretty_lines.append(line) if line.startswith("**INT.") or line.startswith("**EXT."): pretty_lines.append("") # ๋นˆ ์ค„ ์ถ”๊ฐ€ formatted += "\n".join(pretty_lines) # 5) ํŽ˜์ด์ง€ ์ˆ˜(์Šคํฌ๋ฆฝํŠธ ๊ทœ์น™: 1 ํŽ˜์ด์ง€ โ‰ˆ 55 ๋ผ์ธ) ๊ณ„์‚ฐ page_count = len(screenplay_text.splitlines()) / 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 )