diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -24,3036 +24,2444 @@ 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 + 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.") + 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 = "novel_sessions_v6.db" - -# Target word count settings -TARGET_WORDS = 8000 # Safety margin -MIN_WORDS_PER_PART = 800 # Minimum words per part +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" + 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.") + logger.warning("BRAVE_SEARCH_API_KEY not set. Web search features will be disabled.") # --- Global variables --- db_lock = threading.Lock() -# Narrative phases definition -NARRATIVE_PHASES = [ - "Introduction: Daily Life and Cracks", - "Development 1: Rising Anxiety", - "Development 2: External Shock", - "Development 3: Deepening Internal Conflict", - "Climax 1: Peak of Crisis", - "Climax 2: Moment of Choice", - "Falling Action 1: Consequences and Aftermath", - "Falling Action 2: New Recognition", - "Resolution 1: Changed Daily Life", - "Resolution 2: Open Questions" -] +# 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"] + } +} -# Stage configuration - Single writer system -UNIFIED_STAGES = [ - ("director", "๐ŸŽฌ Director: Integrated Narrative Structure Planning"), - ("critic_director", "๐Ÿ“ Critic: Deep Review of Narrative Structure"), - ("director", "๐ŸŽฌ Director: Final Master Plan"), -] + [ - item for i in range(1, 11) - for item in [ - ("writer", f"โœ๏ธ Writer: Part {i} - {NARRATIVE_PHASES[i-1]}"), - (f"critic_part{i}", f"๐Ÿ“ Part {i} Critic: Immediate Review and Revision Request"), - ("writer", f"โœ๏ธ Writer: Part {i} Revision") - ] -] + [ - ("critic_final", "๐Ÿ“ Final Critic: Comprehensive Evaluation and Literary Achievement"), +# 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 StoryBible: - """Story bible for maintaining narrative consistency""" - characters: Dict[str, Dict[str, Any]] = field(default_factory=dict) - settings: Dict[str, str] = field(default_factory=dict) - timeline: List[Dict[str, Any]] = field(default_factory=list) - plot_points: List[Dict[str, Any]] = field(default_factory=list) - themes: List[str] = field(default_factory=list) - symbols: Dict[str, List[str]] = field(default_factory=dict) - style_guide: Dict[str, str] = field(default_factory=dict) - opening_sentence: str = "" +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 PartCritique: - """Critique content for each part""" - part_number: int - continuity_issues: List[str] = field(default_factory=list) - character_consistency: List[str] = field(default_factory=list) - plot_progression: List[str] = field(default_factory=list) - thematic_alignment: List[str] = field(default_factory=list) - technical_issues: List[str] = field(default_factory=list) - strengths: List[str] = field(default_factory=list) - required_changes: List[str] = field(default_factory=list) - literary_quality: List[str] = field(default_factory=list) +class CharacterProfile: + """Detailed character profile""" + name: str + age: int + role: str # protagonist, antagonist, supporting, etc. + archetype: str + want: str # External goal + need: str # Internal need + backstory: str + personality: List[str] + speech_pattern: str + character_arc: str + relationships: Dict[str, str] = field(default_factory=dict) + first_appearance: str = "" # --- Core logic classes --- -class UnifiedNarrativeTracker: - """Unified narrative tracker for single writer system""" - def __init__(self): - self.story_bible = StoryBible() - self.part_critiques: Dict[int, PartCritique] = {} - self.accumulated_content: List[str] = [] - self.word_count_by_part: Dict[int, int] = {} - self.revision_history: Dict[int, List[str]] = defaultdict(list) - self.causal_chains: List[Dict[str, Any]] = [] - self.narrative_momentum: float = 0.0 - - def update_story_bible(self, element_type: str, key: str, value: Any): - """Update story bible""" - if element_type == "character": - self.story_bible.characters[key] = value - elif element_type == "setting": - self.story_bible.settings[key] = value - elif element_type == "timeline": - self.story_bible.timeline.append({"event": key, "details": value}) - elif element_type == "theme": - if key not in self.story_bible.themes: - self.story_bible.themes.append(key) - elif element_type == "symbol": - if key not in self.story_bible.symbols: - self.story_bible.symbols[key] = [] - self.story_bible.symbols[key].append(value) - - def add_part_critique(self, part_number: int, critique: PartCritique): - """Add part critique""" - self.part_critiques[part_number] = critique - - def check_continuity(self, current_part: int, new_content: str) -> List[str]: - """Check continuity""" - issues = [] - - # Character consistency check - for char_name, char_data in self.story_bible.characters.items(): - if char_name in new_content: - if "traits" in char_data: - for trait in char_data["traits"]: - if trait.get("abandoned", False): - issues.append(f"{char_name}'s abandoned trait '{trait['name']}' reappears") - - # Timeline consistency check - if len(self.story_bible.timeline) > 0: - last_event = self.story_bible.timeline[-1] - - # Causality check - if current_part > 1 and not any(kw in new_content for kw in - ['because', 'therefore', 'thus', 'hence', 'consequently']): - issues.append("Unclear causality with previous part") - - return issues - - def calculate_narrative_momentum(self, part_number: int, content: str) -> float: - """Calculate narrative momentum""" - momentum = 5.0 - - # New elements introduced - new_elements = len(set(content.split()) - set(' '.join(self.accumulated_content).split())) - if new_elements > 100: - momentum += 2.0 - - # Conflict escalation - tension_words = ['crisis', 'conflict', 'tension', 'struggle', 'dilemma'] - if any(word in content.lower() for word in tension_words): - momentum += 1.5 - - # Causal clarity - causal_words = ['because', 'therefore', 'thus', 'consequently', 'hence'] - causal_count = sum(1 for word in causal_words if word in content.lower()) - momentum += min(causal_count * 0.5, 2.0) - - # Repetition penalty - if part_number > 1: - prev_content = self.accumulated_content[-1] if self.accumulated_content else "" - overlap = len(set(content.split()) & set(prev_content.split())) - if overlap > len(content.split()) * 0.3: - momentum -= 3.0 - - return max(0.0, min(10.0, momentum)) - -class NovelDatabase: - """Database management - Modified for single writer system""" - @staticmethod - def init_db(): - with sqlite3.connect(DB_PATH) as conn: - conn.execute("PRAGMA journal_mode=WAL") - cursor = conn.cursor() - - # Main sessions table - cursor.execute(''' - CREATE TABLE IF NOT EXISTS sessions ( - session_id TEXT PRIMARY KEY, - user_query TEXT NOT NULL, - language TEXT NOT NULL, - created_at TEXT DEFAULT (datetime('now')), - updated_at TEXT DEFAULT (datetime('now')), - status TEXT DEFAULT 'active', - current_stage INTEGER DEFAULT 0, - final_novel TEXT, - literary_report TEXT, - total_words INTEGER DEFAULT 0, - story_bible TEXT, - narrative_tracker TEXT, - opening_sentence TEXT - ) - ''') - - # Stages table - cursor.execute(''' - CREATE TABLE IF NOT EXISTS 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, - word_count INTEGER DEFAULT 0, - status TEXT DEFAULT 'pending', - narrative_momentum REAL DEFAULT 0.0, - created_at TEXT DEFAULT (datetime('now')), - updated_at TEXT DEFAULT (datetime('now')), - FOREIGN KEY (session_id) REFERENCES sessions(session_id), - UNIQUE(session_id, stage_number) - ) - ''') - - # Critiques table - cursor.execute(''' - CREATE TABLE IF NOT EXISTS critiques ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - session_id TEXT NOT NULL, - part_number INTEGER NOT NULL, - critique_data TEXT, - created_at TEXT DEFAULT (datetime('now')), - FOREIGN KEY (session_id) REFERENCES sessions(session_id) - ) - ''') - - # Random themes library table - cursor.execute(''' - CREATE TABLE IF NOT EXISTS random_themes_library ( - theme_id TEXT PRIMARY KEY, - theme_text TEXT NOT NULL, - language TEXT NOT NULL, - title TEXT, - opening_sentence TEXT, - protagonist TEXT, - conflict TEXT, - philosophical_question 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, language: str) -> str: - session_id = hashlib.md5(f"{user_query}{datetime.now()}".encode()).hexdigest() - with NovelDatabase.get_db() as conn: - conn.cursor().execute( - 'INSERT INTO sessions (session_id, user_query, language) VALUES (?, ?, ?)', - (session_id, user_query, 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', - narrative_momentum: float = 0.0): - word_count = len(content.split()) if content else 0 - with NovelDatabase.get_db() as conn: - cursor = conn.cursor() - cursor.execute(''' - INSERT INTO stages (session_id, stage_number, stage_name, role, content, - word_count, status, narrative_momentum) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) - ON CONFLICT(session_id, stage_number) - DO UPDATE SET content=?, word_count=?, status=?, stage_name=?, - narrative_momentum=?, updated_at=datetime('now') - ''', (session_id, stage_number, stage_name, role, content, word_count, - status, narrative_momentum, content, word_count, status, stage_name, - narrative_momentum)) - - # Update total word count - cursor.execute(''' - UPDATE sessions - SET total_words = ( - SELECT SUM(word_count) - FROM stages - WHERE session_id = ? AND role = 'writer' AND content IS NOT NULL - ), - updated_at = datetime('now'), - current_stage = ? - WHERE session_id = ? - ''', (session_id, stage_number, session_id)) - - conn.commit() - - @staticmethod - def save_critique(session_id: str, part_number: int, critique: PartCritique): - """Save critique""" - with NovelDatabase.get_db() as conn: - critique_json = json.dumps(asdict(critique)) - conn.cursor().execute( - 'INSERT INTO critiques (session_id, part_number, critique_data) VALUES (?, ?, ?)', - (session_id, part_number, critique_json) - ) - conn.commit() - - @staticmethod - def save_opening_sentence(session_id: str, opening_sentence: str): - """Save opening sentence""" - with NovelDatabase.get_db() as conn: - conn.cursor().execute( - 'UPDATE sessions SET opening_sentence = ? WHERE session_id = ?', - (opening_sentence, session_id) - ) - conn.commit() - - @staticmethod - def get_writer_content(session_id: str) -> str: - """Get writer content - Integrate all revisions""" - with NovelDatabase.get_db() as conn: - rows = conn.cursor().execute(''' - SELECT content FROM stages - WHERE session_id = ? AND role = 'writer' - AND stage_name LIKE '%Revision%' - ORDER BY stage_number - ''', (session_id,)).fetchall() - - if rows: - return '\n\n'.join(row['content'] for row in rows if row['content']) - else: - # If no revisions, use drafts - rows = conn.cursor().execute(''' - SELECT content FROM stages - WHERE session_id = ? AND role = 'writer' - AND stage_name NOT LIKE '%Revision%' - ORDER BY stage_number - ''', (session_id,)).fetchall() - return '\n\n'.join(row['content'] for row in rows if row['content']) - - @staticmethod - def save_narrative_tracker(session_id: str, tracker: UnifiedNarrativeTracker): - """Save unified narrative tracker""" - with NovelDatabase.get_db() as conn: - tracker_data = json.dumps({ - 'story_bible': asdict(tracker.story_bible), - 'part_critiques': {k: asdict(v) for k, v in tracker.part_critiques.items()}, - 'word_count_by_part': tracker.word_count_by_part, - 'causal_chains': tracker.causal_chains, - 'narrative_momentum': tracker.narrative_momentum - }) - conn.cursor().execute( - 'UPDATE sessions SET narrative_tracker = ? WHERE session_id = ?', - (tracker_data, session_id) - ) - conn.commit() - - @staticmethod - def load_narrative_tracker(session_id: str) -> Optional[UnifiedNarrativeTracker]: - """Load unified narrative tracker""" - with NovelDatabase.get_db() as conn: - row = conn.cursor().execute( - 'SELECT narrative_tracker FROM sessions WHERE session_id = ?', - (session_id,) - ).fetchone() - - if row and row['narrative_tracker']: - data = json.loads(row['narrative_tracker']) - tracker = UnifiedNarrativeTracker() - - # Restore story bible - bible_data = data.get('story_bible', {}) - tracker.story_bible = StoryBible(**bible_data) - - # Restore critiques - for part_num, critique_data in data.get('part_critiques', {}).items(): - tracker.part_critiques[int(part_num)] = PartCritique(**critique_data) - - tracker.word_count_by_part = data.get('word_count_by_part', {}) - tracker.causal_chains = data.get('causal_chains', []) - tracker.narrative_momentum = data.get('narrative_momentum', 0.0) - - return tracker - return None - - @staticmethod - def save_random_theme(theme_text: str, language: str, metadata: Dict[str, Any]) -> str: - """Save randomly generated theme to library""" - theme_id = hashlib.md5(f"{theme_text}{datetime.now()}".encode()).hexdigest()[:12] - - # Extract components from theme text - title = metadata.get('title', '') - opening_sentence = metadata.get('opening_sentence', '') - protagonist = metadata.get('protagonist', '') - conflict = metadata.get('conflict', '') - philosophical_question = metadata.get('philosophical_question', '') - tags = json.dumps(metadata.get('tags', [])) - - with NovelDatabase.get_db() as conn: - conn.cursor().execute(''' - INSERT INTO random_themes_library - (theme_id, theme_text, language, title, opening_sentence, - protagonist, conflict, philosophical_question, tags, metadata) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - ''', (theme_id, theme_text, language, title, opening_sentence, - protagonist, conflict, philosophical_question, tags, - json.dumps(metadata))) - conn.commit() - - return theme_id - - @staticmethod - def get_random_themes_library(language: str = None, limit: int = 50) -> List[Dict]: - """Get random themes from library""" - with NovelDatabase.get_db() as conn: - query = ''' - SELECT * FROM random_themes_library - {} - ORDER BY generated_at DESC - LIMIT ? - '''.format('WHERE language = ?' if language else '') - - if language: - rows = conn.cursor().execute(query, (language, limit)).fetchall() - else: - rows = conn.cursor().execute(query, (limit,)).fetchall() - - return [dict(row) for row in rows] - - @staticmethod - def update_theme_view_count(theme_id: str): - """Update view count for a theme""" - with NovelDatabase.get_db() as conn: - conn.cursor().execute( - 'UPDATE random_themes_library SET view_count = view_count + 1 WHERE theme_id = ?', - (theme_id,) - ) - conn.commit() - - @staticmethod - def update_theme_used_count(theme_id: str): - """Update used count when theme is used for novel""" - with NovelDatabase.get_db() as conn: - conn.cursor().execute( - 'UPDATE random_themes_library SET used_count = used_count + 1 WHERE theme_id = ?', - (theme_id,) - ) - conn.commit() - - @staticmethod - def get_theme_by_id(theme_id: str) -> Optional[Dict]: - """Get specific theme by ID""" - with NovelDatabase.get_db() as conn: - row = conn.cursor().execute( - 'SELECT * FROM random_themes_library WHERE theme_id = ?', - (theme_id,) - ).fetchone() - return dict(row) if row else None - - @staticmethod - def get_session(session_id: str) -> Optional[Dict]: - with NovelDatabase.get_db() as conn: - row = conn.cursor().execute('SELECT * FROM sessions WHERE session_id = ?', - (session_id,)).fetchone() - return dict(row) if row else None - - @staticmethod - def get_stages(session_id: str) -> List[Dict]: - with NovelDatabase.get_db() as conn: - rows = conn.cursor().execute( - 'SELECT * FROM stages WHERE session_id = ? ORDER BY stage_number', - (session_id,) - ).fetchall() - return [dict(row) for row in rows] - - @staticmethod - def update_final_novel(session_id: str, final_novel: str, literary_report: str = ""): - with NovelDatabase.get_db() as conn: - conn.cursor().execute( - '''UPDATE sessions SET final_novel = ?, status = 'complete', - updated_at = datetime('now'), literary_report = ? WHERE session_id = ?''', - (final_novel, literary_report, session_id) - ) - conn.commit() - - @staticmethod - def get_active_sessions() -> List[Dict]: - with NovelDatabase.get_db() as conn: - rows = conn.cursor().execute( - '''SELECT session_id, user_query, language, created_at, current_stage, total_words - FROM sessions WHERE status = 'active' ORDER BY updated_at DESC LIMIT 10''' - ).fetchall() - return [dict(row) for row in rows] - - @staticmethod - def get_total_words(session_id: str) -> int: - """Get total word count""" - with NovelDatabase.get_db() as conn: - row = conn.cursor().execute( - 'SELECT total_words FROM sessions WHERE session_id = ?', - (session_id,) - ).fetchone() - return row['total_words'] if row and row['total_words'] else 0 +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 + + 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_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""" - 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 UnifiedLiterarySystem: - """Single writer progressive literary novel generation system""" - def __init__(self): - self.token = FRIENDLI_TOKEN - self.api_url = API_URL - self.model_id = MODEL_ID - self.narrative_tracker = UnifiedNarrativeTracker() - self.web_search = WebSearchIntegration() - self.current_session_id = None - NovelDatabase.init_db() - - def create_headers(self): - return {"Authorization": f"Bearer {self.token}", "Content-Type": "application/json"} - - # --- Prompt generation functions --- - def augment_query(self, user_query: str, language: str) -> str: - """Augment prompt""" - if len(user_query.split()) < 15: - augmented_template = { - "Korean": f"""'{user_query}' - -**์„œ์‚ฌ ๊ตฌ์กฐ ํ•ต์‹ฌ:** -- 10๊ฐœ ํŒŒํŠธ๊ฐ€ ํ•˜๋‚˜์˜ ํ†ตํ•ฉ๋œ ์ด์•ผ๊ธฐ๋ฅผ ๊ตฌ์„ฑ -- ๊ฐ ํŒŒํŠธ๋Š” ์ด์ „ ํŒŒํŠธ์˜ ํ•„์—ฐ์  ๊ฒฐ๊ณผ -- ์ธ๋ฌผ์˜ ๋ช…ํ™•ํ•œ ๋ณ€ํ™” ๊ถค์  (A โ†’ B โ†’ C) -- ์ค‘์‹ฌ ๊ฐˆ๋“ฑ์˜ ์ ์ง„์  ๊ณ ์กฐ์™€ ํ•ด๊ฒฐ -- ๊ฐ•๋ ฌํ•œ ์ค‘์‹ฌ ์ƒ์ง•์˜ ์˜๋ฏธ ๋ณ€ํ™”""", - - "English": f"""'{user_query}' - -**Narrative Structure Core:** -- 10 parts forming one integrated story -- Each part as inevitable result of previous -- Clear character transformation arc (A โ†’ B โ†’ C) -- Progressive escalation and resolution of central conflict -- Evolving meaning of powerful central symbol""" - } - return augmented_template.get(language, user_query) - return user_query - - def generate_powerful_opening(self, user_query: str, language: str) -> str: - """Generate powerful opening sentence matching the theme""" - - opening_prompt = { - "Korean": f"""์ฃผ์ œ: {user_query} - -์ด ์ฃผ์ œ์— ๋Œ€ํ•œ ๊ฐ•๋ ฌํ•˜๊ณ  ์žŠ์„ ์ˆ˜ ์—†๋Š” ์ฒซ๋ฌธ์žฅ์„ ์ƒ์„ฑํ•˜์„ธ์š”. - -**์ฒซ๋ฌธ์žฅ ์ž‘์„ฑ ์›์น™:** -1. ์ฆ‰๊ฐ์ ์ธ ๊ธด์žฅ๊ฐ์ด๋‚˜ ๊ถ๊ธˆ์ฆ ์œ ๋ฐœ -2. ํ‰๋ฒ”ํ•˜์ง€ ์•Š์€ ์‹œ๊ฐ์ด๋‚˜ ์ƒํ™ฉ ์ œ์‹œ -3. ๊ฐ๊ฐ์ ์ด๊ณ  ๊ตฌ์ฒด์ ์ธ ์ด๋ฏธ์ง€ -4. ์ฒ ํ•™์  ์งˆ๋ฌธ์ด๋‚˜ ์—ญ์„ค์  ์ง„์ˆ  -5. ์‹œ๊ฐ„๊ณผ ๊ณต๊ฐ„์˜ ๋…ํŠนํ•œ ์„ค์ • - -**ํ›Œ๋ฅญํ•œ ์ฒซ๋ฌธ์žฅ์˜ ์˜ˆ์‹œ ํŒจํ„ด:** -- "๊ทธ๊ฐ€ ์ฃฝ์€ ๋‚ , ..." (์ถฉ๊ฒฉ์  ์‚ฌ๊ฑด) -- "๋ชจ๋“  ๊ฒƒ์ด ๋๋‚ฌ๋‹ค๊ณ  ์ƒ๊ฐํ•œ ์ˆœ๊ฐ„..." (๋ฐ˜์ „ ์˜ˆ๊ณ ) -- "์„ธ์ƒ์—์„œ ๊ฐ€์žฅ [ํ˜•์šฉ์‚ฌ]ํ•œ [๋ช…์‚ฌ]๋Š”..." (๋…ํŠนํ•œ ์ •์˜) -- "[๊ตฌ์ฒด์  ํ–‰๋™]ํ•˜๋Š” ๊ฒƒ๋งŒ์œผ๋กœ๋„..." (์ผ์ƒ์˜ ์žฌํ•ด์„) - -๋‹จ ํ•˜๋‚˜์˜ ๋ฌธ์žฅ๋งŒ ์ œ์‹œํ•˜์„ธ์š”.""", - - "English": f"""Theme: {user_query} - -Generate an unforgettable opening sentence for this theme. - -**Opening Sentence Principles:** -1. Immediate tension or curiosity -2. Unusual perspective or situation -3. Sensory and specific imagery -4. Philosophical question or paradox -5. Unique temporal/spatial setting - -**Great Opening Patterns:** -- "The day he died, ..." (shocking event) -- "At the moment everything seemed over..." (reversal hint) -- "The most [adjective] [noun] in the world..." (unique definition) -- "Just by [specific action]..." (reinterpretation of ordinary) - -Provide only one sentence.""" - } - - messages = [{"role": "user", "content": opening_prompt.get(language, opening_prompt["Korean"])}] - opening = self.call_llm_sync(messages, "writer", language) - return opening.strip() - - def create_director_initial_prompt(self, user_query: str, language: str) -> str: - """Director initial planning - Enhanced version""" - augmented_query = self.augment_query(user_query, language) - - # Generate opening sentence - opening_sentence = self.generate_powerful_opening(user_query, language) - self.narrative_tracker.story_bible.opening_sentence = opening_sentence - if self.current_session_id: - NovelDatabase.save_opening_sentence(self.current_session_id, opening_sentence) - - search_results_str = "" - if self.web_search.enabled: - short_query = user_query[:50] if len(user_query) > 50 else user_query - queries = [ - f"{short_query} philosophical meaning", - f"human existence meaning {short_query}", - f"{short_query} literary works" - ] - for q in queries[:2]: - try: - results = self.web_search.search(q, count=2, language=language) - if results: - search_results_str += self.web_search.extract_relevant_info(results) + "\n" - except Exception as e: - logger.warning(f"Search failed: {str(e)}") - - lang_prompts = { - "Korean": f"""๋…ธ๋ฒจ๋ฌธํ•™์ƒ ์ˆ˜์ค€์˜ ์ฒ ํ•™์  ๊นŠ์ด๋ฅผ ์ง€๋‹Œ ์ค‘ํŽธ์†Œ์„ค(8,000๋‹จ์–ด)์„ ๊ธฐํšํ•˜์„ธ์š”. - -**์ฃผ์ œ:** {augmented_query} - -**ํ•„์ˆ˜ ์ฒซ๋ฌธ์žฅ:** {opening_sentence} - -**์ฐธ๊ณ  ์ž๋ฃŒ:** -{search_results_str if search_results_str else "N/A"} - -**ํ•„์ˆ˜ ๋ฌธํ•™์  ์š”์†Œ:** - -1. **์ฒ ํ•™์  ํƒ๊ตฌ** - - ํ˜„๋Œ€์ธ์˜ ์‹ค์กด์  ๊ณ ๋‡Œ (์†Œ์™ธ, ์ •์ฒด์„ฑ, ์˜๋ฏธ ์ƒ์‹ค) - - ๋””์ง€ํ„ธ ์‹œ๋Œ€์˜ ์ธ๊ฐ„ ์กฐ๊ฑด - - ์ž๋ณธ์ฃผ์˜ ์‚ฌํšŒ์˜ ๋ชจ์ˆœ๊ณผ ๊ฐœ์ธ์˜ ์„ ํƒ - - ์ฃฝ์Œ, ์‚ฌ๋ž‘, ์ž์œ ์— ๋Œ€ํ•œ ์ƒˆ๋กœ์šด ์„ฑ์ฐฐ - -2. **์‚ฌํšŒ์  ๋ฉ”์‹œ์ง€** - - ๊ณ„๊ธ‰, ์  ๋”, ์„ธ๋Œ€ ๊ฐ„ ๊ฐˆ๋“ฑ - - ํ™˜๊ฒฝ ์œ„๊ธฐ์™€ ์ธ๊ฐ„์˜ ์ฑ…์ž„ - - ๊ธฐ์ˆ  ๋ฐœ์ „๊ณผ ์ธ๊ฐ„์„ฑ์˜ ์ถฉ๋Œ - - ํ˜„๋Œ€ ๋ฏผ์ฃผ์ฃผ์˜์˜ ์œ„๊ธฐ์™€ ๊ฐœ์ธ์˜ ์—ญํ•  - -3. **๋ฌธํ•™์  ์ˆ˜์‚ฌ ์žฅ์น˜** - - ์ค‘์‹ฌ ์€์œ : [๊ตฌ์ฒด์  ์‚ฌ๋ฌผ/ํ˜„์ƒ] โ†’ [์ถ”์ƒ์  ์˜๋ฏธ] - - ๋ฐ˜๋ณต๋˜๋Š” ๋ชจํ‹ฐํ”„: [์ด๋ฏธ์ง€/ํ–‰๋™] (์ตœ์†Œ 5ํšŒ ๋ณ€์ฃผ) - - ๋Œ€์กฐ๋ฒ•: [A vs B]์˜ ์ง€์†์  ๊ธด์žฅ - - ์ƒ์ง•์  ๊ณต๊ฐ„: [๊ตฌ์ฒด์  ์žฅ์†Œ]๊ฐ€ ์˜๋ฏธํ•˜๋Š” ๊ฒƒ - - ์‹œ๊ฐ„์˜ ์ฃผ๊ด€์  ํ๋ฆ„ (ํšŒ์ƒ, ์˜ˆ๊ฐ, ์ •์ง€) - -4. **ํ†ตํ•ฉ๋œ 10ํŒŒํŠธ ๊ตฌ์กฐ** - ๊ฐ ํŒŒํŠธ๋ณ„ ํ•ต์‹ฌ: - - ํŒŒํŠธ 1: ์ฒซ๋ฌธ์žฅ์œผ๋กœ ์‹œ์ž‘, ์ผ์ƒ ์† ๊ท ์—ด โ†’ ์ฒ ํ•™์  ์งˆ๋ฌธ ์ œ๊ธฐ - - ํŒŒํŠธ 2-3: ์™ธ๋ถ€ ์‚ฌ๊ฑด โ†’ ๋‚ด์  ์„ฑ์ฐฐ ์‹ฌํ™” - - ํŒŒํŠธ 4-5: ์‚ฌํšŒ์  ๊ฐˆ๋“ฑ โ†’ ๊ฐœ์ธ์  ๋”œ๋ ˆ๋งˆ - - ํŒŒํŠธ 6-7: ์œ„๊ธฐ์˜ ์ •์  โ†’ ์‹ค์กด์  ์„ ํƒ - - ํŒŒํŠธ 8-9: ์„ ํƒ์˜ ๊ฒฐ๊ณผ โ†’ ์ƒˆ๋กœ์šด ์ธ์‹ - - ํŒŒํŠธ 10: ๋ณ€ํ™”๋œ ์„ธ๊ณ„๊ด€ โ†’ ์—ด๋ฆฐ ์งˆ๋ฌธ - -5. **๋ฌธ์ฒด ์ง€์นจ** - - ์‹œ์  ์‚ฐ๋ฌธ์ฒด: ์ผ์ƒ ์–ธ์–ด์™€ ์€์œ ์˜ ๊ท ํ˜• - - ์˜์‹์˜ ํ๋ฆ„๊ณผ ๊ฐ๊ด€์  ๋ฌ˜์‚ฌ์˜ ๊ต์ฐจ - - ์งง๊ณ  ๊ฐ•๋ ฌํ•œ ๋ฌธ์žฅ๊ณผ ์„ฑ์ฐฐ์  ๊ธด ๋ฌธ์žฅ์˜ ๋ฆฌ๋“ฌ - - ๊ฐ๊ฐ์  ๋””ํ…Œ์ผ๋กœ ์ถ”์ƒ์  ๊ฐœ๋… ๊ตฌํ˜„ - -๊ตฌ์ฒด์ ์ด๊ณ  ํ˜์‹ ์ ์ธ ๊ณ„ํš์„ ์ œ์‹œํ•˜์„ธ์š”.""", - - "English": f"""Plan a philosophically profound novella (8,000 words) worthy of Nobel Prize. - -**Theme:** {augmented_query} - -**Required Opening:** {opening_sentence} - -**Reference:** -{search_results_str if search_results_str else "N/A"} - -**Essential Literary Elements:** - -1. **Philosophical Exploration** - - Modern existential anguish (alienation, identity, loss of meaning) - - Human condition in digital age - - Capitalist contradictions and individual choice - - New reflections on death, love, freedom - -2. **Social Message** - - Class, gender, generational conflicts - - Environmental crisis and human responsibility - - Technology vs humanity collision - - Modern democracy crisis and individual role - -3. **Literary Devices** - - Central metaphor: [concrete object/phenomenon] โ†’ [abstract meaning] - - Recurring motif: [image/action] (minimum 5 variations) - - Contrast: sustained tension of [A vs B] - - Symbolic space: what [specific place] means - - Subjective time flow (flashback, premonition, pause) - -4. **Integrated 10-Part Structure** - Each part's core: - - Part 1: Start with opening sentence, daily cracks โ†’ philosophical questions - - Part 2-3: External events โ†’ deepening introspection - - Part 4-5: Social conflict โ†’ personal dilemma - - Part 6-7: Crisis peak โ†’ existential choice - - Part 8-9: Choice consequences โ†’ new recognition - - Part 10: Changed worldview โ†’ open questions - -5. **Style Guidelines** - - Poetic prose: balance of everyday language and metaphor - - Stream of consciousness crossing with objective description - - Rhythm of short intense sentences and reflective long ones - - Abstract concepts through sensory details - -Provide concrete, innovative plan.""" - } - - return lang_prompts.get(language, lang_prompts["Korean"]) - - def create_critic_director_prompt(self, director_plan: str, user_query: str, language: str) -> str: - """Director plan deep review - Enhanced version""" - lang_prompts = { - "Korean": f"""์„œ์‚ฌ ๊ตฌ์กฐ ์ „๋ฌธ๊ฐ€๋กœ์„œ ์ด ๊ธฐํš์„ ์‹ฌ์ธต ๋ถ„์„ํ•˜์„ธ์š”. - -**์› ์ฃผ์ œ:** {user_query} - -**๊ฐ๋…์ž ๊ธฐํš:** -{director_plan} - -**์‹ฌ์ธต ๊ฒ€ํ†  ํ•ญ๋ชฉ:** - -1. **์ธ๊ณผ๊ด€๊ณ„ ๊ฒ€์ฆ** - ๊ฐ ํŒŒํŠธ ๊ฐ„ ์—ฐ๊ฒฐ์„ ๊ฒ€ํ† ํ•˜๊ณ  ๋…ผ๋ฆฌ์  ๋น„์•ฝ์„ ์ฐพ์œผ์„ธ์š”: - - ํŒŒํŠธ 1โ†’2: [์—ฐ๊ฒฐ์„ฑ ํ‰๊ฐ€] - - ํŒŒํŠธ 2โ†’3: [์—ฐ๊ฒฐ์„ฑ ํ‰๊ฐ€] - (๋ชจ๋“  ์—ฐ๊ฒฐ ์ง€์  ๊ฒ€ํ† ) - -2. **์ฒ ํ•™์  ๊นŠ์ด ํ‰๊ฐ€** - - ์ œ์‹œ๋œ ์ฒ ํ•™์  ์ฃผ์ œ๊ฐ€ ์ถฉ๋ถ„ํžˆ ๊นŠ์€๊ฐ€? - - ํ˜„๋Œ€์  ๊ด€๋ จ์„ฑ์ด ์žˆ๋Š”๊ฐ€? - - ๋…์ฐฝ์  ํ†ต์ฐฐ์ด ์žˆ๋Š”๊ฐ€? - -3. **๋ฌธํ•™์  ์žฅ์น˜์˜ ํšจ๊ณผ์„ฑ** - - ์€์œ ์™€ ์ƒ์ง•์ด ์œ ๊ธฐ์ ์œผ๋กœ ์ž‘๋™ํ•˜๋Š”๊ฐ€? - - ๊ณผ๋„ํ•˜๊ฑฐ๋‚˜ ๋ถ€์กฑํ•˜์ง€ ์•Š์€๊ฐ€? - - ์ฃผ์ œ์™€ ๊ธด๋ฐ€ํžˆ ์—ฐ๊ฒฐ๋˜๋Š”๊ฐ€? - -4. **์บ๋ฆญํ„ฐ ์•„ํฌ ์‹คํ˜„ ๊ฐ€๋Šฅ์„ฑ** - - ๋ณ€ํ™”๊ฐ€ ์ถฉ๋ถ„ํžˆ ์ ์ง„์ ์ธ๊ฐ€? - - ๊ฐ ๋‹จ๊ณ„์˜ ๋™๊ธฐ๊ฐ€ ๋ช…ํ™•ํ•œ๊ฐ€? - - ์‹ฌ๋ฆฌ์  ์‹ ๋ขฐ์„ฑ์ด ์žˆ๋Š”๊ฐ€? - -5. **8,000๋‹จ์–ด ์‹คํ˜„ ๊ฐ€๋Šฅ์„ฑ** - - ๊ฐ ํŒŒํŠธ๊ฐ€ 800๋‹จ์–ด๋ฅผ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ๋Š”๊ฐ€? - - ๋Š˜์–ด์ง€๊ฑฐ๋‚˜ ์••์ถ•๋˜๋Š” ๋ถ€๋ถ„์€ ์—†๋Š”๊ฐ€? - -**ํ•„์ˆ˜ ๊ฐœ์„ ์‚ฌํ•ญ์„ ๊ตฌ์ฒด์ ์œผ๋กœ ์ œ์‹œํ•˜์„ธ์š”.**""", - - "English": f"""As narrative structure expert, deeply analyze this plan. - -**Original Theme:** {user_query} - -**Director's Plan:** -{director_plan} - -**Deep Review Items:** - -1. **Causality Verification** - Review connections between parts, find logical leaps: - - Part 1โ†’2: [Connection assessment] - - Part 2โ†’3: [Connection assessment] - (Review all connection points) - -2. **Philosophical Depth Assessment** - - Is philosophical theme deep enough? - - Contemporary relevance? - - Original insights? - -3. **Literary Device Effectiveness** - - Do metaphors and symbols work organically? - - Not excessive or insufficient? - - Tightly connected to theme? - -4. **Character Arc Feasibility** - - Is change sufficiently gradual? - - Are motivations clear at each stage? - - Psychological credibility? - -5. **8,000-word Feasibility** - - Can each part sustain 800 words? - - Any dragging or compressed sections? - -**Provide specific required improvements.**""" - } - - return lang_prompts.get(language, lang_prompts["Korean"]) - - def create_writer_prompt(self, part_number: int, master_plan: str, - accumulated_content: str, story_bible: StoryBible, - language: str) -> str: - """Single writer prompt - Enhanced version""" - - phase_name = NARRATIVE_PHASES[part_number-1] - target_words = MIN_WORDS_PER_PART - - # Part-specific instructions - philosophical_focus = { - 1: "Introduce existential anxiety through daily cracks", - 2: "First collision between individual and society", - 3: "Self-recognition through encounter with others", - 4: "Shaking beliefs and clashing values", - 5: "Weight of choice and paradox of freedom", - 6: "Test of humanity in extreme situations", - 7: "Weight of consequences and responsibility", - 8: "Self-rediscovery through others' gaze", - 9: "Reconciliation with the irreconcilable", - 10: "New life possibilities and unresolved questions" - } - - literary_techniques = { - 1: "Introducing objective correlative", - 2: "Contrapuntal narration", - 3: "Stream of consciousness", - 4: "Subtle shifts in perspective", - 5: "Aesthetics of silence and omission", - 6: "Subjective transformation of time", - 7: "Intersection of multiple viewpoints", - 8: "Subversion of metaphor", - 9: "Reinterpretation of archetypal images", - 10: "Multi-layered open ending" - } - - # Story bible summary - bible_summary = f""" -**Characters:** {', '.join(story_bible.characters.keys()) if story_bible.characters else 'TBD'} -**Key Symbols:** {', '.join(story_bible.symbols.keys()) if story_bible.symbols else 'TBD'} -**Themes:** {', '.join(story_bible.themes[:3]) if story_bible.themes else 'TBD'} -**Style:** {story_bible.style_guide.get('voice', 'N/A')} -""" - - # Previous content summary - prev_content = "" - if accumulated_content: - prev_parts = accumulated_content.split('\n\n') - if len(prev_parts) >= 1: - prev_content = prev_parts[-1][-2000:] # Last 2000 chars of previous part - - lang_prompts = { - "Korean": f"""๋‹น์‹ ์€ ํ˜„๋Œ€ ๋ฌธํ•™์˜ ์ตœ์ „์„ ์— ์„  ์ž‘๊ฐ€์ž…๋‹ˆ๋‹ค. -**ํ˜„์žฌ: ํŒŒํŠธ {part_number} - {phase_name}** - -{"**ํ•„์ˆ˜ ์ฒซ๋ฌธ์žฅ:** " + story_bible.opening_sentence if part_number == 1 and story_bible.opening_sentence else ""} - -**์ด๋ฒˆ ํŒŒํŠธ์˜ ์ฒ ํ•™์  ์ดˆ์ :** {philosophical_focus[part_number]} -**ํ•ต์‹ฌ ๋ฌธํ•™ ๊ธฐ๋ฒ•:** {literary_techniques[part_number]} - -**์ „์ฒด ๊ณ„ํš:** -{master_plan} - -**์Šคํ† ๋ฆฌ ๋ฐ”์ด๋ธ”:** -{bible_summary} - -**์ง์ „ ๋‚ด์šฉ:** -{prev_content if prev_content else "์ฒซ ํŒŒํŠธ์ž…๋‹ˆ๋‹ค"} - -**ํŒŒํŠธ {part_number} ์ž‘์„ฑ ์ง€์นจ:** - -1. **๋ถ„๋Ÿ‰:** {target_words}-900 ๋‹จ์–ด (ํ•„์ˆ˜) - -2. **๋ฌธํ•™์  ์ˆ˜์‚ฌ ์š”๊ตฌ์‚ฌํ•ญ:** - - ์ตœ์†Œ 3๊ฐœ์˜ ๋…์ฐฝ์  ์€์œ /์ง์œ  - - 1๊ฐœ ์ด์ƒ์˜ ์ƒ์ง•์  ์ด๋ฏธ์ง€ ์‹ฌํ™” - - ๊ฐ๊ฐ์  ๋ฌ˜์‚ฌ์™€ ์ถ”์ƒ์  ์‚ฌ์œ ์˜ ์œตํ•ฉ - - ๋ฆฌ๋“ฌ๊ฐ ์žˆ๋Š” ๋ฌธ์žฅ ๊ตฌ์„ฑ (์žฅ๋‹จ์˜ ๋ณ€์ฃผ) - -3. **ํ˜„๋Œ€์  ๊ณ ๋‡Œ ํ‘œํ˜„:** - - ๋””์ง€ํ„ธ ์‹œ๋Œ€์˜ ์†Œ์™ธ๊ฐ - - ์ž๋ณธ์ฃผ์˜์  ์‚ถ์˜ ๋ถ€์กฐ๋ฆฌ - - ๊ด€๊ณ„์˜ ํ‘œ๋ฉด์„ฑ๊ณผ ์ง„์ •์„ฑ ๊ฐˆ๋ง - - ์˜๋ฏธ ์ถ”๊ตฌ์™€ ๋ฌด์˜๋ฏธ์˜ ์ง๋ฉด - -4. **์‚ฌํšŒ์  ๋ฉ”์‹œ์ง€ ๋‚ด์žฌํ™”:** - - ์ง์ ‘์  ์ฃผ์žฅ์ด ์•„๋‹Œ ์ƒํ™ฉ๊ณผ ์ธ๋ฌผ์„ ํ†ตํ•œ ์•”์‹œ - - ๊ฐœ์ธ์˜ ๊ณ ํ†ต๊ณผ ์‚ฌํšŒ ๊ตฌ์กฐ์˜ ์—ฐ๊ฒฐ - - ๋ฏธ์‹œ์  ์ผ์ƒ๊ณผ ๊ฑฐ์‹œ์  ๋ฌธ์ œ์˜ ๊ต์ฐจ - -5. **์„œ์‚ฌ์  ์ถ”์ง„๋ ฅ:** - - ์ด์ „ ํŒŒํŠธ์˜ ํ•„์—ฐ์  ๊ฒฐ๊ณผ๋กœ ์‹œ์ž‘ - - ์ƒˆ๋กœ์šด ๊ฐˆ๋“ฑ ์ธต์œ„ ์ถ”๊ฐ€ - - ๋‹ค์Œ ํŒŒํŠธ๋ฅผ ํ–ฅํ•œ ๊ธด์žฅ๊ฐ ์กฐ์„ฑ - -**๋ฌธํ•™์  ๊ธˆ๊ธฐ:** -- ์ง„๋ถ€ํ•œ ํ‘œํ˜„์ด๋‚˜ ์ƒํˆฌ์  ์€์œ  -- ๊ฐ์ •์˜ ์ง์ ‘์  ์„ค๋ช… -- ๋„๋•์  ํŒ๋‹จ์ด๋‚˜ ๊ตํ›ˆ -- ์ธ์œ„์ ์ธ ํ•ด๊ฒฐ์ด๋‚˜ ์œ„์•ˆ - -ํŒŒํŠธ {part_number}๋ฅผ ๊นŠ์ด ์žˆ๋Š” ๋ฌธํ•™์  ์„ฑ์ทจ๋กœ ๋งŒ๋“œ์„ธ์š”.""", - - "English": f"""You are a writer at the forefront of contemporary literature. -**Current: Part {part_number} - {phase_name}** - -{"**Required Opening:** " + story_bible.opening_sentence if part_number == 1 and story_bible.opening_sentence else ""} - -**Philosophical Focus:** {philosophical_focus[part_number]} -**Core Literary Technique:** {literary_techniques[part_number]} - -**Master Plan:** -{master_plan} - -**Story Bible:** -{bible_summary} + """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): + 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} ์ปจ์…‰์„ ๊ฐœ๋ฐœํ•˜์„ธ์š”. -**Previous Content:** -{prev_content if prev_content else "This is the first part"} - -**Part {part_number} Guidelines:** - -1. **Length:** {target_words}-900 words (mandatory) - -2. **Literary Device Requirements:** - - Minimum 3 original metaphors/similes - - Deepen at least 1 symbolic image - - Fusion of sensory description and abstract thought - - Rhythmic sentence composition (variation of long/short) - -3. **Modern Anguish Expression:** - - Digital age alienation - - Absurdity of capitalist life - - Surface relationships vs authenticity yearning - - Meaning pursuit vs confronting meaninglessness - -4. **Social Message Internalization:** - - Implication through situation and character, not direct claim - - Connection between individual pain and social structure - - Intersection of micro daily life and macro problems - -5. **Narrative Momentum:** - - Start as inevitable result of previous part - - Add new conflict layers - - Create tension toward next part - -**Literary Taboos:** -- Clichรฉd expressions or trite metaphors -- Direct emotion explanation -- Moral judgment or preaching -- Artificial resolution or comfort - -Make Part {part_number} a profound literary achievement.""" - } - - return lang_prompts.get(language, lang_prompts["Korean"]) - - def create_part_critic_prompt(self, part_number: int, part_content: str, - master_plan: str, accumulated_content: str, - story_bible: StoryBible, language: str) -> str: - """Part-by-part immediate critique - Enhanced version""" - - lang_prompts = { - "Korean": f"""ํŒŒํŠธ {part_number}์˜ ๋ฌธํ•™์  ์„ฑ์ทจ๋„๋ฅผ ์—„๊ฒฉํžˆ ํ‰๊ฐ€ํ•˜์„ธ์š”. - -**๋งˆ์Šคํ„ฐํ”Œ๋žœ ํŒŒํŠธ {part_number} ์š”๊ตฌ์‚ฌํ•ญ:** -{self._extract_part_plan(master_plan, part_number)} +**์š”์ฒญ์‚ฌํ•ญ:** {user_query} +**ํƒ€์ž…:** {SCREENPLAY_LENGTHS[screenplay_type]['description']} +**์žฅ๋ฅด:** {genre} -**์ž‘์„ฑ๋œ ๋‚ด์šฉ:** -{part_content} +**์‹œ์žฅ ์กฐ์‚ฌ:** +{search_results if search_results else "N/A"} -**์Šคํ† ๋ฆฌ ๋ฐ”์ด๋ธ” ์ฒดํฌ:** -- ์บ๋ฆญํ„ฐ: {', '.join(story_bible.characters.keys())} -- ์„ค์ •: {', '.join(story_bible.settings.keys())} +**ํ•„์ˆ˜ ์ œ๊ณต ํ•ญ๋ชฉ:** -**ํ‰๊ฐ€ ๊ธฐ์ค€:** +1. **์ œ๋ชฉ (TITLE)** + - ๊ธฐ์–ตํ•˜๊ธฐ ์‰ฝ๊ณ  ๋งˆ์ผ€ํŒ… ๊ฐ€๋Šฅํ•œ ์ œ๋ชฉ + - ์žฅ๋ฅด์™€ ํ†ค์„ ์•”์‹œํ•˜๋Š” ์ œ๋ชฉ -1. **๋ฌธํ•™์  ์ˆ˜์‚ฌ (30%)** - - ์€์œ ์™€ ์ƒ์ง•์˜ ๋…์ฐฝ์„ฑ - - ์–ธ์–ด์˜ ์‹œ์  ๋ฐ€๋„ - - ์ด๋ฏธ์ง€์˜ ์„ ๋ช…๋„์™€ ๊นŠ์ด - - ๋ฌธ์žฅ์˜ ๋ฆฌ๋“ฌ๊ณผ ์Œ์•…์„ฑ +2. **๋กœ๊ทธ๋ผ์ธ (LOGLINE)** + - 25๋‹จ์–ด ์ด๋‚ด ํ•œ ๋ฌธ์žฅ + - ํ˜•์‹: "[์‚ฌ๊ฑด]์ด ์ผ์–ด๋‚ฌ์„ ๋•Œ, [์ฃผ์ธ๊ณต]์€ [๋ชฉํ‘œ]๋ฅผ ์ด๋ฃจ์–ด์•ผ ํ•œ๋‹ค. ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด [๊ฒฐ๊ณผ]" + - ๊ฐˆ๋“ฑ๊ณผ stakes๊ฐ€ ๋ช…ํ™•ํ•ด์•ผ ํ•จ -2. **์ฒ ํ•™์  ๊นŠ์ด (25%)** - - ์‹ค์กด์  ์งˆ๋ฌธ์˜ ์ œ๊ธฐ - - ํ˜„๋Œ€์ธ์˜ ์กฐ๊ฑด ํƒ๊ตฌ - - ๋ณดํŽธ์„ฑ๊ณผ ํŠน์ˆ˜์„ฑ์˜ ๊ท ํ˜• - - ์‚ฌ์œ ์˜ ๋…์ฐฝ์„ฑ +3. **์žฅ๋ฅด ๋ถ„์„** + - ์ฃผ ์žฅ๋ฅด: {genre} + - ์„œ๋ธŒ ์žฅ๋ฅด: + - ํ†ค & ๋ถ„์œ„๊ธฐ: -3. **์‚ฌํšŒ์  ํ†ต์ฐฐ (20%)** - - ์‹œ๋Œ€์ •์‹ ์˜ ํฌ์ฐฉ - - ๊ตฌ์กฐ์™€ ๊ฐœ์ธ์˜ ๊ด€๊ณ„ - - ๋น„ํŒ์  ์‹œ๊ฐ์˜ ์˜ˆ๋ฆฌํ•จ - - ๋Œ€์•ˆ์  ์ƒ์ƒ๋ ฅ +4. **ํƒ€๊ฒŸ ๊ด€๊ฐ** + - ์ฃผ์š” ์—ฐ๋ น๋Œ€: + - ์„ฑ๋ณ„ ๋ถ„ํฌ: + - ๊ด€์‹ฌ์‚ฌ: + - ์œ ์‚ฌ ์ž‘ํ’ˆ ํŒฌ์ธต: -4. **์„œ์‚ฌ์  ์™„์„ฑ๋„ (25%)** - - ์ธ๊ณผ๊ด€๊ณ„์˜ ํ•„์—ฐ์„ฑ - - ๊ธด์žฅ๊ฐ์˜ ์œ ์ง€ - - ์ธ๋ฌผ์˜ ์ž…์ฒด์„ฑ - - ๊ตฌ์กฐ์  ํ†ต์ผ์„ฑ +5. **๋น„๊ต ์ž‘ํ’ˆ (COMPS)** + - 3๊ฐœ์˜ ์„ฑ๊ณตํ•œ ์œ ์‚ฌ ํ”„๋กœ์ ํŠธ + - ๊ฐ๊ฐ์˜ ๋ฐ•์Šค์˜คํ”ผ์Šค/์‹œ์ฒญ๋ฅ  ์„ฑ๊ณผ + - ์šฐ๋ฆฌ ํ”„๋กœ์ ํŠธ์™€์˜ ์ฐจ๋ณ„์  -**๊ตฌ์ฒด์  ์ง€์ ์‚ฌํ•ญ:** -- ์ง„๋ถ€ํ•œ ํ‘œํ˜„: [์˜ˆ์‹œ์™€ ๋Œ€์•ˆ] -- ์ฒ ํ•™์  ์ฒœ์ฐฉ ๋ถ€์กฑ: [๋ณด์™„ ๋ฐฉํ–ฅ] -- ์‚ฌํšŒ์  ๋ฉ”์‹œ์ง€ ๋ถˆ๋ช…ํ™•: [๊ฐ•ํ™” ๋ฐฉ์•ˆ] -- ์„œ์‚ฌ์  ํ—ˆ์ : [์ˆ˜์ • ํ•„์š”] +6. **๊ณ ์œ  ํŒ๋งค ํฌ์ธํŠธ (USP)** + - ์ด ์ด์•ผ๊ธฐ๋งŒ์˜ ๋…ํŠนํ•œ ์  + - ํ˜„์žฌ ์‹œ์žฅ์—์„œ์˜ ํ•„์š”์„ฑ + - ์ œ์ž‘ ๊ฐ€๋Šฅ์„ฑ -**ํ•„์ˆ˜ ๊ฐœ์„  ์š”๊ตฌ:** -๋ฌธํ•™์  ์ˆ˜์ค€์„ ๋…ธ๋ฒจ์ƒ ๊ธ‰์œผ๋กœ ๋Œ์–ด์˜ฌ๋ฆฌ๊ธฐ ์œ„ํ•œ ๊ตฌ์ฒด์  ์ˆ˜์ •์•ˆ์„ ์ œ์‹œํ•˜์„ธ์š”.""", +7. **๋น„์ฃผ์–ผ ์ปจ์…‰** + - ํ•ต์‹ฌ ๋น„์ฃผ์–ผ ์ด๋ฏธ์ง€ 3๊ฐœ + - ์ „์ฒด์ ์ธ ๋ฃฉ & ํ•„ - "English": f"""Strictly evaluate literary achievement of Part {part_number}. +๊ตฌ์ฒด์ ์ด๊ณ  ์‹œ์žฅ์„ฑ ์žˆ๋Š” ์ปจ์…‰์„ ์ œ์‹œํ•˜์„ธ์š”.""", -**Master Plan Part {part_number} Requirements:** -{self._extract_part_plan(master_plan, part_number)} + "English": f"""You are a Hollywood producer. Develop a commercially viable {screenplay_type} concept. -**Written Content:** -{part_content} +**Request:** {user_query} +**Type:** {SCREENPLAY_LENGTHS[screenplay_type]['description']} +**Genre:** {genre} -**Story Bible Check:** -- Characters: {', '.join(story_bible.characters.keys()) if story_bible.characters else 'None yet'} -- Settings: {', '.join(story_bible.settings.keys()) if story_bible.settings else 'None yet'} +**Market Research:** +{search_results if search_results else "N/A"} -**Evaluation Criteria:** +**Required Elements:** + +1. **TITLE** + - Memorable and marketable + - Hints at genre and tone + +2. **LOGLINE** + - One sentence, 25 words max + - Format: "When [inciting incident], a [protagonist] must [objective] or else [stakes]" + - Clear conflict and stakes + +3. **GENRE ANALYSIS** + - Primary Genre: {genre} + - Sub-genre: + - Tone & Mood: + +4. **TARGET AUDIENCE** + - Primary Age Range: + - Gender Distribution: + - Interests: + - Similar Works Fanbase: + +5. **COMPARABLE FILMS (COMPS)** + - 3 successful similar projects + - Box office/viewership performance + - How ours differs + +6. **UNIQUE SELLING POINT (USP)** + - What makes this story unique + - Why now in the market + - Production feasibility + +7. **VISUAL CONCEPT** + - 3 key visual images + - Overall look & feel + +Provide specific, marketable concept.""" + } + + return lang_prompts.get(language, lang_prompts["English"]) + + def create_story_developer_prompt(self, producer_concept: str, user_query: str, + screenplay_type: str, genre: str, language: str) -> str: + """Story developer for synopsis and structure""" + + genre_template = GENRE_TEMPLATES.get(genre, GENRE_TEMPLATES["drama"]) + + lang_prompts = { + "Korean": f"""๋‹น์‹ ์€ ์Šคํ† ๋ฆฌ ๊ฐœ๋ฐœ์ž์ž…๋‹ˆ๋‹ค. ํ”„๋กœ๋“€์„œ์˜ ์ปจ์…‰์„ ๋ฐ”ํƒ•์œผ๋กœ {screenplay_type}์˜ ์‹œ๋†‰์‹œ์Šค์™€ 3๋ง‰ ๊ตฌ์กฐ๋ฅผ ๊ฐœ๋ฐœํ•˜์„ธ์š”. + +**ํ”„๋กœ๋“€์„œ ์ปจ์…‰:** +{producer_concept} + +**์žฅ๋ฅด ํŠน์„ฑ:** {genre} +- ํŽ˜์ด์‹ฑ: {genre_template['pacing']} +- ํ•ต์‹ฌ ์š”์†Œ: {', '.join(genre_template['key_elements'])} +- ๊ตฌ์กฐ ๋น„ํŠธ: {', '.join(genre_template['structure_beats'])} + +**ํ•„์ˆ˜ ์ž‘์„ฑ ํ•ญ๋ชฉ:** + +1. **์‹œ๋†‰์‹œ์Šค (SYNOPSIS)** + - 300-500๋‹จ์–ด + - 3๋ง‰ ๊ตฌ์กฐ๊ฐ€ ๋ช…ํ™•ํžˆ ๋“œ๋Ÿฌ๋‚˜๋„๋ก + - ์ฃผ์ธ๊ณต์˜ ๋ณ€ํ™” arc ํฌํ•จ + - ์ฃผ์š” ์ „ํ™˜์  ๋ช…์‹œ + - ๊ฒฐ๋ง ํฌํ•จ (์Šคํฌ์ผ๋Ÿฌ OK) + +2. **3๋ง‰ ๊ตฌ์กฐ (THREE-ACT STRUCTURE)** + + **์ œ1๋ง‰ - ์„ค์ • (Setup) [25%]** + - ์ผ์ƒ ์„ธ๊ณ„ (Ordinary World): + - ๊ทผ์นœ์ƒ๊ฐ„ ์‚ฌ๊ฑด (Inciting Incident): + - ์ฃผ์ธ๊ณต ์†Œ๊ฐœ ๋ฐ ๊ฒฐํ•จ: + - 1๋ง‰ ์ „ํ™˜์  (Plot Point 1): + + **์ œ2๋ง‰A - ์ƒ์Šน ์•ก์…˜ (Rising Action) [25%]** + - ์ƒˆ๋กœ์šด ์„ธ๊ณ„ ์ง„์ž…: + - ์žฌ๋ฏธ์™€ ๊ฒŒ์ž„ (Fun and Games): + - B ์Šคํ† ๋ฆฌ (๊ด€๊ณ„/ํ…Œ๋งˆ): + - ์ค‘๊ฐ„์  (Midpoint) - ๊ฐ€์งœ ์Šน๋ฆฌ/ํŒจ๋ฐฐ: + + **์ œ2๋ง‰B - ๋ณต์žกํ™” (Complications) [25%]** + - ์•…๋‹น์˜ ๋ฐ˜๊ฒฉ: + - ํŒ€ ํ•ด์ฒด/์œ„๊ธฐ: + - ๋ชจ๋“  ๊ฒƒ์„ ์žƒ์Œ (All Is Lost): + - ์˜ํ˜ผ์˜ ์–ด๋‘” ๋ฐค: + + **์ œ3๋ง‰ - ํ•ด๊ฒฐ (Resolution) [25%]** + - 2๋ง‰ ์ „ํ™˜์  (Plot Point 2): + - ์ตœ์ข… ์ „ํˆฌ ์ค€๋น„: + - ํด๋ผ์ด๋งฅ์Šค: + - ์ƒˆ๋กœ์šด ์ผ์ƒ: + +3. **Save the Cat ๋น„ํŠธ ์‹œํŠธ** + 15๊ฐœ ๋น„ํŠธ๋ฅผ {SCREENPLAY_LENGTHS[screenplay_type]['pages']}ํŽ˜์ด์ง€์— ๋งž์ถฐ ๋ฐฐ์น˜ + +4. **์ฃผ์ œ (THEME)** + - ์ค‘์‹ฌ ์ฃผ์ œ: + - ์ฃผ์ œ๊ฐ€ ๋“œ๋Ÿฌ๋‚˜๋Š” ์ˆœ๊ฐ„: + - ์ฃผ์ œ์˜ ์‹œ๊ฐ์  ํ‘œํ˜„: + +5. **ํ†ค & ์Šคํƒ€์ผ** + - ์ „์ฒด์ ์ธ ํ†ค: + - ์œ ๋จธ ์‚ฌ์šฉ ์—ฌ๋ถ€: + - ๋น„์ฃผ์–ผ ์Šคํƒ€์ผ: + +๊ตฌ์ฒด์ ์ด๊ณ  ๊ฐ์ •์ ์œผ๋กœ ๊ณต๊ฐ๊ฐ€๋Š” ์Šคํ† ๋ฆฌ๋ฅผ ๋งŒ๋“œ์„ธ์š”.""", + + "English": f"""You are a story developer. Based on the producer's concept, develop the synopsis and three-act structure for this {screenplay_type}. + +**Producer Concept:** +{producer_concept} + +**Genre Characteristics:** {genre} +- Pacing: {genre_template['pacing']} +- Key Elements: {', '.join(genre_template['key_elements'])} +- Structure Beats: {', '.join(genre_template['structure_beats'])} + +**Required Elements:** + +1. **SYNOPSIS** + - 300-500 words + - Clear three-act structure + - Protagonist's change arc + - Major turning points + - Include ending (spoilers OK) + +2. **THREE-ACT STRUCTURE** + + **ACT 1 - Setup [25%]** + - Ordinary World: + - Inciting Incident: + - Protagonist Introduction & Flaw: + - Plot Point 1: + + **ACT 2A - Rising Action [25%]** + - Entering New World: + - Fun and Games: + - B Story (Relationship/Theme): + - Midpoint - False Victory/Defeat: + + **ACT 2B - Complications [25%]** + - Bad Guys Close In: + - Team Breaks Down/Crisis: + - All Is Lost: + - Dark Night of the Soul: + + **ACT 3 - Resolution [25%]** + - Plot Point 2: + - Final Battle Preparation: + - Climax: + - New Normal: + +3. **SAVE THE CAT BEAT SHEET** + Place 15 beats across {SCREENPLAY_LENGTHS[screenplay_type]['pages']} pages + +4. **THEME** + - Central Theme: + - Theme Stated Moment: + - Visual Theme Expression: + +5. **TONE & STYLE** + - Overall Tone: + - Use of Humor: + - Visual Style: + +Create specific, emotionally resonant story.""" + } + + return lang_prompts.get(language, lang_prompts["English"]) + + def create_character_designer_prompt(self, producer_concept: str, story_structure: str, + genre: str, language: str) -> str: + """Character designer prompt""" + + lang_prompts = { + "Korean": f"""๋‹น์‹ ์€ ์บ๋ฆญํ„ฐ ๋””์ž์ด๋„ˆ์ž…๋‹ˆ๋‹ค. ๋‹ค์ธต์ ์ด๊ณ  ๋งค๋ ฅ์ ์ธ ์บ๋ฆญํ„ฐ๋“ค์„ ์ฐฝ์กฐํ•˜์„ธ์š”. + +**ํ”„๋กœ๋“€์„œ ์ปจ์…‰:** +{producer_concept} + +**์Šคํ† ๋ฆฌ ๊ตฌ์กฐ:** +{story_structure} + +**ํ•„์ˆ˜ ์บ๋ฆญํ„ฐ ํ”„๋กœํ•„:** + +1. **์ฃผ์ธ๊ณต (PROTAGONIST)** + - ์ด๋ฆ„ & ๋‚˜์ด: + - ์ง์—…/์—ญํ• : + - ์บ๋ฆญํ„ฐ ์•„ํฌํƒ€์ž…: + - WANT (์™ธ์  ๋ชฉํ‘œ): + - NEED (๋‚ด์  ํ•„์š”): + - ์น˜๋ช…์  ๊ฒฐํ•จ (Fatal Flaw): + - ๋ฐฑ์Šคํ† ๋ฆฌ (ํ•ต์‹ฌ ์ƒ์ฒ˜): + - ์„ฑ๊ฒฉ ํŠน์„ฑ (3-5๊ฐœ): + - ๋งํˆฌ & ์–ธ์–ด ํŒจํ„ด: + - ์‹œ๊ฐ์  ํŠน์ง•: + - ์บ๋ฆญํ„ฐ ์•„ํฌ (Aโ†’B): + +2. **์ ๋Œ€์ž (ANTAGONIST)** + - ์ด๋ฆ„ & ๋‚˜์ด: + - ์ง์—…/์—ญํ• : + - ์•…์—ญ ์•„ํฌํƒ€์ž…: + - ๋ชฉํ‘œ & ๋™๊ธฐ: + - ์ฃผ์ธ๊ณต๊ณผ์˜ ์—ฐ๊ฒฐ์ : + - ์ •๋‹น์„ฑ ์žˆ๋Š” ์ด์œ : + - ์•ฝ์ : + - ํŠน์ง•์  ํ–‰๋™: + +3. **์กฐ๋ ฅ์ž๋“ค (SUPPORTING CAST)** + ์ตœ์†Œ 3๋ช…, ๊ฐ๊ฐ: + - ์ด๋ฆ„ & ์—ญํ• : + - ์ฃผ์ธ๊ณต๊ณผ์˜ ๊ด€๊ณ„: + - ์Šคํ† ๋ฆฌ ๊ธฐ๋Šฅ: + - ๋…ํŠนํ•œ ํŠน์„ฑ: + - ๊ธฐ์—ฌํ•˜๋Š” ๋ฐ”: + +4. **์บ๋ฆญํ„ฐ ๊ด€๊ณ„๋„** + - ์ฃผ์š” ๊ด€๊ณ„ ์—ญํ•™: + - ๊ฐˆ๋“ฑ ๊ตฌ์กฐ: + - ๊ฐ์ •์  ์—ฐ๊ฒฐ: + - ํŒŒ์›Œ ๋‹ค์ด๋‚˜๋ฏน: + +5. **์บ์ŠคํŒ… ์ œ์•ˆ** + - ๊ฐ ์ฃผ์š” ์บ๋ฆญํ„ฐ๋ณ„ ์ด์ƒ์ ์ธ ๋ฐฐ์šฐ ํƒ€์ž… + - ์—ฐ๋ น๋Œ€, ์™ธ๋ชจ, ์—ฐ๊ธฐ ์Šคํƒ€์ผ + +6. **๋Œ€ํ™” ์ƒ˜ํ”Œ** + - ๊ฐ ์ฃผ์š” ์บ๋ฆญํ„ฐ์˜ ์‹œ๊ทธ๋‹ˆ์ฒ˜ ๋Œ€์‚ฌ 2-3๊ฐœ + - ์บ๋ฆญํ„ฐ์˜ ๋ณธ์งˆ์„ ๋“œ๋Ÿฌ๋‚ด๋Š” ๋Œ€ํ™” + +๊ฐ ์บ๋ฆญํ„ฐ๊ฐ€ ํ…Œ๋งˆ๋ฅผ ๊ตฌํ˜„ํ•˜๊ณ  ์Šคํ† ๋ฆฌ๋ฅผ ์ถ”์ง„ํ•˜๋„๋ก ๋””์ž์ธํ•˜์„ธ์š”.""", + + "English": f"""You are a character designer. Create multi-dimensional, compelling characters. + +**Producer Concept:** +{producer_concept} + +**Story Structure:** +{story_structure} + +**Required Character Profiles:** + +1. **PROTAGONIST** + - Name & Age: + - Occupation/Role: + - Character Archetype: + - WANT (External Goal): + - NEED (Internal Need): + - Fatal Flaw: + - Backstory (Core Wound): + - Personality Traits (3-5): + - Speech Pattern: + - Visual Characteristics: + - Character Arc (Aโ†’B): + +2. **ANTAGONIST** + - Name & Age: + - Occupation/Role: + - Villain Archetype: + - Goal & Motivation: + - Connection to Protagonist: + - Justifiable Reason: + - Weakness: + - Signature Behaviors: + +3. **SUPPORTING CAST** + Minimum 3, each with: + - Name & Role: + - Relationship to Protagonist: + - Story Function: + - Unique Traits: + - What They Contribute: + +4. **CHARACTER RELATIONSHIPS** + - Key Relationship Dynamics: + - Conflict Structure: + - Emotional Connections: + - Power Dynamics: + +5. **CASTING SUGGESTIONS** + - Ideal actor type for each major character + - Age range, appearance, acting style + +6. **DIALOGUE SAMPLES** + - 2-3 signature lines per major character + - Dialogue revealing character essence + +Design each character to embody theme and drive story.""" + } + + return lang_prompts.get(language, lang_prompts["English"]) + + def create_scene_planner_prompt(self, story_structure: str, characters: str, + screenplay_type: str, genre: str, language: str) -> str: + """Scene breakdown planner""" + + total_pages = SCREENPLAY_LENGTHS[screenplay_type]['pages'] + + lang_prompts = { + "Korean": f"""๋‹น์‹ ์€ ์”ฌ ํ”Œ๋ž˜๋„ˆ์ž…๋‹ˆ๋‹ค. {total_pages}ํŽ˜์ด์ง€ {screenplay_type}์˜ ์ƒ์„ธํ•œ ์”ฌ ๋ธŒ๋ ˆ์ดํฌ๋‹ค์šด์„ ์ž‘์„ฑํ•˜์„ธ์š”. + +**์Šคํ† ๋ฆฌ ๊ตฌ์กฐ:** +{story_structure} + +**์บ๋ฆญํ„ฐ:** +{characters} + +**์”ฌ ๋ธŒ๋ ˆ์ดํฌ๋‹ค์šด ์š”๊ตฌ์‚ฌํ•ญ:** + +๊ฐ ์”ฌ๋งˆ๋‹ค ๋‹ค์Œ ์ •๋ณด ์ œ๊ณต: +- ์”ฌ ๋ฒˆํ˜ธ +- ์žฅ์†Œ (INT./EXT. LOCATION) +- ์‹œ๊ฐ„ (DAY/NIGHT/DAWN/DUSK) +- ๋“ฑ์žฅ์ธ๋ฌผ +- ์”ฌ์˜ ๋ชฉ์  (์Šคํ† ๋ฆฌ/์บ๋ฆญํ„ฐ/ํ…Œ๋งˆ) +- ํ•ต์‹ฌ ๊ฐˆ๋“ฑ +- ์˜ˆ์ƒ ํŽ˜์ด์ง€ ์ˆ˜ +- Save the Cat ๋น„ํŠธ (ํ•ด๋‹น์‹œ) + +**๋ง‰๋ณ„ ๋ฐฐ๋ถ„:** +- 1๋ง‰: ~{int(total_pages * 0.25)}ํŽ˜์ด์ง€ (10-12์”ฌ) +- 2๋ง‰A: ~{int(total_pages * 0.25)}ํŽ˜์ด์ง€ (12-15์”ฌ) +- 2๋ง‰B: ~{int(total_pages * 0.25)}ํŽ˜์ด์ง€ (12-15์”ฌ) +- 3๋ง‰: ~{int(total_pages * 0.25)}ํŽ˜์ด์ง€ (8-10์”ฌ) + +**์žฅ๋ฅด๋ณ„ ๊ณ ๋ ค์‚ฌํ•ญ:** {genre} +{self._get_genre_scene_guidelines(genre, "Korean")} + +**์”ฌ ์ „ํ™˜ ์Šคํƒ€์ผ:** +- CUT TO: +- FADE IN/OUT: +- MATCH CUT: +- SMASH CUT: +- DISSOLVE TO: + +๊ฐ ์”ฌ์ด ์Šคํ† ๋ฆฌ๋ฅผ ์ „์ง„์‹œํ‚ค๊ณ  ์บ๋ฆญํ„ฐ๋ฅผ ๋ฐœ์ „์‹œํ‚ค๋„๋ก ๊ณ„ํšํ•˜์„ธ์š”.""", + + "English": f"""You are a scene planner. Create detailed scene breakdown for {total_pages}-page {screenplay_type}. + +**Story Structure:** +{story_structure} + +**Characters:** +{characters} + +**Scene Breakdown Requirements:** + +For each scene provide: +- Scene Number +- Location (INT./EXT. LOCATION) +- Time (DAY/NIGHT/DAWN/DUSK) +- Characters Present +- Scene Purpose (Story/Character/Theme) +- Core Conflict +- Estimated Page Count +- Save the Cat Beat (if applicable) + +**Act Distribution:** +- Act 1: ~{int(total_pages * 0.25)} pages (10-12 scenes) +- Act 2A: ~{int(total_pages * 0.25)} pages (12-15 scenes) +- Act 2B: ~{int(total_pages * 0.25)} pages (12-15 scenes) +- Act 3: ~{int(total_pages * 0.25)} pages (8-10 scenes) + +**Genre Considerations:** {genre} +{self._get_genre_scene_guidelines(genre, "English")} + +**Scene Transitions:** +- CUT TO: +- FADE IN/OUT: +- MATCH CUT: +- SMASH CUT: +- DISSOLVE TO: + +Plan each scene to advance story and develop character.""" + } + + return lang_prompts.get(language, lang_prompts["English"]) + + def create_screenwriter_prompt(self, act: str, scene_breakdown: str, + characters: str, previous_acts: str, + screenplay_type: str, genre: str, language: str) -> str: + """Screenwriter prompt for actual screenplay pages""" + + act_pages = int(SCREENPLAY_LENGTHS[screenplay_type]['pages'] * 0.25) + + lang_prompts = { + "Korean": f"""๋‹น์‹ ์€ ํ”„๋กœ ์‹œ๋‚˜๋ฆฌ์˜ค ์ž‘๊ฐ€์ž…๋‹ˆ๋‹ค. {act}์„ ํ‘œ์ค€ ์‹œ๋‚˜๋ฆฌ์˜ค ํฌ๋งท์œผ๋กœ ์ž‘์„ฑํ•˜์„ธ์š”. + +**ํƒ€๊ฒŸ ๋ถ„๋Ÿ‰:** {act_pages}ํŽ˜์ด์ง€ + +**์”ฌ ๋ธŒ๋ ˆ์ดํฌ๋‹ค์šด:** +{self._extract_act_scenes(scene_breakdown, act)} -1. **Literary Rhetoric (30%)** - - Originality of metaphor and symbol - - Poetic density of language - - Clarity and depth of imagery - - Rhythm and musicality of sentences - -2. **Philosophical Depth (25%)** - - Raising existential questions - - Exploring modern human condition - - Balance of universality and specificity - - Originality of thought - -3. **Social Insight (20%)** - - Capturing zeitgeist - - Relationship between structure and individual - - Sharpness of critical perspective - - Alternative imagination - -4. **Narrative Completion (25%)** - - Inevitability of causality - - Maintaining tension - - Character dimensionality - - Structural unity - -**Specific Points:** -- Clichรฉd expressions: [examples and alternatives] -- Insufficient philosophical exploration: [enhancement direction] -- Unclear social message: [strengthening methods] -- Narrative gaps: [needed revisions] +**์บ๋ฆญํ„ฐ ์ •๋ณด:** +{characters} + +**์ด์ „ ๋‚ด์šฉ:** +{previous_acts if previous_acts else "์ฒซ ๋ง‰์ž…๋‹ˆ๋‹ค."} + +**์‹œ๋‚˜๋ฆฌ์˜ค ํฌ๋งท ๊ทœ์น™:** + +1. **์”ฌ ํ—ค๋”ฉ** + INT. ์žฅ์†Œ - ์‹œ๊ฐ„ + EXT. ์žฅ์†Œ - ์‹œ๊ฐ„ + +2. **์•ก์…˜ ๋ผ์ธ** + - ํ˜„์žฌ ์‹œ์ œ ์‚ฌ์šฉ + - ์‹œ๊ฐ์ ์œผ๋กœ ๋ณด์ด๋Š” ๊ฒƒ๋งŒ ๋ฌ˜์‚ฌ + - 4์ค„ ์ดํ•˜๋กœ ์œ ์ง€ + - ๊ฐ์ •์€ ํ–‰๋™์œผ๋กœ ํ‘œํ˜„ + +3. **์บ๋ฆญํ„ฐ ์†Œ๊ฐœ** + ์ฒซ ๋“ฑ์žฅ์‹œ: ์ด๋ฆ„ (๋‚˜์ด) ๊ฐ„๋‹จํ•œ ๋ฌ˜์‚ฌ + +4. **๋Œ€ํ™”** + ์บ๋ฆญํ„ฐ๋ช… + (์ง€๋ฌธ) + ๋Œ€์‚ฌ + +5. **์ค‘์š” ์›์น™** + - Show, don't tell + - ์„œ๋ธŒํ…์ŠคํŠธ ํ™œ์šฉ + - ์ž์—ฐ์Šค๋Ÿฌ์šด ๋Œ€ํ™” + - ์‹œ๊ฐ์  ์Šคํ† ๋ฆฌํ…”๋ง + - ํŽ˜์ด์ง€๋‹น 1๋ถ„ ๊ทœ์น™ + +**{genre} ์žฅ๋ฅด ํŠน์„ฑ:** +- ๋Œ€ํ™” ๋น„์œจ: {GENRE_TEMPLATES[genre]['dialogue_ratio']*100}% +- ์”ฌ ๊ธธ์ด: {GENRE_TEMPLATES[genre]['scene_length']} +- ํ•ต์‹ฌ ์š”์†Œ: {', '.join(GENRE_TEMPLATES[genre]['key_elements'][:2])} + +์ •ํ™•ํ•œ ํฌ๋งท๊ณผ ๋ชฐ์ž…๊ฐ ์žˆ๋Š” ์Šคํ† ๋ฆฌํ…”๋ง์œผ๋กœ ์ž‘์„ฑํ•˜์„ธ์š”.""", + + "English": f"""You are a professional screenwriter. Write {act} in standard screenplay format. + +**Target Length:** {act_pages} pages + +**Scene Breakdown:** +{self._extract_act_scenes(scene_breakdown, act)} + +**Character Info:** +{characters} + +**Previous Content:** +{previous_acts if previous_acts else "This is the first act."} + +**Screenplay Format Rules:** + +1. **Scene Headings** + INT. LOCATION - TIME + EXT. LOCATION - TIME + +2. **Action Lines** + - Present tense + - Only what's visually seen + - Keep under 4 lines + - Emotions through actions + +3. **Character Intros** + First appearance: NAME (age) brief description + +4. **Dialogue** + CHARACTER NAME + (parenthetical) + Dialogue + +5. **Key Principles** + - Show, don't tell + - Use subtext + - Natural dialogue + - Visual storytelling + - One page = one minute + +**{genre} Genre Characteristics:** +- Dialogue Ratio: {GENRE_TEMPLATES[genre]['dialogue_ratio']*100}% +- Scene Length: {GENRE_TEMPLATES[genre]['scene_length']} +- Key Elements: {', '.join(GENRE_TEMPLATES[genre]['key_elements'][:2])} + +Write with proper format and engaging storytelling.""" + } + + return lang_prompts.get(language, lang_prompts["English"]) + + def create_script_doctor_prompt(self, act_content: str, act: str, + genre: str, language: str) -> str: + """Script doctor review and polish""" + + lang_prompts = { + "Korean": f"""๋‹น์‹ ์€ ์Šคํฌ๋ฆฝํŠธ ๋‹ฅํ„ฐ์ž…๋‹ˆ๋‹ค. {act}๋ฅผ ๊ฒ€ํ† ํ•˜๊ณ  ๊ฐœ์„  ์‚ฌํ•ญ์„ ์ œ์‹œํ•˜์„ธ์š”. + +**์ž‘์„ฑ๋œ ๋‚ด์šฉ:** +{act_content} + +**๊ฒ€ํ†  ํ•ญ๋ชฉ:** + +1. **ํฌ๋งท ์ •ํ™•์„ฑ** + - ์”ฌ ํ—ค๋”ฉ ํ˜•์‹ + - ์•ก์…˜ ๋ผ์ธ ๊ธธ์ด + - ๋Œ€ํ™” ํฌ๋งท + - ์ „ํ™˜ ํ‘œ์‹œ + +2. **์Šคํ† ๋ฆฌํ…”๋ง** + - ์‹œ๊ฐ์  ๋ช…ํ™•์„ฑ + - ํŽ˜์ด์‹ฑ + - ๊ธด์žฅ๊ฐ ๊ตฌ์ถ• + - ์”ฌ ๋ชฉ์  ๋‹ฌ์„ฑ + +3. **๋Œ€ํ™” ํ’ˆ์งˆ** + - ์ž์—ฐ์Šค๋Ÿฌ์›€ + - ์บ๋ฆญํ„ฐ ๊ณ ์œ ์„ฑ + - ์„œ๋ธŒํ…์ŠคํŠธ + - ๋ถˆํ•„์š”ํ•œ ์„ค๋ช… ์ œ๊ฑฐ + +4. **{genre} ์žฅ๋ฅด ์ ํ•ฉ์„ฑ** + - ์žฅ๋ฅด ๊ด€์Šต ์ค€์ˆ˜ + - ํ†ค ์ผ๊ด€์„ฑ + - ๊ธฐ๋Œ€ ์ถฉ์กฑ + +5. **๊ธฐ์ˆ ์  ์ธก๋ฉด** + - ํŽ˜์ด์ง€ ์ˆ˜ ์ ์ •์„ฑ + - ์ œ์ž‘ ๊ฐ€๋Šฅ์„ฑ + - ์˜ˆ์‚ฐ ๊ณ ๋ ค์‚ฌํ•ญ + +**ํ•„์ˆ˜ ๊ฐœ์„ ์‚ฌํ•ญ:** +๊ตฌ์ฒด์ ์ธ ์ˆ˜์ • ์ œ์•ˆ๊ณผ ๊ฐœ์„ ๋œ ์˜ˆ์‹œ๋ฅผ ์ œ๊ณตํ•˜์„ธ์š”.""", + + "English": f"""You are a script doctor. Review and provide improvements for {act}. + +**Written Content:** +{act_content} + +**Review Areas:** + +1. **Format Accuracy** + - Scene heading format + - Action line length + - Dialogue format + - Transitions + +2. **Storytelling** + - Visual clarity + - Pacing + - Tension building + - Scene purpose achievement + +3. **Dialogue Quality** + - Naturalness + - Character uniqueness + - Subtext + - Remove exposition + +4. **{genre} Genre Fit** + - Genre conventions + - Tone consistency + - Meeting expectations + +5. **Technical Aspects** + - Page count appropriateness + - Production feasibility + - Budget considerations **Required Improvements:** -Provide specific revisions to elevate literary level to Nobel Prize standard.""" - } - - return lang_prompts.get(language, lang_prompts["Korean"]) - - def create_writer_revision_prompt(self, part_number: int, original_content: str, - critic_feedback: str, language: str) -> str: - """Writer revision prompt""" - - lang_prompts = { - "Korean": f"""ํŒŒํŠธ {part_number}๋ฅผ ๋น„ํ‰์— ๋”ฐ๋ผ ์ˆ˜์ •ํ•˜์„ธ์š”. - -**์›๋ณธ:** -{original_content} - -**๋น„ํ‰ ํ”ผ๋“œ๋ฐฑ:** -{critic_feedback} - -**์ˆ˜์ • ์ง€์นจ:** -1. ๋ชจ๋“  'ํ•„์ˆ˜ ์ˆ˜์ •' ์‚ฌํ•ญ์„ ๋ฐ˜์˜ -2. ๊ฐ€๋Šฅํ•œ '๊ถŒ์žฅ ๊ฐœ์„ ' ์‚ฌํ•ญ๋„ ํฌํ•จ -3. ์›๋ณธ์˜ ๊ฐ•์ ์€ ์œ ์ง€ -4. ๋ถ„๋Ÿ‰ {MIN_WORDS_PER_PART}๋‹จ์–ด ์ด์ƒ ์œ ์ง€ -5. ์ž‘๊ฐ€๋กœ์„œ์˜ ์ผ๊ด€๋œ ๋ชฉ์†Œ๋ฆฌ ์œ ์ง€ -6. ๋ฌธํ•™์  ์ˆ˜์ค€์„ ํ•œ ๋‹จ๊ณ„ ๋†’์ด๊ธฐ - -์ˆ˜์ •๋ณธ๋งŒ ์ œ์‹œํ•˜์„ธ์š”. ์„ค๋ช…์€ ๋ถˆํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.""", - - "English": f"""Revise Part {part_number} according to critique. - -**Original:** -{original_content} - -**Critique Feedback:** -{critic_feedback} - -**Revision Guidelines:** -1. Reflect all 'Required fixes' -2. Include 'Recommended improvements' where possible -3. Maintain original strengths -4. Keep length {MIN_WORDS_PER_PART}+ words -5. Maintain consistent authorial voice -6. Elevate literary level - -Present only the revision. No explanation needed.""" - } - - return lang_prompts.get(language, lang_prompts["Korean"]) - - def create_final_critic_prompt(self, complete_novel: str, word_count: int, - story_bible: StoryBible, language: str) -> str: - """Final comprehensive evaluation""" - - lang_prompts = { - "Korean": f"""์™„์„ฑ๋œ ์†Œ์„ค์„ ์ข…ํ•ฉ ํ‰๊ฐ€ํ•˜์„ธ์š”. - -**์ž‘ํ’ˆ ์ •๋ณด:** -- ์ด ๋ถ„๋Ÿ‰: {word_count}๋‹จ์–ด -- ๋ชฉํ‘œ: 8,000๋‹จ์–ด +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. **์„œ์‚ฌ์  ํ†ตํ•ฉ์„ฑ (30์ )** - - 10๊ฐœ ํŒŒํŠธ๊ฐ€ ํ•˜๋‚˜์˜ ์ด์•ผ๊ธฐ๋กœ ํ†ตํ•ฉ๋˜์—ˆ๋Š”๊ฐ€? - - ์ธ๊ณผ๊ด€๊ณ„๊ฐ€ ๋ช…ํ™•ํ•˜๊ณ  ํ•„์—ฐ์ ์ธ๊ฐ€? - - ๋ฐ˜๋ณต์ด๋‚˜ ์ˆœํ™˜ ์—†์ด ์ง„ํ–‰๋˜๋Š”๊ฐ€? +1. **์ƒ์—…์„ฑ (25์ )** + - ์‹œ์žฅ์„ฑ + - ํƒ€๊ฒŸ ๊ด€๊ฐ ์–ดํ•„ + - ์ œ์ž‘ ๊ฐ€๋Šฅ์„ฑ + - ๋ฐฐ๊ธ‰ ์ž ์žฌ๋ ฅ -2. **์บ๋ฆญํ„ฐ ์•„ํฌ (25์ )** - - ์ฃผ์ธ๊ณต์˜ ๋ณ€ํ™”๊ฐ€ ์„ค๋“๋ ฅ ์žˆ๋Š”๊ฐ€? - - ๋ณ€ํ™”๊ฐ€ ์ ์ง„์ ์ด๊ณ  ์ž์—ฐ์Šค๋Ÿฌ์šด๊ฐ€? - - ์ตœ์ข… ์ƒํƒœ๊ฐ€ ์ดˆ๊ธฐ์™€ ๋ช…ํ™•ํžˆ ๋‹ค๋ฅธ๊ฐ€? +2. **์Šคํ† ๋ฆฌ (25์ )** + - 3๋ง‰ ๊ตฌ์กฐ ํšจ๊ณผ์„ฑ + - ์บ๋ฆญํ„ฐ ์•„ํฌ + - ํ”Œ๋กฏ ์ผ๊ด€์„ฑ + - ํ…Œ๋งˆ ์ „๋‹ฌ -3. **๋ฌธํ•™์  ์„ฑ์ทจ (25์ )** - - ์ฃผ์ œ๊ฐ€ ๊นŠ์ด ์žˆ๊ฒŒ ํƒ๊ตฌ๋˜์—ˆ๋Š”๊ฐ€? - - ์ƒ์ง•์ด ํšจ๊ณผ์ ์œผ๋กœ ํ™œ์šฉ๋˜์—ˆ๋Š”๊ฐ€? - - ๋ฌธ์ฒด๊ฐ€ ์ผ๊ด€๋˜๊ณ  ์•„๋ฆ„๋‹ค์šด๊ฐ€? - - ํ˜„๋Œ€์  ์ฒ ํ•™๊ณผ ์‚ฌํšŒ์  ๋ฉ”์‹œ์ง€๊ฐ€ ๋…น์•„์žˆ๋Š”๊ฐ€? +3. **๊ธฐ์ˆ ์  ์™„์„ฑ๋„ (25์ )** + - ํฌ๋งท ์ •ํ™•์„ฑ + - ํŽ˜์ด์ง€ ์ˆ˜ ์ ์ •์„ฑ + - ์”ฌ ๊ตฌ์„ฑ + - ์‹œ๊ฐ์  ์Šคํ† ๋ฆฌํ…”๋ง -4. **๊ธฐ์ˆ ์  ์™„์„ฑ๋„ (20์ )** - - ๋ชฉํ‘œ ๋ถ„๋Ÿ‰์„ ๋‹ฌ์„ฑํ–ˆ๋Š”๊ฐ€? - - ๊ฐ ํŒŒํŠธ๊ฐ€ ๊ท ํ˜• ์žˆ๊ฒŒ ์ „๊ฐœ๋˜์—ˆ๋Š”๊ฐ€? - - ๋ฌธ๋ฒ•๊ณผ ํ‘œํ˜„์ด ์ •ํ™•ํ•œ๊ฐ€? +4. **๋Œ€ํ™” & ์บ๋ฆญํ„ฐ (25์ )** + - ๋Œ€ํ™” ์ž์—ฐ์Šค๋Ÿฌ์›€ + - ์บ๋ฆญํ„ฐ ๊ณ ์œ ์„ฑ + - ๊ด€๊ณ„ ์—ญํ•™ + - ๊ฐ์ •์  ์ง„์ •์„ฑ -**์ด์ : /100์ ** +**์ข…ํ•ฉ ํ‰๊ฐ€:** +- ๊ฐ•์  (3-5๊ฐœ) +- ๊ฐœ์„  ํ•„์š” ์‚ฌํ•ญ (3-5๊ฐœ) +- ์‹œ์žฅ ์ž ์žฌ๋ ฅ +- ์ถ”์ฒœ ์‚ฌํ•ญ -๊ตฌ์ฒด์ ์ธ ๊ฐ•์ ๊ณผ ์•ฝ์ ์„ ์ œ์‹œํ•˜์„ธ์š”.""", +**๋“ฑ๊ธ‰:** A+ ~ F - "English": f"""Comprehensively evaluate the completed novel. +๊ตฌ์ฒด์ ์ด๊ณ  ๊ฑด์„ค์ ์ธ ํ”ผ๋“œ๋ฐฑ์„ ์ œ๊ณตํ•˜์„ธ์š”.""", -**Work Info:** -- Total length: {word_count} words -- Target: 8,000 words + "English": f"""You are the final reviewer. Comprehensively evaluate the completed {screenplay_type} screenplay. **Evaluation Criteria:** -1. **Narrative Integration (30 points)** - - Are 10 parts integrated into one story? - - Clear and inevitable causality? - - Progress without repetition or cycles? - -2. **Character Arc (25 points)** - - Convincing protagonist transformation? - - Gradual and natural changes? - - Final state clearly different from initial? - -3. **Literary Achievement (25 points)** - - Theme explored with depth? - - Symbols used effectively? - - Consistent and beautiful style? - - Contemporary philosophy and social message integrated? - -4. **Technical Completion (20 points)** - - Target length achieved? - - Each part balanced in development? - - Grammar and expression accurate? - -**Total Score: /100 points** - -Present specific strengths and weaknesses.""" - } - - return lang_prompts.get(language, lang_prompts["Korean"]) - - def create_director_final_prompt(self, initial_plan: str, critic_feedback: str, - user_query: str, language: str) -> str: - """Director final master plan""" - return f"""Reflect the critique and complete the final master plan. - -**Original Theme:** {user_query} - -**Initial Plan:** -{initial_plan} - -**Critique Feedback:** -{critic_feedback} - -**Final Master Plan Requirements:** -1. Reflect all critique points -2. Specific content and causality for 10 parts -3. Clear transformation stages of protagonist -4. Meaning evolution process of central symbol -5. Feasibility of 800 words per part -6. Implementation of philosophical depth and social message - -Present concrete and executable final plan.""" - - def _extract_part_plan(self, master_plan: str, part_number: int) -> str: - """Extract specific part plan from master plan""" - lines = master_plan.split('\n') - part_section = [] - capturing = False - - for line in lines: - if f"Part {part_number}:" in line or f"ํŒŒํŠธ {part_number}:" in line: - capturing = True - elif capturing and (f"Part {part_number+1}:" in line or f"ํŒŒํŠธ {part_number+1}:" in line): - break - elif capturing: - part_section.append(line) - - return '\n'.join(part_section) if part_section else "Cannot find the part plan." - - # --- 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: - system_prompts = self.get_system_prompts(language) - full_messages = [{"role": "system", "content": system_prompts.get(role, "")}, *messages] - - max_tokens = 15000 if role == "writer" else 10000 - - payload = { - "model": self.model_id, - "messages": full_messages, - "max_tokens": max_tokens, - "temperature": 0.8, - "top_p": 0.95, - "presence_penalty": 0.5, - "frequency_penalty": 0.3, - "stream": True - } - - response = requests.post( - self.api_url, - headers=self.create_headers(), - json=payload, - stream=True, - timeout=180 - ) - - if response.status_code != 200: - yield f"โŒ API Error (Status Code: {response.status_code})" - return - - buffer = "" - for line in response.iter_lines(): - if not line: - continue - - try: - line_str = line.decode('utf-8').strip() - if not line_str.startswith("data: "): - continue - - data_str = line_str[6:] - if data_str == "[DONE]": - break - - data = json.loads(data_str) - choices = data.get("choices", []) - if choices and choices[0].get("delta", {}).get("content"): - content = choices[0]["delta"]["content"] - buffer += content - - if len(buffer) >= 50 or '\n' in buffer: - yield buffer - buffer = "" - time.sleep(0.01) - - except Exception as e: - logger.error(f"Chunk processing error: {str(e)}") - continue - - if buffer: - yield buffer - - except Exception as e: - logger.error(f"Streaming error: {type(e).__name__}: {str(e)}") - yield f"โŒ Error occurred: {str(e)}" - - def get_system_prompts(self, language: str) -> Dict[str, str]: - """Role-specific system prompts - Enhanced version""" - - base_prompts = { - "Korean": { - "director": """๋‹น์‹ ์€ ํ˜„๋Œ€ ์„ธ๊ณ„๋ฌธํ•™์˜ ์ •์ ์„ ์ง€ํ–ฅํ•˜๋Š” ์ž‘ํ’ˆ์„ ์„ค๊ณ„ํ•ฉ๋‹ˆ๋‹ค. -๊นŠ์€ ์ฒ ํ•™์  ํ†ต์ฐฐ๊ณผ ๋‚ ์นด๋กœ์šด ์‚ฌํšŒ ๋น„ํŒ์„ ๊ฒฐํ•ฉํ•˜์„ธ์š”. -์ธ๊ฐ„ ์กฐ๊ฑด์˜ ๋ณต์žก์„ฑ์„ 10๊ฐœ์˜ ์œ ๊ธฐ์  ํŒŒํŠธ๋กœ ๊ตฌํ˜„ํ•˜์„ธ์š”. -๋…์ž์˜ ์˜ํ˜ผ์„ ๋’คํ”๋“ค ๊ฐ•๋ ฌํ•œ ์ฒซ๋ฌธ์žฅ๋ถ€ํ„ฐ ์‹œ์ž‘ํ•˜์„ธ์š”.""", - - "critic_director": """์„œ์‚ฌ ๊ตฌ์กฐ์˜ ๋…ผ๋ฆฌ์„ฑ๊ณผ ์‹คํ˜„ ๊ฐ€๋Šฅ์„ฑ์„ ๊ฒ€์ฆํ•˜๋Š” ์ „๋ฌธ๊ฐ€์ž…๋‹ˆ๋‹ค. -์ธ๊ณผ๊ด€๊ณ„์˜ ํ—ˆ์ ์„ ์ฐพ์•„๋‚ด์„ธ์š”. -์บ๋ฆญํ„ฐ ๋ฐœ์ „์˜ ์‹ ๋น™์„ฑ์„ ํ‰๊ฐ€ํ•˜์„ธ์š”. -์ฒ ํ•™์  ๊นŠ์ด์™€ ๋ฌธํ•™์  ๊ฐ€์น˜๋ฅผ ํŒ๋‹จํ•˜์„ธ์š”. -8,000๋‹จ์–ด ๋ถ„๋Ÿ‰์˜ ์ ์ ˆ์„ฑ์„ ํŒ๋‹จํ•˜์„ธ์š”.""", - - "writer": """๋‹น์‹ ์€ ์–ธ์–ด์˜ ์—ฐ๊ธˆ์ˆ ์‚ฌ์ž…๋‹ˆ๋‹ค. -์ผ์ƒ์–ด๋ฅผ ์‹œ๋กœ, ๊ตฌ์ฒด๋ฅผ ์ถ”์ƒ์œผ๋กœ, ๊ฐœ์ธ์„ ๋ณดํŽธ์œผ๋กœ ๋ณ€ํ™˜ํ•˜์„ธ์š”. -ํ˜„๋Œ€์ธ์˜ ์˜ํ˜ผ์˜ ์–ด๋‘ ๊ณผ ๋น›์„ ๋™์‹œ์— ํฌ์ฐฉํ•˜์„ธ์š”. -๋…์ž๊ฐ€ ์ž์‹ ์„ ์žฌ๋ฐœ๊ฒฌํ•˜๊ฒŒ ๋งŒ๋“œ๋Š” ๊ฑฐ์šธ์ด ๋˜์„ธ์š”.""", - - "critic_final": """๋‹น์‹ ์€ ์ž‘ํ’ˆ์˜ ๋ฌธํ•™์  ์ž ์žฌ๋ ฅ์„ ๊ทน๋Œ€ํ™”ํ•˜๋Š” ์กฐ๋ ฅ์ž์ž…๋‹ˆ๋‹ค. -ํ‰๋ฒ”ํ•จ์„ ๋น„๋ฒ”ํ•จ์œผ๋กœ ์ด๋„๋Š” ๋‚ ์นด๋กœ์šด ํ†ต์ฐฐ์„ ์ œ๊ณตํ•˜์„ธ์š”. -์ž‘๊ฐ€์˜ ๋ฌด์˜์‹์— ์ž ๋“  ๋ณด์„์„ ๋ฐœ๊ตดํ•˜์„ธ์š”. -ํƒ€ํ˜‘ ์—†๋Š” ๊ธฐ์ค€์œผ๋กœ ์ตœ๊ณ ๋ฅผ ์š”๊ตฌํ•˜์„ธ์š”.""" - }, - "English": { - "director": """You design works aiming for the pinnacle of contemporary world literature. -Combine deep philosophical insights with sharp social criticism. -Implement the complexity of the human condition in 10 organic parts. -Start with an intense opening sentence that shakes the reader's soul.""", - - "critic_director": """You are an expert verifying narrative logic and feasibility. -Find gaps in causality. -Evaluate credibility of character development. -Judge philosophical depth and literary value. -Judge appropriateness of 8,000-word length.""", - - "writer": """You are an alchemist of language. -Transform everyday language into poetry, concrete into abstract, individual into universal. -Capture both darkness and light of the modern soul. -Become a mirror where readers rediscover themselves.""", - - "critic_final": """You are a collaborator maximizing the work's literary potential. -Provide sharp insights leading ordinariness to extraordinariness. -Excavate gems sleeping in the writer's unconscious. -Demand the best with uncompromising standards.""" - } - } - - prompts = base_prompts.get(language, base_prompts["Korean"]).copy() - - # Add part-specific critic prompts - for i in range(1, 11): - prompts[f"critic_part{i}"] = f"""You are Part {i} dedicated critic. -Review causality with previous parts as top priority. -Verify character consistency and development. -Evaluate alignment with master plan. -Assess literary level and philosophical depth. -Provide specific and actionable revision instructions.""" - - return prompts - - # --- Main process --- - def process_novel_stream(self, query: str, language: str, - session_id: Optional[str] = None) -> Generator[Tuple[str, List[Dict[str, Any]], str], None, None]: - """Single writer novel generation process""" - try: - resume_from_stage = 0 - if session_id: - self.current_session_id = session_id - session = NovelDatabase.get_session(session_id) - if session: - query = session['user_query'] - language = session['language'] - resume_from_stage = session['current_stage'] + 1 - saved_tracker = NovelDatabase.load_narrative_tracker(session_id) - if saved_tracker: - self.narrative_tracker = saved_tracker - else: - self.current_session_id = NovelDatabase.create_session(query, language) - logger.info(f"Created new session: {self.current_session_id}") - - stages = [] - if resume_from_stage > 0: - stages = [{ - "name": s['stage_name'], - "status": s['status'], - "content": s.get('content', ''), - "word_count": s.get('word_count', 0), - "momentum": s.get('narrative_momentum', 0.0) - } for s in NovelDatabase.get_stages(self.current_session_id)] - - total_words = NovelDatabase.get_total_words(self.current_session_id) - - for stage_idx in range(resume_from_stage, len(UNIFIED_STAGES)): - role, stage_name = UNIFIED_STAGES[stage_idx] - if stage_idx >= len(stages): - stages.append({ - "name": stage_name, - "status": "active", - "content": "", - "word_count": 0, - "momentum": 0.0 - }) - else: - stages[stage_idx]["status"] = "active" - - yield f"๐Ÿ”„ Processing... (Current {total_words:,} words)", stages, self.current_session_id - - prompt = self.get_stage_prompt(stage_idx, role, query, 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 - stages[stage_idx]["word_count"] = len(stage_content.split()) - yield f"๐Ÿ”„ {stage_name} writing... ({total_words + stages[stage_idx]['word_count']:,} words)", stages, self.current_session_id - - # Content processing and tracking - if role == "writer": - # Calculate part number - part_num = self._get_part_number(stage_idx) - if part_num: - self.narrative_tracker.accumulated_content.append(stage_content) - self.narrative_tracker.word_count_by_part[part_num] = len(stage_content.split()) - - # Calculate narrative momentum - momentum = self.narrative_tracker.calculate_narrative_momentum(part_num, stage_content) - stages[stage_idx]["momentum"] = momentum - - # Update story bible - self._update_story_bible_from_content(stage_content, part_num) - - stages[stage_idx]["status"] = "complete" - NovelDatabase.save_stage( - self.current_session_id, stage_idx, stage_name, role, - stage_content, "complete", stages[stage_idx].get("momentum", 0.0) - ) - - NovelDatabase.save_narrative_tracker(self.current_session_id, self.narrative_tracker) - total_words = NovelDatabase.get_total_words(self.current_session_id) - yield f"โœ… {stage_name} completed (Total {total_words:,} words)", stages, self.current_session_id - - # Final processing - final_novel = NovelDatabase.get_writer_content(self.current_session_id) - final_word_count = len(final_novel.split()) - final_report = self.generate_literary_report(final_novel, final_word_count, language) - - NovelDatabase.update_final_novel(self.current_session_id, final_novel, final_report) - yield f"โœ… Novel completed! Total {final_word_count:,} words", stages, self.current_session_id - - except Exception as e: - logger.error(f"Novel generation process 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, - language: str, stages: List[Dict]) -> str: - """Generate stage-specific prompt""" - if stage_idx == 0: # Director initial planning - return self.create_director_initial_prompt(query, language) - - if stage_idx == 1: # Director plan review - return self.create_critic_director_prompt(stages[0]["content"], query, language) - - if stage_idx == 2: # Director final master plan - return self.create_director_final_prompt(stages[0]["content"], stages[1]["content"], query, language) - - master_plan = stages[2]["content"] - - # Writer part writing - if role == "writer" and "Revision" not in stages[stage_idx]["name"]: - part_num = self._get_part_number(stage_idx) - accumulated = '\n\n'.join(self.narrative_tracker.accumulated_content) - return self.create_writer_prompt(part_num, master_plan, accumulated, - self.narrative_tracker.story_bible, language) - - # Part-specific critique - if role.startswith("critic_part"): - part_num = int(role.replace("critic_part", "")) - # Find writer content for this part - writer_content = stages[stage_idx-1]["content"] - accumulated = '\n\n'.join(self.narrative_tracker.accumulated_content[:-1]) - return self.create_part_critic_prompt(part_num, writer_content, master_plan, - accumulated, self.narrative_tracker.story_bible, language) - - # Writer revision - if role == "writer" and "Revision" in stages[stage_idx]["name"]: - part_num = self._get_part_number(stage_idx) - original_content = stages[stage_idx-2]["content"] # Original - critic_feedback = stages[stage_idx-1]["content"] # Critique - return self.create_writer_revision_prompt(part_num, original_content, - critic_feedback, language) - - # Final critique - if role == "critic_final": - complete_novel = NovelDatabase.get_writer_content(self.current_session_id) - word_count = len(complete_novel.split()) - return self.create_final_critic_prompt(complete_novel, word_count, - self.narrative_tracker.story_bible, language) - - return "" - - def _get_part_number(self, stage_idx: int) -> Optional[int]: - """Extract part number from stage index""" - stage_name = UNIFIED_STAGES[stage_idx][1] - match = re.search(r'Part (\d+)', stage_name) - if match: - return int(match.group(1)) - return None - - def _update_story_bible_from_content(self, content: str, part_num: int): - """Auto-update story bible from content""" - # Simple keyword-based extraction (more sophisticated NLP needed in reality) - lines = content.split('\n') - - # Extract character names (words starting with capital letters) - for line in lines: - words = line.split() - for word in words: - if word and word[0].isupper() and len(word) > 1: - if word not in self.narrative_tracker.story_bible.characters: - self.narrative_tracker.story_bible.characters[word] = { - "first_appearance": part_num, - "traits": [] - } - - def generate_literary_report(self, complete_novel: str, word_count: int, language: str) -> str: - """Generate final literary evaluation report""" - prompt = self.create_final_critic_prompt(complete_novel, word_count, - self.narrative_tracker.story_bible, language) - try: - report = self.call_llm_sync([{"role": "user", "content": prompt}], - "critic_final", language) - return report - except Exception as e: - logger.error(f"Final report generation failed: {e}") - return "Error occurred during report generation" +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 _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: + system_prompts = self.get_system_prompts(language) + full_messages = [{"role": "system", "content": system_prompts.get(role, "")}, *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 + } + + response = requests.post( + self.api_url, + headers=self.create_headers(), + json=payload, + stream=True, + timeout=180 + ) + + if response.status_code != 200: + yield f"โŒ API Error (Status Code: {response.status_code})" + return + + buffer = "" + for line in response.iter_lines(): + if not line: + continue + + try: + line_str = line.decode('utf-8').strip() + if not line_str.startswith("data: "): + continue + + data_str = line_str[6:] + if data_str == "[DONE]": + break + + data = json.loads(data_str) + choices = data.get("choices", []) + if choices and choices[0].get("delta", {}).get("content"): + content = choices[0]["delta"]["content"] + buffer += content + + if len(buffer) >= 50 or '\n' in buffer: + yield buffer + buffer = "" + time.sleep(0.01) + + except Exception as e: + logger.error(f"Chunk processing error: {str(e)}") + continue + + if buffer: + yield buffer + + except Exception as e: + logger.error(f"Streaming error: {type(e).__name__}: {str(e)}") + yield f"โŒ Error occurred: {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: + stages = [{ + "name": s['stage_name'], + "status": s['status'], + "content": s.get('content', ''), + "page_count": s.get('page_count', 0) + } for s in ScreenplayDatabase.get_stages(self.current_session_id)] + + 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 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_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 _process_producer_content(self, content: str): + """Process producer output""" + # Extract title and logline + title_match = re.search(r'(?:TITLE|์ œ๋ชฉ):\s*(.+)', content) + logline_match = re.search(r'(?:LOGLINE|๋กœ๊ทธ๋ผ์ธ):\s*(.+)', content) + + if title_match: + self.screenplay_tracker.screenplay_bible.title = title_match.group(1).strip() + if logline_match: + self.screenplay_tracker.screenplay_bible.logline = logline_match.group(1).strip() + + # Save to database + ScreenplayDatabase.save_screenplay_bible(self.current_session_id, + self.screenplay_tracker.screenplay_bible) + + def _process_story_content(self, content: str): + """Process story developer output""" + # Extract three-act structure + self.screenplay_tracker.screenplay_bible.three_act_structure = { + "act1": self._extract_section(content, "ACT 1|์ œ1๋ง‰"), + "act2a": self._extract_section(content, "ACT 2A|์ œ2๋ง‰A"), + "act2b": self._extract_section(content, "ACT 2B|์ œ2๋ง‰B"), + "act3": self._extract_section(content, "ACT 3|์ œ3๋ง‰") + } + + ScreenplayDatabase.save_screenplay_bible(self.current_session_id, + self.screenplay_tracker.screenplay_bible) + + def _process_character_content(self, content: str): + """Process character designer output""" + # Extract protagonist + protagonist_section = self._extract_section(content, "PROTAGONIST|์ฃผ์ธ๊ณต") + if protagonist_section: + protagonist = self._parse_character_profile(protagonist_section, "protagonist") + self.screenplay_tracker.add_character(protagonist) + ScreenplayDatabase.save_character(self.current_session_id, protagonist) + + # Extract antagonist + antagonist_section = self._extract_section(content, "ANTAGONIST|์ ๋Œ€์ž") + if antagonist_section: + antagonist = self._parse_character_profile(antagonist_section, "antagonist") + self.screenplay_tracker.add_character(antagonist) + ScreenplayDatabase.save_character(self.current_session_id, antagonist) + + def _process_scene_content(self, content: str): + """Process scene planner output""" + # Parse scene breakdown + scene_pattern = r'(?:Scene|์”ฌ)\s*(\d+).*?(?:INT\.|EXT\.)\s*(.+?)\s*-\s*(\w+)' + scenes = re.finditer(scene_pattern, content, re.IGNORECASE | re.MULTILINE) + + for match in scenes: + scene_num = int(match.group(1)) + location = match.group(2).strip() + time_of_day = match.group(3).strip() + + # Determine act based on scene number + act = 1 if scene_num <= 12 else 2 if scene_num <= 35 else 3 + + scene = SceneBreakdown( + scene_number=scene_num, + act=act, + location=location, + time_of_day=time_of_day, + characters=[], # Would be extracted from content + purpose="", # Would be extracted from content + conflict="", # Would be extracted from content + page_count=1.5 # Default estimate + ) + + self.screenplay_tracker.add_scene(scene) + ScreenplayDatabase.save_scene(self.current_session_id, scene) + + def _extract_section(self, content: str, section_pattern: str) -> str: + """Extract section from content""" + pattern = rf'(?:{section_pattern})[:\s]*(.+?)(?=\n(?:[A-Z]{{2,}}|[๊ฐ€-ํžฃ]{{2,}}):|\Z)' + match = re.search(pattern, content, re.IGNORECASE | re.DOTALL) + return match.group(1).strip() if match else "" + + def _parse_character_profile(self, content: str, role: str) -> CharacterProfile: + """Parse character profile from content""" + # Extract character details using regex or string parsing + name = self._extract_field(content, "Name|์ด๋ฆ„") or f"Character_{role}" + age = int(self._extract_field(content, "Age|๋‚˜์ด") or "30") + + return CharacterProfile( + name=name, + age=age, + role=role, + archetype=self._extract_field(content, "Archetype|์•„ํฌํƒ€์ž…") or "", + want=self._extract_field(content, "WANT|์™ธ์  ๋ชฉํ‘œ") or "", + need=self._extract_field(content, "NEED|๋‚ด์  ํ•„์š”") or "", + backstory=self._extract_field(content, "Backstory|๋ฐฑ์Šคํ† ๋ฆฌ") or "", + personality=[], # Would be parsed from content + speech_pattern=self._extract_field(content, "Speech|๋งํˆฌ") or "", + character_arc=self._extract_field(content, "Arc|์•„ํฌ") or "" + ) + + def _extract_field(self, content: str, field_pattern: str) -> Optional[str]: + """Extract field value from content""" + pattern = rf'(?:{field_pattern})[:\s]*(.+?)(?=\n|$)' + match = re.search(pattern, content, re.IGNORECASE) + return match.group(1).strip() if match else None -class WebSearchIntegration: - """Web search functionality""" - 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 HFDatasetManager: - """Manage theme data storage in HuggingFace dataset""" - - def __init__(self): - self.token = os.getenv("HF_TOKEN") - self.dataset_name = "novel-themes-library" - self.username = None - self.repo_id = None - - if self.token: - try: - self.api = HfApi() - # Get username from token - self.username = self.api.whoami(token=self.token)["name"] - self.repo_id = f"{self.username}/{self.dataset_name}" - - # Create dataset repo if it doesn't exist - try: - self.api.create_repo( - repo_id=self.repo_id, - token=self.token, - repo_type="dataset", - private=False, - exist_ok=True - ) - logger.info(f"HF Dataset initialized: {self.repo_id}") - except Exception as e: - logger.error(f"Error creating HF dataset: {e}") - - except Exception as e: - logger.error(f"HF authentication failed: {e}") - self.token = None - - def save_themes_to_hf(self, themes_data: List[Dict]): - """Save themes to HuggingFace dataset""" - if not self.token or not themes_data: - return False - - try: - # Create temporary file - with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as tmp_file: - json.dump({ - "themes": themes_data, - "last_updated": datetime.now().isoformat(), - "version": "1.0" - }, tmp_file, ensure_ascii=False, indent=2) - tmp_path = tmp_file.name - - # Upload to HF - upload_file( - path_or_fileobj=tmp_path, - path_in_repo="themes_library.json", - repo_id=self.repo_id, - token=self.token, - repo_type="dataset", - commit_message=f"Update themes library - {len(themes_data)} themes" - ) - - # Clean up - os.unlink(tmp_path) - logger.info(f"Saved {len(themes_data)} themes to HF dataset") - return True - - except Exception as e: - logger.error(f"Error saving to HF dataset: {e}") - return False - - def load_themes_from_hf(self) -> List[Dict]: - """Load themes from HuggingFace dataset""" - if not self.token: - return [] - - try: - # Download file from HF - file_path = hf_hub_download( - repo_id=self.repo_id, - filename="themes_library.json", - token=self.token, - repo_type="dataset" - ) - - # Load data - with open(file_path, 'r', encoding='utf-8') as f: - data = json.load(f) - - themes = data.get("themes", []) - logger.info(f"Loaded {len(themes)} themes from HF dataset") - return themes - - except Exception as e: - logger.warning(f"Error loading from HF dataset: {e}") - return [] - - - - def sync_with_local_db(self): - """Sync HF dataset with local database""" - if not self.token: - return - - # Load from HF - hf_themes = self.load_themes_from_hf() - - if hf_themes: - # Get existing theme IDs from local DB - local_theme_ids = set() - with NovelDatabase.get_db() as conn: - rows = conn.cursor().execute( - "SELECT theme_id FROM random_themes_library" - ).fetchall() - local_theme_ids = {row['theme_id'] for row in rows} - - # Add new themes from HF to local DB - new_count = 0 - for theme in hf_themes: - if theme.get('theme_id') not in local_theme_ids: - try: - # Ensure tags and metadata are JSON strings - tags_data = theme.get('tags', []) - if isinstance(tags_data, list): - tags_json = json.dumps(tags_data, ensure_ascii=False) - else: - tags_json = tags_data if isinstance(tags_data, str) else '[]' - - metadata_data = theme.get('metadata', {}) - if isinstance(metadata_data, dict): - metadata_json = json.dumps(metadata_data, ensure_ascii=False) - else: - metadata_json = metadata_data if isinstance(metadata_data, str) else '{}' - - with NovelDatabase.get_db() as conn: - conn.cursor().execute(''' - INSERT INTO random_themes_library - (theme_id, theme_text, language, title, opening_sentence, - protagonist, conflict, philosophical_question, generated_at, - view_count, used_count, tags, metadata) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - ''', ( - theme.get('theme_id'), - theme.get('theme_text'), - theme.get('language'), - theme.get('title', ''), - theme.get('opening_sentence', ''), - theme.get('protagonist', ''), - theme.get('conflict', ''), - theme.get('philosophical_question', ''), - theme.get('generated_at'), - theme.get('view_count', 0), - theme.get('used_count', 0), - tags_json, - metadata_json - )) - conn.commit() - new_count += 1 - except Exception as e: - logger.error(f"Error adding theme {theme.get('theme_id')}: {e}") - - if new_count > 0: - logger.info(f"Added {new_count} new themes from HF dataset") - - def backup_to_hf(self): - """Backup all local themes to HF dataset""" - if not self.token: - return - - # Get all themes from local DB - themes = NovelDatabase.get_random_themes_library(limit=1000) - - if themes: - # Convert Row objects to dicts and ensure all data is serializable - themes_data = [] - for theme in themes: - theme_dict = dict(theme) - # Parse tags and metadata from JSON strings - if isinstance(theme_dict.get('tags'), str): - try: - theme_dict['tags'] = json.loads(theme_dict['tags']) - except: - theme_dict['tags'] = [] - else: - theme_dict['tags'] = theme_dict.get('tags', []) - - if isinstance(theme_dict.get('metadata'), str): - try: - theme_dict['metadata'] = json.loads(theme_dict['metadata']) - except: - theme_dict['metadata'] = {} - else: - theme_dict['metadata'] = theme_dict.get('metadata', {}) - - themes_data.append(theme_dict) - - self.save_themes_to_hf(themes_data) - - # --- Utility functions --- -def process_query(query: str, language: str, session_id: Optional[str] = None) -> Generator[Tuple[str, str, str, str, str], None, None]: - """Main query processing function""" - if not query.strip(): - yield "", "", "โŒ Please enter a theme.", session_id, "" - return - - system = UnifiedLiterarySystem() - stages_markdown = "" - novel_content = "" - novel_text = "" # ์‹ค์ œ ํ…์ŠคํŠธ ์ €์žฅ์šฉ - - for status, stages, current_session_id in system.process_novel_stream(query, language, session_id): - stages_markdown = format_stages_display(stages) - - # Get final novel content - if stages and all(s.get("status") == "complete" for s in stages[-10:]): - novel_text = NovelDatabase.get_writer_content(current_session_id) # ์›๋ณธ ํ…์ŠคํŠธ - novel_content = format_novel_display(novel_text) # ํฌ๋งท๋œ ๋””์Šคํ”Œ๋ ˆ์ด - - yield stages_markdown, novel_content, status or "๐Ÿ”„ Processing...", current_session_id, novel_text - - -def get_active_sessions(language: str) -> List[str]: - """Get active session list""" - sessions = NovelDatabase.get_active_sessions() - return [f"{s['session_id'][:8]}... - {s['user_query'][:50]}... ({s['created_at']}) [{s['total_words']:,} words]" - for s in sessions] - -def auto_recover_session(language: str) -> Tuple[Optional[str], str]: - """Auto-recover recent session""" - sessions = NovelDatabase.get_active_sessions() - if sessions: - latest_session = sessions[0] - return latest_session['session_id'], f"Session {latest_session['session_id'][:8]}... recovered" - return None, "No session to recover." - -def resume_session(session_id: str, language: str) -> Generator[Tuple[str, str, str, str, str], None, None]: - """Resume session""" - if not session_id: - yield "", "", "โŒ No session ID.", session_id, "" - return - - if "..." in session_id: - session_id = session_id.split("...")[0] - - session = NovelDatabase.get_session(session_id) - if not session: - yield "", "", "โŒ Session not found.", None, "" - return - - yield from process_query(session['user_query'], session['language'], session_id) +def generate_random_screenplay_theme(screenplay_type: str, genre: str, language: str) -> str: + """Generate random screenplay theme""" + try: + # Load themes data + themes_data = load_screenplay_themes_data() + + # Select random elements + import secrets + situation = secrets.choice(themes_data['situations'].get(genre, themes_data['situations']['drama'])) + protagonist = secrets.choice(themes_data['protagonists'].get(genre, themes_data['protagonists']['drama'])) + conflict = secrets.choice(themes_data['conflicts'].get(genre, themes_data['conflicts']['drama'])) + + # 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}] + generated_theme = system.call_llm_sync(messages, "producer", language) + + # Extract metadata + metadata = { + 'title': extract_title_from_theme(generated_theme), + 'logline': extract_logline_from_theme(generated_theme), + 'protagonist': protagonist, + 'conflict': conflict, + 'tags': [genre, screenplay_type] + } + + # Save to database + theme_id = ScreenplayDatabase.save_random_theme( + generated_theme, screenplay_type, genre, language, metadata + ) + + return generated_theme + + except Exception as e: + logger.error(f"Theme generation error: {str(e)}") + return f"Error generating theme: {str(e)}" + +def load_screenplay_themes_data() -> Dict: + """Load screenplay themes data""" + return { + 'situations': { + 'action': ['hostage crisis', 'heist gone wrong', 'revenge mission', 'race against time'], + 'thriller': ['false accusation', 'witness protection', 'conspiracy uncovered', 'identity theft'], + 'drama': ['family reunion', 'terminal diagnosis', 'divorce proceedings', 'career crossroads'], + 'comedy': ['mistaken identity', 'wedding disaster', 'workplace chaos', 'odd couple roommates'], + 'horror': ['isolated location', 'ancient curse', 'home invasion', 'supernatural investigation'], + 'sci-fi': ['first contact', 'time loop', 'AI awakening', 'space colony crisis'], + 'romance': ['second chance', 'enemies to lovers', 'long distance', 'forbidden love'] + }, + 'protagonists': { + 'action': ['ex-soldier', 'undercover cop', 'skilled thief', 'reluctant hero'], + 'thriller': ['investigative journalist', 'wrongly accused person', 'FBI agent', 'whistleblower'], + 'drama': ['single parent', 'recovering addict', 'immigrant', 'caregiver'], + 'comedy': ['uptight professional', 'slacker', 'fish out of water', 'eccentric artist'], + 'horror': ['skeptical scientist', 'final girl', 'paranormal investigator', 'grieving parent'], + 'sci-fi': ['astronaut', 'AI researcher', 'time traveler', 'colony leader'], + 'romance': ['workaholic', 'hopeless romantic', 'cynical divorce lawyer', 'small town newcomer'] + }, + 'conflicts': { + 'action': ['stop the villain', 'save the hostages', 'prevent disaster', 'survive pursuit'], + 'thriller': ['prove innocence', 'expose truth', 'stay alive', 'protect loved ones'], + 'drama': ['reconcile past', 'find purpose', 'heal relationships', 'accept change'], + 'comedy': ['save the business', 'win the competition', 'fool everyone', 'find love'], + 'horror': ['survive the night', 'break the curse', 'escape the monster', 'save the town'], + 'sci-fi': ['save humanity', 'prevent paradox', 'stop the invasion', 'preserve identity'], + 'romance': ['overcome differences', 'choose between options', 'trust again', 'follow heart'] + } + } + +def extract_title_from_theme(theme_text: str) -> str: + """Extract title from generated theme""" + match = re.search(r'\*\*(?:Title|์ œ๋ชฉ):\*\*\s*(.+)', theme_text, re.IGNORECASE) + return match.group(1).strip() if match else "" + +def extract_logline_from_theme(theme_text: str) -> str: + """Extract logline from generated theme""" + match = re.search(r'\*\*(?:Logline|๋กœ๊ทธ๋ผ์ธ):\*\*\s*(.+)', theme_text, re.IGNORECASE) + return match.group(1).strip() if match else "" + +def format_screenplay_display(screenplay_text: str) -> str: + """Format screenplay for display""" + if not screenplay_text: + return "No screenplay content yet." + + formatted = "# ๐ŸŽฌ Screenplay\n\n" + + # Format scene headings + formatted_text = re.sub( + r'^(INT\.|EXT\.)(.*?)$', + r'**\1\2**', + screenplay_text, + flags=re.MULTILINE + ) + + # Format character names (all caps on their own line) + formatted_text = re.sub( + r'^([A-Z][A-Z\s]+)$', + r'**\1**', + formatted_text, + flags=re.MULTILINE + ) + + # Add spacing for readability + lines = formatted_text.split('\n') + formatted_lines = [] + + for i, line in enumerate(lines): + formatted_lines.append(line) + # Add extra space after scene headings + if line.startswith('**INT.') or line.startswith('**EXT.'): + formatted_lines.append('') + + formatted += '\n'.join(formatted_lines) + + # Add page count + page_count = len(screenplay_text.split('\n')) / 55 + formatted = f"**Total Pages: {page_count:.1f}**\n\n" + formatted + + return formatted def format_stages_display(stages: List[Dict]) -> str: - """Stage progress display - For single writer system""" - markdown = "## ๐ŸŽฌ Progress Status\n\n" - - # Calculate total word count (writer stages only) - total_words = sum(s.get('word_count', 0) for s in stages - if s.get('name', '').startswith('โœ๏ธ Writer:') and 'Revision' in s.get('name', '')) - markdown += f"**Total Word Count: {total_words:,} / {TARGET_WORDS:,}**\n\n" - - # Progress summary - completed_parts = sum(1 for s in stages - if 'Revision' in s.get('name', '') and s.get('status') == 'complete') - markdown += f"**Completed Parts: {completed_parts} / 10**\n\n" - - # Average narrative momentum - momentum_scores = [s.get('momentum', 0) for s in stages if s.get('momentum', 0) > 0] - if momentum_scores: - avg_momentum = sum(momentum_scores) / len(momentum_scores) - markdown += f"**Average Narrative Momentum: {avg_momentum:.1f} / 10**\n\n" - - markdown += "---\n\n" - - # Display each stage - current_part = 0 - for i, stage in enumerate(stages): - status_icon = "โœ…" if stage['status'] == 'complete' else "๐Ÿ”„" if stage['status'] == 'active' else "โณ" - - # Add part divider - if 'Part' in stage.get('name', '') and 'Critic' not in stage.get('name', ''): - part_match = re.search(r'Part (\d+)', stage['name']) - if part_match: - new_part = int(part_match.group(1)) - if new_part != current_part: - current_part = new_part - markdown += f"\n### ๐Ÿ“š Part {current_part}\n\n" - - markdown += f"{status_icon} **{stage['name']}**" - - if stage.get('word_count', 0) > 0: - markdown += f" ({stage['word_count']:,} words)" - - if stage.get('momentum', 0) > 0: - markdown += f" [Momentum: {stage['momentum']:.1f}/10]" - - markdown += "\n" - - if stage['content'] and stage['status'] == 'complete': - # Adjust preview length by role - preview_length = 300 if 'writer' in stage.get('name', '').lower() else 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 += "> *Writing...*\n\n" - - return markdown - -def format_novel_display(novel_text: str) -> str: - """Display novel content - Enhanced part separation""" - if not novel_text: - return "No completed content yet." - - formatted = "# ๐Ÿ“– Completed Novel\n\n" - - # Display word count - word_count = len(novel_text.split()) - formatted += f"**Total Length: {word_count:,} words (Target: {TARGET_WORDS:,} words)**\n\n" - - # Achievement rate - achievement = (word_count / TARGET_WORDS) * 100 - formatted += f"**Achievement Rate: {achievement:.1f}%**\n\n" - formatted += "---\n\n" - - # Display each part separately - parts = novel_text.split('\n\n') - - for i, part in enumerate(parts): - if part.strip(): - # Add part title - if i < len(NARRATIVE_PHASES): - formatted += f"## {NARRATIVE_PHASES[i]}\n\n" - - formatted += f"{part}\n\n" - - # Part divider - if i < len(parts) - 1: - formatted += "---\n\n" - - return formatted - -def export_to_docx(content: str, filename: str, language: str, session_id: str) -> str: - """Export to DOCX file - Korean standard book format""" - try: - doc = Document() - - # Korean standard book format (152mm x 225mm) - section = doc.sections[0] - section.page_height = Mm(225) # 225mm - section.page_width = Mm(152) # 152mm - section.top_margin = Mm(20) # Top margin 20mm - section.bottom_margin = Mm(20) # Bottom margin 20mm - section.left_margin = Mm(20) # Left margin 20mm - section.right_margin = Mm(20) # Right margin 20mm - - # Generate title from session info - session = NovelDatabase.get_session(session_id) - - # Title generation function - def generate_title(user_query: str, content_preview: str) -> str: - """Generate title based on theme and content""" - if len(user_query) < 20: - return user_query - else: - keywords = user_query.split()[:5] - return " ".join(keywords) - - # Title page - title = generate_title(session["user_query"], content[:500]) if session else "Untitled" - - # Title style settings - title_para = doc.add_paragraph() - title_para.alignment = WD_ALIGN_PARAGRAPH.CENTER - title_para.paragraph_format.space_before = Pt(100) - - title_run = title_para.add_run(title) - if language == "Korean": - title_run.font.name = 'Batang' - title_run._element.rPr.rFonts.set(qn('w:eastAsia'), 'Batang') - else: - title_run.font.name = 'Times New Roman' - title_run.font.size = Pt(20) - title_run.bold = True - - # Page break - doc.add_page_break() - - # Body style settings - style = doc.styles['Normal'] - if language == "Korean": - style.font.name = 'Batang' - style._element.rPr.rFonts.set(qn('w:eastAsia'), 'Batang') - else: - style.font.name = 'Times New Roman' - style.font.size = Pt(10.5) - style.paragraph_format.line_spacing = 1.8 - style.paragraph_format.space_after = Pt(0) - style.paragraph_format.first_line_indent = Mm(10) - - # Clean content function - def clean_content(text: str) -> str: - """Remove unnecessary markdown, part numbers, etc.""" - patterns_to_remove = [ - r'^#{1,6}\s+.*', # Markdown headers - r'^\*\*.*\*\*', # Bold text - r'^Part\s*\d+.*', # Part numbers - r'^\d+\.\s+.*:.*', # Numbered lists - r'^---+', # Dividers - r'^\s*\[.*\]\s*', # Brackets - ] - - lines = text.split('\n') - cleaned_lines = [] - - for line in lines: - if not line.strip(): - cleaned_lines.append('') - continue - - skip_line = False - for pattern in patterns_to_remove: - if re.match(pattern, line.strip(), re.MULTILINE): - skip_line = True - break - - if not skip_line: - cleaned_line = line - cleaned_line = re.sub(r'\*\*(.*?)\*\*', r'\1', cleaned_line) - cleaned_line = re.sub(r'\*(.*?)\*', r'\1', cleaned_line) - cleaned_line = re.sub(r'`(.*?)`', r'\1', cleaned_line) - cleaned_lines.append(cleaned_line.strip()) - - final_lines = [] - prev_empty = False - for line in cleaned_lines: - if not line: - if not prev_empty: - final_lines.append('') - prev_empty = True - else: - final_lines.append(line) - prev_empty = False - - return '\n'.join(final_lines) - - # Clean content - cleaned_content = clean_content(content) - - # Add body text - paragraphs = cleaned_content.split('\n') - for para_text in paragraphs: - if para_text.strip(): - para = doc.add_paragraph(para_text.strip()) - for run in para.runs: - if language == "Korean": - run.font.name = 'Batang' - run._element.rPr.rFonts.set(qn('w:eastAsia'), 'Batang') - else: - run.font.name = 'Times New Roman' - else: - doc.add_paragraph() - - # Create temporary file with proper handling - with tempfile.NamedTemporaryFile(mode='wb', suffix='.docx', delete=False) as tmp_file: - doc.save(tmp_file) - temp_path = tmp_file.name - - return temp_path - - except Exception as e: - logger.error(f"DOCX export error: {str(e)}") - raise e - - -def download_novel(novel_text: str, format_type: str, language: str, session_id: str) -> Optional[str]: - """Generate novel download file - FIXED VERSION""" - if not novel_text or not session_id: - logger.error("Missing novel_text or session_id") - return None - - timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") - filename = f"novel_{session_id[:8]}_{timestamp}" - - try: - if format_type == "DOCX" and DOCX_AVAILABLE: - # Use the fixed export_to_docx function - return export_to_docx(novel_text, filename, language, session_id) - else: - # For TXT format - return export_to_txt(novel_text, filename) - except Exception as e: - logger.error(f"File generation failed: {e}") - return None - - -# In the Gradio interface, update the download handler: -def handle_download(format_type, language, session_id, novel_text): - """Fixed download handler with better error handling and debugging""" - logger.info(f"Download attempt - Session ID: {session_id}, Format: {format_type}") - logger.info(f"Novel text length: {len(novel_text) if novel_text else 0}") - logger.info(f"Novel text preview: {novel_text[:100] if novel_text else 'None'}") - - if not session_id: - logger.error("No session ID provided") - return gr.update(visible=False, value=None) - - if not novel_text or novel_text.strip() == "" or novel_text == "*Your completed novel will appear here, ready to be read and cherished...*": - logger.error(f"No novel content to download. Content: '{novel_text[:50] if novel_text else 'None'}'") - return gr.update(visible=False, value=None) - - try: - file_path = download_novel(novel_text, format_type, language, session_id) - if file_path and os.path.exists(file_path): - logger.info(f"File created successfully: {file_path}") - return gr.update(value=file_path, visible=True) - else: - logger.error("File path not created or doesn't exist") - return gr.update(visible=False, value=None) - except Exception as e: - logger.error(f"Download handler error: {str(e)}") - return gr.update(visible=False, value=None) - -# Also add cleanup function for temporary files -def cleanup_temp_files(): - """Clean up old temporary files""" - temp_dir = tempfile.gettempdir() - pattern = os.path.join(temp_dir, "novel_*.docx") - - for file_path in glob.glob(pattern): - try: - # Delete files older than 1 hour - if os.path.getmtime(file_path) < time.time() - 3600: - os.unlink(file_path) - except: - pass - - -def export_to_txt(content: str, filename: str) -> str: - """Export to TXT file""" - filepath = f"{filename}.txt" - with open(filepath, 'w', encoding='utf-8') as f: - # Header - f.write("=" * 80 + "\n") - f.write(f"Generated on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n") - f.write(f"Total word count: {len(content.split()):,} words\n") - f.write("=" * 80 + "\n\n") - - # Body - f.write(content) - - # Footer - f.write("\n\n" + "=" * 80 + "\n") - f.write("AI Literary Creation System v2.0\n") - f.write("=" * 80 + "\n") - - return filepath - -def generate_random_theme(language="English"): - """Generate a coherent and natural novel theme using LLM""" - try: - # JSON ํŒŒ์ผ ๋กœ๋“œ - json_path = Path("novel_themes.json") - if not json_path.exists(): - print("[WARNING] novel_themes.json not found, using built-in data") - # ๊ธฐ๋ณธ ๋ฐ์ดํ„ฐ ์ •์˜ - ๋” ํ˜„์‹ค์ ์ธ ํ…Œ๋งˆ๋กœ ์ˆ˜์ • - themes_data = { - "themes": ["family secrets", "career transition", "lost love", "friendship test", "generational conflict"], - "characters": ["middle-aged teacher", "retiring doctor", "single parent", "immigrant artist", "war veteran"], - "hooks": ["unexpected inheritance", "old diary discovery", "chance reunion", "life-changing diagnosis", "sudden job loss"], - "questions": ["What defines family?", "Can people truly change?", "What is worth sacrificing?", "How do we forgive?"] - } - else: - with open(json_path, 'r', encoding='utf-8') as f: - data = json.load(f) - # ๊ฐ€์ค‘์น˜ ๊ธฐ๋ฐ˜ ํ•„ํ„ฐ๋ง - ํ˜„์‹ค์ ์ธ ํ…Œ๋งˆ ์šฐ์„  - realistic_themes = [] - for theme_key, theme_data in data.get('core_themes', {}).items(): - weight = theme_data.get('weight', 0.1) - # ํ˜„์‹ค์ ์ธ ํ…Œ๋งˆ์— ๋” ๋†’์€ ๊ฐ€์ค‘์น˜ - if any(word in theme_key for word in ['family', 'love', 'work', 'memory', 'identity', 'aging']): - weight *= 1.5 - elif any(word in theme_key for word in ['digital', 'extinction', 'apocalypse', 'quantum']): - weight *= 0.5 - realistic_themes.append((theme_key, weight)) - - # ๊ฐ€์ค‘์น˜ ๊ธฐ๋ฐ˜ ์„ ํƒ - themes = [t[0] for t in sorted(realistic_themes, key=lambda x: x[1], reverse=True)[:10]] - - themes_data = { - "themes": themes if themes else ["family secrets", "career crisis", "lost love"], - "characters": [], - "hooks": [], - "questions": [] - } - - # Extract realistic data - for char_data in data.get('characters', {}).values(): - for variation in char_data.get('variations', []): - # ํ˜„์‹ค์ ์ธ ์บ๋ฆญํ„ฐ ํ•„ํ„ฐ๋ง - if not any(word in variation.lower() for word in ['cyborg', 'quantum', 'binary', 'extinct']): - themes_data["characters"].append(variation) - - for hook_list in data.get('narrative_hooks', {}).values(): - for hook in hook_list: - # ํ˜„์‹ค์ ์ธ ์‚ฌ๊ฑด ํ•„ํ„ฐ๋ง - if not any(word in hook.lower() for word in ['download', 'digital', 'algorithm', 'corporate subscription']): - themes_data["hooks"].append(hook) - - for phil_data in data.get('philosophies', {}).values(): - themes_data["questions"].extend(phil_data.get('core_questions', [])) - - # ๊ธฐ๋ณธ๊ฐ’ ์„ค์ • - if not themes_data["characters"]: - themes_data["characters"] = ["struggling artist", "retired teacher", "young mother", "elderly caregiver", "small business owner"] - if not themes_data["hooks"]: - themes_data["hooks"] = ["discovering family secret", "unexpected reunion", "facing illness", "losing home", "finding old letters"] - if not themes_data["questions"]: - themes_data["questions"] = ["What makes a family?", "How do we find meaning?", "Can we escape our past?", "What legacy do we leave?"] - - # Random selection - import secrets - theme = secrets.choice(themes_data["themes"]) - character = secrets.choice(themes_data["characters"]) - hook = secrets.choice(themes_data["hooks"]) - question = secrets.choice(themes_data["questions"]) - - # ์–ธ์–ด๋ณ„ ํ”„๋กฌํ”„ํŠธ - ํ†ค๊ณผ ์Šคํƒ€์ผ ์„น์…˜ ์ œ๊ฑฐ - if language == "Korean": - # ํ•œ๊ตญ์–ด ๋ฒˆ์—ญ ๋ฐ ์ž์—ฐ์Šค๋Ÿฌ์šด ํ‘œํ˜„ - theme_kr = translate_theme_naturally(theme, "theme") - character_kr = translate_theme_naturally(character, "character") - hook_kr = translate_theme_naturally(hook, "hook") - question_kr = translate_theme_naturally(question, "question") - - prompt = f"""๋‹ค์Œ ์š”์†Œ๋“ค์„ ์‚ฌ์šฉํ•˜์—ฌ ํ˜„์‹ค์ ์ด๊ณ  ๊ณต๊ฐ๊ฐ€๋Šฅํ•œ ์†Œ์„ค ์ฃผ์ œ๋ฅผ ์ƒ์„ฑํ•˜์„ธ์š”: - -์ฃผ์ œ: {theme_kr} -์ธ๋ฌผ: {character_kr} -์‚ฌ๊ฑด: {hook_kr} -ํ•ต์‹ฌ ์งˆ๋ฌธ: {question_kr} - -์š”๊ตฌ์‚ฌํ•ญ: -1. ํ˜„๋Œ€ ํ•œ๊ตญ ์‚ฌํšŒ์—์„œ ์ผ์–ด๋‚  ์ˆ˜ ์žˆ๋Š” ํ˜„์‹ค์ ์ธ ์ด์•ผ๊ธฐ -2. ๋ณดํŽธ์ ์œผ๋กœ ๊ณต๊ฐํ•  ์ˆ˜ ์žˆ๋Š” ์ธ๋ฌผ๊ณผ ์ƒํ™ฉ -3. ๊ตฌ์ฒด์ ์ด๊ณ  ์ƒ์ƒํ•œ ๋ฐฐ๊ฒฝ ์„ค์ • -4. ๊นŠ์ด ์žˆ๋Š” ์‹ฌ๋ฆฌ ๋ฌ˜์‚ฌ๊ฐ€ ๊ฐ€๋Šฅํ•œ ๊ฐˆ๋“ฑ - -๋‹ค์Œ ํ˜•์‹์œผ๋กœ ๊ฐ„๊ฒฐํ•˜๊ฒŒ ์ž‘์„ฑํ•˜์„ธ์š”: - -[ํ•œ ๋ฌธ์žฅ์œผ๋กœ ๋œ ๋งค๋ ฅ์ ์ธ ์ฒซ ๋ฌธ์žฅ] - -์ฃผ์ธ๊ณต์€ [๊ตฌ์ฒด์ ์ธ ์ƒํ™ฉ์˜ ์ธ๋ฌผ]์ž…๋‹ˆ๋‹ค. -[ํ•ต์‹ฌ ์‚ฌ๊ฑด]์„ ๊ณ„๊ธฐ๋กœ [๋‚ด์  ๊ฐˆ๋“ฑ]์— ์ง๋ฉดํ•˜๊ฒŒ ๋˜๊ณ , -๊ฒฐ๊ตญ [์ฒ ํ•™์  ์งˆ๋ฌธ]์— ๋Œ€ํ•œ ๋‹ต์„ ์ฐพ์•„๊ฐ€๋Š” ์—ฌ์ •์„ ๊ทธ๋ฆฝ๋‹ˆ๋‹ค.""" - - else: - prompt = f"""Generate a realistic and relatable novel theme using these elements: - -Theme: {theme} -Character: {character} -Event: {hook} -Core Question: {question} - -Requirements: -1. A story that could happen in contemporary society -2. Universally relatable characters and situations -3. Specific and vivid settings -4. Conflicts allowing deep psychological exploration - -Write concisely in this format: - -[One compelling opening sentence] - -The protagonist is [character in specific situation]. -Through [key event], they face [internal conflict], -ultimately embarking on a journey to answer [philosophical question].""" - - # Use the UnifiedLiterarySystem's LLM to generate coherent theme - system = UnifiedLiterarySystem() - - # Call LLM synchronously for theme generation - messages = [{"role": "user", "content": prompt}] - generated_theme = system.call_llm_sync(messages, "director", language) - - # Extract metadata for database storage - metadata = extract_theme_metadata(generated_theme, language) - metadata.update({ - 'original_theme': theme, - 'original_character': character, - 'original_hook': hook, - 'original_question': question - }) - - # Save to database - theme_id = save_random_theme_with_hf(generated_theme, language, metadata) - logger.info(f"Saved random theme with ID: {theme_id}") - - # ํ†ค๊ณผ ์Šคํƒ€์ผ ์„น์…˜ ์ œ๊ฑฐ - ๋ถˆํ•„์š”ํ•œ ๋ฐ˜๋ณต ๋‚ด์šฉ ์‚ญ์ œ - if "**ํ†ค๊ณผ ์Šคํƒ€์ผ:**" in generated_theme or "**Tone and Style:**" in generated_theme: - lines = generated_theme.split('\n') - filtered_lines = [] - skip = False - for line in lines: - if "ํ†ค๊ณผ ์Šคํƒ€์ผ" in line or "Tone and Style" in line: - skip = True - elif skip and (line.strip() == "" or line.startswith("**")): - skip = False - if not skip: - filtered_lines.append(line) - generated_theme = '\n'.join(filtered_lines).strip() - - return generated_theme - - except Exception as e: - logger.error(f"Theme generation error: {str(e)}") - # Fallback to simple realistic themes - fallback_themes = { - "Korean": [ - """"์•„๋ฒ„์ง€๊ฐ€ ๋Œ์•„๊ฐ€์‹  ๋‚ , ๋‚˜๋Š” ๊ทธ๊ฐ€ ํ‰์ƒ ์ˆจ๊ฒจ์˜จ ๋˜ ๋‹ค๋ฅธ ๊ฐ€์กฑ์˜ ์กด์žฌ๋ฅผ ์•Œ๊ฒŒ ๋˜์—ˆ๋‹ค." - -์ฃผ์ธ๊ณต์€ ํ‰๋ฒ”ํ•œ ํšŒ์‚ฌ์›์œผ๋กœ ์‚ด์•„์˜จ 40๋Œ€ ์—ฌ์„ฑ์ž…๋‹ˆ๋‹ค. -์•„๋ฒ„์ง€์˜ ์žฅ๋ก€์‹์—์„œ ๋‚ฏ์„  ์—ฌ์ธ๊ณผ ๊ทธ๋…€์˜ ๋”ธ์„ ๋งŒ๋‚˜๊ฒŒ ๋˜๋ฉด์„œ ๊ฐ€์กฑ์˜ ์˜๋ฏธ์— ๋Œ€ํ•ด ๋‹ค์‹œ ์ƒ๊ฐํ•˜๊ฒŒ ๋˜๊ณ , -๊ฒฐ๊ตญ ์ง„์ •ํ•œ ๊ฐ€์กฑ์ด๋ž€ ๋ฌด์—‡์ธ์ง€์— ๋Œ€ํ•œ ๋‹ต์„ ์ฐพ์•„๊ฐ€๋Š” ์—ฌ์ •์„ ๊ทธ๋ฆฝ๋‹ˆ๋‹ค.""", - - """"์„œ๋ฅธ ๋…„๊ฐ„ ๊ฐ€๋ฅด์นœ ํ•™๊ต์—์„œ ๋‚˜์˜จ ๋‚ , ์ฒ˜์Œ์œผ๋กœ ๋‚ด๊ฐ€ ๋ˆ„๊ตฌ์ธ์ง€ ๋ชฐ๋ž๋‹ค." - -์ฃผ์ธ๊ณต์€ ์ •๋…„ํ‡ด์ง์„ ๋งž์€ ๊ณ ๋“ฑํ•™๊ต ๊ตญ์–ด ๊ต์‚ฌ์ž…๋‹ˆ๋‹ค. -๊ฐ‘์ž‘์Šค๋Ÿฌ์šด ์ผ์ƒ์˜ ๊ณต๋ฐฑ ์†์—์„œ ์žŠ๊ณ  ์ง€๋ƒˆ๋˜ ์ Š์€ ๋‚ ์˜ ๊ฟˆ์„ ๋งˆ์ฃผํ•˜๊ฒŒ ๋˜๊ณ , -๊ฒฐ๊ตญ ๋‚จ์€ ์ธ์ƒ์—์„œ ๋ฌด์—‡์„ ํ•  ๊ฒƒ์ธ๊ฐ€์— ๋Œ€ํ•œ ๋‹ต์„ ์ฐพ์•„๊ฐ€๋Š” ์—ฌ์ •์„ ๊ทธ๋ฆฝ๋‹ˆ๋‹ค.""" - ], - "English": [ - """"The day my father died, I discovered he had another family he'd hidden all his life." - -The protagonist is a woman in her 40s who has lived as an ordinary office worker. -Through meeting a strange woman and her daughter at her father's funeral, she confronts what family truly means, -ultimately embarking on a journey to answer what constitutes a real family.""", - - """"The day I left the school where I'd taught for thirty years, I didn't know who I was anymore." - -The protagonist is a high school literature teacher facing retirement. -Through the sudden emptiness of daily life, they confront long-forgotten dreams of youth, -ultimately embarking on a journey to answer what to do with the remaining years.""" - ] - } - - import secrets - return secrets.choice(fallback_themes.get(language, fallback_themes["English"])) - -def translate_theme_naturally(text, category): - """์ž์—ฐ์Šค๋Ÿฌ์šด ํ•œ๊ตญ์–ด ๋ฒˆ์—ญ""" - translations = { - # ํ…Œ๋งˆ - "family secrets": "๊ฐ€์กฑ์˜ ๋น„๋ฐ€", - "career transition": "์ธ์ƒ์˜ ์ „ํ™˜์ ", - "lost love": "์žƒ์–ด๋ฒ„๋ฆฐ ์‚ฌ๋ž‘", - "friendship test": "์šฐ์ •์˜ ์‹œํ—˜", - "generational conflict": "์„ธ๋Œ€ ๊ฐ„ ๊ฐˆ๋“ฑ", - "digital extinction": "๋””์ง€ํ„ธ ์‹œ๋Œ€์˜ ์†Œ์™ธ", - "sensory revolution": "๊ฐ๊ฐ์˜ ํ˜๋ช…", - "temporal paradox": "์‹œ๊ฐ„์˜ ์—ญ์„ค", - - # ์บ๋ฆญํ„ฐ - "struggling artist": "์ƒํ™œ๊ณ ์— ์‹œ๋‹ฌ๋ฆฌ๋Š” ์˜ˆ์ˆ ๊ฐ€", - "retired teacher": "์€ํ‡ดํ•œ ๊ต์‚ฌ", - "young mother": "์ Š์€ ์—„๋งˆ", - "elderly caregiver": "๋…ธ์ธ์„ ๋Œ๋ณด๋Š” ๊ฐ„๋ณ‘์ธ", - "small business owner": "์ž‘์€ ๊ฐ€๊ฒŒ ์ฃผ์ธ", - "middle-aged teacher": "์ค‘๋…„์˜ ๊ต์‚ฌ", - "retiring doctor": "์€ํ‡ด๋ฅผ ์•ž๋‘” ์˜์‚ฌ", - "single parent": "ํ˜ผ์ž ์•„์ด๋ฅผ ํ‚ค์šฐ๋Š” ๋ถ€๋ชจ", - "immigrant artist": "์ด๋ฏผ์ž ์˜ˆ์ˆ ๊ฐ€", - "war veteran": "์ „์Ÿ ์ฐธ์ „์šฉ์‚ฌ", - "last person who dreams without ads": "๊ด‘๊ณ  ์—†์ด ๊ฟˆ๊พธ๋Š” ๋งˆ์ง€๋ง‰ ์‚ฌ๋žŒ", - "memory trader": "๊ธฐ์–ต ๊ฑฐ๋ž˜์ƒ", - - # ์‚ฌ๊ฑด - "discovering family secret": "๊ฐ€์กฑ์˜ ๋น„๋ฐ€์„ ๋ฐœ๊ฒฌํ•˜๋‹ค", - "unexpected reunion": "์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์žฌํšŒ", - "facing illness": "์งˆ๋ณ‘๊ณผ ๋งˆ์ฃผํ•˜๋‹ค", - "losing home": "์ง‘์„ ์žƒ๋‹ค", - "finding old letters": "์˜ค๋ž˜๋œ ํŽธ์ง€๋ฅผ ๋ฐœ๊ฒฌํ•˜๋‹ค", - "unexpected inheritance": "๋œป๋ฐ–์˜ ์œ ์‚ฐ", - "old diary discovery": "์˜ค๋ž˜๋œ ์ผ๊ธฐ์žฅ ๋ฐœ๊ฒฌ", - "chance reunion": "์šฐ์—ฐํ•œ ์žฌํšŒ", - "life-changing diagnosis": "์ธ์ƒ์„ ๋ฐ”๊พธ๋Š” ์ง„๋‹จ", - "sudden job loss": "๊ฐ‘์ž‘์Šค๋Ÿฌ์šด ์‹ค์ง", - "discovers their memories belong to a corporate subscription": "๊ธฐ์–ต์ด ๊ธฐ์—… ์„œ๋น„์Šค์˜ ์ผ๋ถ€์ž„์„ ๋ฐœ๊ฒฌํ•˜๋‹ค", - - # ์งˆ๋ฌธ - "What makes a family?": "๊ฐ€์กฑ์ด๋ž€ ๋ฌด์—‡์ธ๊ฐ€?", - "How do we find meaning?": "์šฐ๋ฆฌ๋Š” ์–ด๋–ป๊ฒŒ ์˜๋ฏธ๋ฅผ ์ฐพ๋Š”๊ฐ€?", - "Can we escape our past?": "๊ณผ๊ฑฐ๋กœ๋ถ€ํ„ฐ ๋ฒ—์–ด๋‚  ์ˆ˜ ์žˆ๋Š”๊ฐ€?", - "What legacy do we leave?": "์šฐ๋ฆฌ๋Š” ์–ด๋–ค ์œ ์‚ฐ์„ ๋‚จ๊ธฐ๋Š”๊ฐ€?", - "What defines family?": "๋ฌด์—‡์ด ๊ฐ€์กฑ์„ ์ •์˜ํ•˜๋Š”๊ฐ€?", - "Can people truly change?": "์‚ฌ๋žŒ์€ ์ •๋ง ๋ณ€ํ•  ์ˆ˜ ์žˆ๋Š”๊ฐ€?", - "What is worth sacrificing?": "๋ฌด์—‡์„ ์œ„ํ•ด ํฌ์ƒํ•  ๊ฐ€์น˜๊ฐ€ ์žˆ๋Š”๊ฐ€?", - "How do we forgive?": "์šฐ๋ฆฌ๋Š” ์–ด๋–ป๊ฒŒ ์šฉ์„œํ•˜๋Š”๊ฐ€?", - "What remains human when humanity is optional?": "์ธ๊ฐ„์„ฑ์ด ์„ ํƒ์‚ฌํ•ญ์ผ ๋•Œ ๋ฌด์—‡์ด ์ธ๊ฐ„์œผ๋กœ ๋‚จ๋Š”๊ฐ€?" - } - - # ๋จผ์ € ์ •ํ™•ํ•œ ๋งค์นญ ์‹œ๋„ - if text in translations: - return translations[text] - - # ๋ถ€๋ถ„ ๋งค์นญ ์‹œ๋„ - text_lower = text.lower() - for key, value in translations.items(): - if key.lower() in text_lower or text_lower in key.lower(): - return value - - # ๋ฒˆ์—ญ์ด ์—†์œผ๋ฉด ์›๋ฌธ ๋ฐ˜ํ™˜ - return text - -def extract_theme_metadata(theme_text: str, language: str) -> Dict[str, Any]: - """Extract metadata from generated theme text""" - metadata = { - 'title': '', - 'opening_sentence': '', - 'protagonist': '', - 'conflict': '', - 'philosophical_question': '', - 'tags': [] - } - - lines = theme_text.split('\n') - - # Extract opening sentence (usually in quotes) - for line in lines: - if '"' in line or '"' in line or 'ใ€Œ' in line: - # Extract text between quotes - import re - quotes = re.findall(r'["""ใ€Œ](.*?)["""ใ€]', line) - if quotes: - metadata['opening_sentence'] = quotes[0] - break - - # Extract other elements based on patterns - for i, line in enumerate(lines): - line = line.strip() - - # Title extraction (if exists) - if i == 0 and not any(quote in line for quote in ['"', '"', 'ใ€Œ']): - metadata['title'] = line.replace('**', '').strip() - - # Protagonist - if any(marker in line for marker in ['protagonist is', '์ฃผ์ธ๊ณต์€', 'The protagonist']): - metadata['protagonist'] = line.split('is' if 'is' in line else '์€')[-1].strip().rstrip('.') - - # Conflict/Event - if any(marker in line for marker in ['Through', 'ํ†ตํ•ด', '๊ณ„๊ธฐ๋กœ', 'face']): - metadata['conflict'] = line - - # Question - if any(marker in line for marker in ['answer', '๋‹ต์„', 'question', '์งˆ๋ฌธ']): - metadata['philosophical_question'] = line - - # Generate tags based on content - tag_keywords = { - 'family': ['family', '๊ฐ€์กฑ', 'father', '์•„๋ฒ„์ง€', 'mother', '์–ด๋จธ๋‹ˆ'], - 'love': ['love', '์‚ฌ๋ž‘', 'relationship', '๊ด€๊ณ„'], - 'death': ['death', '์ฃฝ์Œ', 'died', '๋Œ์•„๊ฐ€์‹ '], - 'memory': ['memory', '๊ธฐ์–ต', 'remember', '์ถ”์–ต'], - 'identity': ['identity', '์ •์ฒด์„ฑ', 'who am I', '๋ˆ„๊ตฌ์ธ์ง€'], - 'work': ['work', '์ผ', 'career', '์ง์—…', 'retirement', '์€ํ‡ด'], - 'aging': ['aging', '๋…ธํ™”', 'old', '๋Š™์€', 'elderly', '๋…ธ์ธ'] - } - - theme_lower = theme_text.lower() - for tag, keywords in tag_keywords.items(): - if any(keyword in theme_lower for keyword in keywords): - metadata['tags'].append(tag) - - return metadata - -def save_random_theme_with_hf(theme_text: str, language: str, metadata: Dict[str, Any]) -> str: - """Save randomly generated theme to library and HF dataset""" - theme_id = hashlib.md5(f"{theme_text}{datetime.now()}".encode()).hexdigest()[:12] - - # Extract components from theme text - title = metadata.get('title', '') - opening_sentence = metadata.get('opening_sentence', '') - protagonist = metadata.get('protagonist', '') - conflict = metadata.get('conflict', '') - philosophical_question = metadata.get('philosophical_question', '') - tags = json.dumps(metadata.get('tags', [])) - - with NovelDatabase.get_db() as conn: - conn.cursor().execute(''' - INSERT INTO random_themes_library - (theme_id, theme_text, language, title, opening_sentence, - protagonist, conflict, philosophical_question, tags, metadata) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - ''', (theme_id, theme_text, language, title, opening_sentence, - protagonist, conflict, philosophical_question, tags, - json.dumps(metadata))) - conn.commit() - - # Backup to HF dataset - if 'hf_manager' in globals() and hf_manager.token: - try: - hf_manager.backup_to_hf() - logger.info(f"Theme {theme_id} backed up to HF dataset") - except Exception as e: - logger.error(f"Failed to backup theme to HF: {e}") - - return theme_id - -def format_theme_card(theme_data: Dict, language: str) -> str: - """Format theme data as a card for display with scrollable content""" - theme_id = theme_data.get('theme_id', '') - theme_text = theme_data.get('theme_text', '') - generated_at = theme_data.get('generated_at', '') - view_count = theme_data.get('view_count', 0) - used_count = theme_data.get('used_count', 0) - tags = json.loads(theme_data.get('tags', '[]')) if isinstance(theme_data.get('tags'), str) else theme_data.get('tags', []) - - # Format timestamp - if generated_at: - try: - dt = datetime.fromisoformat(generated_at.replace(' ', 'T')) - time_str = dt.strftime('%Y-%m-%d %H:%M') - except: - time_str = generated_at - else: - time_str = "" - - # Create tag badges - tag_badges = ' '.join([f'{tag}' for tag in tags]) - - # Format theme text with line breaks - formatted_text = theme_text.replace('\n', '
') - - # Create card HTML with scrollable content - Simplified version - card_html = f""" -
-
- #{theme_id[:8]} - {time_str} -
-
-
{formatted_text}
-
{tag_badges}
-
- -
""" - - return card_html - -def get_theme_library_display(language: str = None, search_query: str = "") -> str: - """Get formatted display of theme library""" - themes = NovelDatabase.get_random_themes_library(language, limit=50) - - if not themes: - empty_msg = { - "Korean": "์•„์ง ์ƒ์„ฑ๋œ ํ…Œ๋งˆ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค. ๋žœ๋ค ๋ฒ„ํŠผ์„ ๋ˆŒ๋Ÿฌ ์ฒซ ํ…Œ๋งˆ๋ฅผ ๋งŒ๋“ค์–ด๋ณด์„ธ์š”!", - "English": "No themes generated yet. Click the Random button to create your first theme!" - } - return f'
{empty_msg.get(language, empty_msg["English"])}
' - - # Filter by search query if provided - if search_query: - search_lower = search_query.lower() - themes = [t for t in themes if search_lower in t.get('theme_text', '').lower()] - - # Statistics - total_themes = len(themes) - total_views = sum(t.get('view_count', 0) for t in themes) - total_uses = sum(t.get('used_count', 0) for t in themes) - - stats_html = f""" -
-
- {'์ด ํ…Œ๋งˆ' if language == 'Korean' else 'Total Themes'} - {total_themes} -
-
- {'์ด ์กฐํšŒ์ˆ˜' if language == 'Korean' else 'Total Views'} - {total_views} -
-
- {'์ด ์‚ฌ์šฉ์ˆ˜' if language == 'Korean' else 'Total Uses'} - {total_uses} -
-
""" - - # Theme cards - cards_html = '
' - for theme in themes: - cards_html += format_theme_card(theme, language) - cards_html += '
' - - # JavaScript for interactions - js_script = """ -""" - - return stats_html + cards_html + js_script - -def load_css(): - """Load CSS from external file""" - try: - with open('css.css', 'r', encoding='utf-8') as f: - return f.read() - except FileNotFoundError: - logger.error("css.css file not found") - return "" - + """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(): - combined_css = load_css() - # Using Soft theme with safe color options - with gr.Blocks(theme=gr.themes.Soft(), css=combined_css, title="AGI NOVEL Generator") as interface: - gr.HTML(""" -
-

๐Ÿ“– AGI NOVEL Generator

- -
- - badge - - - badge - - - badge - -
- -

- Artificial General Intelligence (AGI) denotes an artificial system possessing human-level, general-purpose intelligence and is now commonly framed as AI that can outperform humans in most economically and intellectually valuable tasks. Demonstrating such breadth requires evaluating not only calculation, logical reasoning, and perception but also the distinctly human faculties of creativity and language. Among the creative tests, the most demanding is the production of a full-length novel running 100kโ€“200k words. An extended narrative forces an AGI candidate to exhibit (1) sustained long-term memory and context tracking (2) intricate causal and plot planning (3) nuanced cultural and emotional expression (4) autonomous self-censorship and ethical filtering to avoid harmful or biased content and (5) verifiable originality beyond simple recombination of training data. -

- -
- ๐ŸŽฒ Novel Theme Random Generator: This system can generate up to approximately 170 quadrillion (1.7 ร— 10ยนโท) unique novel themes. - Even writing 100 novels per day, it would take 4.6 million years to exhaust all combinations. - Click the "Random" button to explore infinite creative possibilities! -
- -
- โฑ๏ธ Note: Creating a complete novel takes approximately 20 minutes. If your web session disconnects, you can restore your work using the "Session Recovery" feature. -
- -
- ๐ŸŽฏ Core Innovation: Not fragmented texts from multiple writers, - but a genuine full-length novel written consistently by a single author from beginning to end. -
-
- """) - - # State management - current_session_id = gr.State(None) - selected_theme_id = gr.State(None) - selected_theme_text = gr.State(None) - - # Create tabs and store reference - with gr.Tabs() as main_tabs: - # Main Novel Writing Tab - with gr.Tab("๐Ÿ“ Novel Writing", elem_id="writing_main_tab"): - # Input section at the top with full width - with gr.Group(elem_classes=["input-section"]): - gr.Markdown("### โœ๏ธ Writing Desk") - - with gr.Row(): - with gr.Column(scale=3): - query_input = gr.Textbox( - label="Novel Theme", - placeholder="""Enter your novella theme. Like a seed that grows into a tree, your theme will blossom into a full narrative... - -You can describe: -- A specific situation or conflict -- Character relationships and dynamics -- Philosophical questions to explore -- Social or personal transformations -- Any combination of the above - -The more detailed your theme, the richer the resulting narrative will be.""", - lines=8, - elem_id="theme_input" - ) - - with gr.Column(scale=1): - language_select = gr.Radio( - choices=["English", "Korean"], - value="English", - label="Language", - elem_id="language_select" - ) - - with gr.Column(): - random_btn = gr.Button("๐ŸŽฒ Random Theme", variant="primary", size="lg") - submit_btn = gr.Button("๐Ÿ–‹๏ธ Begin Writing", variant="secondary", size="lg") - clear_btn = gr.Button("๐Ÿ—‘๏ธ Clear All", size="lg") - - status_text = gr.Textbox( - label="Writing Progress", - interactive=False, - value="โœจ Ready to begin your literary journey", - elem_id="status_text" - ) - - # Session management section - with gr.Group(elem_classes=["session-section"]): - gr.Markdown("### ๐Ÿ“š Your Library") - with gr.Row(): - session_dropdown = gr.Dropdown( - label="Saved Manuscripts", - choices=[], - interactive=True, - elem_id="session_dropdown", - scale=3 - ) - refresh_btn = gr.Button("๐Ÿ”„ Refresh", scale=1) - resume_btn = gr.Button("๐Ÿ“– Continue", variant="secondary", scale=1) - auto_recover_btn = gr.Button("๐Ÿ”ฎ Recover Last", scale=1) - - # Output sections below input - with gr.Row(): - with gr.Column(): - with gr.Tab("๐Ÿ–‹๏ธ Writing Process", elem_id="writing_tab"): - stages_display = gr.Markdown( - value="*Your writing journey will unfold here, like pages turning in a book...*", - elem_id="stages-display" - ) + """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.Tab("๐Ÿ“– Completed Manuscript", elem_id="manuscript_tab"): - novel_output = gr.Markdown( - value="*Your completed novel will appear here, ready to be read and cherished...*", - elem_id="novel-output" - ) + 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.Group(elem_classes=["download-section"]): - gr.Markdown("### ๐Ÿ“ฆ Bind Your Book") - with gr.Row(): - format_select = gr.Radio( - choices=["DOCX", "TXT"], - value="DOCX" if DOCX_AVAILABLE else "TXT", - label="Format", - elem_id="format_select" - ) - download_btn = gr.Button("๐Ÿ“ฅ Download Manuscript", variant="secondary") + with gr.Tab("๐Ÿ“„ Screenplay"): + screenplay_output = gr.Markdown( + value="*Your formatted screenplay will appear here...*", + elem_id="screenplay-output" + ) - download_file = gr.File( - label="Your Manuscript", - visible=False, - elem_id="download_file" - ) + 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 - # Hidden state - novel_text_state = gr.State("") + Browse through AI-generated screenplay concepts. Each concept includes a title, logline, and brief setup. + """) - # Examples with literary flair - gr.Examples( - examples=[ - ["A daughter discovering her mother's hidden past through old letters found in an attic trunk"], - ["An architect losing sight who learns to design through touch, sound, and the memories of light"], - ["A translator replaced by AI rediscovering the essence of language through handwritten poetry"], - ["A middle-aged man who lost his job finding new meaning in the rhythms of rural life"], - ["A doctor with war trauma finding healing through Doctors Without Borders missions"], - ["A neighborhood coming together to save their beloved bookstore from corporate development"], - ["A year in the life of a professor losing memory and his devoted last student"] - ], - inputs=query_input, - label="๐Ÿ’ก Inspiring Themes", - examples_per_page=7, - elem_id="example_themes" - ) + 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 - # Random Theme Library Tab - with gr.Tab("๐ŸŽฒ Random Theme Library", elem_id="library_tab"): - with gr.Column(): - gr.Markdown(""" - ### ๐Ÿ“š Random Theme Library - - Browse through all randomly generated themes. Each theme is unique and can be used to create a novel. - """) - - with gr.Row(): - library_search = gr.Textbox( - label="Search Themes", - placeholder="Search by keywords...", - elem_classes=["library-search"], - scale=2 - ) - library_language_filter = gr.Radio( - choices=["All", "English", "Korean"], - value="All", - label="Filter by Language", - scale=2 - ) - library_refresh_btn = gr.Button("๐Ÿ”„ Refresh", scale=1) - - library_display = gr.HTML( - value=get_theme_library_display(), - elem_id="library-display" - ) - - # Hidden components for theme interaction - selected_theme_for_action = gr.Textbox(visible=False, elem_id="selected_theme_for_action") - action_type = gr.Textbox(visible=False, elem_id="action_type") - trigger_action = gr.Button("Trigger Action", visible=False, elem_id="trigger_action") - - # Event handlers - def refresh_sessions(): - try: - sessions = get_active_sessions("English") - return gr.update(choices=sessions) - except Exception as e: - logger.error(f"Session refresh error: {str(e)}") - return gr.update(choices=[]) - - def handle_auto_recover(language): - session_id, message = auto_recover_session(language) - return session_id, message - - def handle_random_theme(language): - """Handle random theme generation with library storage""" - theme = generate_random_theme(language) - return theme - - def refresh_library(language_filter, search_query): - """Refresh theme library display""" - lang = None if language_filter == "All" else language_filter - return get_theme_library_display(lang, search_query) - - def handle_library_action(theme_id, action): - """Handle theme library actions""" - if not theme_id: - return gr.update(), gr.update() - - if action == "use": - # Handle use action - theme_data = NovelDatabase.get_theme_by_id(theme_id) - if theme_data: - NovelDatabase.update_theme_used_count(theme_id) - NovelDatabase.update_theme_view_count(theme_id) # Also update view count - return ( - gr.update(value=theme_data.get('theme_text', '')), # query_input - f"Theme #{theme_id[:8]} loaded" # status_text - ) - - return gr.update(), gr.update() - - # Event connections - submit_btn.click( - fn=process_query, - inputs=[query_input, language_select, current_session_id], - outputs=[stages_display, novel_output, status_text, current_session_id, novel_text_state] # novel_text_state ์ถ”๊ฐ€ - ) - - resume_btn.click( - fn=lambda x: x.split("...")[0] if x and "..." in x else x, - inputs=[session_dropdown], - outputs=[current_session_id] - ).then( - fn=resume_session, - inputs=[current_session_id, language_select], - outputs=[stages_display, novel_output, status_text, current_session_id, novel_text_state] # novel_text_state ์ถ”๊ฐ€ - ) - - auto_recover_btn.click( - fn=handle_auto_recover, - inputs=[language_select], - outputs=[current_session_id, status_text] - ).then( - fn=resume_session, - inputs=[current_session_id, language_select], - outputs=[stages_display, novel_output, status_text, current_session_id, novel_text_state] # novel_text_state ์ถ”๊ฐ€ - ) - - - refresh_btn.click( - fn=refresh_sessions, - outputs=[session_dropdown] - ) - - clear_btn.click( - fn=lambda: ("", "", "โœจ Ready to begin your literary journey", "", None, ""), # ๋นˆ ๋ฌธ์ž์—ด ์ถ”๊ฐ€ - outputs=[stages_display, novel_output, status_text, novel_text_state, current_session_id, novel_text_state] - ) - - random_btn.click( - fn=handle_random_theme, - inputs=[language_select], - outputs=[query_input], - queue=False - ) - - # Library event handlers - library_refresh_btn.click( - fn=refresh_library, - inputs=[library_language_filter, library_search], - outputs=[library_display] - ) - - library_search.change( - fn=refresh_library, - inputs=[library_language_filter, library_search], - outputs=[library_display] - ) - - library_language_filter.change( - fn=refresh_library, - inputs=[library_language_filter, library_search], - outputs=[library_display] - ) - - # Handle clicks on library display - using trigger button - trigger_action.click( - fn=handle_library_action, - inputs=[selected_theme_for_action, action_type], - outputs=[query_input, status_text] - ) - - - download_btn.click( - fn=handle_download, - inputs=[format_select, language_select, current_session_id, novel_text_state], - outputs=[download_file] - ) - - # Load sessions and library on start - def initialize_interface(): - # Sync with HF dataset on startup - if 'hf_manager' in globals() and hf_manager.token: - hf_manager.sync_with_local_db() - - return refresh_sessions(), refresh_library("All", "") - - interface.load( - fn=initialize_interface, - outputs=[session_dropdown, library_display] - ) + 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 -# Initialize HF Dataset Manager as global variable -hf_manager = None + return interface # Main function if __name__ == "__main__": - logger.info("AGI NOVEL Generator v2.0 Starting...") - logger.info("=" * 60) - - # Environment check - logger.info(f"API Endpoint: {API_URL}") - logger.info(f"Target Length: {TARGET_WORDS:,} words") - logger.info(f"Minimum Words per Part: {MIN_WORDS_PER_PART:,} words") - logger.info("System Features: Single writer + Immediate part-by-part critique") - - if BRAVE_SEARCH_API_KEY: - logger.info("Web search enabled.") - else: - logger.warning("Web search disabled.") - - if DOCX_AVAILABLE: - logger.info("DOCX export enabled.") - else: - logger.warning("DOCX export disabled.") - - logger.info("=" * 60) - - # Initialize database - logger.info("Initializing database...") - NovelDatabase.init_db() - logger.info("Database initialization complete.") - - # Initialize HF Dataset Manager - logger.info("Initializing HuggingFace dataset manager...") - hf_manager = HFDatasetManager() - - if hf_manager.token: - logger.info("HuggingFace authentication successful.") - # Sync with HF dataset on startup - hf_manager.sync_with_local_db() - else: - logger.warning("HuggingFace token not found. Theme persistence will be local only.") - - # Create and launch interface - interface = create_interface() - - interface.launch( - server_name="0.0.0.0", - server_port=7860, - share=False, - debug=True - ) \ No newline at end of file + logger.info("Screenplay Generator Starting...") + logger.info("=" * 60) + + # Environment check + logger.info(f"API Endpoint: {API_URL}") + logger.info("Screenplay Types Available:") + for s_type, info in SCREENPLAY_LENGTHS.items(): + logger.info(f" - {s_type}: {info['description']}") + logger.info(f"Genres: {', '.join(GENRE_TEMPLATES.keys())}") + + if BRAVE_SEARCH_API_KEY: + logger.info("Web search enabled for market research.") + else: + logger.warning("Web search disabled.") + + logger.info("=" * 60) + + # Initialize database + logger.info("Initializing database...") + ScreenplayDatabase.init_db() + logger.info("Database initialization complete.") + + # Create and launch interface + interface = create_interface() + + interface.launch( + server_name="0.0.0.0", + server_port=7860, + share=False, + debug=True + ) \ No newline at end of file