Spaces:
Running
Running
| 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 --- | |
| 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) | |
| 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:" | |
| 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""" | |
| 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() | |
| 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() | |
| 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 | |
| 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() | |
| 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() | |
| 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() | |
| 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() | |
| 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 "" | |
| 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() | |
| 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 | |
| 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] | |
| 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] | |
| 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(""" | |
| <div class="main-header"> | |
| <h1 class="header-title">🎬 AI Screenplay Generator</h1> | |
| <p class="header-description"> | |
| 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. | |
| </p> | |
| </div> | |
| """) | |
| # 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="<p>Library feature coming soon...</p>" | |
| ) | |
| # 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 | |
| ) |