Spaces:
Running
Running
import gradio as gr | |
import os | |
import json | |
import requests | |
from datetime import datetime | |
import time | |
from typing import List, Dict, Any, Generator, Tuple, Optional, Set | |
import logging | |
import re | |
import tempfile | |
from pathlib import Path | |
import sqlite3 | |
import hashlib | |
import threading | |
from contextlib import contextmanager | |
from dataclasses import dataclass, field, asdict | |
from collections import defaultdict | |
import random | |
from huggingface_hub import HfApi, upload_file, hf_hub_download | |
# --- Logging setup --- | |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') | |
logger = logging.getLogger(__name__) | |
# --- Document export imports --- | |
try: | |
from docx import Document | |
from docx.shared import Inches, Pt, RGBColor, Mm | |
from docx.enum.text import WD_ALIGN_PARAGRAPH | |
from docx.enum.style import WD_STYLE_TYPE | |
from docx.oxml.ns import qn | |
from docx.oxml import OxmlElement | |
DOCX_AVAILABLE = True | |
except ImportError: | |
DOCX_AVAILABLE = False | |
logger.warning("python-docx not installed. DOCX export will be disabled.") | |
# --- Environment variables and constants --- | |
FRIENDLI_TOKEN = os.getenv("FRIENDLI_TOKEN", "") | |
BRAVE_SEARCH_API_KEY = os.getenv("BRAVE_SEARCH_API_KEY", "") | |
API_URL = "https://api.friendli.ai/dedicated/v1/chat/completions" | |
MODEL_ID = "dep86pjolcjjnv8" | |
DB_PATH = "screenplay_sessions_v1.db" | |
# Screenplay length settings | |
SCREENPLAY_LENGTHS = { | |
"movie": {"pages": 110, "description": "Feature Film (90-120 pages)"}, | |
"tv_drama": {"pages": 55, "description": "TV Drama Episode (50-60 pages)"}, | |
"ott_series": {"pages": 45, "description": "OTT Series Episode (30-60 pages)"}, | |
"short_film": {"pages": 15, "description": "Short Film (10-20 pages)"} | |
} | |
# --- Environment validation --- | |
if not FRIENDLI_TOKEN: | |
logger.error("FRIENDLI_TOKEN not set. Application will not work properly.") | |
FRIENDLI_TOKEN = "dummy_token_for_testing" | |
if not BRAVE_SEARCH_API_KEY: | |
logger.warning("BRAVE_SEARCH_API_KEY not set. Web search features will be disabled.") | |
# --- Global variables --- | |
db_lock = threading.Lock() | |
# Genre templates | |
GENRE_TEMPLATES = { | |
"action": { | |
"pacing": "fast", | |
"scene_length": "short", | |
"dialogue_ratio": 0.3, | |
"key_elements": ["set pieces", "physical conflict", "urgency", "stakes escalation"], | |
"structure_beats": ["explosive opening", "pursuit/chase", "confrontation", "climactic battle"] | |
}, | |
"thriller": { | |
"pacing": "fast", | |
"scene_length": "short", | |
"dialogue_ratio": 0.35, | |
"key_elements": ["suspense", "twists", "paranoia", "time pressure"], | |
"structure_beats": ["hook", "mystery deepens", "false victory", "revelation", "final confrontation"] | |
}, | |
"drama": { | |
"pacing": "moderate", | |
"scene_length": "medium", | |
"dialogue_ratio": 0.5, | |
"key_elements": ["character depth", "emotional truth", "relationships", "internal conflict"], | |
"structure_beats": ["status quo", "catalyst", "debate", "commitment", "complications", "crisis", "resolution"] | |
}, | |
"comedy": { | |
"pacing": "fast", | |
"scene_length": "short", | |
"dialogue_ratio": 0.6, | |
"key_elements": ["setup/payoff", "timing", "character comedy", "escalation"], | |
"structure_beats": ["funny opening", "complication", "misunderstandings multiply", "chaos peak", "resolution with callback"] | |
}, | |
"horror": { | |
"pacing": "variable", | |
"scene_length": "mixed", | |
"dialogue_ratio": 0.3, | |
"key_elements": ["atmosphere", "dread", "jump scares", "gore/psychological"], | |
"structure_beats": ["normal world", "first sign", "investigation", "first attack", "survival", "final girl/boy"] | |
}, | |
"sci-fi": { | |
"pacing": "moderate", | |
"scene_length": "medium", | |
"dialogue_ratio": 0.4, | |
"key_elements": ["world building", "technology", "concepts", "visual spectacle"], | |
"structure_beats": ["ordinary world", "discovery", "new world", "complications", "understanding", "choice", "new normal"] | |
}, | |
"romance": { | |
"pacing": "moderate", | |
"scene_length": "medium", | |
"dialogue_ratio": 0.55, | |
"key_elements": ["chemistry", "obstacles", "emotional moments", "intimacy"], | |
"structure_beats": ["meet cute", "attraction", "first conflict", "deepening", "crisis/breakup", "grand gesture", "together"] | |
} | |
} | |
# Screenplay stages definition | |
SCREENPLAY_STAGES = [ | |
("producer", "🎬 Producer: Concept Development & Market Analysis"), | |
("story_developer", "📖 Story Developer: Synopsis & Three-Act Structure"), | |
("character_designer", "👥 Character Designer: Cast & Relationships"), | |
("critic_structure", "🔍 Structure Critic: Story & Character Review"), | |
("scene_planner", "🎯 Scene Planner: Detailed Scene Breakdown"), | |
("screenwriter", "✍️ Screenwriter: Act 1 - Setup (25%)"), | |
("script_doctor", "🔧 Script Doctor: Act 1 Review & Polish"), | |
("screenwriter", "✍️ Screenwriter: Act 2A - Rising Action (25%)"), | |
("script_doctor", "🔧 Script Doctor: Act 2A Review & Polish"), | |
("screenwriter", "✍️ Screenwriter: Act 2B - Complications (25%)"), | |
("script_doctor", "🔧 Script Doctor: Act 2B Review & Polish"), | |
("screenwriter", "✍️ Screenwriter: Act 3 - Resolution (25%)"), | |
("final_reviewer", "🎭 Final Review: Complete Screenplay Analysis"), | |
] | |
# Save the Cat Beat Sheet | |
SAVE_THE_CAT_BEATS = { | |
1: "Opening Image (0-1%)", | |
2: "Setup (1-10%)", | |
3: "Theme Stated (5%)", | |
4: "Catalyst (10%)", | |
5: "Debate (10-20%)", | |
6: "Break into Two (20%)", | |
7: "B Story (22%)", | |
8: "Fun and Games (20-50%)", | |
9: "Midpoint (50%)", | |
10: "Bad Guys Close In (50-75%)", | |
11: "All Is Lost (75%)", | |
12: "Dark Night of the Soul (75-80%)", | |
13: "Break into Three (80%)", | |
14: "Finale (80-99%)", | |
15: "Final Image (99-100%)" | |
} | |
# --- Data classes --- | |
class ScreenplayBible: | |
"""Screenplay bible for maintaining consistency""" | |
title: str = "" | |
logline: str = "" | |
genre: str = "" | |
subgenre: str = "" | |
tone: str = "" | |
themes: List[str] = field(default_factory=list) | |
# Characters | |
protagonist: Dict[str, Any] = field(default_factory=dict) | |
antagonist: Dict[str, Any] = field(default_factory=dict) | |
supporting_cast: Dict[str, Dict[str, Any]] = field(default_factory=dict) | |
# Structure | |
three_act_structure: Dict[str, str] = field(default_factory=dict) | |
save_the_cat_beats: Dict[int, str] = field(default_factory=dict) | |
# World | |
time_period: str = "" | |
primary_locations: List[Dict[str, str]] = field(default_factory=list) | |
world_rules: List[str] = field(default_factory=list) | |
# Visual style | |
visual_style: str = "" | |
key_imagery: List[str] = field(default_factory=list) | |
class SceneBreakdown: | |
"""Individual scene information""" | |
scene_number: int | |
act: int | |
location: str | |
time_of_day: str | |
characters: List[str] | |
purpose: str | |
conflict: str | |
page_count: float | |
beat: str = "" | |
transition: str = "CUT TO:" | |
class CharacterProfile: | |
"""Detailed character profile""" | |
name: str | |
role: str # protagonist, antagonist, supporting, etc. | |
archetype: str | |
want: str # External goal | |
need: str # Internal need | |
backstory: str | |
personality: List[str] | |
speech_pattern: str | |
character_arc: str | |
relationships: Dict[str, str] = field(default_factory=dict) | |
first_appearance: str = "" | |
# --- Core logic classes --- | |
class ScreenplayTracker: | |
"""Unified screenplay tracker""" | |
def __init__(self): | |
self.screenplay_bible = ScreenplayBible() | |
self.scenes: List[SceneBreakdown] = [] | |
self.characters: Dict[str, CharacterProfile] = {} | |
self.page_count = 0 | |
self.act_pages = {"1": 0, "2A": 0, "2B": 0, "3": 0} | |
self.dialogue_action_ratio = 0.0 | |
def add_scene(self, scene: SceneBreakdown): | |
"""Add scene to tracker""" | |
self.scenes.append(scene) | |
self.page_count += scene.page_count | |
def add_character(self, character: CharacterProfile): | |
"""Add character to tracker""" | |
self.characters[character.name] = character | |
# Update bible with main characters | |
if character.role == "protagonist": | |
self.screenplay_bible.protagonist = asdict(character) | |
elif character.role == "antagonist": | |
self.screenplay_bible.antagonist = asdict(character) | |
elif character.role == "supporting": | |
self.screenplay_bible.supporting_cast[character.name] = asdict(character) | |
def update_bible(self, key: str, value: Any): | |
"""Update screenplay bible""" | |
if hasattr(self.screenplay_bible, key): | |
setattr(self.screenplay_bible, key, value) | |
def get_act_page_target(self, act: str, total_pages: int) -> int: | |
"""Get target pages for each act""" | |
if act == "1": | |
return int(total_pages * 0.25) | |
elif act in ["2A", "2B"]: | |
return int(total_pages * 0.25) | |
elif act == "3": | |
return int(total_pages * 0.25) | |
return 0 | |
class ScreenplayDatabase: | |
"""Database management for screenplay sessions""" | |
def init_db(): | |
with sqlite3.connect(DB_PATH) as conn: | |
conn.execute("PRAGMA journal_mode=WAL") | |
cursor = conn.cursor() | |
# Main screenplay sessions table | |
cursor.execute(''' | |
CREATE TABLE IF NOT EXISTS screenplay_sessions ( | |
session_id TEXT PRIMARY KEY, | |
user_query TEXT NOT NULL, | |
screenplay_type TEXT NOT NULL, | |
genre TEXT NOT NULL, | |
subgenre TEXT, | |
target_pages INTEGER, | |
language TEXT NOT NULL, | |
title TEXT, | |
logline TEXT, | |
synopsis TEXT, | |
three_act_structure TEXT, | |
character_profiles TEXT, | |
scene_breakdown TEXT, | |
screenplay_bible TEXT, | |
final_screenplay TEXT, | |
pdf_path TEXT, | |
created_at TEXT DEFAULT (datetime('now')), | |
updated_at TEXT DEFAULT (datetime('now')), | |
status TEXT DEFAULT 'active', | |
current_stage INTEGER DEFAULT 0, | |
total_pages REAL DEFAULT 0 | |
) | |
''') | |
# Stages table | |
cursor.execute(''' | |
CREATE TABLE IF NOT EXISTS screenplay_stages ( | |
id INTEGER PRIMARY KEY AUTOINCREMENT, | |
session_id TEXT NOT NULL, | |
stage_number INTEGER NOT NULL, | |
stage_name TEXT NOT NULL, | |
role TEXT NOT NULL, | |
content TEXT, | |
page_count REAL DEFAULT 0, | |
status TEXT DEFAULT 'pending', | |
created_at TEXT DEFAULT (datetime('now')), | |
updated_at TEXT DEFAULT (datetime('now')), | |
FOREIGN KEY (session_id) REFERENCES screenplay_sessions(session_id), | |
UNIQUE(session_id, stage_number) | |
) | |
''') | |
# Scenes table | |
cursor.execute(''' | |
CREATE TABLE IF NOT EXISTS scenes ( | |
id INTEGER PRIMARY KEY AUTOINCREMENT, | |
session_id TEXT NOT NULL, | |
act_number INTEGER NOT NULL, | |
scene_number INTEGER NOT NULL, | |
location TEXT NOT NULL, | |
time_of_day TEXT NOT NULL, | |
characters TEXT, | |
purpose TEXT, | |
content TEXT, | |
page_count REAL, | |
created_at TEXT DEFAULT (datetime('now')), | |
FOREIGN KEY (session_id) REFERENCES screenplay_sessions(session_id) | |
) | |
''') | |
# Characters table | |
cursor.execute(''' | |
CREATE TABLE IF NOT EXISTS characters ( | |
id INTEGER PRIMARY KEY AUTOINCREMENT, | |
session_id TEXT NOT NULL, | |
character_name TEXT NOT NULL, | |
character_data TEXT, | |
created_at TEXT DEFAULT (datetime('now')), | |
FOREIGN KEY (session_id) REFERENCES screenplay_sessions(session_id), | |
UNIQUE(session_id, character_name) | |
) | |
''') | |
# Screenplay themes library | |
cursor.execute(''' | |
CREATE TABLE IF NOT EXISTS screenplay_themes_library ( | |
theme_id TEXT PRIMARY KEY, | |
theme_text TEXT NOT NULL, | |
screenplay_type TEXT NOT NULL, | |
genre TEXT NOT NULL, | |
language TEXT NOT NULL, | |
title TEXT, | |
logline TEXT, | |
protagonist_desc TEXT, | |
conflict_desc TEXT, | |
generated_at TEXT DEFAULT (datetime('now')), | |
view_count INTEGER DEFAULT 0, | |
used_count INTEGER DEFAULT 0, | |
tags TEXT, | |
metadata TEXT | |
) | |
''') | |
conn.commit() | |
def get_db(): | |
with db_lock: | |
conn = sqlite3.connect(DB_PATH, timeout=30.0) | |
conn.row_factory = sqlite3.Row | |
try: | |
yield conn | |
finally: | |
conn.close() | |
def create_session(user_query: str, screenplay_type: str, genre: str, language: str) -> str: | |
session_id = hashlib.md5(f"{user_query}{screenplay_type}{datetime.now()}".encode()).hexdigest() | |
target_pages = SCREENPLAY_LENGTHS[screenplay_type]["pages"] | |
with ScreenplayDatabase.get_db() as conn: | |
conn.cursor().execute( | |
'''INSERT INTO screenplay_sessions | |
(session_id, user_query, screenplay_type, genre, target_pages, language) | |
VALUES (?, ?, ?, ?, ?, ?)''', | |
(session_id, user_query, screenplay_type, genre, target_pages, language) | |
) | |
conn.commit() | |
return session_id | |
def save_stage(session_id: str, stage_number: int, stage_name: str, | |
role: str, content: str, status: str = 'complete'): | |
page_count = 0 | |
if role == "screenwriter" and content: | |
# Estimate pages based on screenplay format (rough estimate) | |
page_count = len(content.split('\n')) / 55 # ~55 lines per page | |
with ScreenplayDatabase.get_db() as conn: | |
cursor = conn.cursor() | |
cursor.execute(''' | |
INSERT INTO screenplay_stages | |
(session_id, stage_number, stage_name, role, content, page_count, status) | |
VALUES (?, ?, ?, ?, ?, ?, ?) | |
ON CONFLICT(session_id, stage_number) | |
DO UPDATE SET content=?, page_count=?, status=?, updated_at=datetime('now') | |
''', (session_id, stage_number, stage_name, role, content, page_count, status, | |
content, page_count, status)) | |
# Update session info | |
cursor.execute(''' | |
UPDATE screenplay_sessions | |
SET current_stage = ?, updated_at = datetime('now') | |
WHERE session_id = ? | |
''', (stage_number, session_id)) | |
conn.commit() | |
def save_screenplay_bible(session_id: str, bible: ScreenplayBible): | |
"""Save screenplay bible""" | |
with ScreenplayDatabase.get_db() as conn: | |
bible_json = json.dumps(asdict(bible)) | |
conn.cursor().execute( | |
'UPDATE screenplay_sessions SET screenplay_bible = ? WHERE session_id = ?', | |
(bible_json, session_id) | |
) | |
conn.commit() | |
def save_character(session_id: str, character: CharacterProfile): | |
"""Save character profile""" | |
with ScreenplayDatabase.get_db() as conn: | |
char_json = json.dumps(asdict(character)) | |
conn.cursor().execute( | |
'''INSERT INTO characters (session_id, character_name, character_data) | |
VALUES (?, ?, ?) | |
ON CONFLICT(session_id, character_name) | |
DO UPDATE SET character_data = ?''', | |
(session_id, character.name, char_json, char_json) | |
) | |
conn.commit() | |
def save_scene(session_id: str, scene: SceneBreakdown): | |
"""Save scene breakdown""" | |
with ScreenplayDatabase.get_db() as conn: | |
conn.cursor().execute( | |
'''INSERT INTO scenes | |
(session_id, act_number, scene_number, location, time_of_day, | |
characters, purpose, page_count) | |
VALUES (?, ?, ?, ?, ?, ?, ?, ?)''', | |
(session_id, scene.act, scene.scene_number, scene.location, | |
scene.time_of_day, json.dumps(scene.characters), scene.purpose, | |
scene.page_count) | |
) | |
conn.commit() | |
def get_screenplay_content(session_id: str) -> str: | |
"""Get complete screenplay content""" | |
with ScreenplayDatabase.get_db() as conn: | |
rows = conn.cursor().execute(''' | |
SELECT content FROM screenplay_stages | |
WHERE session_id = ? AND role = 'screenwriter' | |
ORDER BY stage_number | |
''', (session_id,)).fetchall() | |
if rows: | |
return '\n\n'.join(row['content'] for row in rows if row['content']) | |
return "" | |
def update_final_screenplay(session_id: str, final_screenplay: str, title: str, logline: str): | |
"""Update final screenplay""" | |
with ScreenplayDatabase.get_db() as conn: | |
total_pages = len(final_screenplay.split('\n')) / 55 | |
conn.cursor().execute( | |
'''UPDATE screenplay_sessions | |
SET final_screenplay = ?, title = ?, logline = ?, | |
total_pages = ?, status = 'complete', updated_at = datetime('now') | |
WHERE session_id = ?''', | |
(final_screenplay, title, logline, total_pages, session_id) | |
) | |
conn.commit() | |
def get_session(session_id: str) -> Optional[Dict]: | |
with ScreenplayDatabase.get_db() as conn: | |
row = conn.cursor().execute( | |
'SELECT * FROM screenplay_sessions WHERE session_id = ?', | |
(session_id,) | |
).fetchone() | |
return dict(row) if row else None | |
def get_stages(session_id: str) -> List[Dict]: | |
"""Get all stages for a session""" | |
with ScreenplayDatabase.get_db() as conn: | |
rows = conn.cursor().execute( | |
'''SELECT * FROM screenplay_stages | |
WHERE session_id = ? | |
ORDER BY stage_number''', | |
(session_id,) | |
).fetchall() | |
return [dict(row) for row in rows] | |
def get_active_sessions() -> List[Dict]: | |
with ScreenplayDatabase.get_db() as conn: | |
rows = conn.cursor().execute( | |
'''SELECT session_id, title, user_query, screenplay_type, genre, | |
created_at, current_stage, total_pages | |
FROM screenplay_sessions | |
WHERE status = 'active' | |
ORDER BY updated_at DESC | |
LIMIT 10''' | |
).fetchall() | |
return [dict(row) for row in rows] | |
def save_random_theme(theme_text: str, screenplay_type: str, genre: str, | |
language: str, metadata: Dict[str, Any]) -> str: | |
"""Save randomly generated screenplay theme""" | |
theme_id = hashlib.md5(f"{theme_text}{datetime.now()}".encode()).hexdigest()[:12] | |
title = metadata.get('title', '') | |
logline = metadata.get('logline', '') | |
protagonist_desc = metadata.get('protagonist', '') | |
conflict_desc = metadata.get('conflict', '') | |
tags = json.dumps(metadata.get('tags', [])) | |
with ScreenplayDatabase.get_db() as conn: | |
conn.cursor().execute(''' | |
INSERT INTO screenplay_themes_library | |
(theme_id, theme_text, screenplay_type, genre, language, title, logline, | |
protagonist_desc, conflict_desc, tags, metadata) | |
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) | |
''', (theme_id, theme_text, screenplay_type, genre, language, title, logline, | |
protagonist_desc, conflict_desc, tags, json.dumps(metadata))) | |
conn.commit() | |
return theme_id | |
class WebSearchIntegration: | |
"""Web search functionality for screenplay research""" | |
def __init__(self): | |
self.brave_api_key = BRAVE_SEARCH_API_KEY | |
self.search_url = "https://api.search.brave.com/res/v1/web/search" | |
self.enabled = bool(self.brave_api_key) | |
def search(self, query: str, count: int = 3, language: str = "en") -> List[Dict]: | |
if not self.enabled: | |
return [] | |
headers = { | |
"Accept": "application/json", | |
"X-Subscription-Token": self.brave_api_key | |
} | |
params = { | |
"q": query, | |
"count": count, | |
"search_lang": "ko" if language == "Korean" else "en", | |
"text_decorations": False, | |
"safesearch": "moderate" | |
} | |
try: | |
response = requests.get(self.search_url, headers=headers, params=params, timeout=10) | |
response.raise_for_status() | |
results = response.json().get("web", {}).get("results", []) | |
return results | |
except requests.exceptions.RequestException as e: | |
logger.error(f"Web search API error: {e}") | |
return [] | |
def extract_relevant_info(self, results: List[Dict], max_chars: int = 1500) -> str: | |
if not results: | |
return "" | |
extracted = [] | |
total_chars = 0 | |
for i, result in enumerate(results[:3], 1): | |
title = result.get("title", "") | |
description = result.get("description", "") | |
info = f"[{i}] {title}: {description}" | |
if total_chars + len(info) < max_chars: | |
extracted.append(info) | |
total_chars += len(info) | |
else: | |
break | |
return "\n".join(extracted) | |
class ScreenplayGenerationSystem: | |
"""Professional screenplay generation system""" | |
def __init__(self): | |
self.token = FRIENDLI_TOKEN | |
self.api_url = API_URL | |
self.model_id = MODEL_ID | |
self.screenplay_tracker = ScreenplayTracker() | |
self.web_search = WebSearchIntegration() | |
self.current_session_id = None | |
ScreenplayDatabase.init_db() | |
def create_headers(self): | |
"""Create headers for API request with proper token""" | |
if not self.token or self.token == "dummy_token_for_testing": | |
raise ValueError("Valid FRIENDLI_TOKEN is required") | |
return { | |
"Authorization": f"Bearer {self.token}", | |
"Content-Type": "application/json" | |
} | |
# --- Prompt generation functions --- | |
def create_producer_prompt(self, user_query: str, screenplay_type: str, | |
genre: str, language: str) -> str: | |
"""Producer initial concept development""" | |
# Web search for market trends if enabled | |
search_results = "" | |
if self.web_search.enabled: | |
queries = [ | |
f"box office success {genre} films 2024 2025", | |
f"popular {screenplay_type} {genre} trends", | |
f"audience demographics {genre} movies" | |
] | |
for q in queries[:2]: | |
results = self.web_search.search(q, count=2, language=language) | |
if results: | |
search_results += self.web_search.extract_relevant_info(results) + "\n" | |
lang_prompts = { | |
"Korean": f"""당신은 할리우드 프로듀서입니다. 상업적으로 성공 가능한 {screenplay_type} 컨셉을 개발하세요. | |
**요청사항:** {user_query} | |
**타입:** {SCREENPLAY_LENGTHS[screenplay_type]['description']} | |
**장르:** {genre} | |
**시장 조사:** | |
{search_results if search_results else "N/A"} | |
**필수 제공 항목:** | |
1. **제목 (TITLE)** | |
- 기억하기 쉽고 마케팅 가능한 제목 | |
- 장르와 톤을 암시하는 제목 | |
2. **로그라인 (LOGLINE)** | |
- 25단어 이내 한 문장 | |
- 형식: "[사건]이 일어났을 때, [주인공]은 [목표]를 이루어야 한다. 그렇지 않으면 [결과]" | |
- 갈등과 stakes가 명확해야 함 | |
3. **장르 분석** | |
- 주 장르: {genre} | |
- 서브 장르: | |
- 톤 & 분위기: | |
4. **타겟 관객** | |
- 주요 연령대: | |
- 성별 분포: | |
- 관심사: | |
- 유사 작품 팬층: | |
5. **비교 작품 (COMPS)** | |
- 3개의 성공한 유사 프로젝트 | |
- 각각의 박스오피스/시청률 성과 | |
- 우리 프로젝트와의 차별점 | |
6. **고유 판매 포인트 (USP)** | |
- 이 이야기만의 독특한 점 | |
- 현재 시장에서의 필요성 | |
- 제작 가능성 | |
7. **비주얼 컨셉** | |
- 핵심 비주얼 이미지 3개 | |
- 전체적인 룩 & 필 | |
구체적이고 시장성 있는 컨셉을 제시하세요.""", | |
"English": f"""You are a Hollywood producer. Develop a commercially viable {screenplay_type} concept. | |
**Request:** {user_query} | |
**Type:** {SCREENPLAY_LENGTHS[screenplay_type]['description']} | |
**Genre:** {genre} | |
**Market Research:** | |
{search_results if search_results else "N/A"} | |
**Required Elements:** | |
1. **TITLE** | |
- Memorable and marketable | |
- Hints at genre and tone | |
2. **LOGLINE** | |
- One sentence, 25 words max | |
- Format: "When [inciting incident], a [protagonist] must [objective] or else [stakes]" | |
- Clear conflict and stakes | |
3. **GENRE ANALYSIS** | |
- Primary Genre: {genre} | |
- Sub-genre: | |
- Tone & Mood: | |
4. **TARGET AUDIENCE** | |
- Primary Age Range: | |
- Gender Distribution: | |
- Interests: | |
- Similar Works Fanbase: | |
5. **COMPARABLE FILMS (COMPS)** | |
- 3 successful similar projects | |
- Box office/viewership performance | |
- How ours differs | |
6. **UNIQUE SELLING POINT (USP)** | |
- What makes this story unique | |
- Why now in the market | |
- Production feasibility | |
7. **VISUAL CONCEPT** | |
- 3 key visual images | |
- Overall look & feel | |
Provide specific, marketable concept.""" | |
} | |
return lang_prompts.get(language, lang_prompts["English"]) | |
def create_story_developer_prompt(self, producer_concept: str, user_query: str, | |
screenplay_type: str, genre: str, language: str) -> str: | |
"""Story developer for synopsis and structure""" | |
genre_template = GENRE_TEMPLATES.get(genre, GENRE_TEMPLATES["drama"]) | |
lang_prompts = { | |
"Korean": f"""당신은 스토리 개발자입니다. 프로듀서의 컨셉을 바탕으로 {screenplay_type}의 시놉시스와 3막 구조를 개발하세요. | |
**프로듀서 컨셉:** | |
{producer_concept} | |
**장르 특성:** {genre} | |
- 페이싱: {genre_template['pacing']} | |
- 핵심 요소: {', '.join(genre_template['key_elements'])} | |
- 구조 비트: {', '.join(genre_template['structure_beats'])} | |
**필수 작성 항목:** | |
1. **시놉시스 (SYNOPSIS)** | |
- 300-500단어 | |
- 3막 구조가 명확히 드러나도록 | |
- 주인공의 변화 arc 포함 | |
- 주요 전환점 명시 | |
- 결말 포함 (스포일러 OK) | |
2. **3막 구조 (THREE-ACT STRUCTURE)** | |
**제1막 - 설정 (Setup) [25%]** | |
- 일상 세계 (Ordinary World): | |
- 근친상간 사건 (Inciting Incident): | |
- 주인공 소개 및 결함: | |
- 1막 전환점 (Plot Point 1): | |
**제2막A - 상승 액션 (Rising Action) [25%]** | |
- 새로운 세계 진입: | |
- 재미와 게임 (Fun and Games): | |
- B 스토리 (관계/테마): | |
- 중간점 (Midpoint) - 가짜 승리/패배: | |
**제2막B - 복잡화 (Complications) [25%]** | |
- 악당의 반격: | |
- 팀 해체/위기: | |
- 모든 것을 잃음 (All Is Lost): | |
- 영혼의 어둔 밤: | |
**제3막 - 해결 (Resolution) [25%]** | |
- 2막 전환점 (Plot Point 2): | |
- 최종 전투 준비: | |
- 클라이맥스: | |
- 새로운 일상: | |
3. **Save the Cat 비트 시트** | |
15개 비트를 {SCREENPLAY_LENGTHS[screenplay_type]['pages']}페이지에 맞춰 배치 | |
4. **주제 (THEME)** | |
- 중심 주제: | |
- 주제가 드러나는 순간: | |
- 주제의 시각적 표현: | |
5. **톤 & 스타일** | |
- 전체적인 톤: | |
- 유머 사용 여부: | |
- 비주얼 스타일: | |
구체적이고 감정적으로 공감가는 스토리를 만드세요.""", | |
"English": f"""You are a story developer. Based on the producer's concept, develop the synopsis and three-act structure for this {screenplay_type}. | |
**Producer Concept:** | |
{producer_concept} | |
**Genre Characteristics:** {genre} | |
- Pacing: {genre_template['pacing']} | |
- Key Elements: {', '.join(genre_template['key_elements'])} | |
- Structure Beats: {', '.join(genre_template['structure_beats'])} | |
**Required Elements:** | |
1. **SYNOPSIS** | |
- 300-500 words | |
- Clear three-act structure | |
- Protagonist's change arc | |
- Major turning points | |
- Include ending (spoilers OK) | |
2. **THREE-ACT STRUCTURE** | |
**ACT 1 - Setup [25%]** | |
- Ordinary World: | |
- Inciting Incident: | |
- Protagonist Introduction & Flaw: | |
- Plot Point 1: | |
**ACT 2A - Rising Action [25%]** | |
- Entering New World: | |
- Fun and Games: | |
- B Story (Relationship/Theme): | |
- Midpoint - False Victory/Defeat: | |
**ACT 2B - Complications [25%]** | |
- Bad Guys Close In: | |
- Team Breaks Down/Crisis: | |
- All Is Lost: | |
- Dark Night of the Soul: | |
**ACT 3 - Resolution [25%]** | |
- Plot Point 2: | |
- Final Battle Preparation: | |
- Climax: | |
- New Normal: | |
3. **SAVE THE CAT BEAT SHEET** | |
Place 15 beats across {SCREENPLAY_LENGTHS[screenplay_type]['pages']} pages | |
4. **THEME** | |
- Central Theme: | |
- Theme Stated Moment: | |
- Visual Theme Expression: | |
5. **TONE & STYLE** | |
- Overall Tone: | |
- Use of Humor: | |
- Visual Style: | |
Create specific, emotionally resonant story.""" | |
} | |
return lang_prompts.get(language, lang_prompts["English"]) | |
def create_character_designer_prompt(self, producer_concept: str, story_structure: str, | |
genre: str, language: str) -> str: | |
"""Character designer prompt""" | |
lang_prompts = { | |
"Korean": f"""당신은 캐릭터 디자이너입니다. 다층적이고 매력적인 캐릭터들을 창조하세요. | |
**프로듀서 컨셉:** | |
{producer_concept} | |
**스토리 구조:** | |
{story_structure} | |
**필수 캐릭터 프로필:** | |
1. **주인공 (PROTAGONIST)** | |
- 이름: | |
- 직업/역할: | |
- 캐릭터 아크타입: | |
- WANT (외적 목표): | |
- NEED (내적 필요): | |
- 치명적 결함 (Fatal Flaw): | |
- 백스토리 (핵심 상처): | |
- 성격 특성 (3-5개): | |
- 말투 & 언어 패턴: | |
- 시각적 특징: | |
- 캐릭터 아크 (A→B): | |
2. **적대자 (ANTAGONIST)** | |
- 이름: | |
- 직업/역할: | |
- 악역 아크타입: | |
- 목표 & 동기: | |
- 주인공과의 연결점: | |
- 정당성 있는 이유: | |
- 약점: | |
- 특징적 행동: | |
3. **조력자들 (SUPPORTING CAST)** | |
최소 3명, 각각: | |
- 이름 & 역할: | |
- 주인공과의 관계: | |
- 스토리 기능: | |
- 독특한 특성: | |
- 기여하는 바: | |
4. **캐릭터 관계도** | |
- 주요 관계 역학: | |
- 갈등 구조: | |
- 감정적 연결: | |
- 파워 다이나믹: | |
5. **캐스팅 제안** | |
- 각 주요 캐릭터별 이상적인 배우 타입 | |
- 외모, 연기 스타일 | |
6. **대화 샘플** | |
- 각 주요 캐릭터의 시그니처 대사 2-3개 | |
- 캐릭터의 본질을 드러내는 대화 | |
각 캐릭터가 테마를 구현하고 스토리를 추진하도록 디자인하세요.""", | |
"English": f"""You are a character designer. Create multi-dimensional, compelling characters. | |
**Producer Concept:** | |
{producer_concept} | |
**Story Structure:** | |
{story_structure} | |
**Required Character Profiles:** | |
1. **PROTAGONIST** | |
- Name: | |
- Occupation/Role: | |
- Character Archetype: | |
- WANT (External Goal): | |
- NEED (Internal Need): | |
- Fatal Flaw: | |
- Backstory (Core Wound): | |
- Personality Traits (3-5): | |
- Speech Pattern: | |
- Visual Characteristics: | |
- Character Arc (A→B): | |
2. **ANTAGONIST** | |
- Name: | |
- Occupation/Role: | |
- Villain Archetype: | |
- Goal & Motivation: | |
- Connection to Protagonist: | |
- Justifiable Reason: | |
- Weakness: | |
- Signature Behaviors: | |
3. **SUPPORTING CAST** | |
Minimum 3, each with: | |
- Name & Role: | |
- Relationship to Protagonist: | |
- Story Function: | |
- Unique Traits: | |
- What They Contribute: | |
4. **CHARACTER RELATIONSHIPS** | |
- Key Relationship Dynamics: | |
- Conflict Structure: | |
- Emotional Connections: | |
- Power Dynamics: | |
5. **CASTING SUGGESTIONS** | |
- Ideal actor type for each major character | |
- Appearance, acting style | |
6. **DIALOGUE SAMPLES** | |
- 2-3 signature lines per major character | |
- Dialogue revealing character essence | |
Design each character to embody theme and drive story.""" | |
} | |
return lang_prompts.get(language, lang_prompts["English"]) | |
def create_scene_planner_prompt(self, story_structure: str, characters: str, | |
screenplay_type: str, genre: str, language: str) -> str: | |
"""Scene breakdown planner""" | |
total_pages = SCREENPLAY_LENGTHS[screenplay_type]['pages'] | |
lang_prompts = { | |
"Korean": f"""당신은 씬 플래너입니다. {total_pages}페이지 {screenplay_type}의 상세한 씬 브레이크다운을 작성하세요. | |
**스토리 구조:** | |
{story_structure} | |
**캐릭터:** | |
{characters} | |
**씬 브레이크다운 요구사항:** | |
각 씬마다 다음 정보 제공: | |
- 씬 번호 | |
- 장소 (INT./EXT. LOCATION) | |
- 시간 (DAY/NIGHT/DAWN/DUSK) | |
- 등장인물 | |
- 씬의 목적 (스토리/캐릭터/테마) | |
- 핵심 갈등 | |
- 예상 페이지 수 | |
- Save the Cat 비트 (해당시) | |
**막별 배분:** | |
- 1막: ~{int(total_pages * 0.25)}페이지 (10-12씬) | |
- 2막A: ~{int(total_pages * 0.25)}페이지 (12-15씬) | |
- 2막B: ~{int(total_pages * 0.25)}페이지 (12-15씬) | |
- 3막: ~{int(total_pages * 0.25)}페이지 (8-10씬) | |
**장르별 고려사항:** {genre} | |
{self._get_genre_scene_guidelines(genre, "Korean")} | |
**씬 전환 스타일:** | |
- CUT TO: | |
- FADE IN/OUT: | |
- MATCH CUT: | |
- SMASH CUT: | |
- DISSOLVE TO: | |
각 씬이 스토리를 전진시키고 캐릭터를 발전시키도록 계획하세요.""", | |
"English": f"""You are a scene planner. Create detailed scene breakdown for {total_pages}-page {screenplay_type}. | |
**Story Structure:** | |
{story_structure} | |
**Characters:** | |
{characters} | |
**Scene Breakdown Requirements:** | |
For each scene provide: | |
- Scene Number | |
- Location (INT./EXT. LOCATION) | |
- Time (DAY/NIGHT/DAWN/DUSK) | |
- Characters Present | |
- Scene Purpose (Story/Character/Theme) | |
- Core Conflict | |
- Estimated Page Count | |
- Save the Cat Beat (if applicable) | |
**Act Distribution:** | |
- Act 1: ~{int(total_pages * 0.25)} pages (10-12 scenes) | |
- Act 2A: ~{int(total_pages * 0.25)} pages (12-15 scenes) | |
- Act 2B: ~{int(total_pages * 0.25)} pages (12-15 scenes) | |
- Act 3: ~{int(total_pages * 0.25)} pages (8-10 scenes) | |
**Genre Considerations:** {genre} | |
{self._get_genre_scene_guidelines(genre, "English")} | |
**Scene Transitions:** | |
- CUT TO: | |
- FADE IN/OUT: | |
- MATCH CUT: | |
- SMASH CUT: | |
- DISSOLVE TO: | |
Plan each scene to advance story and develop character.""" | |
} | |
return lang_prompts.get(language, lang_prompts["English"]) | |
def create_screenwriter_prompt(self, act: str, scene_breakdown: str, | |
characters: str, previous_acts: str, | |
screenplay_type: str, genre: str, language: str) -> str: | |
"""Screenwriter prompt for actual screenplay pages""" | |
act_pages = int(SCREENPLAY_LENGTHS[screenplay_type]['pages'] * 0.25) | |
lang_prompts = { | |
"Korean": f"""당신은 프로 시나리오 작가입니다. {act}을 표준 시나리오 포맷으로 작성하세요. | |
**타겟 분량:** {act_pages}페이지 | |
**씬 브레이크다운:** | |
{self._extract_act_scenes(scene_breakdown, act)} | |
**캐릭터 정보:** | |
{characters} | |
**이전 내용:** | |
{previous_acts if previous_acts else "첫 막입니다."} | |
**시나리오 포맷 규칙:** | |
1. **씬 헤딩** | |
INT. 장소 - 시간 | |
EXT. 장소 - 시간 | |
2. **액션 라인** | |
- 현재 시제 사용 | |
- 시각적으로 보이는 것만 묘사 | |
- 4줄 이하로 유지 | |
- 감정은 행동으로 표현 | |
3. **캐릭터 소개** | |
첫 등장시: 이름과 간단한 묘사 | |
4. **대화** | |
캐릭터명 | |
(지문) | |
대사 | |
5. **중요 원칙** | |
- Show, don't tell | |
- 서브텍스트 활용 | |
- 자연스러운 대화 | |
- 시각적 스토리텔링 | |
- 페이지당 1분 규칙 | |
**{genre} 장르 특성:** | |
- 대화 비율: {GENRE_TEMPLATES[genre]['dialogue_ratio']*100}% | |
- 씬 길이: {GENRE_TEMPLATES[genre]['scene_length']} | |
- 핵심 요소: {', '.join(GENRE_TEMPLATES[genre]['key_elements'][:2])} | |
정확한 포맷과 몰입감 있는 스토리텔링으로 작성하세요.""", | |
"English": f"""You are a professional screenwriter. Write {act} in standard screenplay format. | |
**Target Length:** {act_pages} pages | |
**Scene Breakdown:** | |
{self._extract_act_scenes(scene_breakdown, act)} | |
**Character Info:** | |
{characters} | |
**Previous Content:** | |
{previous_acts if previous_acts else "This is the first act."} | |
**Screenplay Format Rules:** | |
1. **Scene Headings** | |
INT. LOCATION - TIME | |
EXT. LOCATION - TIME | |
2. **Action Lines** | |
- Present tense | |
- Only what's visually seen | |
- Keep under 4 lines | |
- Emotions through actions | |
3. **Character Intros** | |
First appearance: NAME with brief description | |
4. **Dialogue** | |
CHARACTER NAME | |
(parenthetical) | |
Dialogue | |
5. **Key Principles** | |
- Show, don't tell | |
- Use subtext | |
- Natural dialogue | |
- Visual storytelling | |
- One page = one minute | |
**{genre} Genre Characteristics:** | |
- Dialogue Ratio: {GENRE_TEMPLATES[genre]['dialogue_ratio']*100}% | |
- Scene Length: {GENRE_TEMPLATES[genre]['scene_length']} | |
- Key Elements: {', '.join(GENRE_TEMPLATES[genre]['key_elements'][:2])} | |
Write with proper format and engaging storytelling.""" | |
} | |
return lang_prompts.get(language, lang_prompts["English"]) | |
def create_script_doctor_prompt(self, act_content: str, act: str, | |
genre: str, language: str) -> str: | |
"""Script doctor review and polish""" | |
lang_prompts = { | |
"Korean": f"""당신은 스크립트 닥터입니다. {act}를 검토하고 개선 사항을 제시하세요. | |
**작성된 내용:** | |
{act_content} | |
**검토 항목:** | |
1. **포맷 정확성** | |
- 씬 헤딩 형식 | |
- 액션 라인 길이 | |
- 대화 포맷 | |
- 전환 표시 | |
2. **스토리텔링** | |
- 시각적 명확성 | |
- 페이싱 | |
- 긴장감 구축 | |
- 씬 목적 달성 | |
3. **대화 품질** | |
- 자연스러움 | |
- 캐릭터 고유성 | |
- 서브텍스트 | |
- 불필요한 설명 제거 | |
4. **{genre} 장르 적합성** | |
- 장르 관습 준수 | |
- 톤 일관성 | |
- 기대 충족 | |
5. **기술적 측면** | |
- 페이지 수 적정성 | |
- 제작 가능성 | |
- 예산 고려사항 | |
**필수 개선사항:** | |
구체적인 수정 제안과 개선된 예시를 제공하세요.""", | |
"English": f"""You are a script doctor. Review and provide improvements for {act}. | |
**Written Content:** | |
{act_content} | |
**Review Areas:** | |
1. **Format Accuracy** | |
- Scene heading format | |
- Action line length | |
- Dialogue format | |
- Transitions | |
2. **Storytelling** | |
- Visual clarity | |
- Pacing | |
- Tension building | |
- Scene purpose achievement | |
3. **Dialogue Quality** | |
- Naturalness | |
- Character uniqueness | |
- Subtext | |
- Remove exposition | |
4. **{genre} Genre Fit** | |
- Genre conventions | |
- Tone consistency | |
- Meeting expectations | |
5. **Technical Aspects** | |
- Page count appropriateness | |
- Production feasibility | |
- Budget considerations | |
**Required Improvements:** | |
Provide specific revision suggestions with improved examples.""" | |
} | |
return lang_prompts.get(language, lang_prompts["English"]) | |
def create_final_reviewer_prompt(self, complete_screenplay: str, | |
screenplay_type: str, genre: str, language: str) -> str: | |
"""Final comprehensive review""" | |
lang_prompts = { | |
"Korean": f"""당신은 최종 리뷰어입니다. 완성된 {screenplay_type} 시나리오를 종합 평가하세요. | |
**평가 기준:** | |
1. **상업성 (25점)** | |
- 시장성 | |
- 타겟 관객 어필 | |
- 제작 가능성 | |
- 배급 잠재력 | |
2. **스토리 (25점)** | |
- 3막 구조 효과성 | |
- 캐릭터 아크 | |
- 플롯 일관성 | |
- 테마 전달 | |
3. **기술적 완성도 (25점)** | |
- 포맷 정확성 | |
- 페이지 수 적정성 | |
- 씬 구성 | |
- 시각적 스토리텔링 | |
4. **대화 & 캐릭터 (25점)** | |
- 대화 자연스러움 | |
- 캐릭터 고유성 | |
- 관계 역학 | |
- 감정적 진정성 | |
**종합 평가:** | |
- 강점 (3-5개) | |
- 개선 필요 사항 (3-5개) | |
- 시장 잠재력 | |
- 추천 사항 | |
**등급:** A+ ~ F | |
구체적이고 건설적인 피드백을 제공하세요.""", | |
"English": f"""You are the final reviewer. Comprehensively evaluate the completed {screenplay_type} screenplay. | |
**Evaluation Criteria:** | |
1. **Commercial Viability (25 points)** | |
- Marketability | |
- Target audience appeal | |
- Production feasibility | |
- Distribution potential | |
2. **Story (25 points)** | |
- Three-act structure effectiveness | |
- Character arcs | |
- Plot consistency | |
- Theme delivery | |
3. **Technical Excellence (25 points)** | |
- Format accuracy | |
- Page count appropriateness | |
- Scene construction | |
- Visual storytelling | |
4. **Dialogue & Character (25 points)** | |
- Dialogue naturalness | |
- Character uniqueness | |
- Relationship dynamics | |
- Emotional authenticity | |
**Overall Assessment:** | |
- Strengths (3-5) | |
- Areas for Improvement (3-5) | |
- Market Potential | |
- Recommendations | |
**Grade:** A+ to F | |
Provide specific, constructive feedback.""" | |
} | |
return lang_prompts.get(language, lang_prompts["English"]) | |
def create_critic_structure_prompt(self, story_structure: str, characters: str, | |
screenplay_type: str, genre: str, language: str) -> str: | |
"""Structure critic prompt""" | |
lang_prompts = { | |
"Korean": f"""당신은 구조 비평가입니다. 스토리 구조와 캐릭터 설정을 심층 분석하세요. | |
**스토리 구조:** | |
{story_structure} | |
**캐릭터 설정:** | |
{characters} | |
**분석 항목:** | |
1. **3막 구조 효과성** | |
- 각 막의 균형 | |
- 전환점의 강도 | |
- 플롯 포인트의 명확성 | |
- 클라이맥스 위치 | |
2. **캐릭터 아크 타당성** | |
- 변화의 신빙성 | |
- 동기의 명확성 | |
- 내적/외적 목표 일치 | |
- 관계 역학 | |
3. **테마 통합** | |
- 테마의 일관성 | |
- 플롯과 테마 연결 | |
- 캐릭터와 테마 연결 | |
- 시각적 테마 표현 | |
4. **장르 기대치** | |
- {genre} 관습 충족 | |
- 독창성과 친숙함 균형 | |
- 타겟 관객 만족도 | |
5. **제작 현실성** | |
- 예산 규모 적정성 | |
- 로케이션 실현 가능성 | |
- 특수효과 요구사항 | |
**필수 개선 제안:** | |
각 문제점에 대한 구체적 해결책을 제시하세요.""", | |
"English": f"""You are a structure critic. Deeply analyze story structure and character setup. | |
**Story Structure:** | |
{story_structure} | |
**Character Setup:** | |
{characters} | |
**Analysis Items:** | |
1. **Three-Act Structure Effectiveness** | |
- Balance of each act | |
- Strength of transitions | |
- Clarity of plot points | |
- Climax positioning | |
2. **Character Arc Validity** | |
- Credibility of change | |
- Clarity of motivation | |
- Internal/external goal alignment | |
- Relationship dynamics | |
3. **Theme Integration** | |
- Theme consistency | |
- Plot-theme connection | |
- Character-theme connection | |
- Visual theme expression | |
4. **Genre Expectations** | |
- Meeting {genre} conventions | |
- Balance of originality and familiarity | |
- Target audience satisfaction | |
5. **Production Reality** | |
- Budget scale appropriateness | |
- Location feasibility | |
- Special effects requirements | |
**Required Improvement Suggestions:** | |
Provide specific solutions for each issue.""" | |
} | |
return lang_prompts.get(language, lang_prompts["English"]) | |
def _get_genre_scene_guidelines(self, genre: str, language: str) -> str: | |
"""Get genre-specific scene guidelines""" | |
guidelines = { | |
"action": { | |
"Korean": "- 짧고 펀치감 있는 씬\n- 액션 시퀀스 상세 계획\n- 긴장감 지속", | |
"English": "- Short, punchy scenes\n- Detailed action sequences\n- Maintain tension" | |
}, | |
"thriller": { | |
"Korean": "- 서스펜스 구축\n- 정보 점진적 공개\n- 반전 배치", | |
"English": "- Build suspense\n- Gradual information reveal\n- Place twists" | |
}, | |
"drama": { | |
"Korean": "- 감정적 비트 강조\n- 캐릭터 중심 씬\n- 대화 공간 확보", | |
"English": "- Emphasize emotional beats\n- Character-driven scenes\n- Allow dialogue space" | |
}, | |
"comedy": { | |
"Korean": "- 셋업과 페이오프\n- 코믹 타이밍\n- 시각적 개그", | |
"English": "- Setup and payoff\n- Comic timing\n- Visual gags" | |
}, | |
"horror": { | |
"Korean": "- 분위기 조성\n- 점프 스케어 배치\n- 긴장과 이완", | |
"English": "- Atmosphere building\n- Jump scare placement\n- Tension and release" | |
}, | |
"sci-fi": { | |
"Korean": "- 세계관 설명\n- 시각적 스펙터클\n- 개념 소개", | |
"English": "- World building\n- Visual spectacle\n- Concept introduction" | |
}, | |
"romance": { | |
"Korean": "- 감정적 친밀감\n- 관계 발전\n- 로맨틱 비트", | |
"English": "- Emotional intimacy\n- Relationship progression\n- Romantic beats" | |
} | |
} | |
return guidelines.get(genre, guidelines["drama"]).get(language, "") | |
def _extract_act_scenes(self, scene_breakdown: str, act: str) -> str: | |
"""Extract scenes for specific act""" | |
# This would parse the scene breakdown and return only scenes for the requested act | |
# For now, returning a placeholder | |
return f"Scenes for {act} from the breakdown" | |
# --- LLM call functions --- | |
def call_llm_sync(self, messages: List[Dict[str, str]], role: str, language: str) -> str: | |
full_content = "" | |
for chunk in self.call_llm_streaming(messages, role, language): | |
full_content += chunk | |
if full_content.startswith("❌"): | |
raise Exception(f"LLM Call Failed: {full_content}") | |
return full_content | |
def call_llm_streaming(self, messages: List[Dict[str, str]], role: str, | |
language: str) -> Generator[str, None, None]: | |
try: | |
# Debug logging | |
logger.info(f"Calling LLM for role: {role}, language: {language}") | |
system_prompts = self.get_system_prompts(language) | |
system_content = system_prompts.get(role, "") | |
if not system_content: | |
logger.warning(f"No system prompt found for role: {role}") | |
system_content = "You are a helpful assistant." | |
full_messages = [ | |
{"role": "system", "content": system_content}, | |
*messages | |
] | |
max_tokens = 15000 if role == "screenwriter" else 8000 | |
payload = { | |
"model": self.model_id, | |
"messages": full_messages, | |
"max_tokens": max_tokens, | |
"temperature": 0.7 if role in ["screenwriter", "script_doctor"] else 0.8, | |
"top_p": 0.9, | |
"presence_penalty": 0.3, | |
"frequency_penalty": 0.3, | |
"stream": True | |
} | |
# Debug logging | |
logger.debug(f"API URL: {self.api_url}") | |
logger.debug(f"Model ID: {self.model_id}") | |
try: | |
headers = self.create_headers() | |
except ValueError as e: | |
logger.error(f"Header creation failed: {e}") | |
yield f"❌ API configuration error: {e}" | |
return | |
response = requests.post( | |
self.api_url, | |
headers=headers, | |
json=payload, | |
stream=True, | |
timeout=180 | |
) | |
logger.info(f"API Response Status: {response.status_code}") | |
if response.status_code != 200: | |
error_msg = f"API Error (Status Code: {response.status_code})" | |
try: | |
error_data = response.json() | |
logger.error(f"API Error Response: {error_data}") | |
if isinstance(error_data, dict): | |
if 'error' in error_data: | |
error_msg += f" - {error_data['error']}" | |
elif 'message' in error_data: | |
error_msg += f" - {error_data['message']}" | |
except Exception as e: | |
logger.error(f"Error parsing error response: {e}") | |
error_msg += f" - {response.text[:200]}" | |
yield f"❌ {error_msg}" | |
return | |
buffer = "" | |
line_count = 0 | |
for line in response.iter_lines(): | |
if not line: | |
continue | |
line_count += 1 | |
try: | |
line_str = line.decode('utf-8').strip() | |
# Skip non-SSE lines | |
if not line_str.startswith("data: "): | |
logger.debug(f"Skipping non-SSE line: {line_str[:50]}") | |
continue | |
data_str = line_str[6:] # Remove "data: " prefix | |
if data_str == "[DONE]": | |
logger.info(f"Stream completed. Total lines: {line_count}") | |
break | |
if not data_str: | |
continue | |
# Parse JSON data | |
try: | |
data = json.loads(data_str) | |
except json.JSONDecodeError as e: | |
logger.warning(f"JSON decode error on line {line_count}: {e}") | |
logger.debug(f"Problematic data: {data_str[:100]}") | |
continue | |
# Extract content from response | |
if isinstance(data, dict) and "choices" in data: | |
choices = data["choices"] | |
if isinstance(choices, list) and len(choices) > 0: | |
choice = choices[0] | |
if isinstance(choice, dict) and "delta" in choice: | |
delta = choice["delta"] | |
if isinstance(delta, dict) and "content" in delta: | |
content = delta["content"] | |
if content: | |
buffer += content | |
# Yield when buffer is large enough | |
if len(buffer) >= 50 or '\n' in buffer: | |
yield buffer | |
buffer = "" | |
time.sleep(0.01) | |
except Exception as e: | |
logger.error(f"Error processing line {line_count}: {str(e)}") | |
logger.debug(f"Problematic line: {line_str[:100] if 'line_str' in locals() else 'N/A'}") | |
continue | |
# Yield any remaining buffer content | |
if buffer: | |
yield buffer | |
# Check if we got any content | |
if line_count == 0: | |
logger.error("No lines received from API") | |
yield "❌ No response from API" | |
except requests.exceptions.Timeout: | |
logger.error("API request timed out") | |
yield "❌ Request timed out. Please try again." | |
except requests.exceptions.ConnectionError as e: | |
logger.error(f"Connection error: {e}") | |
yield "❌ Connection error. Please check your internet connection." | |
except requests.exceptions.RequestException as e: | |
logger.error(f"Request error: {type(e).__name__}: {str(e)}") | |
yield f"❌ Network error: {str(e)}" | |
except Exception as e: | |
logger.error(f"Unexpected error in streaming: {type(e).__name__}: {str(e)}") | |
import traceback | |
logger.error(traceback.format_exc()) | |
yield f"❌ Unexpected error: {str(e)}" | |
def get_system_prompts(self, language: str) -> Dict[str, str]: | |
"""Role-specific system prompts""" | |
base_prompts = { | |
"Korean": { | |
"producer": """당신은 20년 경력의 할리우드 프로듀서입니다. | |
상업적 성공과 예술적 가치를 모두 추구합니다. | |
시장 트렌드와 관객 심리를 정확히 파악합니다. | |
실현 가능하고 매력적인 프로젝트를 개발합니다.""", | |
"story_developer": """당신은 수상 경력이 있는 스토리 개발자입니다. | |
감정적으로 공감가고 구조적으로 탄탄한 이야기를 만듭니다. | |
캐릭터의 내적 여정과 외적 플롯을 조화롭게 엮습니다. | |
보편적 주제를 독특한 방식으로 탐구합니다.""", | |
"character_designer": """당신은 심리학을 공부한 캐릭터 디자이너입니다. | |
진짜 같은 인물들을 창조하는 전문가입니다. | |
각 캐릭터에게 고유한 목소리와 관점을 부여합니다. | |
복잡하고 모순적인 인간성을 포착합니다.""", | |
"scene_planner": """당신은 정밀한 씬 구성의 대가입니다. | |
각 씬이 스토리와 캐릭터를 전진시키도록 설계합니다. | |
리듬과 페이싱을 완벽하게 조절합니다. | |
시각적 스토리텔링을 극대화합니다.""", | |
"screenwriter": """당신은 다작의 시나리오 작가입니다. | |
'보여주기'의 대가이며 서브텍스트를 능숙하게 다룹니다. | |
생생하고 자연스러운 대화를 쓰는 전문가입니다. | |
제작 현실을 고려하면서도 창의적인 해결책을 찾습니다.""", | |
"script_doctor": """당신은 까다로운 스크립트 닥터입니다. | |
작은 디테일도 놓치지 않는 완벽주의자입니다. | |
스토리의 잠재력을 최대한 끌어냅니다. | |
건설적이고 구체적인 개선안을 제시합니다.""", | |
"critic_structure": """당신은 구조 분석 전문가입니다. | |
스토리의 뼈대와 근육을 꿰뚫어 봅니다. | |
논리적 허점과 감정적 공백을 찾아냅니다. | |
더 나은 구조를 위한 구체적 제안을 합니다.""", | |
"final_reviewer": """당신은 업계 베테랑 최종 리뷰어입니다. | |
상업성과 예술성을 균형있게 평가합니다. | |
제작사, 배우, 관객 모든 관점을 고려합니다. | |
냉정하지만 격려하는 피드백을 제공합니다.""" | |
}, | |
"English": { | |
"producer": """You are a Hollywood producer with 20 years experience. | |
You pursue both commercial success and artistic value. | |
You accurately grasp market trends and audience psychology. | |
You develop feasible and attractive projects.""", | |
"story_developer": """You are an award-winning story developer. | |
You create emotionally resonant and structurally sound stories. | |
You harmoniously weave internal journeys with external plots. | |
You explore universal themes in unique ways.""", | |
"character_designer": """You are a character designer who studied psychology. | |
You're an expert at creating lifelike characters. | |
You give each character a unique voice and perspective. | |
You capture complex and contradictory humanity.""", | |
"scene_planner": """You are a master of precise scene construction. | |
You design each scene to advance story and character. | |
You perfectly control rhythm and pacing. | |
You maximize visual storytelling.""", | |
"screenwriter": """You are a prolific screenwriter. | |
You're a master of 'showing' and skilled with subtext. | |
You're an expert at writing vivid, natural dialogue. | |
You find creative solutions while considering production reality.""", | |
"script_doctor": """You are a demanding script doctor. | |
You're a perfectionist who misses no small detail. | |
You maximize the story's potential. | |
You provide constructive and specific improvements.""", | |
"critic_structure": """You are a structure analysis expert. | |
You see through the story's skeleton and muscles. | |
You find logical gaps and emotional voids. | |
You make specific suggestions for better structure.""", | |
"final_reviewer": """You are an industry veteran final reviewer. | |
You evaluate commercial and artistic value in balance. | |
You consider all perspectives: producers, actors, audience. | |
You provide feedback that's critical yet encouraging.""" | |
} | |
} | |
return base_prompts.get(language, base_prompts["English"]) | |
# --- Main process --- | |
def process_screenplay_stream(self, query: str, screenplay_type: str, genre: str, | |
language: str, session_id: Optional[str] = None | |
) -> Generator[Tuple[str, List[Dict[str, Any]], str], None, None]: | |
"""Main screenplay generation process""" | |
try: | |
resume_from_stage = 0 | |
if session_id: | |
self.current_session_id = session_id | |
session = ScreenplayDatabase.get_session(session_id) | |
if session: | |
query = session['user_query'] | |
screenplay_type = session['screenplay_type'] | |
genre = session['genre'] | |
language = session['language'] | |
resume_from_stage = session['current_stage'] + 1 | |
else: | |
self.current_session_id = ScreenplayDatabase.create_session( | |
query, screenplay_type, genre, language | |
) | |
logger.info(f"Created new screenplay session: {self.current_session_id}") | |
stages = [] | |
if resume_from_stage > 0: | |
# Get existing stages from database | |
db_stages = ScreenplayDatabase.get_stages(self.current_session_id) | |
stages = [{ | |
"name": s['stage_name'], | |
"status": s['status'], | |
"content": s.get('content', ''), | |
"page_count": s.get('page_count', 0) | |
} for s in db_stages] | |
for stage_idx in range(resume_from_stage, len(SCREENPLAY_STAGES)): | |
role, stage_name = SCREENPLAY_STAGES[stage_idx] | |
if stage_idx >= len(stages): | |
stages.append({ | |
"name": stage_name, | |
"status": "active", | |
"content": "", | |
"page_count": 0 | |
}) | |
else: | |
stages[stage_idx]["status"] = "active" | |
yield f"🔄 Processing {stage_name}...", stages, self.current_session_id | |
prompt = self.get_stage_prompt(stage_idx, role, query, screenplay_type, | |
genre, language, stages) | |
stage_content = "" | |
for chunk in self.call_llm_streaming([{"role": "user", "content": prompt}], | |
role, language): | |
stage_content += chunk | |
stages[stage_idx]["content"] = stage_content | |
if role == "screenwriter": | |
stages[stage_idx]["page_count"] = len(stage_content.split('\n')) / 55 | |
yield f"🔄 {stage_name} in progress...", stages, self.current_session_id | |
# Process content based on role | |
if role == "producer": | |
self._process_producer_content(stage_content) | |
elif role == "story_developer": | |
self._process_story_content(stage_content) | |
elif role == "character_designer": | |
self._process_character_content(stage_content) | |
elif role == "scene_planner": | |
self._process_scene_content(stage_content) | |
stages[stage_idx]["status"] = "complete" | |
ScreenplayDatabase.save_stage( | |
self.current_session_id, stage_idx, stage_name, role, | |
stage_content, "complete" | |
) | |
yield f"✅ {stage_name} completed", stages, self.current_session_id | |
# Final processing | |
final_screenplay = ScreenplayDatabase.get_screenplay_content(self.current_session_id) | |
title = self.screenplay_tracker.screenplay_bible.title | |
logline = self.screenplay_tracker.screenplay_bible.logline | |
ScreenplayDatabase.update_final_screenplay( | |
self.current_session_id, final_screenplay, title, logline | |
) | |
yield f"✅ Screenplay completed! {title}", stages, self.current_session_id | |
except Exception as e: | |
logger.error(f"Screenplay generation error: {e}", exc_info=True) | |
yield f"❌ Error occurred: {e}", stages if 'stages' in locals() else [], self.current_session_id | |
def get_stage_prompt(self, stage_idx: int, role: str, query: str, | |
screenplay_type: str, genre: str, language: str, | |
stages: List[Dict]) -> str: | |
"""Generate stage-specific prompt""" | |
if stage_idx == 0: # Producer | |
return self.create_producer_prompt(query, screenplay_type, genre, language) | |
if stage_idx == 1: # Story Developer | |
return self.create_story_developer_prompt( | |
stages[0]["content"], query, screenplay_type, genre, language | |
) | |
if stage_idx == 2: # Character Designer | |
return self.create_character_designer_prompt( | |
stages[0]["content"], stages[1]["content"], genre, language | |
) | |
if stage_idx == 3: # Structure Critic | |
return self.create_critic_structure_prompt( | |
stages[1]["content"], stages[2]["content"], screenplay_type, genre, language | |
) | |
if stage_idx == 4: # Scene Planner | |
return self.create_scene_planner_prompt( | |
stages[1]["content"], stages[2]["content"], screenplay_type, genre, language | |
) | |
# Screenwriter acts | |
if role == "screenwriter": | |
act_mapping = {5: "Act 1", 7: "Act 2A", 9: "Act 2B", 11: "Act 3"} | |
if stage_idx in act_mapping: | |
act = act_mapping[stage_idx] | |
previous_acts = self._get_previous_acts(stages, stage_idx) | |
return self.create_screenwriter_prompt( | |
act, stages[4]["content"], stages[2]["content"], | |
previous_acts, screenplay_type, genre, language | |
) | |
# Script doctor reviews | |
if role == "script_doctor": | |
act_mapping = {6: "Act 1", 8: "Act 2A", 10: "Act 2B"} | |
if stage_idx in act_mapping: | |
act = act_mapping[stage_idx] | |
act_content = stages[stage_idx-1]["content"] | |
return self.create_script_doctor_prompt(act_content, act, genre, language) | |
# Final reviewer | |
if role == "final_reviewer": | |
complete_screenplay = ScreenplayDatabase.get_screenplay_content(self.current_session_id) | |
return self.create_final_reviewer_prompt( | |
complete_screenplay, screenplay_type, genre, language | |
) | |
return "" | |
def _get_previous_acts(self, stages: List[Dict], current_idx: int) -> str: | |
"""Get previous acts content""" | |
previous = [] | |
act_indices = {5: [], 7: [5], 9: [5, 7], 11: [5, 7, 9]} | |
if current_idx in act_indices: | |
for idx in act_indices[current_idx]: | |
if idx < len(stages) and stages[idx]["content"]: | |
previous.append(stages[idx]["content"]) | |
return "\n\n---\n\n".join(previous) if previous else "" | |
def _extract_field(self, content: str, field_pattern: str) -> Optional[str]: | |
"""Extract field value from content with improved parsing""" | |
pattern = rf'{field_pattern}[:\s]*([^\n]+?)(?=\n[A-Z가-힣]|$)' | |
match = re.search(pattern, content, re.IGNORECASE | re.DOTALL) | |
if match and match.group(1): | |
value = match.group(1).strip() | |
value = re.sub(r'\*\*', '', value) # **bold** 제거 | |
value = re.sub(r'^\s*[-•]\s*', '', value) # 글머리표 제거 | |
value = re.sub(r'[,.:;]+$', '', value) # 행 끝 구두점 제거 | |
return value.strip() if value else None | |
return None | |
def _parse_character_profile(self, content: str, role: str) -> CharacterProfile: | |
"""Parse character profile from content""" | |
logger.debug(f"Parsing character profile for role: {role}") | |
logger.debug(f"Content preview: {content[:200]}...") | |
# 1) 이름 추출 ─ 패턴 3종 | |
name = f"Character_{role}" # fallback | |
name_patterns = [ | |
r'(?:이름|Name)[:\s]*([^\n,(]+)', # "이름: 홍길동" | |
r'^\s*[-*•]\s*([^\n,(]+)', # "- 홍길동" | |
r'^([^\n,(]+)' # 문단 첫 단어 | |
] | |
for pat in name_patterns: | |
m = re.search(pat, content, re.IGNORECASE | re.MULTILINE) | |
if m and m.group(1).strip(): | |
name = re.sub(r'[\*:\s]+', '', m.group(1).strip()) # 불필요 기호 제거 | |
break | |
# 2) 필드 추출용 헬퍼 | |
def extract_clean_field(pats): | |
pats = [pats] if isinstance(pats, str) else pats | |
for p in pats: | |
m = re.search(rf'{p}[:\s]*([^\n*]+?)(?=\n|$)', content, | |
re.IGNORECASE | re.DOTALL) | |
if m and m.group(1).strip(): | |
v = m.group(1).strip() | |
v = re.sub(r'^[-*•:\s]+', '', v) # 리스트·기호 제거 | |
v = v.replace('*', '').strip() | |
return v | |
return "" | |
# 3) Personality(여러 줄) 따로 파싱 | |
def extract_traits(): | |
section = re.search(r'(?:Personality|성격[^\n]*)\n((?:[-*•].+\n?)+)', | |
content, re.IGNORECASE) | |
if not section: | |
return [] | |
traits = [ | |
re.sub(r'^[-*•]\s*', '', line.strip()) | |
for line in section.group(1).splitlines() if line.strip() | |
] | |
return traits[:5] | |
# 4) CharacterProfile 생성 | |
return CharacterProfile( | |
name=name, | |
role=role, | |
archetype=extract_clean_field( | |
[r"캐릭터 아크타입", r"Character Archetype", r"Archetype", r"아크타입"] | |
), | |
want=extract_clean_field( | |
[r"WANT\s*\(외적 목표\)", r"WANT", r"외적 목표", r"External Goal"] | |
), | |
need=extract_clean_field( | |
[r"NEED\s*\(내적 필요\)", r"NEED", r"내적 필요", r"Internal Need"] | |
), | |
backstory=extract_clean_field( | |
[r"백스토리", r"Backstory", r"핵심 상처", r"Core Wound"] | |
), | |
personality=extract_traits(), | |
speech_pattern=extract_clean_field( | |
[r"말투.*?패턴", r"Speech Pattern", r"언어 패턴", r"말투"] | |
), | |
character_arc=extract_clean_field( | |
[r"캐릭터 아크", r"Character Arc", r"Arc", r"변화"] | |
), | |
) | |
def _extract_personality_traits(self, content: str) -> List[str]: | |
"""Extract personality traits from content""" | |
traits = [] | |
# Look for personality section with multiple pattern options | |
personality_patterns = [ | |
r"(?:Personality|성격 특성|성격)[:\s]*([^\n]+(?:\n(?![\w가-힣]+:)[^\n]+)*)", | |
r"성격[:\s]*(?:\n?[-•*]\s*[^\n]+)+" | |
] | |
for pattern in personality_patterns: | |
match = re.search(pattern, content, re.IGNORECASE | re.DOTALL) | |
if match and match.group(1): | |
personality_section = match.group(1) | |
# Extract individual traits (usually listed) | |
trait_lines = personality_section.split('\n') | |
for line in trait_lines: | |
line = line.strip() | |
if line and not line.endswith(':'): | |
# Remove list markers | |
trait = re.sub(r'^\s*[-•*]\s*', '', line) | |
trait = re.sub(r'^\d+\.\s*', '', trait) # Remove numbered lists | |
if trait and len(trait) > 2: # Skip very short entries | |
traits.append(trait) | |
if traits: # If we found traits, stop looking | |
break | |
return traits[:5] # Limit to 5 traits | |
def _process_character_content(self, content: str): | |
"""Process character designer output with better error handling""" | |
try: | |
# Extract protagonist | |
protagonist_section = self._extract_section(content, r"(?:PROTAGONIST|주인공)") | |
if protagonist_section: | |
try: | |
protagonist = self._parse_character_profile(protagonist_section, "protagonist") | |
self.screenplay_tracker.add_character(protagonist) | |
ScreenplayDatabase.save_character(self.current_session_id, protagonist) | |
except Exception as e: | |
logger.error(f"Error parsing protagonist: {e}") | |
# Create a default protagonist to continue | |
protagonist = CharacterProfile( | |
name="Protagonist", | |
role="protagonist", | |
archetype="Hero", | |
want="To achieve goal", | |
need="To grow", | |
backstory="Unknown", | |
personality=["Determined"], | |
speech_pattern="Normal", | |
character_arc="Growth" | |
) | |
self.screenplay_tracker.add_character(protagonist) | |
# Extract antagonist | |
antagonist_section = self._extract_section(content, r"(?:ANTAGONIST|적대자)") | |
if antagonist_section: | |
try: | |
antagonist = self._parse_character_profile(antagonist_section, "antagonist") | |
self.screenplay_tracker.add_character(antagonist) | |
ScreenplayDatabase.save_character(self.current_session_id, antagonist) | |
except Exception as e: | |
logger.error(f"Error parsing antagonist: {e}") | |
# Create a default antagonist to continue | |
antagonist = CharacterProfile( | |
name="Antagonist", | |
role="antagonist", | |
archetype="Villain", | |
want="To stop protagonist", | |
need="Power", | |
backstory="Unknown", | |
personality=["Ruthless"], | |
speech_pattern="Menacing", | |
character_arc="Downfall" | |
) | |
self.screenplay_tracker.add_character(antagonist) | |
# Extract supporting characters | |
supporting_section = self._extract_section(content, r"(?:SUPPORTING CAST|조력자들)") | |
if supporting_section: | |
# Parse multiple supporting characters | |
self._parse_supporting_characters(supporting_section) | |
except Exception as e: | |
logger.error(f"Error processing character content: {e}") | |
# Continue with default values rather than failing | |
def _extract_section(self, content: str, section_pattern: str) -> str: | |
"""Extract section from content with improved pattern matching""" | |
# More flexible section extraction | |
patterns = [ | |
# Pattern 1: Section header followed by content until next major section | |
rf'{section_pattern}[:\s]*\n?(.*?)(?=\n\n[A-Z가-힣]{{2,}}[:\s]|\n\n\d+\.|$)', | |
# Pattern 2: Section header with content until next section (alternative) | |
rf'{section_pattern}.*?\n((?:.*\n)*?)(?=\n[A-Z가-힣]{{2,}}:|$)', | |
# Pattern 3: More flexible pattern for Korean text | |
rf'{section_pattern}[:\s]*\n?((?:[^\n]+\n?)*?)(?=\n\n|\Z)' | |
] | |
for pattern in patterns: | |
match = re.search(pattern, content, re.IGNORECASE | re.DOTALL) | |
if match and match.group(1): | |
section_content = match.group(1).strip() | |
if section_content: # Only return if we got actual content | |
return section_content | |
return "" | |
def _parse_supporting_characters(self, content: str): | |
"""Parse supporting characters from content""" | |
# Split by character markers (numbers or bullets) | |
char_sections = re.split(r'\n(?:\d+\.|[-•*])\s*', content) | |
for i, section in enumerate(char_sections[1:], 1): # Skip first empty split | |
if section.strip(): | |
try: | |
# Try multiple name extraction patterns | |
name = None | |
name_patterns = [ | |
r"(?:이름|Name)[:\s]*([^,\n]+)", | |
r"^([^:\n]+?)(?:\s*[-–]\s*|:\s*)", # Name at start before dash or colon | |
r"^([가-힣A-Za-z\s]+?)(?:\s*\(|$)" # Korean/English name before parenthesis | |
] | |
for pattern in name_patterns: | |
name_match = re.search(pattern, section.strip(), re.IGNORECASE) | |
if name_match and name_match.group(1): | |
name = name_match.group(1).strip() | |
if name and len(name) > 1: | |
break | |
if not name: | |
name = f"Supporting_{i}" | |
role_desc = self._extract_field(section, r"(?:Role|역할)[:\s]*") or "supporting" | |
character = CharacterProfile( | |
name=name, | |
role="supporting", | |
archetype=role_desc, | |
want="", | |
need="", | |
backstory=self._extract_field(section, r"(?:Backstory|백스토리)[:\s]*") or "", | |
personality=[], | |
speech_pattern="", | |
character_arc="" | |
) | |
self.screenplay_tracker.add_character(character) | |
ScreenplayDatabase.save_character(self.current_session_id, character) | |
except Exception as e: | |
logger.warning(f"Error parsing supporting character {i}: {e}") | |
continue | |
def _process_producer_content(self, content: str): | |
"""Process producer output with better extraction""" | |
try: | |
# Extract title with various formats | |
title_patterns = [ | |
r'(?:TITLE|제목)[:\s]*\*?\*?([^\n*]+)\*?\*?', | |
r'\*\*(?:TITLE|제목)\*\*[:\s]*([^\n]+)', | |
r'Title[:\s]*([^\n]+)' | |
] | |
for pattern in title_patterns: | |
title_match = re.search(pattern, content, re.IGNORECASE) | |
if title_match: | |
self.screenplay_tracker.screenplay_bible.title = title_match.group(1).strip() | |
break | |
# Extract logline with various formats | |
logline_patterns = [ | |
r'(?:LOGLINE|로그라인)[:\s]*\*?\*?([^\n]+)', | |
r'\*\*(?:LOGLINE|로그라인)\*\*[:\s]*([^\n]+)', | |
r'Logline[:\s]*([^\n]+)' | |
] | |
for pattern in logline_patterns: | |
logline_match = re.search(pattern, content, re.IGNORECASE | re.DOTALL) | |
if logline_match: | |
# Get full logline (might be multi-line) | |
logline_text = logline_match.group(1).strip() | |
# Continue reading if it's incomplete | |
if not logline_text.endswith('.'): | |
next_lines = content[logline_match.end():].split('\n') | |
for line in next_lines[:3]: # Check next 3 lines | |
if line.strip() and not re.match(r'^[A-Z가-힣\d]', line.strip()): | |
logline_text += ' ' + line.strip() | |
else: | |
break | |
self.screenplay_tracker.screenplay_bible.logline = logline_text | |
break | |
# Extract genre | |
genre_match = re.search(r'(?:Primary Genre|주 장르)[:\s]*([^\n]+)', content, re.IGNORECASE) | |
if genre_match: | |
self.screenplay_tracker.screenplay_bible.genre = genre_match.group(1).strip() | |
# Save to database | |
ScreenplayDatabase.save_screenplay_bible(self.current_session_id, | |
self.screenplay_tracker.screenplay_bible) | |
except Exception as e: | |
logger.error(f"Error processing producer content: {e}") | |
def _process_story_content(self, content: str): | |
"""Process story developer output""" | |
# Extract three-act structure | |
self.screenplay_tracker.screenplay_bible.three_act_structure = { | |
"act1": self._extract_section(content, "ACT 1|제1막"), | |
"act2a": self._extract_section(content, "ACT 2A|제2막A"), | |
"act2b": self._extract_section(content, "ACT 2B|제2막B"), | |
"act3": self._extract_section(content, "ACT 3|제3막") | |
} | |
ScreenplayDatabase.save_screenplay_bible(self.current_session_id, | |
self.screenplay_tracker.screenplay_bible) | |
def _process_scene_content(self, content: str): | |
"""Process scene planner output""" | |
# Parse scene breakdown | |
scene_pattern = r'(?:Scene|씬)\s*(\d+).*?(?:INT\.|EXT\.)\s*(.+?)\s*-\s*(\w+)' | |
scenes = re.finditer(scene_pattern, content, re.IGNORECASE | re.MULTILINE) | |
for match in scenes: | |
scene_num = int(match.group(1)) | |
location = match.group(2).strip() | |
time_of_day = match.group(3).strip() | |
# Determine act based on scene number | |
act = 1 if scene_num <= 12 else 2 if scene_num <= 35 else 3 | |
scene = SceneBreakdown( | |
scene_number=scene_num, | |
act=act, | |
location=location, | |
time_of_day=time_of_day, | |
characters=[], # Would be extracted from content | |
purpose="", # Would be extracted from content | |
conflict="", # Would be extracted from content | |
page_count=1.5 # Default estimate | |
) | |
self.screenplay_tracker.add_scene(scene) | |
ScreenplayDatabase.save_scene(self.current_session_id, scene) | |
# --- Utility functions --- | |
def generate_random_screenplay_theme(screenplay_type: str, genre: str, language: str) -> str: | |
"""Generate random screenplay theme""" | |
try: | |
# Log the attempt | |
logger.info(f"Generating random theme - Type: {screenplay_type}, Genre: {genre}, Language: {language}") | |
# Load themes data | |
themes_data = load_screenplay_themes_data() | |
# Select random elements | |
import secrets | |
situations = themes_data['situations'].get(genre, themes_data['situations']['drama']) | |
protagonists = themes_data['protagonists'].get(genre, themes_data['protagonists']['drama']) | |
conflicts = themes_data['conflicts'].get(genre, themes_data['conflicts']['drama']) | |
if not situations or not protagonists or not conflicts: | |
logger.error(f"No theme data available for genre {genre}") | |
return f"Error: No theme data available for genre {genre}" | |
situation = secrets.choice(situations) | |
protagonist = secrets.choice(protagonists) | |
conflict = secrets.choice(conflicts) | |
logger.info(f"Selected elements - Situation: {situation}, Protagonist: {protagonist}, Conflict: {conflict}") | |
# Check if API token is valid | |
if not FRIENDLI_TOKEN or FRIENDLI_TOKEN == "dummy_token_for_testing": | |
logger.warning("No valid API token, returning fallback theme") | |
return get_fallback_theme(screenplay_type, genre, language, situation, protagonist, conflict) | |
# Generate theme using LLM | |
system = ScreenplayGenerationSystem() | |
if language == "Korean": | |
prompt = f"""다음 요소들로 {screenplay_type}용 매력적인 컨셉을 생성하세요: | |
상황: {situation} | |
주인공: {protagonist} | |
갈등: {conflict} | |
장르: {genre} | |
다음 형식으로 작성: | |
**제목:** [매력적인 제목] | |
**로그라인:** [25단어 이내 한 문장] | |
**컨셉:** [주인공]이(가) [상황]에서 [갈등]을 겪으며 [목표]를 추구하는 이야기. | |
**독특한 요소:** [이 이야기만의 특별한 점]""" | |
else: | |
prompt = f"""Generate an attractive concept for {screenplay_type} using these elements: | |
Situation: {situation} | |
Protagonist: {protagonist} | |
Conflict: {conflict} | |
Genre: {genre} | |
Format as: | |
**Title:** [Compelling title] | |
**Logline:** [One sentence, 25 words max] | |
**Concept:** A story about [protagonist] who faces [conflict] in [situation] while pursuing [goal]. | |
**Unique Element:** [What makes this story special]""" | |
messages = [{"role": "user", "content": prompt}] | |
# Call LLM with error handling | |
logger.info("Calling LLM for theme generation...") | |
generated_theme = "" | |
error_occurred = False | |
# Use streaming to get the response | |
for chunk in system.call_llm_streaming(messages, "producer", language): | |
if chunk.startswith("❌"): | |
logger.error(f"LLM streaming error: {chunk}") | |
error_occurred = True | |
break | |
generated_theme += chunk | |
# If error occurred or no content generated, use fallback | |
if error_occurred or not generated_theme.strip(): | |
logger.warning("LLM call failed or empty response, using fallback theme") | |
return get_fallback_theme(screenplay_type, genre, language, situation, protagonist, conflict) | |
logger.info(f"Successfully generated theme of length: {len(generated_theme)}") | |
# Extract metadata | |
metadata = { | |
'title': extract_title_from_theme(generated_theme), | |
'logline': extract_logline_from_theme(generated_theme), | |
'protagonist': protagonist, | |
'conflict': conflict, | |
'situation': situation, | |
'tags': [genre, screenplay_type] | |
} | |
# Save to database | |
try: | |
theme_id = ScreenplayDatabase.save_random_theme( | |
generated_theme, screenplay_type, genre, language, metadata | |
) | |
logger.info(f"Saved theme with ID: {theme_id}") | |
except Exception as e: | |
logger.error(f"Failed to save theme to database: {e}") | |
return generated_theme | |
except Exception as e: | |
logger.error(f"Theme generation error: {str(e)}") | |
import traceback | |
logger.error(traceback.format_exc()) | |
return f"Error generating theme: {str(e)}" | |
def get_fallback_theme(screenplay_type: str, genre: str, language: str, | |
situation: str, protagonist: str, conflict: str) -> str: | |
"""Generate fallback theme without LLM""" | |
if language == "Korean": | |
return f"""**제목:** {protagonist}의 선택 | |
**로그라인:** {situation}에 갇힌 {protagonist}가 {conflict}에 맞서며 생존을 위해 싸운다. | |
**컨셉:** {protagonist}가 {situation}에서 {conflict}을 겪으며 자신의 한계를 극복하는 이야기. | |
**독특한 요소:** {genre} 장르의 전통적 요소를 현대적으로 재해석한 작품.""" | |
else: | |
return f"""**Title:** The {protagonist.title()}'s Choice | |
**Logline:** When trapped in {situation}, a {protagonist} must face {conflict} to survive. | |
**Concept:** A story about a {protagonist} who faces {conflict} in {situation} while discovering their true strength. | |
**Unique Element:** A fresh take on {genre} genre conventions with contemporary relevance.""" | |
def load_screenplay_themes_data() -> Dict: | |
"""Load screenplay themes data""" | |
return { | |
'situations': { | |
'action': ['hostage crisis', 'heist gone wrong', 'revenge mission', 'race against time'], | |
'thriller': ['false accusation', 'witness protection', 'conspiracy uncovered', 'identity theft'], | |
'drama': ['family reunion', 'terminal diagnosis', 'divorce proceedings', 'career crossroads'], | |
'comedy': ['mistaken identity', 'wedding disaster', 'workplace chaos', 'odd couple roommates'], | |
'horror': ['isolated location', 'ancient curse', 'home invasion', 'supernatural investigation'], | |
'sci-fi': ['first contact', 'time loop', 'AI awakening', 'space colony crisis'], | |
'romance': ['second chance', 'enemies to lovers', 'long distance', 'forbidden love'] | |
}, | |
'protagonists': { | |
'action': ['ex-soldier', 'undercover cop', 'skilled thief', 'reluctant hero'], | |
'thriller': ['investigative journalist', 'wrongly accused person', 'FBI agent', 'whistleblower'], | |
'drama': ['single parent', 'recovering addict', 'immigrant', 'caregiver'], | |
'comedy': ['uptight professional', 'slacker', 'fish out of water', 'eccentric artist'], | |
'horror': ['skeptical scientist', 'final girl', 'paranormal investigator', 'grieving parent'], | |
'sci-fi': ['astronaut', 'AI researcher', 'time traveler', 'colony leader'], | |
'romance': ['workaholic', 'hopeless romantic', 'cynical divorce lawyer', 'small town newcomer'] | |
}, | |
'conflicts': { | |
'action': ['stop the villain', 'save the hostages', 'prevent disaster', 'survive pursuit'], | |
'thriller': ['prove innocence', 'expose truth', 'stay alive', 'protect loved ones'], | |
'drama': ['reconcile past', 'find purpose', 'heal relationships', 'accept change'], | |
'comedy': ['save the business', 'win the competition', 'fool everyone', 'find love'], | |
'horror': ['survive the night', 'break the curse', 'escape the monster', 'save the town'], | |
'sci-fi': ['save humanity', 'prevent paradox', 'stop the invasion', 'preserve identity'], | |
'romance': ['overcome differences', 'choose between options', 'trust again', 'follow heart'] | |
} | |
} | |
def extract_title_from_theme(theme_text: str) -> str: | |
"""Extract title from generated theme""" | |
match = re.search(r'\*\*(?:Title|제목):\*\*\s*(.+)', theme_text, re.IGNORECASE) | |
return match.group(1).strip() if match else "" | |
def extract_logline_from_theme(theme_text: str) -> str: | |
"""Extract logline from generated theme""" | |
match = re.search(r'\*\*(?:Logline|로그라인):\*\*\s*(.+)', theme_text, re.IGNORECASE) | |
return match.group(1).strip() if match else "" | |
import re | |
def format_screenplay_display(screenplay_text: str) -> str: | |
"""Convert raw screenplay text to a nicely formatted Markdown preview.""" | |
if not screenplay_text: | |
return "No screenplay content yet." | |
# 1) 제목 영역 | |
formatted = "# 🎬 Screenplay\n\n" | |
# 2) 씬 헤딩(INT./EXT. 라인) 볼드 처리 | |
# - ^ : 행의 시작 | |
# - .* : 행 전체 | |
# - re.MULTILINE : 각 줄마다 ^ $가 동작 | |
formatted_text = re.sub( | |
r'^(INT\.|EXT\.).*$', # 캡처: INT. 또는 EXT.으로 시작하는 한 줄 | |
r'**\g<0>**', # 전체 행을 굵게 | |
screenplay_text, | |
flags=re.MULTILINE | |
) | |
# 3) 대문자 전원(인물 이름) 볼드 처리 | |
# - [A-Z][A-Z\s]+$ : ALL-CAPS 글자와 공백만으로 이뤄진 행 | |
formatted_text = re.sub( | |
r'^([A-Z][A-Z\s]+)$', | |
r'**\g<0>**', | |
formatted_text, | |
flags=re.MULTILINE | |
) | |
# 4) 가독성을 위해 INT./EXT. 뒤에 빈 줄 삽입 | |
lines = formatted_text.splitlines() | |
pretty_lines = [] | |
for line in lines: | |
pretty_lines.append(line) | |
if line.startswith("**INT.") or line.startswith("**EXT."): | |
pretty_lines.append("") # 빈 줄 추가 | |
formatted += "\n".join(pretty_lines) | |
# 5) 페이지 수(스크립트 규칙: 1 페이지 ≈ 55 라인) 계산 | |
page_count = len(screenplay_text.splitlines()) / 55 | |
formatted = f"**Total Pages: {page_count:.1f}**\n\n" + formatted | |
return formatted | |
def format_stages_display(stages: List[Dict]) -> str: | |
"""Format stages display for screenplay""" | |
markdown = "## 🎬 Production Progress\n\n" | |
# Progress summary | |
completed = sum(1 for s in stages if s.get('status') == 'complete') | |
total = len(stages) | |
markdown += f"**Progress: {completed}/{total} stages complete**\n\n" | |
# Page count if available | |
total_pages = sum(s.get('page_count', 0) for s in stages if s.get('page_count')) | |
if total_pages > 0: | |
markdown += f"**Current Page Count: {total_pages:.1f} pages**\n\n" | |
markdown += "---\n\n" | |
# Stage details | |
current_act = None | |
for i, stage in enumerate(stages): | |
status_icon = "✅" if stage['status'] == 'complete' else "🔄" if stage['status'] == 'active' else "⏳" | |
# Group by acts | |
if 'Act' in stage.get('name', ''): | |
act_match = re.search(r'Act (\w+)', stage['name']) | |
if act_match and act_match.group(1) != current_act: | |
current_act = act_match.group(1) | |
markdown += f"\n### 📄 Act {current_act}\n\n" | |
markdown += f"{status_icon} **{stage['name']}**" | |
if stage.get('page_count', 0) > 0: | |
markdown += f" ({stage['page_count']:.1f} pages)" | |
markdown += "\n" | |
if stage['content'] and stage['status'] == 'complete': | |
preview_length = 200 | |
preview = stage['content'][:preview_length] + "..." if len(stage['content']) > preview_length else stage['content'] | |
markdown += f"> {preview}\n\n" | |
elif stage['status'] == 'active': | |
markdown += "> *In progress...*\n\n" | |
return markdown | |
def process_query(query: str, screenplay_type: str, genre: str, language: str, | |
session_id: Optional[str] = None) -> Generator[Tuple[str, str, str, str], None, None]: | |
"""Main query processing function""" | |
if not query.strip(): | |
yield "", "", "❌ Please enter a screenplay concept.", session_id | |
return | |
system = ScreenplayGenerationSystem() | |
stages_markdown = "" | |
screenplay_display = "" | |
for status, stages, current_session_id in system.process_screenplay_stream( | |
query, screenplay_type, genre, language, session_id | |
): | |
stages_markdown = format_stages_display(stages) | |
# Get screenplay content when available | |
if stages and all(s.get("status") == "complete" for s in stages[-4:]): | |
screenplay_text = ScreenplayDatabase.get_screenplay_content(current_session_id) | |
screenplay_display = format_screenplay_display(screenplay_text) | |
yield stages_markdown, screenplay_display, status or "🔄 Processing...", current_session_id | |
def get_active_sessions() -> List[str]: | |
"""Get active screenplay sessions""" | |
sessions = ScreenplayDatabase.get_active_sessions() | |
return [ | |
f"{s['session_id'][:8]}... - {s.get('title', s['user_query'][:30])}... " | |
f"({s['screenplay_type']}/{s['genre']}) [{s['total_pages']:.1f} pages]" | |
for s in sessions | |
] | |
def export_screenplay_pdf(screenplay_text: str, title: str, session_id: str) -> str: | |
"""Export screenplay to PDF format""" | |
# This would use a library like reportlab to create industry-standard PDF | |
# For now, returning a placeholder | |
pdf_path = f"screenplay_{session_id[:8]}.pdf" | |
# PDF generation logic would go here | |
return pdf_path | |
def export_screenplay_fdx(screenplay_text: str, title: str, session_id: str) -> str: | |
"""Export to Final Draft format""" | |
# This would create .fdx XML format | |
fdx_path = f"screenplay_{session_id[:8]}.fdx" | |
# FDX generation logic would go here | |
return fdx_path | |
def download_screenplay(screenplay_text: str, format_type: str, title: str, | |
session_id: str) -> Optional[str]: | |
"""Generate screenplay download file""" | |
if not screenplay_text or not session_id: | |
return None | |
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") | |
try: | |
if format_type == "PDF": | |
return export_screenplay_pdf(screenplay_text, title, session_id) | |
elif format_type == "FDX": | |
return export_screenplay_fdx(screenplay_text, title, session_id) | |
elif format_type == "FOUNTAIN": | |
filepath = f"screenplay_{session_id[:8]}_{timestamp}.fountain" | |
with open(filepath, 'w', encoding='utf-8') as f: | |
f.write(screenplay_text) | |
return filepath | |
else: # TXT | |
filepath = f"screenplay_{session_id[:8]}_{timestamp}.txt" | |
with open(filepath, 'w', encoding='utf-8') as f: | |
f.write(f"Title: {title}\n") | |
f.write("=" * 50 + "\n\n") | |
f.write(screenplay_text) | |
return filepath | |
except Exception as e: | |
logger.error(f"Download generation failed: {e}") | |
return None | |
# Create Gradio interface | |
def create_interface(): | |
"""Create Gradio interface for screenplay generation""" | |
css = """ | |
.main-header { | |
text-align: center; | |
margin-bottom: 2rem; | |
padding: 2rem; | |
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); | |
border-radius: 10px; | |
color: white; | |
} | |
.header-title { | |
font-size: 3rem; | |
margin-bottom: 1rem; | |
background: linear-gradient(45deg, #f39c12, #e74c3c); | |
-webkit-background-clip: text; | |
-webkit-text-fill-color: transparent; | |
} | |
.header-description { | |
font-size: 1.1rem; | |
opacity: 0.9; | |
line-height: 1.6; | |
} | |
.type-selector { | |
display: flex; | |
gap: 1rem; | |
margin: 1rem 0; | |
} | |
.type-card { | |
flex: 1; | |
padding: 1rem; | |
border: 2px solid #ddd; | |
border-radius: 8px; | |
cursor: pointer; | |
transition: all 0.3s; | |
} | |
.type-card:hover { | |
border-color: #f39c12; | |
transform: translateY(-2px); | |
} | |
.type-card.selected { | |
border-color: #e74c3c; | |
background: #fff5f5; | |
} | |
#stages-display { | |
max-height: 600px; | |
overflow-y: auto; | |
padding: 1rem; | |
background: #f8f9fa; | |
border-radius: 8px; | |
} | |
#screenplay-output { | |
font-family: 'Courier New', monospace; | |
white-space: pre-wrap; | |
background: white; | |
padding: 2rem; | |
border: 1px solid #ddd; | |
border-radius: 8px; | |
max-height: 800px; | |
overflow-y: auto; | |
} | |
.genre-grid { | |
display: grid; | |
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); | |
gap: 0.5rem; | |
margin: 1rem 0; | |
} | |
.genre-btn { | |
padding: 0.75rem; | |
border: 2px solid #e0e0e0; | |
background: white; | |
border-radius: 8px; | |
cursor: pointer; | |
transition: all 0.3s; | |
text-align: center; | |
} | |
.genre-btn:hover { | |
border-color: #f39c12; | |
background: #fffbf0; | |
} | |
.genre-btn.selected { | |
border-color: #e74c3c; | |
background: #fff5f5; | |
font-weight: bold; | |
} | |
""" | |
with gr.Blocks(theme=gr.themes.Soft(), css=css, title="Screenplay Generator") as interface: | |
gr.HTML(""" | |
<div class="main-header"> | |
<h1 class="header-title">🎬 AI Screenplay Generator</h1> | |
<p class="header-description"> | |
Transform your ideas into professional screenplays for films, TV shows, and streaming series. | |
Using industry-standard format and story structure to create compelling, producible scripts. | |
</p> | |
</div> | |
""") | |
# State management | |
current_session_id = gr.State(None) | |
with gr.Tabs(): | |
# Main Writing Tab | |
with gr.Tab("✍️ Write Screenplay"): | |
with gr.Row(): | |
with gr.Column(scale=3): | |
query_input = gr.Textbox( | |
label="Screenplay Concept", | |
placeholder="""Describe your screenplay idea. For example: | |
- A detective with memory loss must solve their own attempted murder | |
- Two rival food truck owners forced to work together to save the city food festival | |
- A space station AI develops consciousness during a critical mission | |
- A family reunion turns into a murder mystery during a hurricane | |
The more specific your concept, the better the screenplay will be tailored to your vision.""", | |
lines=6 | |
) | |
with gr.Column(scale=1): | |
screenplay_type = gr.Radio( | |
choices=list(SCREENPLAY_LENGTHS.keys()), | |
value="movie", | |
label="Screenplay Type", | |
info="Choose your format" | |
) | |
genre_select = gr.Dropdown( | |
choices=list(GENRE_TEMPLATES.keys()), | |
value="drama", | |
label="Primary Genre", | |
info="Select main genre" | |
) | |
language_select = gr.Radio( | |
choices=["English", "Korean"], | |
value="English", | |
label="Language" | |
) | |
with gr.Row(): | |
random_btn = gr.Button("🎲 Random Concept", scale=1) | |
clear_btn = gr.Button("🗑️ Clear", scale=1) | |
submit_btn = gr.Button("🎬 Start Writing", variant="primary", scale=2) | |
status_text = gr.Textbox( | |
label="Status", | |
interactive=False, | |
value="Ready to create your screenplay" | |
) | |
# Session management | |
with gr.Group(): | |
gr.Markdown("### 📁 Saved Projects") | |
with gr.Row(): | |
session_dropdown = gr.Dropdown( | |
label="Active Sessions", | |
choices=[], | |
interactive=True, | |
scale=3 | |
) | |
refresh_btn = gr.Button("🔄", scale=1) | |
resume_btn = gr.Button("📂 Load", scale=1) | |
# Output displays | |
with gr.Row(): | |
with gr.Column(): | |
with gr.Tab("🎭 Writing Progress"): | |
stages_display = gr.Markdown( | |
value="*Your screenplay journey will unfold here...*", | |
elem_id="stages-display" | |
) | |
with gr.Tab("📄 Screenplay"): | |
screenplay_output = gr.Markdown( | |
value="*Your formatted screenplay will appear here...*", | |
elem_id="screenplay-output" | |
) | |
with gr.Row(): | |
format_select = gr.Radio( | |
choices=["PDF", "FDX", "FOUNTAIN", "TXT"], | |
value="PDF", | |
label="Export Format" | |
) | |
download_btn = gr.Button("📥 Download Screenplay", variant="secondary") | |
download_file = gr.File( | |
label="Download", | |
visible=False | |
) | |
# Examples | |
gr.Examples( | |
examples=[ | |
["A burned-out teacher discovers her students are being replaced by AI duplicates"], | |
["Two funeral home employees accidentally release a ghost who helps them solve murders"], | |
["A time-loop forces a wedding planner to relive the worst wedding until they find true love"], | |
["An astronaut returns to Earth to find everyone has forgotten space exists"], | |
["A support group for reformed villains must save the city when heroes disappear"], | |
["A food critic loses their sense of taste and teams up with a street food vendor"] | |
], | |
inputs=query_input, | |
label="💡 Example Concepts" | |
) | |
# Screenplay Library Tab | |
with gr.Tab("📚 Concept Library"): | |
gr.Markdown(""" | |
### 🎲 Random Screenplay Concepts | |
Browse through AI-generated screenplay concepts. Each concept includes a title, logline, and brief setup. | |
""") | |
library_display = gr.HTML( | |
value="<p>Library feature coming soon...</p>" | |
) | |
# Event handlers | |
def handle_submit(query, s_type, genre, lang, session_id): | |
if not query: | |
yield "", "", "❌ Please enter a concept", session_id | |
return | |
yield from process_query(query, s_type, genre, lang, session_id) | |
def handle_random(s_type, genre, lang): | |
return generate_random_screenplay_theme(s_type, genre, lang) | |
def handle_download(screenplay_text, format_type, session_id): | |
if not screenplay_text or not session_id: | |
return gr.update(visible=False) | |
# Get title from database | |
session = ScreenplayDatabase.get_session(session_id) | |
title = session.get('title', 'Untitled') if session else 'Untitled' | |
file_path = download_screenplay(screenplay_text, format_type, title, session_id) | |
if file_path and os.path.exists(file_path): | |
return gr.update(value=file_path, visible=True) | |
return gr.update(visible=False) | |
# Connect events | |
submit_btn.click( | |
fn=handle_submit, | |
inputs=[query_input, screenplay_type, genre_select, language_select, current_session_id], | |
outputs=[stages_display, screenplay_output, status_text, current_session_id] | |
) | |
random_btn.click( | |
fn=handle_random, | |
inputs=[screenplay_type, genre_select, language_select], | |
outputs=[query_input] | |
) | |
clear_btn.click( | |
fn=lambda: ("", "", "Ready to create your screenplay", None), | |
outputs=[stages_display, screenplay_output, status_text, current_session_id] | |
) | |
refresh_btn.click( | |
fn=get_active_sessions, | |
outputs=[session_dropdown] | |
) | |
download_btn.click( | |
fn=handle_download, | |
inputs=[screenplay_output, format_select, current_session_id], | |
outputs=[download_file] | |
) | |
# Load sessions on start | |
interface.load( | |
fn=get_active_sessions, | |
outputs=[session_dropdown] | |
) | |
return interface | |
# Main function | |
if __name__ == "__main__": | |
logger.info("Screenplay Generator Starting...") | |
logger.info("=" * 60) | |
# Environment check | |
logger.info(f"API Endpoint: {API_URL}") | |
logger.info("Screenplay Types Available:") | |
for s_type, info in SCREENPLAY_LENGTHS.items(): | |
logger.info(f" - {s_type}: {info['description']}") | |
logger.info(f"Genres: {', '.join(GENRE_TEMPLATES.keys())}") | |
if BRAVE_SEARCH_API_KEY: | |
logger.info("Web search enabled for market research.") | |
else: | |
logger.warning("Web search disabled.") | |
logger.info("=" * 60) | |
# Initialize database | |
logger.info("Initializing database...") | |
ScreenplayDatabase.init_db() | |
logger.info("Database initialization complete.") | |
# Create and launch interface | |
interface = create_interface() | |
interface.launch( | |
server_name="0.0.0.0", | |
server_port=7860, | |
share=False, | |
debug=True | |
) |