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