openfree commited on
Commit
00b79ac
ยท
verified ยท
1 Parent(s): ee38251

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +870 -925
app.py CHANGED
@@ -24,190 +24,190 @@ logger = logging.getLogger(__name__)
24
 
25
  # --- Document export imports ---
26
  try:
27
- from docx import Document
28
- from docx.shared import Inches, Pt, RGBColor, Mm
29
- from docx.enum.text import WD_ALIGN_PARAGRAPH
30
- from docx.enum.style import WD_STYLE_TYPE
31
- from docx.oxml.ns import qn
32
- from docx.oxml import OxmlElement
33
- DOCX_AVAILABLE = True
34
  except ImportError:
35
- DOCX_AVAILABLE = False
36
- logger.warning("python-docx not installed. DOCX export will be disabled.")
37
 
38
  # --- Environment variables and constants ---
39
  FRIENDLI_TOKEN = os.getenv("FRIENDLI_TOKEN", "")
40
  BRAVE_SEARCH_API_KEY = os.getenv("BRAVE_SEARCH_API_KEY", "")
41
  API_URL = "https://api.friendli.ai/dedicated/v1/chat/completions"
42
  MODEL_ID = "dep86pjolcjjnv8"
43
- DB_PATH = "screenplay_sessions_v1.db"
44
 
45
  # Screenplay length settings
46
  SCREENPLAY_LENGTHS = {
47
- "movie": {"pages": 110, "description": "Feature Film (90-120 pages)"},
48
- "tv_drama": {"pages": 55, "description": "TV Drama Episode (50-60 pages)"},
49
- "ott_series": {"pages": 45, "description": "OTT Series Episode (30-60 pages)"},
50
- "short_film": {"pages": 15, "description": "Short Film (10-20 pages)"}
51
  }
52
 
53
  # --- Environment validation ---
54
  if not FRIENDLI_TOKEN:
55
- logger.error("FRIENDLI_TOKEN not set. Application will not work properly.")
56
- FRIENDLI_TOKEN = "dummy_token_for_testing"
57
 
58
  if not BRAVE_SEARCH_API_KEY:
59
- logger.warning("BRAVE_SEARCH_API_KEY not set. Web search features will be disabled.")
60
 
61
  # --- Global variables ---
62
  db_lock = threading.Lock()
63
 
64
  # Genre templates
65
  GENRE_TEMPLATES = {
66
- "action": {
67
- "pacing": "fast",
68
- "scene_length": "short",
69
- "dialogue_ratio": 0.3,
70
- "key_elements": ["set pieces", "physical conflict", "urgency", "stakes escalation"],
71
- "structure_beats": ["explosive opening", "pursuit/chase", "confrontation", "climactic battle"]
72
- },
73
- "thriller": {
74
- "pacing": "fast",
75
- "scene_length": "short",
76
- "dialogue_ratio": 0.35,
77
- "key_elements": ["suspense", "twists", "paranoia", "time pressure"],
78
- "structure_beats": ["hook", "mystery deepens", "false victory", "revelation", "final confrontation"]
79
- },
80
- "drama": {
81
- "pacing": "moderate",
82
- "scene_length": "medium",
83
- "dialogue_ratio": 0.5,
84
- "key_elements": ["character depth", "emotional truth", "relationships", "internal conflict"],
85
- "structure_beats": ["status quo", "catalyst", "debate", "commitment", "complications", "crisis", "resolution"]
86
- },
87
- "comedy": {
88
- "pacing": "fast",
89
- "scene_length": "short",
90
- "dialogue_ratio": 0.6,
91
- "key_elements": ["setup/payoff", "timing", "character comedy", "escalation"],
92
- "structure_beats": ["funny opening", "complication", "misunderstandings multiply", "chaos peak", "resolution with callback"]
93
- },
94
- "horror": {
95
- "pacing": "variable",
96
- "scene_length": "mixed",
97
- "dialogue_ratio": 0.3,
98
- "key_elements": ["atmosphere", "dread", "jump scares", "gore/psychological"],
99
- "structure_beats": ["normal world", "first sign", "investigation", "first attack", "survival", "final girl/boy"]
100
- },
101
- "sci-fi": {
102
- "pacing": "moderate",
103
- "scene_length": "medium",
104
- "dialogue_ratio": 0.4,
105
- "key_elements": ["world building", "technology", "concepts", "visual spectacle"],
106
- "structure_beats": ["ordinary world", "discovery", "new world", "complications", "understanding", "choice", "new normal"]
107
- },
108
- "romance": {
109
- "pacing": "moderate",
110
- "scene_length": "medium",
111
- "dialogue_ratio": 0.55,
112
- "key_elements": ["chemistry", "obstacles", "emotional moments", "intimacy"],
113
- "structure_beats": ["meet cute", "attraction", "first conflict", "deepening", "crisis/breakup", "grand gesture", "together"]
114
- }
115
  }
116
 
117
  # Screenplay stages definition
118
  SCREENPLAY_STAGES = [
119
- ("producer", "๐ŸŽฌ Producer: Concept Development & Market Analysis"),
120
- ("story_developer", "๐Ÿ“– Story Developer: Synopsis & Three-Act Structure"),
121
- ("character_designer", "๐Ÿ‘ฅ Character Designer: Cast & Relationships"),
122
- ("critic_structure", "๐Ÿ” Structure Critic: Story & Character Review"),
123
- ("scene_planner", "๐ŸŽฏ Scene Planner: Detailed Scene Breakdown"),
124
- ("screenwriter", "โœ๏ธ Screenwriter: Act 1 - Setup (25%)"),
125
- ("script_doctor", "๐Ÿ”ง Script Doctor: Act 1 Review & Polish"),
126
- ("screenwriter", "โœ๏ธ Screenwriter: Act 2A - Rising Action (25%)"),
127
- ("script_doctor", "๐Ÿ”ง Script Doctor: Act 2A Review & Polish"),
128
- ("screenwriter", "โœ๏ธ Screenwriter: Act 2B - Complications (25%)"),
129
- ("script_doctor", "๐Ÿ”ง Script Doctor: Act 2B Review & Polish"),
130
- ("screenwriter", "โœ๏ธ Screenwriter: Act 3 - Resolution (25%)"),
131
- ("final_reviewer", "๐ŸŽญ Final Review: Complete Screenplay Analysis"),
132
  ]
133
 
134
  # Save the Cat Beat Sheet
135
  SAVE_THE_CAT_BEATS = {
136
- 1: "Opening Image (0-1%)",
137
- 2: "Setup (1-10%)",
138
- 3: "Theme Stated (5%)",
139
- 4: "Catalyst (10%)",
140
- 5: "Debate (10-20%)",
141
- 6: "Break into Two (20%)",
142
- 7: "B Story (22%)",
143
- 8: "Fun and Games (20-50%)",
144
- 9: "Midpoint (50%)",
145
- 10: "Bad Guys Close In (50-75%)",
146
- 11: "All Is Lost (75%)",
147
- 12: "Dark Night of the Soul (75-80%)",
148
- 13: "Break into Three (80%)",
149
- 14: "Finale (80-99%)",
150
- 15: "Final Image (99-100%)"
151
  }
152
 
153
  # --- Data classes ---
154
  @dataclass
155
  class ScreenplayBible:
156
- """Screenplay bible for maintaining consistency"""
157
- title: str = ""
158
- logline: str = ""
159
- genre: str = ""
160
- subgenre: str = ""
161
- tone: str = ""
162
- themes: List[str] = field(default_factory=list)
163
-
164
- # Characters
165
- protagonist: Dict[str, Any] = field(default_factory=dict)
166
- antagonist: Dict[str, Any] = field(default_factory=dict)
167
- supporting_cast: Dict[str, Dict[str, Any]] = field(default_factory=dict)
168
-
169
- # Structure
170
- three_act_structure: Dict[str, str] = field(default_factory=dict)
171
- save_the_cat_beats: Dict[int, str] = field(default_factory=dict)
172
-
173
- # World
174
- time_period: str = ""
175
- primary_locations: List[Dict[str, str]] = field(default_factory=list)
176
- world_rules: List[str] = field(default_factory=list)
177
-
178
- # Visual style
179
- visual_style: str = ""
180
- key_imagery: List[str] = field(default_factory=list)
181
 
182
  @dataclass
183
  class SceneBreakdown:
184
- """Individual scene information"""
185
- scene_number: int
186
- act: int
187
- location: str
188
- time_of_day: str
189
- characters: List[str]
190
- purpose: str
191
- conflict: str
192
- page_count: float
193
- beat: str = ""
194
- transition: str = "CUT TO:"
195
 
196
  @dataclass
197
  class CharacterProfile:
198
- """Detailed character profile"""
199
- name: str
200
- age: int
201
- role: str # protagonist, antagonist, supporting, etc.
202
- archetype: str
203
- want: str # External goal
204
- need: str # Internal need
205
- backstory: str
206
- personality: List[str]
207
- speech_pattern: str
208
- character_arc: str
209
- relationships: Dict[str, str] = field(default_factory=dict)
210
- first_appearance: str = ""
211
 
212
  # --- Core logic classes ---
213
  class ScreenplayTracker:
@@ -537,63 +537,51 @@ class ScreenplayDatabase:
537
 
538
  return theme_id
539
 
540
- @staticmethod
541
- def get_stages(session_id: str) -> List[Dict]:
542
- """Get all stages for a session"""
543
- with ScreenplayDatabase.get_db() as conn:
544
- rows = conn.cursor().execute(
545
- '''SELECT * FROM screenplay_stages
546
- WHERE session_id = ?
547
- ORDER BY stage_number''',
548
- (session_id,)
549
- ).fetchall()
550
- return [dict(row) for row in rows]
551
-
552
  class WebSearchIntegration:
553
- """Web search functionality for screenplay research"""
554
- def __init__(self):
555
- self.brave_api_key = BRAVE_SEARCH_API_KEY
556
- self.search_url = "https://api.search.brave.com/res/v1/web/search"
557
- self.enabled = bool(self.brave_api_key)
558
-
559
- def search(self, query: str, count: int = 3, language: str = "en") -> List[Dict]:
560
- if not self.enabled:
561
- return []
562
- headers = {
563
- "Accept": "application/json",
564
- "X-Subscription-Token": self.brave_api_key
565
- }
566
- params = {
567
- "q": query,
568
- "count": count,
569
- "search_lang": "ko" if language == "Korean" else "en",
570
- "text_decorations": False,
571
- "safesearch": "moderate"
572
- }
573
- try:
574
- response = requests.get(self.search_url, headers=headers, params=params, timeout=10)
575
- response.raise_for_status()
576
- results = response.json().get("web", {}).get("results", [])
577
- return results
578
- except requests.exceptions.RequestException as e:
579
- logger.error(f"Web search API error: {e}")
580
- return []
581
-
582
- def extract_relevant_info(self, results: List[Dict], max_chars: int = 1500) -> str:
583
- if not results:
584
- return ""
585
- extracted = []
586
- total_chars = 0
587
- for i, result in enumerate(results[:3], 1):
588
- title = result.get("title", "")
589
- description = result.get("description", "")
590
- info = f"[{i}] {title}: {description}"
591
- if total_chars + len(info) < max_chars:
592
- extracted.append(info)
593
- total_chars += len(info)
594
- else:
595
- break
596
- return "\n".join(extracted)
597
 
598
  class ScreenplayGenerationSystem:
599
  """Professional screenplay generation system"""
@@ -878,7 +866,7 @@ Create specific, emotionally resonant story."""
878
  **ํ•„์ˆ˜ ์บ๋ฆญํ„ฐ ํ”„๋กœํ•„:**
879
 
880
  1. **์ฃผ์ธ๊ณต (PROTAGONIST)**
881
- - ์ด๋ฆ„ & ๋‚˜์ด:
882
  - ์ง์—…/์—ญํ• :
883
  - ์บ๋ฆญํ„ฐ ์•„ํฌํƒ€์ž…:
884
  - WANT (์™ธ์  ๋ชฉํ‘œ):
@@ -891,7 +879,7 @@ Create specific, emotionally resonant story."""
891
  - ์บ๋ฆญํ„ฐ ์•„ํฌ (Aโ†’B):
892
 
893
  2. **์ ๋Œ€์ž (ANTAGONIST)**
894
- - ์ด๋ฆ„ & ๋‚˜์ด:
895
  - ์ง์—…/์—ญํ• :
896
  - ์•…์—ญ ์•„ํฌํƒ€์ž…:
897
  - ๋ชฉํ‘œ & ๋™๊ธฐ:
@@ -902,7 +890,7 @@ Create specific, emotionally resonant story."""
902
 
903
  3. **์กฐ๋ ฅ์ž๋“ค (SUPPORTING CAST)**
904
  ์ตœ์†Œ 3๋ช…, ๊ฐ๊ฐ:
905
- - ์ด๋ฆ„ & ์—ญํ• :
906
  - ์ฃผ์ธ๊ณต๊ณผ์˜ ๊ด€๊ณ„:
907
  - ์Šคํ† ๋ฆฌ ๊ธฐ๋Šฅ:
908
  - ๋…ํŠนํ•œ ํŠน์„ฑ:
@@ -935,7 +923,7 @@ Create specific, emotionally resonant story."""
935
  **Required Character Profiles:**
936
 
937
  1. **PROTAGONIST**
938
- - Name & Age:
939
  - Occupation/Role:
940
  - Character Archetype:
941
  - WANT (External Goal):
@@ -948,7 +936,7 @@ Create specific, emotionally resonant story."""
948
  - Character Arc (Aโ†’B):
949
 
950
  2. **ANTAGONIST**
951
- - Name & Age:
952
  - Occupation/Role:
953
  - Villain Archetype:
954
  - Goal & Motivation:
@@ -959,7 +947,7 @@ Create specific, emotionally resonant story."""
959
 
960
  3. **SUPPORTING CAST**
961
  Minimum 3, each with:
962
- - Name & Role:
963
  - Relationship to Protagonist:
964
  - Story Function:
965
  - Unique Traits:
@@ -1482,8 +1470,6 @@ Provide specific solutions for each issue."""
1482
  raise Exception(f"LLM Call Failed: {full_content}")
1483
  return full_content
1484
 
1485
-
1486
-
1487
  def call_llm_streaming(self, messages: List[Dict[str, str]], role: str,
1488
  language: str) -> Generator[str, None, None]:
1489
  try:
@@ -1634,8 +1620,6 @@ Provide specific solutions for each issue."""
1634
  logger.error(traceback.format_exc())
1635
  yield f"โŒ Unexpected error: {str(e)}"
1636
 
1637
-
1638
-
1639
  def get_system_prompts(self, language: str) -> Dict[str, str]:
1640
  """Role-specific system prompts"""
1641
 
@@ -1726,9 +1710,7 @@ You provide feedback that's critical yet encouraging."""
1726
 
1727
  return base_prompts.get(language, base_prompts["English"])
1728
 
1729
-
1730
-
1731
- # --- Main process ---
1732
  def process_screenplay_stream(self, query: str, screenplay_type: str, genre: str,
1733
  language: str, session_id: Optional[str] = None
1734
  ) -> Generator[Tuple[str, List[Dict[str, Any]], str], None, None]:
@@ -1888,9 +1870,8 @@ You provide feedback that's critical yet encouraging."""
1888
 
1889
  return "\n\n---\n\n".join(previous) if previous else ""
1890
 
1891
-
1892
  def _parse_character_profile(self, content: str, role: str) -> CharacterProfile:
1893
- """Parse character profile from content"""
1894
  # Debug logging
1895
  logger.debug(f"Parsing character profile for role: {role}")
1896
  logger.debug(f"Content preview: {content[:200]}...")
@@ -1898,9 +1879,9 @@ You provide feedback that's critical yet encouraging."""
1898
  # Extract name first - handle various formats
1899
  name = f"Character_{role}" # default
1900
  name_patterns = [
1901
- r'(?:์ด๋ฆ„|Name)[:\s]*([^,\n]+?)(?:\s*\([^)]+\))?\s*,?\s*\d*์„ธ?',
1902
- r'^\s*[-*โ€ข]\s*([^,\n]+?)(?:\s*\([^)]+\))?\s*,?\s*\d*์„ธ?',
1903
- r'^([^,\n]+?)(?:\s*\([^)]+\))?\s*,?\s*\d*์„ธ?'
1904
  ]
1905
 
1906
  for pattern in name_patterns:
@@ -1908,21 +1889,23 @@ You provide feedback that's critical yet encouraging."""
1908
  if name_match:
1909
  extracted_name = name_match.group(1).strip()
1910
  # Remove markdown and extra characters
1911
- extracted_name = re.sub(r'[*:\s]+$', '', extracted_name)
1912
  extracted_name = re.sub(r'^[*:\s]+', '', extracted_name)
 
 
1913
  if extracted_name and len(extracted_name) > 1:
1914
  name = extracted_name
1915
  break
1916
 
1917
- # Extract age with multiple patterns
1918
  age = 30 # default age
1919
  age_patterns = [
1920
  r'(\d+)\s*์„ธ',
1921
  r'(\d+)\s*์‚ด',
1922
- r',\s*(\d+)\s*[,\s]',
 
1923
  r'\((\d+)\)',
1924
- r'Age[:\s]*(\d+)',
1925
- r'๋‚˜์ด[:\s]*(\d+)'
1926
  ]
1927
 
1928
  for pattern in age_patterns:
@@ -1934,16 +1917,18 @@ You provide feedback that's critical yet encouraging."""
1934
  age = extracted_age
1935
  logger.debug(f"Extracted age: {age}")
1936
  break
1937
- except ValueError:
1938
  continue
1939
 
1940
  # Helper function to extract clean fields
1941
- def extract_clean_field(patterns):
1942
  if isinstance(patterns, str):
1943
  patterns = [patterns]
1944
 
 
 
1945
  for pattern in patterns:
1946
- match = re.search(rf'{pattern}[:\s]*([^\n*]+?)(?=\n|$)', content, re.IGNORECASE | re.DOTALL)
1947
  if match:
1948
  value = match.group(1).strip()
1949
  # Clean up the value
@@ -1982,7 +1967,7 @@ You provide feedback that's critical yet encouraging."""
1982
  r"Backstory",
1983
  r"ํ•ต์‹ฌ ์ƒ์ฒ˜",
1984
  r"Core Wound"
1985
- ]),
1986
  personality=self._extract_personality_traits(content),
1987
  speech_pattern=extract_clean_field([
1988
  r"๋งํˆฌ.*?ํŒจํ„ด",
@@ -2003,165 +1988,181 @@ You provide feedback that's critical yet encouraging."""
2003
 
2004
  def _extract_field(self, content: str, field_pattern: str) -> Optional[str]:
2005
  """Extract field value from content with improved parsing"""
2006
- # Use word boundary to prevent partial matches
2007
- pattern = rf'\b{field_pattern}\b[:\s]*([^\n]+?)(?=\n|$)'
2008
- match = re.search(pattern, content, re.IGNORECASE)
2009
- if match:
2010
- value = match.group(1).strip()
2011
- # Remove markdown formatting
2012
- value = re.sub(r'\*+', '', value)
2013
- # Remove list markers
2014
- value = re.sub(r'^\s*[-โ€ข*]\s*', '', value)
2015
- # Remove trailing punctuation
2016
- value = re.sub(r'[,.:;]$', '', value)
2017
- return value.strip()
 
 
 
 
 
 
 
 
 
 
 
 
2018
  return None
2019
 
2020
  def _extract_personality_traits(self, content: str) -> List[str]:
2021
- """Extract personality traits from content"""
2022
- traits = []
2023
- # Look for personality section
2024
- personality_section = self._extract_field(content, r"(?:Personality|์„ฑ๊ฒฉ)[:\s]*")
2025
- if personality_section:
2026
- # Extract individual traits (usually listed)
2027
- trait_lines = personality_section.split('\n')
2028
- for line in trait_lines:
2029
- line = line.strip()
2030
- if line and not line.endswith(':'):
2031
- # Remove list markers
2032
- trait = re.sub(r'^\s*[-โ€ข*]\s*', '', line)
2033
- if trait:
2034
- traits.append(trait)
2035
- return traits[:5] # Limit to 5 traits
 
 
 
 
 
 
 
 
 
 
 
2036
 
2037
  def _process_character_content(self, content: str):
2038
- """Process character designer output with better error handling"""
2039
- try:
2040
- # Extract protagonist
2041
- protagonist_section = self._extract_section(content, r"(?:PROTAGONIST|์ฃผ์ธ๊ณต)")
2042
- if protagonist_section:
2043
- protagonist = self._parse_character_profile(protagonist_section, "protagonist")
2044
- self.screenplay_tracker.add_character(protagonist)
2045
- ScreenplayDatabase.save_character(self.current_session_id, protagonist)
2046
-
2047
- # Extract antagonist
2048
- antagonist_section = self._extract_section(content, r"(?:ANTAGONIST|์ ๋Œ€์ž)")
2049
- if antagonist_section:
2050
- antagonist = self._parse_character_profile(antagonist_section, "antagonist")
2051
- self.screenplay_tracker.add_character(antagonist)
2052
- ScreenplayDatabase.save_character(self.current_session_id, antagonist)
2053
-
2054
- # Extract supporting characters
2055
- supporting_section = self._extract_section(content, r"(?:SUPPORTING CAST|์กฐ๋ ฅ์ž๋“ค)")
2056
- if supporting_section:
2057
- # Parse multiple supporting characters
2058
- self._parse_supporting_characters(supporting_section)
2059
-
2060
- except Exception as e:
2061
- logger.error(f"Error processing character content: {e}")
2062
- # Continue with default values rather than failing
2063
 
2064
  def _parse_supporting_characters(self, content: str):
2065
- """Parse supporting characters from content"""
2066
- # Split by character markers (numbers or bullets)
2067
- char_sections = re.split(r'\n(?:\d+\.|[-โ€ข*])\s*', content)
2068
-
2069
- for i, section in enumerate(char_sections[1:], 1): # Skip first empty split
2070
- if section.strip():
2071
- try:
2072
- name = self._extract_field(section, r"(?:Name|์ด๋ฆ„)[:\s]*") or f"Supporting_{i}"
2073
- role = self._extract_field(section, r"(?:Role|์—ญํ• )[:\s]*") or "supporting"
2074
-
2075
- character = CharacterProfile(
2076
- name=name,
2077
- age=30, # Default age for supporting characters
2078
- role="supporting",
2079
- archetype=role,
2080
- want="",
2081
- need="",
2082
- backstory=self._extract_field(section, r"(?:Backstory|๋ฐฑ์Šคํ† ๋ฆฌ)[:\s]*") or "",
2083
- personality=[],
2084
- speech_pattern="",
2085
- character_arc=""
2086
- )
2087
-
2088
- self.screenplay_tracker.add_character(character)
2089
- ScreenplayDatabase.save_character(self.current_session_id, character)
2090
-
2091
- except Exception as e:
2092
- logger.warning(f"Error parsing supporting character {i}: {e}")
2093
- continue
2094
 
2095
  def _extract_section(self, content: str, section_pattern: str) -> str:
2096
- """Extract section from content with improved pattern matching"""
2097
- # More flexible section extraction
2098
- pattern = rf'{section_pattern}[:\s]*\n?(.*?)(?=\n\n[A-Z๊ฐ€-ํžฃ]{{2,}}[:\s]|\n\n\d+\.|$)'
2099
- match = re.search(pattern, content, re.IGNORECASE | re.DOTALL)
2100
- if match:
2101
- return match.group(1).strip()
2102
-
2103
- # Try alternative pattern
2104
- pattern2 = rf'{section_pattern}.*?\n((?:.*\n)*?)(?=\n[A-Z๊ฐ€-ํžฃ]{{2,}}:|$)'
2105
- match2 = re.search(pattern2, content, re.IGNORECASE | re.DOTALL)
2106
- if match2:
2107
- return match2.group(1).strip()
2108
-
2109
- return ""
2110
 
2111
  def _process_producer_content(self, content: str):
2112
- """Process producer output with better extraction"""
2113
- try:
2114
- # Extract title with various formats
2115
- title_patterns = [
2116
- r'(?:TITLE|์ œ๋ชฉ)[:\s]*\*?\*?([^\n*]+)\*?\*?',
2117
- r'\*\*(?:TITLE|์ œ๋ชฉ)\*\*[:\s]*([^\n]+)',
2118
- r'Title[:\s]*([^\n]+)'
2119
- ]
2120
-
2121
- for pattern in title_patterns:
2122
- title_match = re.search(pattern, content, re.IGNORECASE)
2123
- if title_match:
2124
- self.screenplay_tracker.screenplay_bible.title = title_match.group(1).strip()
2125
- break
2126
-
2127
- # Extract logline with various formats
2128
- logline_patterns = [
2129
- r'(?:LOGLINE|๋กœ๊ทธ๋ผ์ธ)[:\s]*\*?\*?([^\n]+)',
2130
- r'\*\*(?:LOGLINE|๋กœ๊ทธ๋ผ์ธ)\*\*[:\s]*([^\n]+)',
2131
- r'Logline[:\s]*([^\n]+)'
2132
- ]
2133
-
2134
- for pattern in logline_patterns:
2135
- logline_match = re.search(pattern, content, re.IGNORECASE | re.DOTALL)
2136
- if logline_match:
2137
- # Get full logline (might be multi-line)
2138
- logline_text = logline_match.group(1).strip()
2139
- # Continue reading if it's incomplete
2140
- if not logline_text.endswith('.'):
2141
- next_lines = content[logline_match.end():].split('\n')
2142
- for line in next_lines[:3]: # Check next 3 lines
2143
- if line.strip() and not re.match(r'^[A-Z๊ฐ€-ํžฃ\d]', line.strip()):
2144
- logline_text += ' ' + line.strip()
2145
- else:
2146
- break
2147
- self.screenplay_tracker.screenplay_bible.logline = logline_text
2148
- break
2149
-
2150
- # Extract genre
2151
- genre_match = re.search(r'(?:Primary Genre|์ฃผ ์žฅ๋ฅด)[:\s]*([^\n]+)', content, re.IGNORECASE)
2152
- if genre_match:
2153
- self.screenplay_tracker.screenplay_bible.genre = genre_match.group(1).strip()
2154
-
2155
- # Save to database
2156
- ScreenplayDatabase.save_screenplay_bible(self.current_session_id,
2157
- self.screenplay_tracker.screenplay_bible)
2158
-
2159
- except Exception as e:
2160
- logger.error(f"Error processing producer content: {e}")
2161
-
2162
-
2163
-
2164
-
2165
 
2166
  def _process_story_content(self, content: str):
2167
  """Process story developer output"""
@@ -2176,22 +2177,6 @@ You provide feedback that's critical yet encouraging."""
2176
  ScreenplayDatabase.save_screenplay_bible(self.current_session_id,
2177
  self.screenplay_tracker.screenplay_bible)
2178
 
2179
- def _process_character_content(self, content: str):
2180
- """Process character designer output"""
2181
- # Extract protagonist
2182
- protagonist_section = self._extract_section(content, "PROTAGONIST|์ฃผ์ธ๊ณต")
2183
- if protagonist_section:
2184
- protagonist = self._parse_character_profile(protagonist_section, "protagonist")
2185
- self.screenplay_tracker.add_character(protagonist)
2186
- ScreenplayDatabase.save_character(self.current_session_id, protagonist)
2187
-
2188
- # Extract antagonist
2189
- antagonist_section = self._extract_section(content, "ANTAGONIST|์ ๋Œ€์ž")
2190
- if antagonist_section:
2191
- antagonist = self._parse_character_profile(antagonist_section, "antagonist")
2192
- self.screenplay_tracker.add_character(antagonist)
2193
- ScreenplayDatabase.save_character(self.current_session_id, antagonist)
2194
-
2195
  def _process_scene_content(self, content: str):
2196
  """Process scene planner output"""
2197
  # Parse scene breakdown
@@ -2220,46 +2205,6 @@ You provide feedback that's critical yet encouraging."""
2220
  self.screenplay_tracker.add_scene(scene)
2221
  ScreenplayDatabase.save_scene(self.current_session_id, scene)
2222
 
2223
- def _extract_section(self, content: str, section_pattern: str) -> str:
2224
- """Extract section from content"""
2225
- pattern = rf'(?:{section_pattern})[:\s]*(.+?)(?=\n(?:[A-Z]{{2,}}|[๊ฐ€-ํžฃ]{{2,}}):|\Z)'
2226
- match = re.search(pattern, content, re.IGNORECASE | re.DOTALL)
2227
- return match.group(1).strip() if match else ""
2228
-
2229
- def _parse_character_profile(self, content: str, role: str) -> CharacterProfile:
2230
- """Parse character profile from content"""
2231
- # Extract character details using regex or string parsing
2232
- name = self._extract_field(content, "Name|์ด๋ฆ„") or f"Character_{role}"
2233
- age = int(self._extract_field(content, "Age|๋‚˜์ด") or "30")
2234
-
2235
- return CharacterProfile(
2236
- name=name,
2237
- age=age,
2238
- role=role,
2239
- archetype=self._extract_field(content, "Archetype|์•„ํฌํƒ€์ž…") or "",
2240
- want=self._extract_field(content, "WANT|์™ธ์  ๋ชฉํ‘œ") or "",
2241
- need=self._extract_field(content, "NEED|๋‚ด์  ํ•„์š”") or "",
2242
- backstory=self._extract_field(content, "Backstory|๋ฐฑ์Šคํ† ๋ฆฌ") or "",
2243
- personality=[], # Would be parsed from content
2244
- speech_pattern=self._extract_field(content, "Speech|๋งํˆฌ") or "",
2245
- character_arc=self._extract_field(content, "Arc|์•„ํฌ") or ""
2246
- )
2247
-
2248
- def _extract_field(self, content: str, field_pattern: str) -> Optional[str]:
2249
- """Extract field value from content with improved parsing"""
2250
- # More flexible pattern that handles various formats
2251
- pattern = rf'{field_pattern}(.+?)(?=\n[A-Z๊ฐ€-ํžฃ]|$)'
2252
- match = re.search(pattern, content, re.IGNORECASE | re.DOTALL)
2253
- if match:
2254
- value = match.group(1).strip()
2255
- # Remove markdown formatting if present
2256
- value = re.sub(r'\*\*', '', value)
2257
- value = re.sub(r'^\s*[-โ€ข]\s*', '', value)
2258
- # Remove trailing punctuation
2259
- value = re.sub(r'[,.:;]$', '', value)
2260
- return value.strip()
2261
- return None
2262
-
2263
  # --- Utility functions ---
2264
  def generate_random_screenplay_theme(screenplay_type: str, genre: str, language: str) -> str:
2265
  """Generate random screenplay theme"""
@@ -2400,530 +2345,530 @@ def get_fallback_theme(screenplay_type: str, genre: str, language: str,
2400
  **Unique Element:** A fresh take on {genre} genre conventions with contemporary relevance."""
2401
 
2402
  def load_screenplay_themes_data() -> Dict:
2403
- """Load screenplay themes data"""
2404
- return {
2405
- 'situations': {
2406
- 'action': ['hostage crisis', 'heist gone wrong', 'revenge mission', 'race against time'],
2407
- 'thriller': ['false accusation', 'witness protection', 'conspiracy uncovered', 'identity theft'],
2408
- 'drama': ['family reunion', 'terminal diagnosis', 'divorce proceedings', 'career crossroads'],
2409
- 'comedy': ['mistaken identity', 'wedding disaster', 'workplace chaos', 'odd couple roommates'],
2410
- 'horror': ['isolated location', 'ancient curse', 'home invasion', 'supernatural investigation'],
2411
- 'sci-fi': ['first contact', 'time loop', 'AI awakening', 'space colony crisis'],
2412
- 'romance': ['second chance', 'enemies to lovers', 'long distance', 'forbidden love']
2413
- },
2414
- 'protagonists': {
2415
- 'action': ['ex-soldier', 'undercover cop', 'skilled thief', 'reluctant hero'],
2416
- 'thriller': ['investigative journalist', 'wrongly accused person', 'FBI agent', 'whistleblower'],
2417
- 'drama': ['single parent', 'recovering addict', 'immigrant', 'caregiver'],
2418
- 'comedy': ['uptight professional', 'slacker', 'fish out of water', 'eccentric artist'],
2419
- 'horror': ['skeptical scientist', 'final girl', 'paranormal investigator', 'grieving parent'],
2420
- 'sci-fi': ['astronaut', 'AI researcher', 'time traveler', 'colony leader'],
2421
- 'romance': ['workaholic', 'hopeless romantic', 'cynical divorce lawyer', 'small town newcomer']
2422
- },
2423
- 'conflicts': {
2424
- 'action': ['stop the villain', 'save the hostages', 'prevent disaster', 'survive pursuit'],
2425
- 'thriller': ['prove innocence', 'expose truth', 'stay alive', 'protect loved ones'],
2426
- 'drama': ['reconcile past', 'find purpose', 'heal relationships', 'accept change'],
2427
- 'comedy': ['save the business', 'win the competition', 'fool everyone', 'find love'],
2428
- 'horror': ['survive the night', 'break the curse', 'escape the monster', 'save the town'],
2429
- 'sci-fi': ['save humanity', 'prevent paradox', 'stop the invasion', 'preserve identity'],
2430
- 'romance': ['overcome differences', 'choose between options', 'trust again', 'follow heart']
2431
- }
2432
- }
2433
 
2434
  def extract_title_from_theme(theme_text: str) -> str:
2435
- """Extract title from generated theme"""
2436
- match = re.search(r'\*\*(?:Title|์ œ๋ชฉ):\*\*\s*(.+)', theme_text, re.IGNORECASE)
2437
- return match.group(1).strip() if match else ""
2438
 
2439
  def extract_logline_from_theme(theme_text: str) -> str:
2440
- """Extract logline from generated theme"""
2441
- match = re.search(r'\*\*(?:Logline|๋กœ๊ทธ๋ผ์ธ):\*\*\s*(.+)', theme_text, re.IGNORECASE)
2442
- return match.group(1).strip() if match else ""
2443
 
2444
  def format_screenplay_display(screenplay_text: str) -> str:
2445
- """Format screenplay for display"""
2446
- if not screenplay_text:
2447
- return "No screenplay content yet."
2448
-
2449
- formatted = "# ๐ŸŽฌ Screenplay\n\n"
2450
-
2451
- # Format scene headings
2452
- formatted_text = re.sub(
2453
- r'^(INT\.|EXT\.)(.*?)$',
2454
- r'**\1\2**',
2455
- screenplay_text,
2456
- flags=re.MULTILINE
2457
- )
2458
-
2459
- # Format character names (all caps on their own line)
2460
- formatted_text = re.sub(
2461
- r'^([A-Z][A-Z\s]+)$',
2462
- r'**\1**',
2463
- formatted_text,
2464
- flags=re.MULTILINE
2465
- )
2466
-
2467
- # Add spacing for readability
2468
- lines = formatted_text.split('\n')
2469
- formatted_lines = []
2470
-
2471
- for i, line in enumerate(lines):
2472
- formatted_lines.append(line)
2473
- # Add extra space after scene headings
2474
- if line.startswith('**INT.') or line.startswith('**EXT.'):
2475
- formatted_lines.append('')
2476
-
2477
- formatted += '\n'.join(formatted_lines)
2478
-
2479
- # Add page count
2480
- page_count = len(screenplay_text.split('\n')) / 55
2481
- formatted = f"**Total Pages: {page_count:.1f}**\n\n" + formatted
2482
-
2483
- return formatted
2484
 
2485
  def format_stages_display(stages: List[Dict]) -> str:
2486
- """Format stages display for screenplay"""
2487
- markdown = "## ๐ŸŽฌ Production Progress\n\n"
2488
-
2489
- # Progress summary
2490
- completed = sum(1 for s in stages if s.get('status') == 'complete')
2491
- total = len(stages)
2492
- markdown += f"**Progress: {completed}/{total} stages complete**\n\n"
2493
-
2494
- # Page count if available
2495
- total_pages = sum(s.get('page_count', 0) for s in stages if s.get('page_count'))
2496
- if total_pages > 0:
2497
- markdown += f"**Current Page Count: {total_pages:.1f} pages**\n\n"
2498
-
2499
- markdown += "---\n\n"
2500
-
2501
- # Stage details
2502
- current_act = None
2503
- for i, stage in enumerate(stages):
2504
- status_icon = "โœ…" if stage['status'] == 'complete' else "๐Ÿ”„" if stage['status'] == 'active' else "โณ"
2505
-
2506
- # Group by acts
2507
- if 'Act' in stage.get('name', ''):
2508
- act_match = re.search(r'Act (\w+)', stage['name'])
2509
- if act_match and act_match.group(1) != current_act:
2510
- current_act = act_match.group(1)
2511
- markdown += f"\n### ๐Ÿ“„ Act {current_act}\n\n"
2512
-
2513
- markdown += f"{status_icon} **{stage['name']}**"
2514
-
2515
- if stage.get('page_count', 0) > 0:
2516
- markdown += f" ({stage['page_count']:.1f} pages)"
2517
-
2518
- markdown += "\n"
2519
-
2520
- if stage['content'] and stage['status'] == 'complete':
2521
- preview_length = 200
2522
- preview = stage['content'][:preview_length] + "..." if len(stage['content']) > preview_length else stage['content']
2523
- markdown += f"> {preview}\n\n"
2524
- elif stage['status'] == 'active':
2525
- markdown += "> *In progress...*\n\n"
2526
-
2527
- return markdown
2528
 
2529
  def process_query(query: str, screenplay_type: str, genre: str, language: str,
2530
  session_id: Optional[str] = None) -> Generator[Tuple[str, str, str, str], None, None]:
2531
- """Main query processing function"""
2532
- if not query.strip():
2533
- yield "", "", "โŒ Please enter a screenplay concept.", session_id
2534
- return
2535
-
2536
- system = ScreenplayGenerationSystem()
2537
- stages_markdown = ""
2538
- screenplay_display = ""
2539
-
2540
- for status, stages, current_session_id in system.process_screenplay_stream(
2541
- query, screenplay_type, genre, language, session_id
2542
- ):
2543
- stages_markdown = format_stages_display(stages)
2544
-
2545
- # Get screenplay content when available
2546
- if stages and all(s.get("status") == "complete" for s in stages[-4:]):
2547
- screenplay_text = ScreenplayDatabase.get_screenplay_content(current_session_id)
2548
- screenplay_display = format_screenplay_display(screenplay_text)
2549
-
2550
- yield stages_markdown, screenplay_display, status or "๐Ÿ”„ Processing...", current_session_id
2551
 
2552
  def get_active_sessions() -> List[str]:
2553
- """Get active screenplay sessions"""
2554
- sessions = ScreenplayDatabase.get_active_sessions()
2555
- return [
2556
- f"{s['session_id'][:8]}... - {s.get('title', s['user_query'][:30])}... "
2557
- f"({s['screenplay_type']}/{s['genre']}) [{s['total_pages']:.1f} pages]"
2558
- for s in sessions
2559
- ]
2560
 
2561
  def export_screenplay_pdf(screenplay_text: str, title: str, session_id: str) -> str:
2562
- """Export screenplay to PDF format"""
2563
- # This would use a library like reportlab to create industry-standard PDF
2564
- # For now, returning a placeholder
2565
- pdf_path = f"screenplay_{session_id[:8]}.pdf"
2566
- # PDF generation logic would go here
2567
- return pdf_path
2568
 
2569
  def export_screenplay_fdx(screenplay_text: str, title: str, session_id: str) -> str:
2570
- """Export to Final Draft format"""
2571
- # This would create .fdx XML format
2572
- fdx_path = f"screenplay_{session_id[:8]}.fdx"
2573
- # FDX generation logic would go here
2574
- return fdx_path
2575
 
2576
  def download_screenplay(screenplay_text: str, format_type: str, title: str,
2577
  session_id: str) -> Optional[str]:
2578
- """Generate screenplay download file"""
2579
- if not screenplay_text or not session_id:
2580
- return None
2581
-
2582
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
2583
-
2584
- try:
2585
- if format_type == "PDF":
2586
- return export_screenplay_pdf(screenplay_text, title, session_id)
2587
- elif format_type == "FDX":
2588
- return export_screenplay_fdx(screenplay_text, title, session_id)
2589
- elif format_type == "FOUNTAIN":
2590
- filepath = f"screenplay_{session_id[:8]}_{timestamp}.fountain"
2591
- with open(filepath, 'w', encoding='utf-8') as f:
2592
- f.write(screenplay_text)
2593
- return filepath
2594
- else: # TXT
2595
- filepath = f"screenplay_{session_id[:8]}_{timestamp}.txt"
2596
- with open(filepath, 'w', encoding='utf-8') as f:
2597
- f.write(f"Title: {title}\n")
2598
- f.write("=" * 50 + "\n\n")
2599
- f.write(screenplay_text)
2600
- return filepath
2601
- except Exception as e:
2602
- logger.error(f"Download generation failed: {e}")
2603
- return None
2604
 
2605
  # Create Gradio interface
2606
  def create_interface():
2607
- """Create Gradio interface for screenplay generation"""
2608
-
2609
- css = """
2610
- .main-header {
2611
- text-align: center;
2612
- margin-bottom: 2rem;
2613
- padding: 2rem;
2614
- background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
2615
- border-radius: 10px;
2616
- color: white;
2617
- }
2618
-
2619
- .header-title {
2620
- font-size: 3rem;
2621
- margin-bottom: 1rem;
2622
- background: linear-gradient(45deg, #f39c12, #e74c3c);
2623
- -webkit-background-clip: text;
2624
- -webkit-text-fill-color: transparent;
2625
- }
2626
-
2627
- .header-description {
2628
- font-size: 1.1rem;
2629
- opacity: 0.9;
2630
- line-height: 1.6;
2631
- }
2632
-
2633
- .type-selector {
2634
- display: flex;
2635
- gap: 1rem;
2636
- margin: 1rem 0;
2637
- }
2638
-
2639
- .type-card {
2640
- flex: 1;
2641
- padding: 1rem;
2642
- border: 2px solid #ddd;
2643
- border-radius: 8px;
2644
- cursor: pointer;
2645
- transition: all 0.3s;
2646
- }
2647
-
2648
- .type-card:hover {
2649
- border-color: #f39c12;
2650
- transform: translateY(-2px);
2651
- }
2652
-
2653
- .type-card.selected {
2654
- border-color: #e74c3c;
2655
- background: #fff5f5;
2656
- }
2657
-
2658
- #stages-display {
2659
- max-height: 600px;
2660
- overflow-y: auto;
2661
- padding: 1rem;
2662
- background: #f8f9fa;
2663
- border-radius: 8px;
2664
- }
2665
-
2666
- #screenplay-output {
2667
- font-family: 'Courier New', monospace;
2668
- white-space: pre-wrap;
2669
- background: white;
2670
- padding: 2rem;
2671
- border: 1px solid #ddd;
2672
- border-radius: 8px;
2673
- max-height: 800px;
2674
- overflow-y: auto;
2675
- }
2676
-
2677
- .genre-grid {
2678
- display: grid;
2679
- grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
2680
- gap: 0.5rem;
2681
- margin: 1rem 0;
2682
- }
2683
-
2684
- .genre-btn {
2685
- padding: 0.75rem;
2686
- border: 2px solid #e0e0e0;
2687
- background: white;
2688
- border-radius: 8px;
2689
- cursor: pointer;
2690
- transition: all 0.3s;
2691
- text-align: center;
2692
- }
2693
-
2694
- .genre-btn:hover {
2695
- border-color: #f39c12;
2696
- background: #fffbf0;
2697
- }
2698
-
2699
- .genre-btn.selected {
2700
- border-color: #e74c3c;
2701
- background: #fff5f5;
2702
- font-weight: bold;
2703
- }
2704
- """
2705
-
2706
- with gr.Blocks(theme=gr.themes.Soft(), css=css, title="Screenplay Generator") as interface:
2707
- gr.HTML("""
2708
- <div class="main-header">
2709
- <h1 class="header-title">๐ŸŽฌ AI Screenplay Generator</h1>
2710
- <p class="header-description">
2711
- Transform your ideas into professional screenplays for films, TV shows, and streaming series.
2712
- Using industry-standard format and story structure to create compelling, producible scripts.
2713
- </p>
2714
- </div>
2715
- """)
2716
-
2717
- # State management
2718
- current_session_id = gr.State(None)
2719
-
2720
- with gr.Tabs():
2721
- # Main Writing Tab
2722
- with gr.Tab("โœ๏ธ Write Screenplay"):
2723
- with gr.Row():
2724
- with gr.Column(scale=3):
2725
- query_input = gr.Textbox(
2726
- label="Screenplay Concept",
2727
- placeholder="""Describe your screenplay idea. For example:
2728
  - A detective with memory loss must solve their own attempted murder
2729
  - Two rival food truck owners forced to work together to save the city food festival
2730
  - A space station AI develops consciousness during a critical mission
2731
  - A family reunion turns into a murder mystery during a hurricane
2732
 
2733
  The more specific your concept, the better the screenplay will be tailored to your vision.""",
2734
- lines=6
2735
- )
2736
-
2737
- with gr.Column(scale=1):
2738
- screenplay_type = gr.Radio(
2739
- choices=list(SCREENPLAY_LENGTHS.keys()),
2740
- value="movie",
2741
- label="Screenplay Type",
2742
- info="Choose your format"
2743
- )
2744
-
2745
- genre_select = gr.Dropdown(
2746
- choices=list(GENRE_TEMPLATES.keys()),
2747
- value="drama",
2748
- label="Primary Genre",
2749
- info="Select main genre"
2750
- )
2751
-
2752
- language_select = gr.Radio(
2753
- choices=["English", "Korean"],
2754
- value="English",
2755
- label="Language"
2756
- )
2757
-
2758
- with gr.Row():
2759
- random_btn = gr.Button("๐ŸŽฒ Random Concept", scale=1)
2760
- clear_btn = gr.Button("๐Ÿ—‘๏ธ Clear", scale=1)
2761
- submit_btn = gr.Button("๐ŸŽฌ Start Writing", variant="primary", scale=2)
2762
-
2763
- status_text = gr.Textbox(
2764
- label="Status",
2765
- interactive=False,
2766
- value="Ready to create your screenplay"
2767
- )
2768
-
2769
- # Session management
2770
- with gr.Group():
2771
- gr.Markdown("### ๐Ÿ“ Saved Projects")
2772
- with gr.Row():
2773
- session_dropdown = gr.Dropdown(
2774
- label="Active Sessions",
2775
- choices=[],
2776
- interactive=True,
2777
- scale=3
2778
- )
2779
- refresh_btn = gr.Button("๐Ÿ”„", scale=1)
2780
- resume_btn = gr.Button("๐Ÿ“‚ Load", scale=1)
2781
-
2782
- # Output displays
2783
- with gr.Row():
2784
- with gr.Column():
2785
- with gr.Tab("๐ŸŽญ Writing Progress"):
2786
- stages_display = gr.Markdown(
2787
- value="*Your screenplay journey will unfold here...*",
2788
- elem_id="stages-display"
2789
- )
2790
-
2791
- with gr.Tab("๐Ÿ“„ Screenplay"):
2792
- screenplay_output = gr.Markdown(
2793
- value="*Your formatted screenplay will appear here...*",
2794
- elem_id="screenplay-output"
2795
- )
2796
-
2797
- with gr.Row():
2798
- format_select = gr.Radio(
2799
- choices=["PDF", "FDX", "FOUNTAIN", "TXT"],
2800
- value="PDF",
2801
- label="Export Format"
2802
- )
2803
- download_btn = gr.Button("๐Ÿ“ฅ Download Screenplay", variant="secondary")
2804
-
2805
- download_file = gr.File(
2806
- label="Download",
2807
- visible=False
2808
- )
2809
-
2810
- # Examples
2811
- gr.Examples(
2812
- examples=[
2813
- ["A burned-out teacher discovers her students are being replaced by AI duplicates"],
2814
- ["Two funeral home employees accidentally release a ghost who helps them solve murders"],
2815
- ["A time-loop forces a wedding planner to relive the worst wedding until they find true love"],
2816
- ["An astronaut returns to Earth to find everyone has forgotten space exists"],
2817
- ["A support group for reformed villains must save the city when heroes disappear"],
2818
- ["A food critic loses their sense of taste and teams up with a street food vendor"]
2819
- ],
2820
- inputs=query_input,
2821
- label="๐Ÿ’ก Example Concepts"
2822
- )
2823
-
2824
- # Screenplay Library Tab
2825
- with gr.Tab("๐Ÿ“š Concept Library"):
2826
- gr.Markdown("""
2827
- ### ๐ŸŽฒ Random Screenplay Concepts
2828
-
2829
- Browse through AI-generated screenplay concepts. Each concept includes a title, logline, and brief setup.
2830
- """)
2831
-
2832
- library_display = gr.HTML(
2833
- value="<p>Library feature coming soon...</p>"
2834
- )
2835
-
2836
- # Event handlers
2837
- def handle_submit(query, s_type, genre, lang, session_id):
2838
- if not query:
2839
- yield "", "", "โŒ Please enter a concept", session_id
2840
- return
2841
-
2842
- yield from process_query(query, s_type, genre, lang, session_id)
2843
-
2844
- def handle_random(s_type, genre, lang):
2845
- return generate_random_screenplay_theme(s_type, genre, lang)
2846
-
2847
- def handle_download(screenplay_text, format_type, session_id):
2848
- if not screenplay_text or not session_id:
2849
- return gr.update(visible=False)
2850
-
2851
- # Get title from database
2852
- session = ScreenplayDatabase.get_session(session_id)
2853
- title = session.get('title', 'Untitled') if session else 'Untitled'
2854
-
2855
- file_path = download_screenplay(screenplay_text, format_type, title, session_id)
2856
- if file_path and os.path.exists(file_path):
2857
- return gr.update(value=file_path, visible=True)
2858
- return gr.update(visible=False)
2859
-
2860
- # Connect events
2861
- submit_btn.click(
2862
- fn=handle_submit,
2863
- inputs=[query_input, screenplay_type, genre_select, language_select, current_session_id],
2864
- outputs=[stages_display, screenplay_output, status_text, current_session_id]
2865
- )
2866
-
2867
- random_btn.click(
2868
- fn=handle_random,
2869
- inputs=[screenplay_type, genre_select, language_select],
2870
- outputs=[query_input]
2871
- )
2872
-
2873
- clear_btn.click(
2874
- fn=lambda: ("", "", "Ready to create your screenplay", None),
2875
- outputs=[stages_display, screenplay_output, status_text, current_session_id]
2876
- )
2877
-
2878
- refresh_btn.click(
2879
- fn=get_active_sessions,
2880
- outputs=[session_dropdown]
2881
- )
2882
-
2883
- download_btn.click(
2884
- fn=handle_download,
2885
- inputs=[screenplay_output, format_select, current_session_id],
2886
- outputs=[download_file]
2887
- )
2888
-
2889
- # Load sessions on start
2890
- interface.load(
2891
- fn=get_active_sessions,
2892
- outputs=[session_dropdown]
2893
- )
2894
-
2895
- return interface
2896
 
2897
  # Main function
2898
  if __name__ == "__main__":
2899
- logger.info("Screenplay Generator Starting...")
2900
- logger.info("=" * 60)
2901
-
2902
- # Environment check
2903
- logger.info(f"API Endpoint: {API_URL}")
2904
- logger.info("Screenplay Types Available:")
2905
- for s_type, info in SCREENPLAY_LENGTHS.items():
2906
- logger.info(f" - {s_type}: {info['description']}")
2907
- logger.info(f"Genres: {', '.join(GENRE_TEMPLATES.keys())}")
2908
-
2909
- if BRAVE_SEARCH_API_KEY:
2910
- logger.info("Web search enabled for market research.")
2911
- else:
2912
- logger.warning("Web search disabled.")
2913
-
2914
- logger.info("=" * 60)
2915
-
2916
- # Initialize database
2917
- logger.info("Initializing database...")
2918
- ScreenplayDatabase.init_db()
2919
- logger.info("Database initialization complete.")
2920
-
2921
- # Create and launch interface
2922
- interface = create_interface()
2923
-
2924
- interface.launch(
2925
- server_name="0.0.0.0",
2926
- server_port=7860,
2927
- share=False,
2928
- debug=True
2929
- )
 
24
 
25
  # --- Document export imports ---
26
  try:
27
+ from docx import Document
28
+ from docx.shared import Inches, Pt, RGBColor, Mm
29
+ from docx.enum.text import WD_ALIGN_PARAGRAPH
30
+ from docx.enum.style import WD_STYLE_TYPE
31
+ from docx.oxml.ns import qn
32
+ from docx.oxml import OxmlElement
33
+ DOCX_AVAILABLE = True
34
  except ImportError:
35
+ DOCX_AVAILABLE = False
36
+ logger.warning("python-docx not installed. DOCX export will be disabled.")
37
 
38
  # --- Environment variables and constants ---
39
  FRIENDLI_TOKEN = os.getenv("FRIENDLI_TOKEN", "")
40
  BRAVE_SEARCH_API_KEY = os.getenv("BRAVE_SEARCH_API_KEY", "")
41
  API_URL = "https://api.friendli.ai/dedicated/v1/chat/completions"
42
  MODEL_ID = "dep86pjolcjjnv8"
43
+ DB_PATH = "screenplay_sessions_v2.db"
44
 
45
  # Screenplay length settings
46
  SCREENPLAY_LENGTHS = {
47
+ "movie": {"pages": 110, "description": "Feature Film (90-120 pages)"},
48
+ "tv_drama": {"pages": 55, "description": "TV Drama Episode (50-60 pages)"},
49
+ "ott_series": {"pages": 45, "description": "OTT Series Episode (30-60 pages)"},
50
+ "short_film": {"pages": 15, "description": "Short Film (10-20 pages)"}
51
  }
52
 
53
  # --- Environment validation ---
54
  if not FRIENDLI_TOKEN:
55
+ logger.error("FRIENDLI_TOKEN not set. Application will not work properly.")
56
+ FRIENDLI_TOKEN = "dummy_token_for_testing"
57
 
58
  if not BRAVE_SEARCH_API_KEY:
59
+ logger.warning("BRAVE_SEARCH_API_KEY not set. Web search features will be disabled.")
60
 
61
  # --- Global variables ---
62
  db_lock = threading.Lock()
63
 
64
  # Genre templates
65
  GENRE_TEMPLATES = {
66
+ "action": {
67
+ "pacing": "fast",
68
+ "scene_length": "short",
69
+ "dialogue_ratio": 0.3,
70
+ "key_elements": ["set pieces", "physical conflict", "urgency", "stakes escalation"],
71
+ "structure_beats": ["explosive opening", "pursuit/chase", "confrontation", "climactic battle"]
72
+ },
73
+ "thriller": {
74
+ "pacing": "fast",
75
+ "scene_length": "short",
76
+ "dialogue_ratio": 0.35,
77
+ "key_elements": ["suspense", "twists", "paranoia", "time pressure"],
78
+ "structure_beats": ["hook", "mystery deepens", "false victory", "revelation", "final confrontation"]
79
+ },
80
+ "drama": {
81
+ "pacing": "moderate",
82
+ "scene_length": "medium",
83
+ "dialogue_ratio": 0.5,
84
+ "key_elements": ["character depth", "emotional truth", "relationships", "internal conflict"],
85
+ "structure_beats": ["status quo", "catalyst", "debate", "commitment", "complications", "crisis", "resolution"]
86
+ },
87
+ "comedy": {
88
+ "pacing": "fast",
89
+ "scene_length": "short",
90
+ "dialogue_ratio": 0.6,
91
+ "key_elements": ["setup/payoff", "timing", "character comedy", "escalation"],
92
+ "structure_beats": ["funny opening", "complication", "misunderstandings multiply", "chaos peak", "resolution with callback"]
93
+ },
94
+ "horror": {
95
+ "pacing": "variable",
96
+ "scene_length": "mixed",
97
+ "dialogue_ratio": 0.3,
98
+ "key_elements": ["atmosphere", "dread", "jump scares", "gore/psychological"],
99
+ "structure_beats": ["normal world", "first sign", "investigation", "first attack", "survival", "final girl/boy"]
100
+ },
101
+ "sci-fi": {
102
+ "pacing": "moderate",
103
+ "scene_length": "medium",
104
+ "dialogue_ratio": 0.4,
105
+ "key_elements": ["world building", "technology", "concepts", "visual spectacle"],
106
+ "structure_beats": ["ordinary world", "discovery", "new world", "complications", "understanding", "choice", "new normal"]
107
+ },
108
+ "romance": {
109
+ "pacing": "moderate",
110
+ "scene_length": "medium",
111
+ "dialogue_ratio": 0.55,
112
+ "key_elements": ["chemistry", "obstacles", "emotional moments", "intimacy"],
113
+ "structure_beats": ["meet cute", "attraction", "first conflict", "deepening", "crisis/breakup", "grand gesture", "together"]
114
+ }
115
  }
116
 
117
  # Screenplay stages definition
118
  SCREENPLAY_STAGES = [
119
+ ("producer", "๐ŸŽฌ Producer: Concept Development & Market Analysis"),
120
+ ("story_developer", "๐Ÿ“– Story Developer: Synopsis & Three-Act Structure"),
121
+ ("character_designer", "๐Ÿ‘ฅ Character Designer: Cast & Relationships"),
122
+ ("critic_structure", "๐Ÿ” Structure Critic: Story & Character Review"),
123
+ ("scene_planner", "๐ŸŽฏ Scene Planner: Detailed Scene Breakdown"),
124
+ ("screenwriter", "โœ๏ธ Screenwriter: Act 1 - Setup (25%)"),
125
+ ("script_doctor", "๐Ÿ”ง Script Doctor: Act 1 Review & Polish"),
126
+ ("screenwriter", "โœ๏ธ Screenwriter: Act 2A - Rising Action (25%)"),
127
+ ("script_doctor", "๐Ÿ”ง Script Doctor: Act 2A Review & Polish"),
128
+ ("screenwriter", "โœ๏ธ Screenwriter: Act 2B - Complications (25%)"),
129
+ ("script_doctor", "๐Ÿ”ง Script Doctor: Act 2B Review & Polish"),
130
+ ("screenwriter", "โœ๏ธ Screenwriter: Act 3 - Resolution (25%)"),
131
+ ("final_reviewer", "๐ŸŽญ Final Review: Complete Screenplay Analysis"),
132
  ]
133
 
134
  # Save the Cat Beat Sheet
135
  SAVE_THE_CAT_BEATS = {
136
+ 1: "Opening Image (0-1%)",
137
+ 2: "Setup (1-10%)",
138
+ 3: "Theme Stated (5%)",
139
+ 4: "Catalyst (10%)",
140
+ 5: "Debate (10-20%)",
141
+ 6: "Break into Two (20%)",
142
+ 7: "B Story (22%)",
143
+ 8: "Fun and Games (20-50%)",
144
+ 9: "Midpoint (50%)",
145
+ 10: "Bad Guys Close In (50-75%)",
146
+ 11: "All Is Lost (75%)",
147
+ 12: "Dark Night of the Soul (75-80%)",
148
+ 13: "Break into Three (80%)",
149
+ 14: "Finale (80-99%)",
150
+ 15: "Final Image (99-100%)"
151
  }
152
 
153
  # --- Data classes ---
154
  @dataclass
155
  class ScreenplayBible:
156
+ """Screenplay bible for maintaining consistency"""
157
+ title: str = ""
158
+ logline: str = ""
159
+ genre: str = ""
160
+ subgenre: str = ""
161
+ tone: str = ""
162
+ themes: List[str] = field(default_factory=list)
163
+
164
+ # Characters
165
+ protagonist: Dict[str, Any] = field(default_factory=dict)
166
+ antagonist: Dict[str, Any] = field(default_factory=dict)
167
+ supporting_cast: Dict[str, Dict[str, Any]] = field(default_factory=dict)
168
+
169
+ # Structure
170
+ three_act_structure: Dict[str, str] = field(default_factory=dict)
171
+ save_the_cat_beats: Dict[int, str] = field(default_factory=dict)
172
+
173
+ # World
174
+ time_period: str = ""
175
+ primary_locations: List[Dict[str, str]] = field(default_factory=list)
176
+ world_rules: List[str] = field(default_factory=list)
177
+
178
+ # Visual style
179
+ visual_style: str = ""
180
+ key_imagery: List[str] = field(default_factory=list)
181
 
182
  @dataclass
183
  class SceneBreakdown:
184
+ """Individual scene information"""
185
+ scene_number: int
186
+ act: int
187
+ location: str
188
+ time_of_day: str
189
+ characters: List[str]
190
+ purpose: str
191
+ conflict: str
192
+ page_count: float
193
+ beat: str = ""
194
+ transition: str = "CUT TO:"
195
 
196
  @dataclass
197
  class CharacterProfile:
198
+ """Detailed character profile"""
199
+ name: str
200
+ age: int
201
+ role: str # protagonist, antagonist, supporting, etc.
202
+ archetype: str
203
+ want: str # External goal
204
+ need: str # Internal need
205
+ backstory: str
206
+ personality: List[str]
207
+ speech_pattern: str
208
+ character_arc: str
209
+ relationships: Dict[str, str] = field(default_factory=dict)
210
+ first_appearance: str = ""
211
 
212
  # --- Core logic classes ---
213
  class ScreenplayTracker:
 
537
 
538
  return theme_id
539
 
 
 
 
 
 
 
 
 
 
 
 
 
540
  class WebSearchIntegration:
541
+ """Web search functionality for screenplay research"""
542
+ def __init__(self):
543
+ self.brave_api_key = BRAVE_SEARCH_API_KEY
544
+ self.search_url = "https://api.search.brave.com/res/v1/web/search"
545
+ self.enabled = bool(self.brave_api_key)
546
+
547
+ def search(self, query: str, count: int = 3, language: str = "en") -> List[Dict]:
548
+ if not self.enabled:
549
+ return []
550
+ headers = {
551
+ "Accept": "application/json",
552
+ "X-Subscription-Token": self.brave_api_key
553
+ }
554
+ params = {
555
+ "q": query,
556
+ "count": count,
557
+ "search_lang": "ko" if language == "Korean" else "en",
558
+ "text_decorations": False,
559
+ "safesearch": "moderate"
560
+ }
561
+ try:
562
+ response = requests.get(self.search_url, headers=headers, params=params, timeout=10)
563
+ response.raise_for_status()
564
+ results = response.json().get("web", {}).get("results", [])
565
+ return results
566
+ except requests.exceptions.RequestException as e:
567
+ logger.error(f"Web search API error: {e}")
568
+ return []
569
+
570
+ def extract_relevant_info(self, results: List[Dict], max_chars: int = 1500) -> str:
571
+ if not results:
572
+ return ""
573
+ extracted = []
574
+ total_chars = 0
575
+ for i, result in enumerate(results[:3], 1):
576
+ title = result.get("title", "")
577
+ description = result.get("description", "")
578
+ info = f"[{i}] {title}: {description}"
579
+ if total_chars + len(info) < max_chars:
580
+ extracted.append(info)
581
+ total_chars += len(info)
582
+ else:
583
+ break
584
+ return "\n".join(extracted)
585
 
586
  class ScreenplayGenerationSystem:
587
  """Professional screenplay generation system"""
 
866
  **ํ•„์ˆ˜ ์บ๋ฆญํ„ฐ ํ”„๋กœํ•„:**
867
 
868
  1. **์ฃผ์ธ๊ณต (PROTAGONIST)**
869
+ - ์ด๋ฆ„, ๋‚˜์ด:
870
  - ์ง์—…/์—ญํ• :
871
  - ์บ๋ฆญํ„ฐ ์•„ํฌํƒ€์ž…:
872
  - WANT (์™ธ์  ๋ชฉํ‘œ):
 
879
  - ์บ๋ฆญํ„ฐ ์•„ํฌ (Aโ†’B):
880
 
881
  2. **์ ๋Œ€์ž (ANTAGONIST)**
882
+ - ์ด๋ฆ„, ๋‚˜์ด:
883
  - ์ง์—…/์—ญํ• :
884
  - ์•…์—ญ ์•„ํฌํƒ€์ž…:
885
  - ๋ชฉํ‘œ & ๋™๊ธฐ:
 
890
 
891
  3. **์กฐ๋ ฅ์ž๋“ค (SUPPORTING CAST)**
892
  ์ตœ์†Œ 3๋ช…, ๊ฐ๊ฐ:
893
+ - ์ด๋ฆ„, ์—ญํ• :
894
  - ์ฃผ์ธ๊ณต๊ณผ์˜ ๊ด€๊ณ„:
895
  - ์Šคํ† ๋ฆฌ ๊ธฐ๋Šฅ:
896
  - ๋…ํŠนํ•œ ํŠน์„ฑ:
 
923
  **Required Character Profiles:**
924
 
925
  1. **PROTAGONIST**
926
+ - Name, Age:
927
  - Occupation/Role:
928
  - Character Archetype:
929
  - WANT (External Goal):
 
936
  - Character Arc (Aโ†’B):
937
 
938
  2. **ANTAGONIST**
939
+ - Name, Age:
940
  - Occupation/Role:
941
  - Villain Archetype:
942
  - Goal & Motivation:
 
947
 
948
  3. **SUPPORTING CAST**
949
  Minimum 3, each with:
950
+ - Name, Role:
951
  - Relationship to Protagonist:
952
  - Story Function:
953
  - Unique Traits:
 
1470
  raise Exception(f"LLM Call Failed: {full_content}")
1471
  return full_content
1472
 
 
 
1473
  def call_llm_streaming(self, messages: List[Dict[str, str]], role: str,
1474
  language: str) -> Generator[str, None, None]:
1475
  try:
 
1620
  logger.error(traceback.format_exc())
1621
  yield f"โŒ Unexpected error: {str(e)}"
1622
 
 
 
1623
  def get_system_prompts(self, language: str) -> Dict[str, str]:
1624
  """Role-specific system prompts"""
1625
 
 
1710
 
1711
  return base_prompts.get(language, base_prompts["English"])
1712
 
1713
+ # --- Main process ---
 
 
1714
  def process_screenplay_stream(self, query: str, screenplay_type: str, genre: str,
1715
  language: str, session_id: Optional[str] = None
1716
  ) -> Generator[Tuple[str, List[Dict[str, Any]], str], None, None]:
 
1870
 
1871
  return "\n\n---\n\n".join(previous) if previous else ""
1872
 
 
1873
  def _parse_character_profile(self, content: str, role: str) -> CharacterProfile:
1874
+ """Parse character profile from content with improved error handling"""
1875
  # Debug logging
1876
  logger.debug(f"Parsing character profile for role: {role}")
1877
  logger.debug(f"Content preview: {content[:200]}...")
 
1879
  # Extract name first - handle various formats
1880
  name = f"Character_{role}" # default
1881
  name_patterns = [
1882
+ r'(?:์ด๋ฆ„|Name)[:\s]*([^,\n]+?)(?:\s*\([^)]+\))?\s*[,:]?\s*(?:\d+์„ธ)?',
1883
+ r'^\s*[-*โ€ข]\s*([^,\n]+?)(?:\s*\([^)]+\))?\s*[,:]?\s*(?:\d+์„ธ)?',
1884
+ r'^([^,\n]+?)(?:\s*\([^)]+\))?\s*[,:]?\s*(?:\d+์„ธ)?'
1885
  ]
1886
 
1887
  for pattern in name_patterns:
 
1889
  if name_match:
1890
  extracted_name = name_match.group(1).strip()
1891
  # Remove markdown and extra characters
1892
+ extracted_name = re.sub(r'[*:\s]+, '', extracted_name)
1893
  extracted_name = re.sub(r'^[*:\s]+', '', extracted_name)
1894
+ # Remove age if it's part of the name
1895
+ extracted_name = re.sub(r'\s*,?\s*\d+\s*(?:์„ธ|์‚ด)?, '', extracted_name)
1896
  if extracted_name and len(extracted_name) > 1:
1897
  name = extracted_name
1898
  break
1899
 
1900
+ # Extract age with multiple patterns - FIXED VERSION
1901
  age = 30 # default age
1902
  age_patterns = [
1903
  r'(\d+)\s*์„ธ',
1904
  r'(\d+)\s*์‚ด',
1905
+ r'(?:๋‚˜์ด|Age)[:\s]*(\d+)',
1906
+ r',\s*(\d+)\s*(?:์„ธ|์‚ด)?(?:\s|,|$)',
1907
  r'\((\d+)\)',
1908
+ r':\s*\w+\s*,?\s*(\d+)(?:\s|,|$)'
 
1909
  ]
1910
 
1911
  for pattern in age_patterns:
 
1917
  age = extracted_age
1918
  logger.debug(f"Extracted age: {age}")
1919
  break
1920
+ except (ValueError, AttributeError):
1921
  continue
1922
 
1923
  # Helper function to extract clean fields
1924
+ def extract_clean_field(patterns, multiline=False):
1925
  if isinstance(patterns, str):
1926
  patterns = [patterns]
1927
 
1928
+ flags = re.IGNORECASE | re.DOTALL if multiline else re.IGNORECASE
1929
+
1930
  for pattern in patterns:
1931
+ match = re.search(rf'{pattern}[:\s]*([^\n*]+?)(?=\n|$)', content, flags)
1932
  if match:
1933
  value = match.group(1).strip()
1934
  # Clean up the value
 
1967
  r"Backstory",
1968
  r"ํ•ต์‹ฌ ์ƒ์ฒ˜",
1969
  r"Core Wound"
1970
+ ], multiline=True),
1971
  personality=self._extract_personality_traits(content),
1972
  speech_pattern=extract_clean_field([
1973
  r"๋งํˆฌ.*?ํŒจํ„ด",
 
1988
 
1989
  def _extract_field(self, content: str, field_pattern: str) -> Optional[str]:
1990
  """Extract field value from content with improved parsing"""
1991
+ # More flexible pattern that handles various formats
1992
+ patterns = field_pattern.split('|')
1993
+
1994
+ for pattern in patterns:
1995
+ # Try different regex patterns
1996
+ regex_patterns = [
1997
+ rf'\b{pattern}\b[:\s]*([^\n]+?)(?=\n[A-Z๊ฐ€-ํžฃ]|$)',
1998
+ rf'{pattern}[:\s]*([^\n]+)',
1999
+ rf'{pattern}.*?[:\s]+([^\n]+)'
2000
+ ]
2001
+
2002
+ for regex in regex_patterns:
2003
+ match = re.search(regex, content, re.IGNORECASE | re.DOTALL)
2004
+ if match:
2005
+ value = match.group(1).strip()
2006
+ # Remove markdown formatting if present
2007
+ value = re.sub(r'\*\*', '', value)
2008
+ value = re.sub(r'^\s*[-โ€ข*]\s*', '', value)
2009
+ # Remove trailing punctuation
2010
+ value = re.sub(r'[,.:;], '', value)
2011
+ cleaned = value.strip()
2012
+ if cleaned:
2013
+ return cleaned
2014
+
2015
  return None
2016
 
2017
  def _extract_personality_traits(self, content: str) -> List[str]:
2018
+ """Extract personality traits from content"""
2019
+ traits = []
2020
+ # Look for personality section
2021
+ personality_patterns = [
2022
+ r"(?:Personality|์„ฑ๊ฒฉ)[:\s]*([^\n]+(?:\n\s*[-โ€ข*][^\n]+)*)",
2023
+ r"์„ฑ๊ฒฉ ํŠน์„ฑ[:\s]*([^\n]+(?:\n\s*[-โ€ข*][^\n]+)*)",
2024
+ r"Personality Traits[:\s]*([^\n]+(?:\n\s*[-โ€ข*][^\n]+)*)"
2025
+ ]
2026
+
2027
+ for pattern in personality_patterns:
2028
+ match = re.search(pattern, content, re.IGNORECASE | re.DOTALL)
2029
+ if match:
2030
+ personality_section = match.group(1)
2031
+ # Extract individual traits
2032
+ trait_lines = personality_section.split('\n')
2033
+ for line in trait_lines:
2034
+ line = line.strip()
2035
+ if line and not line.endswith(':'):
2036
+ # Remove list markers
2037
+ trait = re.sub(r'^\s*[-โ€ข*]\s*', '', line)
2038
+ trait = re.sub(r'^\d+\.\s*', '', trait)
2039
+ if trait and len(trait) > 2:
2040
+ traits.append(trait)
2041
+ break
2042
+
2043
+ return traits[:5] # Limit to 5 traits
2044
 
2045
  def _process_character_content(self, content: str):
2046
+ """Process character designer output with better error handling"""
2047
+ try:
2048
+ # Extract protagonist
2049
+ protagonist_section = self._extract_section(content, r"(?:PROTAGONIST|์ฃผ์ธ๊ณต)")
2050
+ if protagonist_section:
2051
+ protagonist = self._parse_character_profile(protagonist_section, "protagonist")
2052
+ self.screenplay_tracker.add_character(protagonist)
2053
+ ScreenplayDatabase.save_character(self.current_session_id, protagonist)
2054
+
2055
+ # Extract antagonist
2056
+ antagonist_section = self._extract_section(content, r"(?:ANTAGONIST|์ ๋Œ€์ž)")
2057
+ if antagonist_section:
2058
+ antagonist = self._parse_character_profile(antagonist_section, "antagonist")
2059
+ self.screenplay_tracker.add_character(antagonist)
2060
+ ScreenplayDatabase.save_character(self.current_session_id, antagonist)
2061
+
2062
+ # Extract supporting characters
2063
+ supporting_section = self._extract_section(content, r"(?:SUPPORTING CAST|์กฐ๋ ฅ์ž๋“ค)")
2064
+ if supporting_section:
2065
+ # Parse multiple supporting characters
2066
+ self._parse_supporting_characters(supporting_section)
2067
+
2068
+ except Exception as e:
2069
+ logger.error(f"Error processing character content: {e}")
2070
+ # Continue with default values rather than failing
2071
 
2072
  def _parse_supporting_characters(self, content: str):
2073
+ """Parse supporting characters from content"""
2074
+ # Split by character markers (numbers or bullets)
2075
+ char_sections = re.split(r'\n(?:\d+\.|[-โ€ข*])\s*', content)
2076
+
2077
+ for i, section in enumerate(char_sections[1:], 1): # Skip first empty split
2078
+ if section.strip():
2079
+ try:
2080
+ name = self._extract_field(section, r"(?:Name|์ด๋ฆ„)") or f"Supporting_{i}"
2081
+ role = self._extract_field(section, r"(?:Role|์—ญํ• )") or "supporting"
2082
+
2083
+ character = CharacterProfile(
2084
+ name=name,
2085
+ age=30, # Default age for supporting characters
2086
+ role="supporting",
2087
+ archetype=role,
2088
+ want="",
2089
+ need="",
2090
+ backstory=self._extract_field(section, r"(?:Backstory|๋ฐฑ์Šคํ† ๋ฆฌ)") or "",
2091
+ personality=[],
2092
+ speech_pattern="",
2093
+ character_arc=""
2094
+ )
2095
+
2096
+ self.screenplay_tracker.add_character(character)
2097
+ ScreenplayDatabase.save_character(self.current_session_id, character)
2098
+
2099
+ except Exception as e:
2100
+ logger.warning(f"Error parsing supporting character {i}: {e}")
2101
+ continue
2102
 
2103
  def _extract_section(self, content: str, section_pattern: str) -> str:
2104
+ """Extract section from content with improved pattern matching"""
2105
+ # More flexible section extraction
2106
+ patterns = [
2107
+ rf'{section_pattern}[:\s]*\n?(.*?)(?=\n\n[A-Z๊ฐ€-ํžฃ]{{2,}}[:\s]|\n\n\d+\.|$)',
2108
+ rf'{section_pattern}.*?\n((?:.*\n)*?)(?=\n[A-Z๊ฐ€-ํžฃ]{{2,}}:|$)',
2109
+ rf'{section_pattern}[:\s]*((?:[^\n]+\n?)*?)(?=\n\n|\Z)'
2110
+ ]
2111
+
2112
+ for pattern in patterns:
2113
+ match = re.search(pattern, content, re.IGNORECASE | re.DOTALL)
2114
+ if match:
2115
+ return match.group(1).strip()
2116
+
2117
+ return ""
2118
 
2119
  def _process_producer_content(self, content: str):
2120
+ """Process producer output with better extraction"""
2121
+ try:
2122
+ # Extract title with various formats
2123
+ title_patterns = [
2124
+ r'(?:TITLE|์ œ๋ชฉ)[:\s]*\*?\*?([^\n*]+)\*?\*?',
2125
+ r'\*\*(?:TITLE|์ œ๋ชฉ)\*\*[:\s]*([^\n]+)',
2126
+ r'Title[:\s]*([^\n]+)',
2127
+ r'1\.\s*\*?\*?(?:TITLE|์ œ๋ชฉ).*?[:\s]*([^\n]+)'
2128
+ ]
2129
+
2130
+ for pattern in title_patterns:
2131
+ title_match = re.search(pattern, content, re.IGNORECASE)
2132
+ if title_match:
2133
+ self.screenplay_tracker.screenplay_bible.title = title_match.group(1).strip()
2134
+ break
2135
+
2136
+ # Extract logline with various formats
2137
+ logline_patterns = [
2138
+ r'(?:LOGLINE|๋กœ๊ทธ๋ผ์ธ)[:\s]*\*?\*?([^\n]+(?:\n(?!\s*\n)[^\n]+)*)',
2139
+ r'\*\*(?:LOGLINE|๋กœ๊ทธ๋ผ์ธ)\*\*[:\s]*([^\n]+(?:\n(?!\s*\n)[^\n]+)*)',
2140
+ r'Logline[:\s]*([^\n]+(?:\n(?!\s*\n)[^\n]+)*)',
2141
+ r'2\.\s*\*?\*?(?:LOGLINE|๋กœ๊ทธ๋ผ์ธ).*?[:\s]*([^\n]+(?:\n(?!\s*\n)[^\n]+)*)'
2142
+ ]
2143
+
2144
+ for pattern in logline_patterns:
2145
+ logline_match = re.search(pattern, content, re.IGNORECASE | re.DOTALL)
2146
+ if logline_match:
2147
+ # Get full logline (might be multi-line)
2148
+ logline_text = logline_match.group(1).strip()
2149
+ # Clean up the logline
2150
+ logline_text = re.sub(r'\s+', ' ', logline_text)
2151
+ logline_text = re.sub(r'^[-โ€ข*]\s*', '', logline_text)
2152
+ self.screenplay_tracker.screenplay_bible.logline = logline_text
2153
+ break
2154
+
2155
+ # Extract genre
2156
+ genre_match = re.search(r'(?:Primary Genre|์ฃผ ์žฅ๋ฅด)[:\s]*([^\n]+)', content, re.IGNORECASE)
2157
+ if genre_match:
2158
+ self.screenplay_tracker.screenplay_bible.genre = genre_match.group(1).strip()
2159
+
2160
+ # Save to database
2161
+ ScreenplayDatabase.save_screenplay_bible(self.current_session_id,
2162
+ self.screenplay_tracker.screenplay_bible)
2163
+
2164
+ except Exception as e:
2165
+ logger.error(f"Error processing producer content: {e}")
 
 
 
 
 
 
 
2166
 
2167
  def _process_story_content(self, content: str):
2168
  """Process story developer output"""
 
2177
  ScreenplayDatabase.save_screenplay_bible(self.current_session_id,
2178
  self.screenplay_tracker.screenplay_bible)
2179
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2180
  def _process_scene_content(self, content: str):
2181
  """Process scene planner output"""
2182
  # Parse scene breakdown
 
2205
  self.screenplay_tracker.add_scene(scene)
2206
  ScreenplayDatabase.save_scene(self.current_session_id, scene)
2207
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2208
  # --- Utility functions ---
2209
  def generate_random_screenplay_theme(screenplay_type: str, genre: str, language: str) -> str:
2210
  """Generate random screenplay theme"""
 
2345
  **Unique Element:** A fresh take on {genre} genre conventions with contemporary relevance."""
2346
 
2347
  def load_screenplay_themes_data() -> Dict:
2348
+ """Load screenplay themes data"""
2349
+ return {
2350
+ 'situations': {
2351
+ 'action': ['hostage crisis', 'heist gone wrong', 'revenge mission', 'race against time'],
2352
+ 'thriller': ['false accusation', 'witness protection', 'conspiracy uncovered', 'identity theft'],
2353
+ 'drama': ['family reunion', 'terminal diagnosis', 'divorce proceedings', 'career crossroads'],
2354
+ 'comedy': ['mistaken identity', 'wedding disaster', 'workplace chaos', 'odd couple roommates'],
2355
+ 'horror': ['isolated location', 'ancient curse', 'home invasion', 'supernatural investigation'],
2356
+ 'sci-fi': ['first contact', 'time loop', 'AI awakening', 'space colony crisis'],
2357
+ 'romance': ['second chance', 'enemies to lovers', 'long distance', 'forbidden love']
2358
+ },
2359
+ 'protagonists': {
2360
+ 'action': ['ex-soldier', 'undercover cop', 'skilled thief', 'reluctant hero'],
2361
+ 'thriller': ['investigative journalist', 'wrongly accused person', 'FBI agent', 'whistleblower'],
2362
+ 'drama': ['single parent', 'recovering addict', 'immigrant', 'caregiver'],
2363
+ 'comedy': ['uptight professional', 'slacker', 'fish out of water', 'eccentric artist'],
2364
+ 'horror': ['skeptical scientist', 'final girl', 'paranormal investigator', 'grieving parent'],
2365
+ 'sci-fi': ['astronaut', 'AI researcher', 'time traveler', 'colony leader'],
2366
+ 'romance': ['workaholic', 'hopeless romantic', 'cynical divorce lawyer', 'small town newcomer']
2367
+ },
2368
+ 'conflicts': {
2369
+ 'action': ['stop the villain', 'save the hostages', 'prevent disaster', 'survive pursuit'],
2370
+ 'thriller': ['prove innocence', 'expose truth', 'stay alive', 'protect loved ones'],
2371
+ 'drama': ['reconcile past', 'find purpose', 'heal relationships', 'accept change'],
2372
+ 'comedy': ['save the business', 'win the competition', 'fool everyone', 'find love'],
2373
+ 'horror': ['survive the night', 'break the curse', 'escape the monster', 'save the town'],
2374
+ 'sci-fi': ['save humanity', 'prevent paradox', 'stop the invasion', 'preserve identity'],
2375
+ 'romance': ['overcome differences', 'choose between options', 'trust again', 'follow heart']
2376
+ }
2377
+ }
2378
 
2379
  def extract_title_from_theme(theme_text: str) -> str:
2380
+ """Extract title from generated theme"""
2381
+ match = re.search(r'\*\*(?:Title|์ œ๋ชฉ):\*\*\s*(.+)', theme_text, re.IGNORECASE)
2382
+ return match.group(1).strip() if match else ""
2383
 
2384
  def extract_logline_from_theme(theme_text: str) -> str:
2385
+ """Extract logline from generated theme"""
2386
+ match = re.search(r'\*\*(?:Logline|๋กœ๊ทธ๋ผ์ธ):\*\*\s*(.+)', theme_text, re.IGNORECASE)
2387
+ return match.group(1).strip() if match else ""
2388
 
2389
  def format_screenplay_display(screenplay_text: str) -> str:
2390
+ """Format screenplay for display"""
2391
+ if not screenplay_text:
2392
+ return "No screenplay content yet."
2393
+
2394
+ formatted = "# ๐ŸŽฌ Screenplay\n\n"
2395
+
2396
+ # Format scene headings
2397
+ formatted_text = re.sub(
2398
+ r'^(INT\.|EXT\.)(.*?),
2399
+ r'**\1\2**',
2400
+ screenplay_text,
2401
+ flags=re.MULTILINE
2402
+ )
2403
+
2404
+ # Format character names (all caps on their own line)
2405
+ formatted_text = re.sub(
2406
+ r'^([A-Z][A-Z\s]+),
2407
+ r'**\1**',
2408
+ formatted_text,
2409
+ flags=re.MULTILINE
2410
+ )
2411
+
2412
+ # Add spacing for readability
2413
+ lines = formatted_text.split('\n')
2414
+ formatted_lines = []
2415
+
2416
+ for i, line in enumerate(lines):
2417
+ formatted_lines.append(line)
2418
+ # Add extra space after scene headings
2419
+ if line.startswith('**INT.') or line.startswith('**EXT.'):
2420
+ formatted_lines.append('')
2421
+
2422
+ formatted += '\n'.join(formatted_lines)
2423
+
2424
+ # Add page count
2425
+ page_count = len(screenplay_text.split('\n')) / 55
2426
+ formatted = f"**Total Pages: {page_count:.1f}**\n\n" + formatted
2427
+
2428
+ return formatted
2429
 
2430
  def format_stages_display(stages: List[Dict]) -> str:
2431
+ """Format stages display for screenplay"""
2432
+ markdown = "## ๐ŸŽฌ Production Progress\n\n"
2433
+
2434
+ # Progress summary
2435
+ completed = sum(1 for s in stages if s.get('status') == 'complete')
2436
+ total = len(stages)
2437
+ markdown += f"**Progress: {completed}/{total} stages complete**\n\n"
2438
+
2439
+ # Page count if available
2440
+ total_pages = sum(s.get('page_count', 0) for s in stages if s.get('page_count'))
2441
+ if total_pages > 0:
2442
+ markdown += f"**Current Page Count: {total_pages:.1f} pages**\n\n"
2443
+
2444
+ markdown += "---\n\n"
2445
+
2446
+ # Stage details
2447
+ current_act = None
2448
+ for i, stage in enumerate(stages):
2449
+ status_icon = "โœ…" if stage['status'] == 'complete' else "๐Ÿ”„" if stage['status'] == 'active' else "โณ"
2450
+
2451
+ # Group by acts
2452
+ if 'Act' in stage.get('name', ''):
2453
+ act_match = re.search(r'Act (\w+)', stage['name'])
2454
+ if act_match and act_match.group(1) != current_act:
2455
+ current_act = act_match.group(1)
2456
+ markdown += f"\n### ๐Ÿ“„ Act {current_act}\n\n"
2457
+
2458
+ markdown += f"{status_icon} **{stage['name']}**"
2459
+
2460
+ if stage.get('page_count', 0) > 0:
2461
+ markdown += f" ({stage['page_count']:.1f} pages)"
2462
+
2463
+ markdown += "\n"
2464
+
2465
+ if stage['content'] and stage['status'] == 'complete':
2466
+ preview_length = 200
2467
+ preview = stage['content'][:preview_length] + "..." if len(stage['content']) > preview_length else stage['content']
2468
+ markdown += f"> {preview}\n\n"
2469
+ elif stage['status'] == 'active':
2470
+ markdown += "> *In progress...*\n\n"
2471
+
2472
+ return markdown
2473
 
2474
  def process_query(query: str, screenplay_type: str, genre: str, language: str,
2475
  session_id: Optional[str] = None) -> Generator[Tuple[str, str, str, str], None, None]:
2476
+ """Main query processing function"""
2477
+ if not query.strip():
2478
+ yield "", "", "โŒ Please enter a screenplay concept.", session_id
2479
+ return
2480
+
2481
+ system = ScreenplayGenerationSystem()
2482
+ stages_markdown = ""
2483
+ screenplay_display = ""
2484
+
2485
+ for status, stages, current_session_id in system.process_screenplay_stream(
2486
+ query, screenplay_type, genre, language, session_id
2487
+ ):
2488
+ stages_markdown = format_stages_display(stages)
2489
+
2490
+ # Get screenplay content when available
2491
+ if stages and all(s.get("status") == "complete" for s in stages[-4:]):
2492
+ screenplay_text = ScreenplayDatabase.get_screenplay_content(current_session_id)
2493
+ screenplay_display = format_screenplay_display(screenplay_text)
2494
+
2495
+ yield stages_markdown, screenplay_display, status or "๐Ÿ”„ Processing...", current_session_id
2496
 
2497
  def get_active_sessions() -> List[str]:
2498
+ """Get active screenplay sessions"""
2499
+ sessions = ScreenplayDatabase.get_active_sessions()
2500
+ return [
2501
+ f"{s['session_id'][:8]}... - {s.get('title', s['user_query'][:30])}... "
2502
+ f"({s['screenplay_type']}/{s['genre']}) [{s['total_pages']:.1f} pages]"
2503
+ for s in sessions
2504
+ ]
2505
 
2506
  def export_screenplay_pdf(screenplay_text: str, title: str, session_id: str) -> str:
2507
+ """Export screenplay to PDF format"""
2508
+ # This would use a library like reportlab to create industry-standard PDF
2509
+ # For now, returning a placeholder
2510
+ pdf_path = f"screenplay_{session_id[:8]}.pdf"
2511
+ # PDF generation logic would go here
2512
+ return pdf_path
2513
 
2514
  def export_screenplay_fdx(screenplay_text: str, title: str, session_id: str) -> str:
2515
+ """Export to Final Draft format"""
2516
+ # This would create .fdx XML format
2517
+ fdx_path = f"screenplay_{session_id[:8]}.fdx"
2518
+ # FDX generation logic would go here
2519
+ return fdx_path
2520
 
2521
  def download_screenplay(screenplay_text: str, format_type: str, title: str,
2522
  session_id: str) -> Optional[str]:
2523
+ """Generate screenplay download file"""
2524
+ if not screenplay_text or not session_id:
2525
+ return None
2526
+
2527
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
2528
+
2529
+ try:
2530
+ if format_type == "PDF":
2531
+ return export_screenplay_pdf(screenplay_text, title, session_id)
2532
+ elif format_type == "FDX":
2533
+ return export_screenplay_fdx(screenplay_text, title, session_id)
2534
+ elif format_type == "FOUNTAIN":
2535
+ filepath = f"screenplay_{session_id[:8]}_{timestamp}.fountain"
2536
+ with open(filepath, 'w', encoding='utf-8') as f:
2537
+ f.write(screenplay_text)
2538
+ return filepath
2539
+ else: # TXT
2540
+ filepath = f"screenplay_{session_id[:8]}_{timestamp}.txt"
2541
+ with open(filepath, 'w', encoding='utf-8') as f:
2542
+ f.write(f"Title: {title}\n")
2543
+ f.write("=" * 50 + "\n\n")
2544
+ f.write(screenplay_text)
2545
+ return filepath
2546
+ except Exception as e:
2547
+ logger.error(f"Download generation failed: {e}")
2548
+ return None
2549
 
2550
  # Create Gradio interface
2551
  def create_interface():
2552
+ """Create Gradio interface for screenplay generation"""
2553
+
2554
+ css = """
2555
+ .main-header {
2556
+ text-align: center;
2557
+ margin-bottom: 2rem;
2558
+ padding: 2rem;
2559
+ background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
2560
+ border-radius: 10px;
2561
+ color: white;
2562
+ }
2563
+
2564
+ .header-title {
2565
+ font-size: 3rem;
2566
+ margin-bottom: 1rem;
2567
+ background: linear-gradient(45deg, #f39c12, #e74c3c);
2568
+ -webkit-background-clip: text;
2569
+ -webkit-text-fill-color: transparent;
2570
+ }
2571
+
2572
+ .header-description {
2573
+ font-size: 1.1rem;
2574
+ opacity: 0.9;
2575
+ line-height: 1.6;
2576
+ }
2577
+
2578
+ .type-selector {
2579
+ display: flex;
2580
+ gap: 1rem;
2581
+ margin: 1rem 0;
2582
+ }
2583
+
2584
+ .type-card {
2585
+ flex: 1;
2586
+ padding: 1rem;
2587
+ border: 2px solid #ddd;
2588
+ border-radius: 8px;
2589
+ cursor: pointer;
2590
+ transition: all 0.3s;
2591
+ }
2592
+
2593
+ .type-card:hover {
2594
+ border-color: #f39c12;
2595
+ transform: translateY(-2px);
2596
+ }
2597
+
2598
+ .type-card.selected {
2599
+ border-color: #e74c3c;
2600
+ background: #fff5f5;
2601
+ }
2602
+
2603
+ #stages-display {
2604
+ max-height: 600px;
2605
+ overflow-y: auto;
2606
+ padding: 1rem;
2607
+ background: #f8f9fa;
2608
+ border-radius: 8px;
2609
+ }
2610
+
2611
+ #screenplay-output {
2612
+ font-family: 'Courier New', monospace;
2613
+ white-space: pre-wrap;
2614
+ background: white;
2615
+ padding: 2rem;
2616
+ border: 1px solid #ddd;
2617
+ border-radius: 8px;
2618
+ max-height: 800px;
2619
+ overflow-y: auto;
2620
+ }
2621
+
2622
+ .genre-grid {
2623
+ display: grid;
2624
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
2625
+ gap: 0.5rem;
2626
+ margin: 1rem 0;
2627
+ }
2628
+
2629
+ .genre-btn {
2630
+ padding: 0.75rem;
2631
+ border: 2px solid #e0e0e0;
2632
+ background: white;
2633
+ border-radius: 8px;
2634
+ cursor: pointer;
2635
+ transition: all 0.3s;
2636
+ text-align: center;
2637
+ }
2638
+
2639
+ .genre-btn:hover {
2640
+ border-color: #f39c12;
2641
+ background: #fffbf0;
2642
+ }
2643
+
2644
+ .genre-btn.selected {
2645
+ border-color: #e74c3c;
2646
+ background: #fff5f5;
2647
+ font-weight: bold;
2648
+ }
2649
+ """
2650
+
2651
+ with gr.Blocks(theme=gr.themes.Soft(), css=css, title="Screenplay Generator") as interface:
2652
+ gr.HTML("""
2653
+ <div class="main-header">
2654
+ <h1 class="header-title">๐ŸŽฌ AI Screenplay Generator</h1>
2655
+ <p class="header-description">
2656
+ Transform your ideas into professional screenplays for films, TV shows, and streaming series.
2657
+ Using industry-standard format and story structure to create compelling, producible scripts.
2658
+ </p>
2659
+ </div>
2660
+ """)
2661
+
2662
+ # State management
2663
+ current_session_id = gr.State(None)
2664
+
2665
+ with gr.Tabs():
2666
+ # Main Writing Tab
2667
+ with gr.Tab("โœ๏ธ Write Screenplay"):
2668
+ with gr.Row():
2669
+ with gr.Column(scale=3):
2670
+ query_input = gr.Textbox(
2671
+ label="Screenplay Concept",
2672
+ placeholder="""Describe your screenplay idea. For example:
2673
  - A detective with memory loss must solve their own attempted murder
2674
  - Two rival food truck owners forced to work together to save the city food festival
2675
  - A space station AI develops consciousness during a critical mission
2676
  - A family reunion turns into a murder mystery during a hurricane
2677
 
2678
  The more specific your concept, the better the screenplay will be tailored to your vision.""",
2679
+ lines=6
2680
+ )
2681
+
2682
+ with gr.Column(scale=1):
2683
+ screenplay_type = gr.Radio(
2684
+ choices=list(SCREENPLAY_LENGTHS.keys()),
2685
+ value="movie",
2686
+ label="Screenplay Type",
2687
+ info="Choose your format"
2688
+ )
2689
+
2690
+ genre_select = gr.Dropdown(
2691
+ choices=list(GENRE_TEMPLATES.keys()),
2692
+ value="drama",
2693
+ label="Primary Genre",
2694
+ info="Select main genre"
2695
+ )
2696
+
2697
+ language_select = gr.Radio(
2698
+ choices=["English", "Korean"],
2699
+ value="English",
2700
+ label="Language"
2701
+ )
2702
+
2703
+ with gr.Row():
2704
+ random_btn = gr.Button("๐ŸŽฒ Random Concept", scale=1)
2705
+ clear_btn = gr.Button("๐Ÿ—‘๏ธ Clear", scale=1)
2706
+ submit_btn = gr.Button("๐ŸŽฌ Start Writing", variant="primary", scale=2)
2707
+
2708
+ status_text = gr.Textbox(
2709
+ label="Status",
2710
+ interactive=False,
2711
+ value="Ready to create your screenplay"
2712
+ )
2713
+
2714
+ # Session management
2715
+ with gr.Group():
2716
+ gr.Markdown("### ๐Ÿ“ Saved Projects")
2717
+ with gr.Row():
2718
+ session_dropdown = gr.Dropdown(
2719
+ label="Active Sessions",
2720
+ choices=[],
2721
+ interactive=True,
2722
+ scale=3
2723
+ )
2724
+ refresh_btn = gr.Button("๐Ÿ”„", scale=1)
2725
+ resume_btn = gr.Button("๐Ÿ“‚ Load", scale=1)
2726
+
2727
+ # Output displays
2728
+ with gr.Row():
2729
+ with gr.Column():
2730
+ with gr.Tab("๐ŸŽญ Writing Progress"):
2731
+ stages_display = gr.Markdown(
2732
+ value="*Your screenplay journey will unfold here...*",
2733
+ elem_id="stages-display"
2734
+ )
2735
+
2736
+ with gr.Tab("๐Ÿ“„ Screenplay"):
2737
+ screenplay_output = gr.Markdown(
2738
+ value="*Your formatted screenplay will appear here...*",
2739
+ elem_id="screenplay-output"
2740
+ )
2741
+
2742
+ with gr.Row():
2743
+ format_select = gr.Radio(
2744
+ choices=["PDF", "FDX", "FOUNTAIN", "TXT"],
2745
+ value="PDF",
2746
+ label="Export Format"
2747
+ )
2748
+ download_btn = gr.Button("๐Ÿ“ฅ Download Screenplay", variant="secondary")
2749
+
2750
+ download_file = gr.File(
2751
+ label="Download",
2752
+ visible=False
2753
+ )
2754
+
2755
+ # Examples
2756
+ gr.Examples(
2757
+ examples=[
2758
+ ["A burned-out teacher discovers her students are being replaced by AI duplicates"],
2759
+ ["Two funeral home employees accidentally release a ghost who helps them solve murders"],
2760
+ ["A time-loop forces a wedding planner to relive the worst wedding until they find true love"],
2761
+ ["An astronaut returns to Earth to find everyone has forgotten space exists"],
2762
+ ["A support group for reformed villains must save the city when heroes disappear"],
2763
+ ["A food critic loses their sense of taste and teams up with a street food vendor"]
2764
+ ],
2765
+ inputs=query_input,
2766
+ label="๐Ÿ’ก Example Concepts"
2767
+ )
2768
+
2769
+ # Screenplay Library Tab
2770
+ with gr.Tab("๐Ÿ“š Concept Library"):
2771
+ gr.Markdown("""
2772
+ ### ๐ŸŽฒ Random Screenplay Concepts
2773
+
2774
+ Browse through AI-generated screenplay concepts. Each concept includes a title, logline, and brief setup.
2775
+ """)
2776
+
2777
+ library_display = gr.HTML(
2778
+ value="<p>Library feature coming soon...</p>"
2779
+ )
2780
+
2781
+ # Event handlers
2782
+ def handle_submit(query, s_type, genre, lang, session_id):
2783
+ if not query:
2784
+ yield "", "", "โŒ Please enter a concept", session_id
2785
+ return
2786
+
2787
+ yield from process_query(query, s_type, genre, lang, session_id)
2788
+
2789
+ def handle_random(s_type, genre, lang):
2790
+ return generate_random_screenplay_theme(s_type, genre, lang)
2791
+
2792
+ def handle_download(screenplay_text, format_type, session_id):
2793
+ if not screenplay_text or not session_id:
2794
+ return gr.update(visible=False)
2795
+
2796
+ # Get title from database
2797
+ session = ScreenplayDatabase.get_session(session_id)
2798
+ title = session.get('title', 'Untitled') if session else 'Untitled'
2799
+
2800
+ file_path = download_screenplay(screenplay_text, format_type, title, session_id)
2801
+ if file_path and os.path.exists(file_path):
2802
+ return gr.update(value=file_path, visible=True)
2803
+ return gr.update(visible=False)
2804
+
2805
+ # Connect events
2806
+ submit_btn.click(
2807
+ fn=handle_submit,
2808
+ inputs=[query_input, screenplay_type, genre_select, language_select, current_session_id],
2809
+ outputs=[stages_display, screenplay_output, status_text, current_session_id]
2810
+ )
2811
+
2812
+ random_btn.click(
2813
+ fn=handle_random,
2814
+ inputs=[screenplay_type, genre_select, language_select],
2815
+ outputs=[query_input]
2816
+ )
2817
+
2818
+ clear_btn.click(
2819
+ fn=lambda: ("", "", "Ready to create your screenplay", None),
2820
+ outputs=[stages_display, screenplay_output, status_text, current_session_id]
2821
+ )
2822
+
2823
+ refresh_btn.click(
2824
+ fn=get_active_sessions,
2825
+ outputs=[session_dropdown]
2826
+ )
2827
+
2828
+ download_btn.click(
2829
+ fn=handle_download,
2830
+ inputs=[screenplay_output, format_select, current_session_id],
2831
+ outputs=[download_file]
2832
+ )
2833
+
2834
+ # Load sessions on start
2835
+ interface.load(
2836
+ fn=get_active_sessions,
2837
+ outputs=[session_dropdown]
2838
+ )
2839
+
2840
+ return interface
2841
 
2842
  # Main function
2843
  if __name__ == "__main__":
2844
+ logger.info("Screenplay Generator Starting...")
2845
+ logger.info("=" * 60)
2846
+
2847
+ # Environment check
2848
+ logger.info(f"API Endpoint: {API_URL}")
2849
+ logger.info("Screenplay Types Available:")
2850
+ for s_type, info in SCREENPLAY_LENGTHS.items():
2851
+ logger.info(f" - {s_type}: {info['description']}")
2852
+ logger.info(f"Genres: {', '.join(GENRE_TEMPLATES.keys())}")
2853
+
2854
+ if BRAVE_SEARCH_API_KEY:
2855
+ logger.info("Web search enabled for market research.")
2856
+ else:
2857
+ logger.warning("Web search disabled.")
2858
+
2859
+ logger.info("=" * 60)
2860
+
2861
+ # Initialize database
2862
+ logger.info("Initializing database...")
2863
+ ScreenplayDatabase.init_db()
2864
+ logger.info("Database initialization complete.")
2865
+
2866
+ # Create and launch interface
2867
+ interface = create_interface()
2868
+
2869
+ interface.launch(
2870
+ server_name="0.0.0.0",
2871
+ server_port=7860,
2872
+ share=False,
2873
+ debug=True
2874
+ )