openfree commited on
Commit
c1fd40d
ยท
verified ยท
1 Parent(s): 71ec00a

Update app-backup.py

Browse files
Files changed (1) hide show
  1. app-backup.py +327 -380
app-backup.py CHANGED
@@ -197,7 +197,6 @@ class SceneBreakdown:
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
@@ -537,18 +536,6 @@ 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):
@@ -878,7 +865,7 @@ Create specific, emotionally resonant story."""
878
  **ํ•„์ˆ˜ ์บ๋ฆญํ„ฐ ํ”„๋กœํ•„:**
879
 
880
  1. **์ฃผ์ธ๊ณต (PROTAGONIST)**
881
- - ์ด๋ฆ„ & ๋‚˜์ด:
882
  - ์ง์—…/์—ญํ• :
883
  - ์บ๋ฆญํ„ฐ ์•„ํฌํƒ€์ž…:
884
  - WANT (์™ธ์  ๋ชฉํ‘œ):
@@ -891,7 +878,7 @@ Create specific, emotionally resonant story."""
891
  - ์บ๋ฆญํ„ฐ ์•„ํฌ (Aโ†’B):
892
 
893
  2. **์ ๋Œ€์ž (ANTAGONIST)**
894
- - ์ด๋ฆ„ & ๋‚˜์ด:
895
  - ์ง์—…/์—ญํ• :
896
  - ์•…์—ญ ์•„ํฌํƒ€์ž…:
897
  - ๋ชฉํ‘œ & ๋™๊ธฐ:
@@ -916,7 +903,7 @@ Create specific, emotionally resonant story."""
916
 
917
  5. **์บ์ŠคํŒ… ์ œ์•ˆ**
918
  - ๊ฐ ์ฃผ์š” ์บ๋ฆญํ„ฐ๋ณ„ ์ด์ƒ์ ์ธ ๋ฐฐ์šฐ ํƒ€์ž…
919
- - ์—ฐ๋ น๋Œ€, ์™ธ๋ชจ, ์—ฐ๊ธฐ ์Šคํƒ€์ผ
920
 
921
  6. **๋Œ€ํ™” ์ƒ˜ํ”Œ**
922
  - ๊ฐ ์ฃผ์š” ์บ๋ฆญํ„ฐ์˜ ์‹œ๊ทธ๋‹ˆ์ฒ˜ ๋Œ€์‚ฌ 2-3๊ฐœ
@@ -935,7 +922,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 +935,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:
@@ -973,7 +960,7 @@ Create specific, emotionally resonant story."""
973
 
974
  5. **CASTING SUGGESTIONS**
975
  - Ideal actor type for each major character
976
- - Age range, appearance, acting style
977
 
978
  6. **DIALOGUE SAMPLES**
979
  - 2-3 signature lines per major character
@@ -1104,7 +1091,7 @@ Plan each scene to advance story and develop character."""
1104
  - ๊ฐ์ •์€ ํ–‰๋™์œผ๋กœ ํ‘œํ˜„
1105
 
1106
  3. **์บ๋ฆญํ„ฐ ์†Œ๊ฐœ**
1107
- ์ฒซ ๋“ฑ์žฅ์‹œ: ์ด๋ฆ„ (๋‚˜์ด) ๊ฐ„๋‹จํ•œ ๋ฌ˜์‚ฌ
1108
 
1109
  4. **๋Œ€ํ™”**
1110
  ์บ๋ฆญํ„ฐ๋ช…
@@ -1151,7 +1138,7 @@ Plan each scene to advance story and develop character."""
1151
  - Emotions through actions
1152
 
1153
  3. **Character Intros**
1154
- First appearance: NAME (age) brief description
1155
 
1156
  4. **Dialogue**
1157
  CHARACTER NAME
@@ -1482,8 +1469,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:
@@ -1604,7 +1589,6 @@ Provide specific solutions for each issue."""
1604
  yield buffer
1605
  buffer = ""
1606
  time.sleep(0.01)
1607
-
1608
  except Exception as e:
1609
  logger.error(f"Error processing line {line_count}: {str(e)}")
1610
  logger.debug(f"Problematic line: {line_str[:100] if 'line_str' in locals() else 'N/A'}")
@@ -1634,8 +1618,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 +1708,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]:
@@ -1889,279 +1869,296 @@ You provide feedback that's critical yet encouraging."""
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]}...")
1897
-
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:
1907
- name_match = re.search(pattern, content, re.IGNORECASE | re.MULTILINE)
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:
1929
- age_match = re.search(pattern, content, re.IGNORECASE)
1930
- if age_match:
1931
- try:
1932
- extracted_age = int(age_match.group(1))
1933
- if 10 <= extracted_age <= 100: # Reasonable age range
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
1950
- value = re.sub(r'^[-*โ€ข:\s]+', '', value)
1951
- value = re.sub(r'[*]+', '', value)
1952
- value = re.sub(r'\s+', ' ', value)
1953
- if value:
1954
- return value
1955
  return ""
1956
-
1957
- # Extract all fields
1958
- profile = CharacterProfile(
 
 
 
 
 
 
 
 
 
 
 
 
1959
  name=name,
1960
- age=age,
1961
  role=role,
1962
- archetype=extract_clean_field([
1963
- r"์บ๋ฆญํ„ฐ ์•„ํฌํƒ€์ž…",
1964
- r"Character Archetype",
1965
- r"Archetype",
1966
- r"์•„ํฌํƒ€์ž…"
1967
- ]),
1968
- want=extract_clean_field([
1969
- r"WANT\s*\(์™ธ์  ๋ชฉํ‘œ\)",
1970
- r"WANT",
1971
- r"์™ธ์  ๋ชฉํ‘œ",
1972
- r"External Goal"
1973
- ]),
1974
- need=extract_clean_field([
1975
- r"NEED\s*\(๋‚ด์  ํ•„์š”\)",
1976
- r"NEED",
1977
- r"๋‚ด์  ํ•„์š”",
1978
- r"Internal Need"
1979
- ]),
1980
- backstory=extract_clean_field([
1981
- r"๋ฐฑ์Šคํ† ๋ฆฌ",
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"๋งํˆฌ.*?ํŒจํ„ด",
1989
- r"Speech Pattern",
1990
- r"์–ธ์–ด ํŒจํ„ด",
1991
- r"๋งํˆฌ"
1992
- ]),
1993
- character_arc=extract_clean_field([
1994
- r"์บ๋ฆญํ„ฐ ์•„ํฌ",
1995
- r"Character Arc",
1996
- r"Arc",
1997
- r"๋ณ€ํ™”"
1998
- ])
1999
  )
2000
-
2001
- logger.debug(f"Parsed character: {profile.name}, age: {profile.age}")
2002
- return profile
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 +2173,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 +2201,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"""
@@ -2441,46 +2382,52 @@ def extract_logline_from_theme(theme_text: str) -> str:
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"""
@@ -2926,4 +2873,4 @@ if __name__ == "__main__":
2926
  server_port=7860,
2927
  share=False,
2928
  debug=True
2929
- )
 
197
  class CharacterProfile:
198
  """Detailed character profile"""
199
  name: str
 
200
  role: str # protagonist, antagonist, supporting, etc.
201
  archetype: str
202
  want: str # External goal
 
536
 
537
  return theme_id
538
 
 
 
 
 
 
 
 
 
 
 
 
 
539
  class WebSearchIntegration:
540
  """Web search functionality for screenplay research"""
541
  def __init__(self):
 
865
  **ํ•„์ˆ˜ ์บ๋ฆญํ„ฐ ํ”„๋กœํ•„:**
866
 
867
  1. **์ฃผ์ธ๊ณต (PROTAGONIST)**
868
+ - ์ด๋ฆ„:
869
  - ์ง์—…/์—ญํ• :
870
  - ์บ๋ฆญํ„ฐ ์•„ํฌํƒ€์ž…:
871
  - WANT (์™ธ์  ๋ชฉํ‘œ):
 
878
  - ์บ๋ฆญํ„ฐ ์•„ํฌ (Aโ†’B):
879
 
880
  2. **์ ๋Œ€์ž (ANTAGONIST)**
881
+ - ์ด๋ฆ„:
882
  - ์ง์—…/์—ญํ• :
883
  - ์•…์—ญ ์•„ํฌํƒ€์ž…:
884
  - ๋ชฉํ‘œ & ๋™๊ธฐ:
 
903
 
904
  5. **์บ์ŠคํŒ… ์ œ์•ˆ**
905
  - ๊ฐ ์ฃผ์š” ์บ๋ฆญํ„ฐ๋ณ„ ์ด์ƒ์ ์ธ ๋ฐฐ์šฐ ํƒ€์ž…
906
+ - ์™ธ๋ชจ, ์—ฐ๊ธฐ ์Šคํƒ€์ผ
907
 
908
  6. **๋Œ€ํ™” ์ƒ˜ํ”Œ**
909
  - ๊ฐ ์ฃผ์š” ์บ๋ฆญํ„ฐ์˜ ์‹œ๊ทธ๋‹ˆ์ฒ˜ ๋Œ€์‚ฌ 2-3๊ฐœ
 
922
  **Required Character Profiles:**
923
 
924
  1. **PROTAGONIST**
925
+ - Name:
926
  - Occupation/Role:
927
  - Character Archetype:
928
  - WANT (External Goal):
 
935
  - Character Arc (Aโ†’B):
936
 
937
  2. **ANTAGONIST**
938
+ - Name:
939
  - Occupation/Role:
940
  - Villain Archetype:
941
  - Goal & Motivation:
 
960
 
961
  5. **CASTING SUGGESTIONS**
962
  - Ideal actor type for each major character
963
+ - Appearance, acting style
964
 
965
  6. **DIALOGUE SAMPLES**
966
  - 2-3 signature lines per major character
 
1091
  - ๊ฐ์ •์€ ํ–‰๋™์œผ๋กœ ํ‘œํ˜„
1092
 
1093
  3. **์บ๋ฆญํ„ฐ ์†Œ๊ฐœ**
1094
+ ์ฒซ ๋“ฑ์žฅ์‹œ: ์ด๋ฆ„๊ณผ ๊ฐ„๋‹จํ•œ ๋ฌ˜์‚ฌ
1095
 
1096
  4. **๋Œ€ํ™”**
1097
  ์บ๋ฆญํ„ฐ๋ช…
 
1138
  - Emotions through actions
1139
 
1140
  3. **Character Intros**
1141
+ First appearance: NAME with brief description
1142
 
1143
  4. **Dialogue**
1144
  CHARACTER NAME
 
1469
  raise Exception(f"LLM Call Failed: {full_content}")
1470
  return full_content
1471
 
 
 
1472
  def call_llm_streaming(self, messages: List[Dict[str, str]], role: str,
1473
  language: str) -> Generator[str, None, None]:
1474
  try:
 
1589
  yield buffer
1590
  buffer = ""
1591
  time.sleep(0.01)
 
1592
  except Exception as e:
1593
  logger.error(f"Error processing line {line_count}: {str(e)}")
1594
  logger.debug(f"Problematic line: {line_str[:100] if 'line_str' in locals() else 'N/A'}")
 
1618
  logger.error(traceback.format_exc())
1619
  yield f"โŒ Unexpected error: {str(e)}"
1620
 
 
 
1621
  def get_system_prompts(self, language: str) -> Dict[str, str]:
1622
  """Role-specific system prompts"""
1623
 
 
1708
 
1709
  return base_prompts.get(language, base_prompts["English"])
1710
 
1711
+ # --- Main process ---
 
 
1712
  def process_screenplay_stream(self, query: str, screenplay_type: str, genre: str,
1713
  language: str, session_id: Optional[str] = None
1714
  ) -> Generator[Tuple[str, List[Dict[str, Any]], str], None, None]:
 
1869
  return "\n\n---\n\n".join(previous) if previous else ""
1870
 
1871
 
1872
+ def _extract_field(self, content: str, field_pattern: str) -> Optional[str]:
1873
+ """Extract field value from content with improved parsing"""
1874
+ pattern = rf'{field_pattern}[:\s]*([^\n]+?)(?=\n[A-Z๊ฐ€-ํžฃ]|$)'
1875
+ match = re.search(pattern, content, re.IGNORECASE | re.DOTALL)
1876
+ if match and match.group(1):
1877
+ value = match.group(1).strip()
1878
+ value = re.sub(r'\*\*', '', value) # **bold** ์ œ๊ฑฐ
1879
+ value = re.sub(r'^\s*[-โ€ข]\s*', '', value) # ๊ธ€๋จธ๋ฆฌํ‘œ ์ œ๊ฑฐ
1880
+ value = re.sub(r'[,.:;]+$', '', value) # ํ–‰ ๋ ๊ตฌ๋‘์  ์ œ๊ฑฐ
1881
+ return value.strip() if value else None
1882
+ return None
1883
+
1884
+
1885
+
1886
+
1887
  def _parse_character_profile(self, content: str, role: str) -> CharacterProfile:
1888
  """Parse character profile from content"""
 
1889
  logger.debug(f"Parsing character profile for role: {role}")
1890
  logger.debug(f"Content preview: {content[:200]}...")
1891
+
1892
+ # 1) ์ด๋ฆ„ ์ถ”์ถœ โ”€ ํŒจํ„ด 3์ข…
1893
+ name = f"Character_{role}" # fallback
1894
  name_patterns = [
1895
+ r'(?:์ด๋ฆ„|Name)[:\s]*([^\n,(]+)', # "์ด๋ฆ„: ํ™๊ธธ๋™"
1896
+ r'^\s*[-*โ€ข]\s*([^\n,(]+)', # "- ํ™๊ธธ๋™"
1897
+ r'^([^\n,(]+)' # ๋ฌธ๋‹จ ์ฒซ ๋‹จ์–ด
1898
  ]
1899
+ for pat in name_patterns:
1900
+ m = re.search(pat, content, re.IGNORECASE | re.MULTILINE)
1901
+ if m and m.group(1).strip():
1902
+ name = re.sub(r'[\*:\s]+', '', m.group(1).strip()) # ๋ถˆํ•„์š” ๊ธฐํ˜ธ ์ œ๊ฑฐ
1903
+ break
1904
+
1905
+ # 2) ํ•„๋“œ ์ถ”์ถœ์šฉ ํ—ฌํผ
1906
+ def extract_clean_field(pats):
1907
+ pats = [pats] if isinstance(pats, str) else pats
1908
+ for p in pats:
1909
+ m = re.search(rf'{p}[:\s]*([^\n*]+?)(?=\n|$)', content,
1910
+ re.IGNORECASE | re.DOTALL)
1911
+ if m and m.group(1).strip():
1912
+ v = m.group(1).strip()
1913
+ v = re.sub(r'^[-*โ€ข:\s]+', '', v) # ๋ฆฌ์ŠคํŠธยท๊ธฐํ˜ธ ์ œ๊ฑฐ
1914
+ v = v.replace('*', '').strip()
1915
+ return v
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1916
  return ""
1917
+
1918
+ # 3) Personality(์—ฌ๋Ÿฌ ์ค„) ๋”ฐ๋กœ ํŒŒ์‹ฑ
1919
+ def extract_traits():
1920
+ section = re.search(r'(?:Personality|์„ฑ๊ฒฉ[^\n]*)\n((?:[-*โ€ข].+\n?)+)',
1921
+ content, re.IGNORECASE)
1922
+ if not section:
1923
+ return []
1924
+ traits = [
1925
+ re.sub(r'^[-*โ€ข]\s*', '', line.strip())
1926
+ for line in section.group(1).splitlines() if line.strip()
1927
+ ]
1928
+ return traits[:5]
1929
+
1930
+ # 4) CharacterProfile ์ƒ์„ฑ
1931
+ return CharacterProfile(
1932
  name=name,
 
1933
  role=role,
1934
+ archetype=extract_clean_field(
1935
+ [r"์บ๋ฆญํ„ฐ ์•„ํฌํƒ€์ž…", r"Character Archetype", r"Archetype", r"์•„ํฌํƒ€์ž…"]
1936
+ ),
1937
+ want=extract_clean_field(
1938
+ [r"WANT\s*\(์™ธ์  ๋ชฉํ‘œ\)", r"WANT", r"์™ธ์  ๋ชฉํ‘œ", r"External Goal"]
1939
+ ),
1940
+ need=extract_clean_field(
1941
+ [r"NEED\s*\(๋‚ด์  ํ•„์š”\)", r"NEED", r"๋‚ด์  ํ•„์š”", r"Internal Need"]
1942
+ ),
1943
+ backstory=extract_clean_field(
1944
+ [r"๋ฐฑ์Šคํ† ๋ฆฌ", r"Backstory", r"ํ•ต์‹ฌ ์ƒ์ฒ˜", r"Core Wound"]
1945
+ ),
1946
+ personality=extract_traits(),
1947
+ speech_pattern=extract_clean_field(
1948
+ [r"๋งํˆฌ.*?ํŒจํ„ด", r"Speech Pattern", r"์–ธ์–ด ํŒจํ„ด", r"๋งํˆฌ"]
1949
+ ),
1950
+ character_arc=extract_clean_field(
1951
+ [r"์บ๋ฆญํ„ฐ ์•„ํฌ", r"Character Arc", r"Arc", r"๋ณ€ํ™”"]
1952
+ ),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1953
  )
1954
+
1955
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1956
 
1957
  def _extract_personality_traits(self, content: str) -> List[str]:
1958
+ """Extract personality traits from content"""
1959
+ traits = []
1960
+ # Look for personality section with multiple pattern options
1961
+ personality_patterns = [
1962
+ r"(?:Personality|์„ฑ๊ฒฉ ํŠน์„ฑ|์„ฑ๊ฒฉ)[:\s]*([^\n]+(?:\n(?![\w๊ฐ€-ํžฃ]+:)[^\n]+)*)",
1963
+ r"์„ฑ๊ฒฉ[:\s]*(?:\n?[-โ€ข*]\s*[^\n]+)+"
1964
+ ]
1965
+
1966
+ for pattern in personality_patterns:
1967
+ match = re.search(pattern, content, re.IGNORECASE | re.DOTALL)
1968
+ if match and match.group(1):
1969
+ personality_section = match.group(1)
1970
+ # Extract individual traits (usually listed)
1971
+ trait_lines = personality_section.split('\n')
1972
+ for line in trait_lines:
1973
+ line = line.strip()
1974
+ if line and not line.endswith(':'):
1975
+ # Remove list markers
1976
+ trait = re.sub(r'^\s*[-โ€ข*]\s*', '', line)
1977
+ trait = re.sub(r'^\d+\.\s*', '', trait) # Remove numbered lists
1978
+ if trait and len(trait) > 2: # Skip very short entries
1979
+ traits.append(trait)
1980
+ if traits: # If we found traits, stop looking
1981
+ break
1982
+
1983
+ return traits[:5] # Limit to 5 traits
1984
 
1985
  def _process_character_content(self, content: str):
1986
+ """Process character designer output with better error handling"""
1987
+ try:
1988
+ # Extract protagonist
1989
+ protagonist_section = self._extract_section(content, r"(?:PROTAGONIST|์ฃผ์ธ๊ณต)")
1990
+ if protagonist_section:
1991
+ try:
1992
+ protagonist = self._parse_character_profile(protagonist_section, "protagonist")
1993
+ self.screenplay_tracker.add_character(protagonist)
1994
+ ScreenplayDatabase.save_character(self.current_session_id, protagonist)
1995
+ except Exception as e:
1996
+ logger.error(f"Error parsing protagonist: {e}")
1997
+ # Create a default protagonist to continue
1998
+ protagonist = CharacterProfile(
1999
+ name="Protagonist",
2000
+ role="protagonist",
2001
+ archetype="Hero",
2002
+ want="To achieve goal",
2003
+ need="To grow",
2004
+ backstory="Unknown",
2005
+ personality=["Determined"],
2006
+ speech_pattern="Normal",
2007
+ character_arc="Growth"
2008
+ )
2009
+ self.screenplay_tracker.add_character(protagonist)
2010
+
2011
+ # Extract antagonist
2012
+ antagonist_section = self._extract_section(content, r"(?:ANTAGONIST|์ ๋Œ€์ž)")
2013
+ if antagonist_section:
2014
+ try:
2015
+ antagonist = self._parse_character_profile(antagonist_section, "antagonist")
2016
+ self.screenplay_tracker.add_character(antagonist)
2017
+ ScreenplayDatabase.save_character(self.current_session_id, antagonist)
2018
+ except Exception as e:
2019
+ logger.error(f"Error parsing antagonist: {e}")
2020
+ # Create a default antagonist to continue
2021
+ antagonist = CharacterProfile(
2022
+ name="Antagonist",
2023
+ role="antagonist",
2024
+ archetype="Villain",
2025
+ want="To stop protagonist",
2026
+ need="Power",
2027
+ backstory="Unknown",
2028
+ personality=["Ruthless"],
2029
+ speech_pattern="Menacing",
2030
+ character_arc="Downfall"
2031
+ )
2032
+ self.screenplay_tracker.add_character(antagonist)
2033
+
2034
+ # Extract supporting characters
2035
+ supporting_section = self._extract_section(content, r"(?:SUPPORTING CAST|์กฐ๋ ฅ์ž๋“ค)")
2036
+ if supporting_section:
2037
+ # Parse multiple supporting characters
2038
+ self._parse_supporting_characters(supporting_section)
2039
+
2040
+ except Exception as e:
2041
+ logger.error(f"Error processing character content: {e}")
2042
+ # Continue with default values rather than failing
2043
 
2044
  def _extract_section(self, content: str, section_pattern: str) -> str:
2045
+ """Extract section from content with improved pattern matching"""
2046
+ # More flexible section extraction
2047
+ patterns = [
2048
+ # Pattern 1: Section header followed by content until next major section
2049
+ rf'{section_pattern}[:\s]*\n?(.*?)(?=\n\n[A-Z๊ฐ€-ํžฃ]{{2,}}[:\s]|\n\n\d+\.|$)',
2050
+ # Pattern 2: Section header with content until next section (alternative)
2051
+ rf'{section_pattern}.*?\n((?:.*\n)*?)(?=\n[A-Z๊ฐ€-ํžฃ]{{2,}}:|$)',
2052
+ # Pattern 3: More flexible pattern for Korean text
2053
+ rf'{section_pattern}[:\s]*\n?((?:[^\n]+\n?)*?)(?=\n\n|\Z)'
2054
+ ]
2055
+
2056
+ for pattern in patterns:
2057
+ match = re.search(pattern, content, re.IGNORECASE | re.DOTALL)
2058
+ if match and match.group(1):
2059
+ section_content = match.group(1).strip()
2060
+ if section_content: # Only return if we got actual content
2061
+ return section_content
2062
+
2063
+ return ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2064
 
2065
+ def _parse_supporting_characters(self, content: str):
2066
+ """Parse supporting characters from content"""
2067
+ # Split by character markers (numbers or bullets)
2068
+ char_sections = re.split(r'\n(?:\d+\.|[-โ€ข*])\s*', content)
2069
+
2070
+ for i, section in enumerate(char_sections[1:], 1): # Skip first empty split
2071
+ if section.strip():
2072
+ try:
2073
+ # Try multiple name extraction patterns
2074
+ name = None
2075
+ name_patterns = [
2076
+ r"(?:์ด๋ฆ„|Name)[:\s]*([^,\n]+)",
2077
+ r"^([^:\n]+?)(?:\s*[-โ€“]\s*|:\s*)", # Name at start before dash or colon
2078
+ r"^([๊ฐ€-ํžฃA-Za-z\s]+?)(?:\s*\(|$)" # Korean/English name before parenthesis
2079
+ ]
2080
+
2081
+ for pattern in name_patterns:
2082
+ name_match = re.search(pattern, section.strip(), re.IGNORECASE)
2083
+ if name_match and name_match.group(1):
2084
+ name = name_match.group(1).strip()
2085
+ if name and len(name) > 1:
2086
+ break
2087
+
2088
+ if not name:
2089
+ name = f"Supporting_{i}"
2090
+
2091
+ role_desc = self._extract_field(section, r"(?:Role|์—ญํ• )[:\s]*") or "supporting"
2092
+
2093
+ character = CharacterProfile(
2094
+ name=name,
2095
+ role="supporting",
2096
+ archetype=role_desc,
2097
+ want="",
2098
+ need="",
2099
+ backstory=self._extract_field(section, r"(?:Backstory|๋ฐฑ์Šคํ† ๋ฆฌ)[:\s]*") or "",
2100
+ personality=[],
2101
+ speech_pattern="",
2102
+ character_arc=""
2103
+ )
2104
+
2105
+ self.screenplay_tracker.add_character(character)
2106
+ ScreenplayDatabase.save_character(self.current_session_id, character)
2107
+
2108
+ except Exception as e:
2109
+ logger.warning(f"Error parsing supporting character {i}: {e}")
2110
+ continue
2111
 
2112
+ def _process_producer_content(self, content: str):
2113
+ """Process producer output with better extraction"""
2114
+ try:
2115
+ # Extract title with various formats
2116
+ title_patterns = [
2117
+ r'(?:TITLE|์ œ๋ชฉ)[:\s]*\*?\*?([^\n*]+)\*?\*?',
2118
+ r'\*\*(?:TITLE|์ œ๋ชฉ)\*\*[:\s]*([^\n]+)',
2119
+ r'Title[:\s]*([^\n]+)'
2120
+ ]
2121
+
2122
+ for pattern in title_patterns:
2123
+ title_match = re.search(pattern, content, re.IGNORECASE)
2124
+ if title_match:
2125
+ self.screenplay_tracker.screenplay_bible.title = title_match.group(1).strip()
2126
+ break
2127
+
2128
+ # Extract logline with various formats
2129
+ logline_patterns = [
2130
+ r'(?:LOGLINE|๋กœ๊ทธ๋ผ์ธ)[:\s]*\*?\*?([^\n]+)',
2131
+ r'\*\*(?:LOGLINE|๋กœ๊ทธ๋ผ์ธ)\*\*[:\s]*([^\n]+)',
2132
+ r'Logline[:\s]*([^\n]+)'
2133
+ ]
2134
+
2135
+ for pattern in logline_patterns:
2136
+ logline_match = re.search(pattern, content, re.IGNORECASE | re.DOTALL)
2137
+ if logline_match:
2138
+ # Get full logline (might be multi-line)
2139
+ logline_text = logline_match.group(1).strip()
2140
+ # Continue reading if it's incomplete
2141
+ if not logline_text.endswith('.'):
2142
+ next_lines = content[logline_match.end():].split('\n')
2143
+ for line in next_lines[:3]: # Check next 3 lines
2144
+ if line.strip() and not re.match(r'^[A-Z๊ฐ€-ํžฃ\d]', line.strip()):
2145
+ logline_text += ' ' + line.strip()
2146
+ else:
2147
+ break
2148
+ self.screenplay_tracker.screenplay_bible.logline = logline_text
2149
+ break
2150
+
2151
+ # Extract genre
2152
+ genre_match = re.search(r'(?:Primary Genre|์ฃผ ์žฅ๋ฅด)[:\s]*([^\n]+)', content, re.IGNORECASE)
2153
+ if genre_match:
2154
+ self.screenplay_tracker.screenplay_bible.genre = genre_match.group(1).strip()
2155
+
2156
+ # Save to database
2157
+ ScreenplayDatabase.save_screenplay_bible(self.current_session_id,
2158
+ self.screenplay_tracker.screenplay_bible)
2159
+
2160
+ except Exception as e:
2161
+ logger.error(f"Error processing producer content: {e}")
2162
 
2163
  def _process_story_content(self, content: str):
2164
  """Process story developer output"""
 
2173
  ScreenplayDatabase.save_screenplay_bible(self.current_session_id,
2174
  self.screenplay_tracker.screenplay_bible)
2175
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2176
  def _process_scene_content(self, content: str):
2177
  """Process scene planner output"""
2178
  # Parse scene breakdown
 
2201
  self.screenplay_tracker.add_scene(scene)
2202
  ScreenplayDatabase.save_scene(self.current_session_id, scene)
2203
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2204
  # --- Utility functions ---
2205
  def generate_random_screenplay_theme(screenplay_type: str, genre: str, language: str) -> str:
2206
  """Generate random screenplay theme"""
 
2382
  match = re.search(r'\*\*(?:Logline|๋กœ๊ทธ๋ผ์ธ):\*\*\s*(.+)', theme_text, re.IGNORECASE)
2383
  return match.group(1).strip() if match else ""
2384
 
2385
+ import re
2386
+
2387
  def format_screenplay_display(screenplay_text: str) -> str:
2388
+ """Convert raw screenplay text to a nicely formatted Markdown preview."""
2389
+
2390
+ if not screenplay_text:
2391
+ return "No screenplay content yet."
2392
+
2393
+ # 1) ์ œ๋ชฉ ์˜์—ญ
2394
+ formatted = "# ๐ŸŽฌ Screenplay\n\n"
2395
+
2396
+ # 2) ์”ฌ ํ—ค๋”ฉ(INT./EXT. ๋ผ์ธ) ๋ณผ๋“œ ์ฒ˜๋ฆฌ
2397
+ # - ^ : ํ–‰์˜ ์‹œ์ž‘
2398
+ # - .* : ํ–‰ ์ „์ฒด
2399
+ # - re.MULTILINE : ๊ฐ ์ค„๋งˆ๋‹ค ^ $๊ฐ€ ๋™์ž‘
2400
+ formatted_text = re.sub(
2401
+ r'^(INT\.|EXT\.).*$', # ์บก์ฒ˜: INT. ๋˜๋Š” EXT.์œผ๋กœ ์‹œ์ž‘ํ•˜๋Š” ํ•œ ์ค„
2402
+ r'**\g<0>**', # ์ „์ฒด ํ–‰์„ ๊ตต๊ฒŒ
2403
+ screenplay_text,
2404
+ flags=re.MULTILINE
2405
+ )
2406
+
2407
+ # 3) ๋Œ€๋ฌธ์ž ์ „์›(์ธ๋ฌผ ์ด๋ฆ„) ๋ณผ๋“œ ์ฒ˜๋ฆฌ
2408
+ # - [A-Z][A-Z\s]+$ : ALL-CAPS ๊ธ€์ž์™€ ๊ณต๋ฐฑ๋งŒ์œผ๋กœ ์ด๋ค„์ง„ ํ–‰
2409
+ formatted_text = re.sub(
2410
+ r'^([A-Z][A-Z\s]+)$',
2411
+ r'**\g<0>**',
2412
+ formatted_text,
2413
+ flags=re.MULTILINE
2414
+ )
2415
+
2416
+ # 4) ๊ฐ€๋…์„ฑ์„ ์œ„ํ•ด INT./EXT. ๋’ค์— ๋นˆ ์ค„ ์‚ฝ์ž…
2417
+ lines = formatted_text.splitlines()
2418
+ pretty_lines = []
2419
+ for line in lines:
2420
+ pretty_lines.append(line)
2421
+ if line.startswith("**INT.") or line.startswith("**EXT."):
2422
+ pretty_lines.append("") # ๋นˆ ์ค„ ์ถ”๊ฐ€
2423
+
2424
+ formatted += "\n".join(pretty_lines)
2425
+
2426
+ # 5) ํŽ˜์ด์ง€ ์ˆ˜(์Šคํฌ๋ฆฝํŠธ ๊ทœ์น™: 1 ํŽ˜์ด์ง€ โ‰ˆ 55 ๋ผ์ธ) ๊ณ„์‚ฐ
2427
+ page_count = len(screenplay_text.splitlines()) / 55
2428
+ formatted = f"**Total Pages: {page_count:.1f}**\n\n" + formatted
2429
+ return formatted
2430
+
2431
 
2432
  def format_stages_display(stages: List[Dict]) -> str:
2433
  """Format stages display for screenplay"""
 
2873
  server_port=7860,
2874
  share=False,
2875
  debug=True
2876
+ )