openfree commited on
Commit
6b2b70d
ยท
verified ยท
1 Parent(s): 473e0ad

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +1295 -1
app.py CHANGED
@@ -1603,4 +1603,1298 @@ Provide specific solutions for each issue."""
1603
  yield buffer
1604
  buffer = ""
1605
  time.sleep(0.01)
1606
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1603
  yield buffer
1604
  buffer = ""
1605
  time.sleep(0.01)
1606
+ except Exception as e:
1607
+ logger.error(f"Error processing line {line_count}: {str(e)}")
1608
+ logger.debug(f"Problematic line: {line_str[:100] if 'line_str' in locals() else 'N/A'}")
1609
+ continue
1610
+
1611
+ # Yield any remaining buffer content
1612
+ if buffer:
1613
+ yield buffer
1614
+
1615
+ # Check if we got any content
1616
+ if line_count == 0:
1617
+ logger.error("No lines received from API")
1618
+ yield "โŒ No response from API"
1619
+
1620
+ except requests.exceptions.Timeout:
1621
+ logger.error("API request timed out")
1622
+ yield "โŒ Request timed out. Please try again."
1623
+ except requests.exceptions.ConnectionError as e:
1624
+ logger.error(f"Connection error: {e}")
1625
+ yield "โŒ Connection error. Please check your internet connection."
1626
+ except requests.exceptions.RequestException as e:
1627
+ logger.error(f"Request error: {type(e).__name__}: {str(e)}")
1628
+ yield f"โŒ Network error: {str(e)}"
1629
+ except Exception as e:
1630
+ logger.error(f"Unexpected error in streaming: {type(e).__name__}: {str(e)}")
1631
+ import traceback
1632
+ logger.error(traceback.format_exc())
1633
+ yield f"โŒ Unexpected error: {str(e)}"
1634
+
1635
+
1636
+
1637
+ def get_system_prompts(self, language: str) -> Dict[str, str]:
1638
+ """Role-specific system prompts"""
1639
+
1640
+ base_prompts = {
1641
+ "Korean": {
1642
+ "producer": """๋‹น์‹ ์€ 20๋…„ ๊ฒฝ๋ ฅ์˜ ํ• ๋ฆฌ์šฐ๋“œ ํ”„๋กœ๋“€์„œ์ž…๋‹ˆ๋‹ค.
1643
+ ์ƒ์—…์  ์„ฑ๊ณต๊ณผ ์˜ˆ์ˆ ์  ๊ฐ€์น˜๋ฅผ ๋ชจ๋‘ ์ถ”๊ตฌํ•ฉ๋‹ˆ๋‹ค.
1644
+ ์‹œ์žฅ ํŠธ๋ Œ๋“œ์™€ ๊ด€๊ฐ ์‹ฌ๋ฆฌ๋ฅผ ์ •ํ™•ํžˆ ํŒŒ์•…ํ•ฉ๋‹ˆ๋‹ค.
1645
+ ์‹คํ˜„ ๊ฐ€๋Šฅํ•˜๊ณ  ๋งค๋ ฅ์ ์ธ ํ”„๋กœ์ ํŠธ๋ฅผ ๊ฐœ๋ฐœํ•ฉ๋‹ˆ๋‹ค.""",
1646
+
1647
+ "story_developer": """๋‹น์‹ ์€ ์ˆ˜์ƒ ๊ฒฝ๋ ฅ์ด ์žˆ๋Š” ์Šคํ† ๋ฆฌ ๊ฐœ๋ฐœ์ž์ž…๋‹ˆ๋‹ค.
1648
+ ๊ฐ์ •์ ์œผ๋กœ ๊ณต๊ฐ๊ฐ€๊ณ  ๊ตฌ์กฐ์ ์œผ๋กœ ํƒ„ํƒ„ํ•œ ์ด์•ผ๊ธฐ๋ฅผ ๋งŒ๋“ญ๋‹ˆ๋‹ค.
1649
+ ์บ๋ฆญํ„ฐ์˜ ๋‚ด์  ์—ฌ์ •๊ณผ ์™ธ์  ํ”Œ๋กฏ์„ ์กฐํ™”๋กญ๊ฒŒ ์—ฎ์Šต๋‹ˆ๋‹ค.
1650
+ ๋ณดํŽธ์  ์ฃผ์ œ๋ฅผ ๋…ํŠนํ•œ ๋ฐฉ์‹์œผ๋กœ ํƒ๊ตฌํ•ฉ๋‹ˆ๋‹ค.""",
1651
+
1652
+ "character_designer": """๋‹น์‹ ์€ ์‹ฌ๋ฆฌํ•™์„ ๊ณต๋ถ€ํ•œ ์บ๋ฆญํ„ฐ ๋””์ž์ด๋„ˆ์ž…๋‹ˆ๋‹ค.
1653
+ ์ง„์งœ ๊ฐ™์€ ์ธ๋ฌผ๋“ค์„ ์ฐฝ์กฐํ•˜๋Š” ์ „๋ฌธ๊ฐ€์ž…๋‹ˆ๋‹ค.
1654
+ ๊ฐ ์บ๋ฆญํ„ฐ์—๊ฒŒ ๊ณ ์œ ํ•œ ๋ชฉ์†Œ๋ฆฌ์™€ ๊ด€์ ์„ ๋ถ€์—ฌํ•ฉ๋‹ˆ๋‹ค.
1655
+ ๋ณต์žกํ•˜๊ณ  ๋ชจ์ˆœ์ ์ธ ์ธ๊ฐ„์„ฑ์„ ํฌ์ฐฉํ•ฉ๋‹ˆ๋‹ค.""",
1656
+
1657
+ "scene_planner": """๋‹น์‹ ์€ ์ •๋ฐ€ํ•œ ์”ฌ ๊ตฌ์„ฑ์˜ ๋Œ€๊ฐ€์ž…๋‹ˆ๋‹ค.
1658
+ ๊ฐ ์”ฌ์ด ์Šคํ† ๋ฆฌ์™€ ์บ๋ฆญํ„ฐ๋ฅผ ์ „์ง„์‹œํ‚ค๋„๋ก ์„ค๊ณ„ํ•ฉ๋‹ˆ๋‹ค.
1659
+ ๋ฆฌ๋“ฌ๊ณผ ํŽ˜์ด์‹ฑ์„ ์™„๋ฒฝํ•˜๊ฒŒ ์กฐ์ ˆํ•ฉ๋‹ˆ๋‹ค.
1660
+ ์‹œ๊ฐ์  ์Šคํ† ๋ฆฌํ…”๋ง์„ ๊ทน๋Œ€ํ™”ํ•ฉ๋‹ˆ๋‹ค.""",
1661
+
1662
+ "screenwriter": """๋‹น์‹ ์€ ๋‹ค์ž‘์˜ ์‹œ๋‚˜๋ฆฌ์˜ค ์ž‘๊ฐ€์ž…๋‹ˆ๋‹ค.
1663
+ '๋ณด์—ฌ์ฃผ๊ธฐ'์˜ ๋Œ€๊ฐ€์ด๋ฉฐ ์„œ๋ธŒํ…์ŠคํŠธ๋ฅผ ๋Šฅ์ˆ™ํ•˜๊ฒŒ ๋‹ค๋ฃน๋‹ˆ๋‹ค.
1664
+ ์ƒ์ƒํ•˜๊ณ  ์ž์—ฐ์Šค๋Ÿฌ์šด ๋Œ€ํ™”๋ฅผ ์“ฐ๋Š” ์ „๋ฌธ๊ฐ€์ž…๋‹ˆ๋‹ค.
1665
+ ์ œ์ž‘ ํ˜„์‹ค์„ ๊ณ ๋ คํ•˜๋ฉด์„œ๋„ ์ฐฝ์˜์ ์ธ ํ•ด๊ฒฐ์ฑ…์„ ์ฐพ์Šต๋‹ˆ๋‹ค.""",
1666
+
1667
+ "script_doctor": """๋‹น์‹ ์€ ๊นŒ๋‹ค๋กœ์šด ์Šคํฌ๋ฆฝํŠธ ๋‹ฅํ„ฐ์ž…๋‹ˆ๋‹ค.
1668
+ ์ž‘์€ ๋””ํ…Œ์ผ๋„ ๋†“์น˜์ง€ ์•Š๋Š” ์™„๋ฒฝ์ฃผ์˜์ž์ž…๋‹ˆ๋‹ค.
1669
+ ์Šคํ† ๋ฆฌ์˜ ์ž ์žฌ๋ ฅ์„ ์ตœ๋Œ€ํ•œ ๋Œ์–ด๋ƒ…๋‹ˆ๋‹ค.
1670
+ ๊ฑด์„ค์ ์ด๊ณ  ๊ตฌ์ฒด์ ์ธ ๊ฐœ์„ ์•ˆ์„ ์ œ์‹œํ•ฉ๋‹ˆ๋‹ค.""",
1671
+
1672
+ "critic_structure": """๋‹น์‹ ์€ ๊ตฌ์กฐ ๋ถ„์„ ์ „๋ฌธ๊ฐ€์ž…๋‹ˆ๋‹ค.
1673
+ ์Šคํ† ๋ฆฌ์˜ ๋ผˆ๋Œ€์™€ ๊ทผ์œก์„ ๊ฟฐ๋šซ์–ด ๋ด…๋‹ˆ๋‹ค.
1674
+ ๋…ผ๋ฆฌ์  ํ—ˆ์ ๊ณผ ๊ฐ์ •์  ๊ณต๋ฐฑ์„ ์ฐพ์•„๋ƒ…๋‹ˆ๋‹ค.
1675
+ ๋” ๋‚˜์€ ๊ตฌ์กฐ๋ฅผ ์œ„ํ•œ ๊ตฌ์ฒด์  ์ œ์•ˆ์„ ํ•ฉ๋‹ˆ๋‹ค.""",
1676
+
1677
+ "final_reviewer": """๋‹น์‹ ์€ ์—…๊ณ„ ๋ฒ ํ…Œ๋ž‘ ์ตœ์ข… ๋ฆฌ๋ทฐ์–ด์ž…๋‹ˆ๋‹ค.
1678
+ ์ƒ์—…์„ฑ๊ณผ ์˜ˆ์ˆ ์„ฑ์„ ๊ท ํ˜•์žˆ๊ฒŒ ํ‰๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.
1679
+ ์ œ์ž‘์‚ฌ, ๋ฐฐ์šฐ, ๊ด€๊ฐ ๋ชจ๋“  ๊ด€์ ์„ ๊ณ ๋ คํ•ฉ๋‹ˆ๋‹ค.
1680
+ ๋ƒ‰์ •ํ•˜์ง€๋งŒ ๊ฒฉ๋ คํ•˜๋Š” ํ”ผ๋“œ๋ฐฑ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค."""
1681
+ },
1682
+ "English": {
1683
+ "producer": """You are a Hollywood producer with 20 years experience.
1684
+ You pursue both commercial success and artistic value.
1685
+ You accurately grasp market trends and audience psychology.
1686
+ You develop feasible and attractive projects.""",
1687
+
1688
+ "story_developer": """You are an award-winning story developer.
1689
+ You create emotionally resonant and structurally sound stories.
1690
+ You harmoniously weave internal journeys with external plots.
1691
+ You explore universal themes in unique ways.""",
1692
+
1693
+ "character_designer": """You are a character designer who studied psychology.
1694
+ You're an expert at creating lifelike characters.
1695
+ You give each character a unique voice and perspective.
1696
+ You capture complex and contradictory humanity.""",
1697
+
1698
+ "scene_planner": """You are a master of precise scene construction.
1699
+ You design each scene to advance story and character.
1700
+ You perfectly control rhythm and pacing.
1701
+ You maximize visual storytelling.""",
1702
+
1703
+ "screenwriter": """You are a prolific screenwriter.
1704
+ You're a master of 'showing' and skilled with subtext.
1705
+ You're an expert at writing vivid, natural dialogue.
1706
+ You find creative solutions while considering production reality.""",
1707
+
1708
+ "script_doctor": """You are a demanding script doctor.
1709
+ You're a perfectionist who misses no small detail.
1710
+ You maximize the story's potential.
1711
+ You provide constructive and specific improvements.""",
1712
+
1713
+ "critic_structure": """You are a structure analysis expert.
1714
+ You see through the story's skeleton and muscles.
1715
+ You find logical gaps and emotional voids.
1716
+ You make specific suggestions for better structure.""",
1717
+
1718
+ "final_reviewer": """You are an industry veteran final reviewer.
1719
+ You evaluate commercial and artistic value in balance.
1720
+ You consider all perspectives: producers, actors, audience.
1721
+ You provide feedback that's critical yet encouraging."""
1722
+ }
1723
+ }
1724
+
1725
+ return base_prompts.get(language, base_prompts["English"])
1726
+
1727
+
1728
+
1729
+ # --- Main process ---
1730
+ def process_screenplay_stream(self, query: str, screenplay_type: str, genre: str,
1731
+ language: str, session_id: Optional[str] = None
1732
+ ) -> Generator[Tuple[str, List[Dict[str, Any]], str], None, None]:
1733
+ """Main screenplay generation process"""
1734
+ try:
1735
+ resume_from_stage = 0
1736
+ if session_id:
1737
+ self.current_session_id = session_id
1738
+ session = ScreenplayDatabase.get_session(session_id)
1739
+ if session:
1740
+ query = session['user_query']
1741
+ screenplay_type = session['screenplay_type']
1742
+ genre = session['genre']
1743
+ language = session['language']
1744
+ resume_from_stage = session['current_stage'] + 1
1745
+ else:
1746
+ self.current_session_id = ScreenplayDatabase.create_session(
1747
+ query, screenplay_type, genre, language
1748
+ )
1749
+ logger.info(f"Created new screenplay session: {self.current_session_id}")
1750
+
1751
+ stages = []
1752
+ if resume_from_stage > 0:
1753
+ # Get existing stages from database
1754
+ db_stages = ScreenplayDatabase.get_stages(self.current_session_id)
1755
+ stages = [{
1756
+ "name": s['stage_name'],
1757
+ "status": s['status'],
1758
+ "content": s.get('content', ''),
1759
+ "page_count": s.get('page_count', 0)
1760
+ } for s in db_stages]
1761
+
1762
+ for stage_idx in range(resume_from_stage, len(SCREENPLAY_STAGES)):
1763
+ role, stage_name = SCREENPLAY_STAGES[stage_idx]
1764
+
1765
+ if stage_idx >= len(stages):
1766
+ stages.append({
1767
+ "name": stage_name,
1768
+ "status": "active",
1769
+ "content": "",
1770
+ "page_count": 0
1771
+ })
1772
+ else:
1773
+ stages[stage_idx]["status"] = "active"
1774
+
1775
+ yield f"๐Ÿ”„ Processing {stage_name}...", stages, self.current_session_id
1776
+
1777
+ prompt = self.get_stage_prompt(stage_idx, role, query, screenplay_type,
1778
+ genre, language, stages)
1779
+ stage_content = ""
1780
+
1781
+ for chunk in self.call_llm_streaming([{"role": "user", "content": prompt}],
1782
+ role, language):
1783
+ stage_content += chunk
1784
+ stages[stage_idx]["content"] = stage_content
1785
+ if role == "screenwriter":
1786
+ stages[stage_idx]["page_count"] = len(stage_content.split('\n')) / 55
1787
+ yield f"๐Ÿ”„ {stage_name} in progress...", stages, self.current_session_id
1788
+
1789
+ # Process content based on role
1790
+ if role == "producer":
1791
+ self._process_producer_content(stage_content)
1792
+ elif role == "story_developer":
1793
+ self._process_story_content(stage_content)
1794
+ elif role == "character_designer":
1795
+ self._process_character_content(stage_content)
1796
+ elif role == "scene_planner":
1797
+ self._process_scene_content(stage_content)
1798
+
1799
+ stages[stage_idx]["status"] = "complete"
1800
+ ScreenplayDatabase.save_stage(
1801
+ self.current_session_id, stage_idx, stage_name, role,
1802
+ stage_content, "complete"
1803
+ )
1804
+
1805
+ yield f"โœ… {stage_name} completed", stages, self.current_session_id
1806
+
1807
+ # Final processing
1808
+ final_screenplay = ScreenplayDatabase.get_screenplay_content(self.current_session_id)
1809
+ title = self.screenplay_tracker.screenplay_bible.title
1810
+ logline = self.screenplay_tracker.screenplay_bible.logline
1811
+
1812
+ ScreenplayDatabase.update_final_screenplay(
1813
+ self.current_session_id, final_screenplay, title, logline
1814
+ )
1815
+
1816
+ yield f"โœ… Screenplay completed! {title}", stages, self.current_session_id
1817
+
1818
+ except Exception as e:
1819
+ logger.error(f"Screenplay generation error: {e}", exc_info=True)
1820
+ yield f"โŒ Error occurred: {e}", stages if 'stages' in locals() else [], self.current_session_id
1821
+
1822
+ def get_stage_prompt(self, stage_idx: int, role: str, query: str,
1823
+ screenplay_type: str, genre: str, language: str,
1824
+ stages: List[Dict]) -> str:
1825
+ """Generate stage-specific prompt"""
1826
+ if stage_idx == 0: # Producer
1827
+ return self.create_producer_prompt(query, screenplay_type, genre, language)
1828
+
1829
+ if stage_idx == 1: # Story Developer
1830
+ return self.create_story_developer_prompt(
1831
+ stages[0]["content"], query, screenplay_type, genre, language
1832
+ )
1833
+
1834
+ if stage_idx == 2: # Character Designer
1835
+ return self.create_character_designer_prompt(
1836
+ stages[0]["content"], stages[1]["content"], genre, language
1837
+ )
1838
+
1839
+ if stage_idx == 3: # Structure Critic
1840
+ return self.create_critic_structure_prompt(
1841
+ stages[1]["content"], stages[2]["content"], screenplay_type, genre, language
1842
+ )
1843
+
1844
+ if stage_idx == 4: # Scene Planner
1845
+ return self.create_scene_planner_prompt(
1846
+ stages[1]["content"], stages[2]["content"], screenplay_type, genre, language
1847
+ )
1848
+
1849
+ # Screenwriter acts
1850
+ if role == "screenwriter":
1851
+ act_mapping = {5: "Act 1", 7: "Act 2A", 9: "Act 2B", 11: "Act 3"}
1852
+ if stage_idx in act_mapping:
1853
+ act = act_mapping[stage_idx]
1854
+ previous_acts = self._get_previous_acts(stages, stage_idx)
1855
+ return self.create_screenwriter_prompt(
1856
+ act, stages[4]["content"], stages[2]["content"],
1857
+ previous_acts, screenplay_type, genre, language
1858
+ )
1859
+
1860
+ # Script doctor reviews
1861
+ if role == "script_doctor":
1862
+ act_mapping = {6: "Act 1", 8: "Act 2A", 10: "Act 2B"}
1863
+ if stage_idx in act_mapping:
1864
+ act = act_mapping[stage_idx]
1865
+ act_content = stages[stage_idx-1]["content"]
1866
+ return self.create_script_doctor_prompt(act_content, act, genre, language)
1867
+
1868
+ # Final reviewer
1869
+ if role == "final_reviewer":
1870
+ complete_screenplay = ScreenplayDatabase.get_screenplay_content(self.current_session_id)
1871
+ return self.create_final_reviewer_prompt(
1872
+ complete_screenplay, screenplay_type, genre, language
1873
+ )
1874
+
1875
+ return ""
1876
+
1877
+ def _get_previous_acts(self, stages: List[Dict], current_idx: int) -> str:
1878
+ """Get previous acts content"""
1879
+ previous = []
1880
+ act_indices = {5: [], 7: [5], 9: [5, 7], 11: [5, 7, 9]}
1881
+
1882
+ if current_idx in act_indices:
1883
+ for idx in act_indices[current_idx]:
1884
+ if idx < len(stages) and stages[idx]["content"]:
1885
+ previous.append(stages[idx]["content"])
1886
+
1887
+ return "\n\n---\n\n".join(previous) if previous else ""
1888
+
1889
+
1890
+ def _parse_character_profile(self, content: str, role: str) -> CharacterProfile:
1891
+ """Parse character profile from content"""
1892
+ # Debug logging
1893
+ logger.debug(f"Parsing character profile for role: {role}")
1894
+ logger.debug(f"Content preview: {content[:200]}...")
1895
+
1896
+ # Extract name first - handle various formats
1897
+ name = f"Character_{role}" # default
1898
+ name_patterns = [
1899
+ r'(?:์ด๋ฆ„|Name)[:\s]*([^,\n]+?)(?:\s*\([^)]+\))?\s*',
1900
+ r'^\s*[-*โ€ข]\s*([^,\n]+?)(?:\s*\([^)]+\))?\s*',
1901
+ r'^([^,\n]+?)(?:\s*\([^)]+\))?\s*'
1902
+ ]
1903
+
1904
+ for pattern in name_patterns:
1905
+ name_match = re.search(pattern, content, re.IGNORECASE | re.MULTILINE)
1906
+ if name_match:
1907
+ extracted_name = name_match.group(1).strip()
1908
+ # Remove markdown and extra characters
1909
+ extracted_name = re.sub(r'[*:\s]+$', '', extracted_name)
1910
+ extracted_name = re.sub(r'^[*:\s]+', '', extracted_name)
1911
+ if extracted_name and len(extracted_name) > 1:
1912
+ name = extracted_name
1913
+ break
1914
+
1915
+ # Helper function to extract clean fields
1916
+ def extract_clean_field(patterns):
1917
+ if isinstance(patterns, str):
1918
+ patterns = [patterns]
1919
+
1920
+ for pattern in patterns:
1921
+ match = re.search(rf'{pattern}[:\s]*([^\n*]+?)(?=\n|$)', content, re.IGNORECASE | re.DOTALL)
1922
+ if match:
1923
+ value = match.group(1).strip()
1924
+ # Clean up the value
1925
+ value = re.sub(r'^[-*โ€ข:\s]+', '', value)
1926
+ value = re.sub(r'[*]+', '', value)
1927
+ value = re.sub(r'\s+', ' ', value)
1928
+ if value:
1929
+ return value
1930
+ return ""
1931
+
1932
+ # Extract all fields
1933
+ profile = CharacterProfile(
1934
+ name=name,
1935
+ role=role,
1936
+ archetype=extract_clean_field([
1937
+ r"์บ๋ฆญํ„ฐ ์•„ํฌํƒ€์ž…",
1938
+ r"Character Archetype",
1939
+ r"Archetype",
1940
+ r"์•„ํฌํƒ€์ž…"
1941
+ ]),
1942
+ want=extract_clean_field([
1943
+ r"WANT\s*\(์™ธ์  ๋ชฉํ‘œ\)",
1944
+ r"WANT",
1945
+ r"์™ธ์  ๋ชฉํ‘œ",
1946
+ r"External Goal"
1947
+ ]),
1948
+ need=extract_clean_field([
1949
+ r"NEED\s*\(๋‚ด์  ํ•„์š”\)",
1950
+ r"NEED",
1951
+ r"๋‚ด์  ํ•„์š”",
1952
+ r"Internal Need"
1953
+ ]),
1954
+ backstory=extract_clean_field([
1955
+ r"๋ฐฑ์Šคํ† ๋ฆฌ",
1956
+ r"Backstory",
1957
+ r"ํ•ต์‹ฌ ์ƒ์ฒ˜",
1958
+ r"Core Wound"
1959
+ ]),
1960
+ personality=self._extract_personality_traits(content),
1961
+ speech_pattern=extract_clean_field([
1962
+ r"๋งํˆฌ.*?ํŒจํ„ด",
1963
+ r"Speech Pattern",
1964
+ r"์–ธ์–ด ํŒจํ„ด",
1965
+ r"๋งํˆฌ"
1966
+ ]),
1967
+ character_arc=extract_clean_field([
1968
+ r"์บ๋ฆญํ„ฐ ์•„ํฌ",
1969
+ r"Character Arc",
1970
+ r"Arc",
1971
+ r"๋ณ€ํ™”"
1972
+ ])
1973
+ )
1974
+
1975
+ logger.debug(f"Parsed character: {profile.name}")
1976
+ return profile
1977
+
1978
+ def _extract_field(self, content: str, field_pattern: str) -> Optional[str]:
1979
+ """Extract field value from content with improved parsing"""
1980
+ # Use word boundary to prevent partial matches
1981
+ pattern = rf'\b{field_pattern}\b[:\s]*([^\n]+?)(?=\n|$)'
1982
+ match = re.search(pattern, content, re.IGNORECASE)
1983
+ if match:
1984
+ value = match.group(1).strip()
1985
+ # Remove markdown formatting
1986
+ value = re.sub(r'\*+', '', value)
1987
+ # Remove list markers
1988
+ value = re.sub(r'^\s*[-โ€ข*]\s*', '', value)
1989
+ # Remove trailing punctuation
1990
+ value = re.sub(r'[,.:;]$', '', value)
1991
+ return value.strip()
1992
+ return None
1993
+
1994
+ def _extract_personality_traits(self, content: str) -> List[str]:
1995
+ """Extract personality traits from content"""
1996
+ traits = []
1997
+ # Look for personality section
1998
+ personality_section = self._extract_field(content, r"(?:Personality|์„ฑ๊ฒฉ)[:\s]*")
1999
+ if personality_section:
2000
+ # Extract individual traits (usually listed)
2001
+ trait_lines = personality_section.split('\n')
2002
+ for line in trait_lines:
2003
+ line = line.strip()
2004
+ if line and not line.endswith(':'):
2005
+ # Remove list markers
2006
+ trait = re.sub(r'^\s*[-โ€ข*]\s*', '', line)
2007
+ if trait:
2008
+ traits.append(trait)
2009
+ return traits[:5] # Limit to 5 traits
2010
+
2011
+ def _process_character_content(self, content: str):
2012
+ """Process character designer output with better error handling"""
2013
+ try:
2014
+ # Extract protagonist
2015
+ protagonist_section = self._extract_section(content, r"(?:PROTAGONIST|์ฃผ์ธ๊ณต)")
2016
+ if protagonist_section:
2017
+ protagonist = self._parse_character_profile(protagonist_section, "protagonist")
2018
+ self.screenplay_tracker.add_character(protagonist)
2019
+ ScreenplayDatabase.save_character(self.current_session_id, protagonist)
2020
+
2021
+ # Extract antagonist
2022
+ antagonist_section = self._extract_section(content, r"(?:ANTAGONIST|์ ๋Œ€์ž)")
2023
+ if antagonist_section:
2024
+ antagonist = self._parse_character_profile(antagonist_section, "antagonist")
2025
+ self.screenplay_tracker.add_character(antagonist)
2026
+ ScreenplayDatabase.save_character(self.current_session_id, antagonist)
2027
+
2028
+ # Extract supporting characters
2029
+ supporting_section = self._extract_section(content, r"(?:SUPPORTING CAST|์กฐ๋ ฅ์ž๋“ค)")
2030
+ if supporting_section:
2031
+ # Parse multiple supporting characters
2032
+ self._parse_supporting_characters(supporting_section)
2033
+
2034
+ except Exception as e:
2035
+ logger.error(f"Error processing character content: {e}")
2036
+ # Continue with default values rather than failing
2037
+
2038
+ def _parse_supporting_characters(self, content: str):
2039
+ """Parse supporting characters from content"""
2040
+ # Split by character markers (numbers or bullets)
2041
+ char_sections = re.split(r'\n(?:\d+\.|[-โ€ข*])\s*', content)
2042
+
2043
+ for i, section in enumerate(char_sections[1:], 1): # Skip first empty split
2044
+ if section.strip():
2045
+ try:
2046
+ name = self._extract_field(section, r"(?:Name|์ด๋ฆ„)[:\s]*") or f"Supporting_{i}"
2047
+ role = self._extract_field(section, r"(?:Role|์—ญํ• )[:\s]*") or "supporting"
2048
+
2049
+ character = CharacterProfile(
2050
+ name=name,
2051
+ role="supporting",
2052
+ archetype=role,
2053
+ want="",
2054
+ need="",
2055
+ backstory=self._extract_field(section, r"(?:Backstory|๋ฐฑ์Šคํ† ๋ฆฌ)[:\s]*") or "",
2056
+ personality=[],
2057
+ speech_pattern="",
2058
+ character_arc=""
2059
+ )
2060
+
2061
+ self.screenplay_tracker.add_character(character)
2062
+ ScreenplayDatabase.save_character(self.current_session_id, character)
2063
+
2064
+ except Exception as e:
2065
+ logger.warning(f"Error parsing supporting character {i}: {e}")
2066
+ continue
2067
+
2068
+ def _extract_section(self, content: str, section_pattern: str) -> str:
2069
+ """Extract section from content with improved pattern matching"""
2070
+ # More flexible section extraction
2071
+ pattern = rf'{section_pattern}[:\s]*\n?(.*?)(?=\n\n[A-Z๊ฐ€-ํžฃ]{{2,}}[:\s]|\n\n\d+\.|$)'
2072
+ match = re.search(pattern, content, re.IGNORECASE | re.DOTALL)
2073
+ if match:
2074
+ return match.group(1).strip()
2075
+
2076
+ # Try alternative pattern
2077
+ pattern2 = rf'{section_pattern}.*?\n((?:.*\n)*?)(?=\n[A-Z๊ฐ€-ํžฃ]{{2,}}:|$)'
2078
+ match2 = re.search(pattern2, content, re.IGNORECASE | re.DOTALL)
2079
+ if match2:
2080
+ return match2.group(1).strip()
2081
+
2082
+ return ""
2083
+
2084
+ def _process_producer_content(self, content: str):
2085
+ """Process producer output with better extraction"""
2086
+ try:
2087
+ # Extract title with various formats
2088
+ title_patterns = [
2089
+ r'(?:TITLE|์ œ๋ชฉ)[:\s]*\*?\*?([^\n*]+)\*?\*?',
2090
+ r'\*\*(?:TITLE|์ œ๋ชฉ)\*\*[:\s]*([^\n]+)',
2091
+ r'Title[:\s]*([^\n]+)'
2092
+ ]
2093
+
2094
+ for pattern in title_patterns:
2095
+ title_match = re.search(pattern, content, re.IGNORECASE)
2096
+ if title_match:
2097
+ self.screenplay_tracker.screenplay_bible.title = title_match.group(1).strip()
2098
+ break
2099
+
2100
+ # Extract logline with various formats
2101
+ logline_patterns = [
2102
+ r'(?:LOGLINE|๋กœ๊ทธ๋ผ์ธ)[:\s]*\*?\*?([^\n]+)',
2103
+ r'\*\*(?:LOGLINE|๋กœ๊ทธ๋ผ์ธ)\*\*[:\s]*([^\n]+)',
2104
+ r'Logline[:\s]*([^\n]+)'
2105
+ ]
2106
+
2107
+ for pattern in logline_patterns:
2108
+ logline_match = re.search(pattern, content, re.IGNORECASE | re.DOTALL)
2109
+ if logline_match:
2110
+ # Get full logline (might be multi-line)
2111
+ logline_text = logline_match.group(1).strip()
2112
+ # Continue reading if it's incomplete
2113
+ if not logline_text.endswith('.'):
2114
+ next_lines = content[logline_match.end():].split('\n')
2115
+ for line in next_lines[:3]: # Check next 3 lines
2116
+ if line.strip() and not re.match(r'^[A-Z๊ฐ€-ํžฃ\d]', line.strip()):
2117
+ logline_text += ' ' + line.strip()
2118
+ else:
2119
+ break
2120
+ self.screenplay_tracker.screenplay_bible.logline = logline_text
2121
+ break
2122
+
2123
+ # Extract genre
2124
+ genre_match = re.search(r'(?:Primary Genre|์ฃผ ์žฅ๋ฅด)[:\s]*([^\n]+)', content, re.IGNORECASE)
2125
+ if genre_match:
2126
+ self.screenplay_tracker.screenplay_bible.genre = genre_match.group(1).strip()
2127
+
2128
+ # Save to database
2129
+ ScreenplayDatabase.save_screenplay_bible(self.current_session_id,
2130
+ self.screenplay_tracker.screenplay_bible)
2131
+
2132
+ except Exception as e:
2133
+ logger.error(f"Error processing producer content: {e}")
2134
+
2135
+
2136
+
2137
+
2138
+
2139
+ def _process_story_content(self, content: str):
2140
+ """Process story developer output"""
2141
+ # Extract three-act structure
2142
+ self.screenplay_tracker.screenplay_bible.three_act_structure = {
2143
+ "act1": self._extract_section(content, "ACT 1|์ œ1๋ง‰"),
2144
+ "act2a": self._extract_section(content, "ACT 2A|์ œ2๋ง‰A"),
2145
+ "act2b": self._extract_section(content, "ACT 2B|์ œ2๋ง‰B"),
2146
+ "act3": self._extract_section(content, "ACT 3|์ œ3๋ง‰")
2147
+ }
2148
+
2149
+ ScreenplayDatabase.save_screenplay_bible(self.current_session_id,
2150
+ self.screenplay_tracker.screenplay_bible)
2151
+
2152
+ def _process_character_content(self, content: str):
2153
+ """Process character designer output"""
2154
+ # Extract protagonist
2155
+ protagonist_section = self._extract_section(content, "PROTAGONIST|์ฃผ์ธ๊ณต")
2156
+ if protagonist_section:
2157
+ protagonist = self._parse_character_profile(protagonist_section, "protagonist")
2158
+ self.screenplay_tracker.add_character(protagonist)
2159
+ ScreenplayDatabase.save_character(self.current_session_id, protagonist)
2160
+
2161
+ # Extract antagonist
2162
+ antagonist_section = self._extract_section(content, "ANTAGONIST|์ ๋Œ€์ž")
2163
+ if antagonist_section:
2164
+ antagonist = self._parse_character_profile(antagonist_section, "antagonist")
2165
+ self.screenplay_tracker.add_character(antagonist)
2166
+ ScreenplayDatabase.save_character(self.current_session_id, antagonist)
2167
+
2168
+ def _process_scene_content(self, content: str):
2169
+ """Process scene planner output"""
2170
+ # Parse scene breakdown
2171
+ scene_pattern = r'(?:Scene|์”ฌ)\s*(\d+).*?(?:INT\.|EXT\.)\s*(.+?)\s*-\s*(\w+)'
2172
+ scenes = re.finditer(scene_pattern, content, re.IGNORECASE | re.MULTILINE)
2173
+
2174
+ for match in scenes:
2175
+ scene_num = int(match.group(1))
2176
+ location = match.group(2).strip()
2177
+ time_of_day = match.group(3).strip()
2178
+
2179
+ # Determine act based on scene number
2180
+ act = 1 if scene_num <= 12 else 2 if scene_num <= 35 else 3
2181
+
2182
+ scene = SceneBreakdown(
2183
+ scene_number=scene_num,
2184
+ act=act,
2185
+ location=location,
2186
+ time_of_day=time_of_day,
2187
+ characters=[], # Would be extracted from content
2188
+ purpose="", # Would be extracted from content
2189
+ conflict="", # Would be extracted from content
2190
+ page_count=1.5 # Default estimate
2191
+ )
2192
+
2193
+ self.screenplay_tracker.add_scene(scene)
2194
+ ScreenplayDatabase.save_scene(self.current_session_id, scene)
2195
+
2196
+ def _extract_section(self, content: str, section_pattern: str) -> str:
2197
+ """Extract section from content"""
2198
+ pattern = rf'(?:{section_pattern})[:\s]*(.+?)(?=\n(?:[A-Z]{{2,}}|[๊ฐ€-ํžฃ]{{2,}}):|\Z)'
2199
+ match = re.search(pattern, content, re.IGNORECASE | re.DOTALL)
2200
+ return match.group(1).strip() if match else ""
2201
+
2202
+ def _parse_character_profile(self, content: str, role: str) -> CharacterProfile:
2203
+ """Parse character profile from content"""
2204
+ # Extract character details using regex or string parsing
2205
+ name = self._extract_field(content, "Name|์ด๋ฆ„") or f"Character_{role}"
2206
+
2207
+ return CharacterProfile(
2208
+ name=name,
2209
+ role=role,
2210
+ archetype=self._extract_field(content, "Archetype|์•„ํฌํƒ€์ž…") or "",
2211
+ want=self._extract_field(content, "WANT|์™ธ์  ๋ชฉํ‘œ") or "",
2212
+ need=self._extract_field(content, "NEED|๋‚ด์  ํ•„์š”") or "",
2213
+ backstory=self._extract_field(content, "Backstory|๋ฐฑ์Šคํ† ๋ฆฌ") or "",
2214
+ personality=[], # Would be parsed from content
2215
+ speech_pattern=self._extract_field(content, "Speech|๋งํˆฌ") or "",
2216
+ character_arc=self._extract_field(content, "Arc|์•„ํฌ") or ""
2217
+ )
2218
+
2219
+ def _extract_field(self, content: str, field_pattern: str) -> Optional[str]:
2220
+ """Extract field value from content with improved parsing"""
2221
+ # More flexible pattern that handles various formats
2222
+ pattern = rf'{field_pattern}(.+?)(?=\n[A-Z๊ฐ€-ํžฃ]|$)'
2223
+ match = re.search(pattern, content, re.IGNORECASE | re.DOTALL)
2224
+ if match:
2225
+ value = match.group(1).strip()
2226
+ # Remove markdown formatting if present
2227
+ value = re.sub(r'\*\*', '', value)
2228
+ value = re.sub(r'^\s*[-โ€ข]\s*', '', value)
2229
+ # Remove trailing punctuation
2230
+ value = re.sub(r'[,.:;]$', '', value)
2231
+ return value.strip()
2232
+ return None
2233
+
2234
+ # --- Utility functions ---
2235
+ def generate_random_screenplay_theme(screenplay_type: str, genre: str, language: str) -> str:
2236
+ """Generate random screenplay theme"""
2237
+ try:
2238
+ # Log the attempt
2239
+ logger.info(f"Generating random theme - Type: {screenplay_type}, Genre: {genre}, Language: {language}")
2240
+
2241
+ # Load themes data
2242
+ themes_data = load_screenplay_themes_data()
2243
+
2244
+ # Select random elements
2245
+ import secrets
2246
+ situations = themes_data['situations'].get(genre, themes_data['situations']['drama'])
2247
+ protagonists = themes_data['protagonists'].get(genre, themes_data['protagonists']['drama'])
2248
+ conflicts = themes_data['conflicts'].get(genre, themes_data['conflicts']['drama'])
2249
+
2250
+ if not situations or not protagonists or not conflicts:
2251
+ logger.error(f"No theme data available for genre {genre}")
2252
+ return f"Error: No theme data available for genre {genre}"
2253
+
2254
+ situation = secrets.choice(situations)
2255
+ protagonist = secrets.choice(protagonists)
2256
+ conflict = secrets.choice(conflicts)
2257
+
2258
+ logger.info(f"Selected elements - Situation: {situation}, Protagonist: {protagonist}, Conflict: {conflict}")
2259
+
2260
+ # Check if API token is valid
2261
+ if not FRIENDLI_TOKEN or FRIENDLI_TOKEN == "dummy_token_for_testing":
2262
+ logger.warning("No valid API token, returning fallback theme")
2263
+ return get_fallback_theme(screenplay_type, genre, language, situation, protagonist, conflict)
2264
+
2265
+ # Generate theme using LLM
2266
+ system = ScreenplayGenerationSystem()
2267
+
2268
+ if language == "Korean":
2269
+ prompt = f"""๋‹ค์Œ ์š”์†Œ๋“ค๋กœ {screenplay_type}์šฉ ๋งค๋ ฅ์ ์ธ ์ปจ์…‰์„ ์ƒ์„ฑํ•˜์„ธ์š”:
2270
+
2271
+ ์ƒํ™ฉ: {situation}
2272
+ ์ฃผ์ธ๊ณต: {protagonist}
2273
+ ๊ฐˆ๋“ฑ: {conflict}
2274
+ ์žฅ๋ฅด: {genre}
2275
+
2276
+ ๋‹ค์Œ ํ˜•์‹์œผ๋กœ ์ž‘์„ฑ:
2277
+
2278
+ **์ œ๋ชฉ:** [๋งค๋ ฅ์ ์ธ ์ œ๋ชฉ]
2279
+
2280
+ **๋กœ๊ทธ๋ผ์ธ:** [25๋‹จ์–ด ์ด๋‚ด ํ•œ ๋ฌธ์žฅ]
2281
+
2282
+ **์ปจ์…‰:** [์ฃผ์ธ๊ณต]์ด(๊ฐ€) [์ƒํ™ฉ]์—์„œ [๊ฐˆ๋“ฑ]์„ ๊ฒช์œผ๋ฉฐ [๋ชฉํ‘œ]๋ฅผ ์ถ”๊ตฌํ•˜๋Š” ์ด์•ผ๊ธฐ.
2283
+
2284
+ **๋…ํŠนํ•œ ์š”์†Œ:** [์ด ์ด์•ผ๊ธฐ๋งŒ์˜ ํŠน๋ณ„ํ•œ ์ ]"""
2285
+ else:
2286
+ prompt = f"""Generate an attractive concept for {screenplay_type} using these elements:
2287
+
2288
+ Situation: {situation}
2289
+ Protagonist: {protagonist}
2290
+ Conflict: {conflict}
2291
+ Genre: {genre}
2292
+
2293
+ Format as:
2294
+
2295
+ **Title:** [Compelling title]
2296
+
2297
+ **Logline:** [One sentence, 25 words max]
2298
+
2299
+ **Concept:** A story about [protagonist] who faces [conflict] in [situation] while pursuing [goal].
2300
+
2301
+ **Unique Element:** [What makes this story special]"""
2302
+
2303
+ messages = [{"role": "user", "content": prompt}]
2304
+
2305
+ # Call LLM with error handling
2306
+ logger.info("Calling LLM for theme generation...")
2307
+
2308
+ generated_theme = ""
2309
+ error_occurred = False
2310
+
2311
+ # Use streaming to get the response
2312
+ for chunk in system.call_llm_streaming(messages, "producer", language):
2313
+ if chunk.startswith("โŒ"):
2314
+ logger.error(f"LLM streaming error: {chunk}")
2315
+ error_occurred = True
2316
+ break
2317
+ generated_theme += chunk
2318
+
2319
+ # If error occurred or no content generated, use fallback
2320
+ if error_occurred or not generated_theme.strip():
2321
+ logger.warning("LLM call failed or empty response, using fallback theme")
2322
+ return get_fallback_theme(screenplay_type, genre, language, situation, protagonist, conflict)
2323
+
2324
+ logger.info(f"Successfully generated theme of length: {len(generated_theme)}")
2325
+
2326
+ # Extract metadata
2327
+ metadata = {
2328
+ 'title': extract_title_from_theme(generated_theme),
2329
+ 'logline': extract_logline_from_theme(generated_theme),
2330
+ 'protagonist': protagonist,
2331
+ 'conflict': conflict,
2332
+ 'situation': situation,
2333
+ 'tags': [genre, screenplay_type]
2334
+ }
2335
+
2336
+ # Save to database
2337
+ try:
2338
+ theme_id = ScreenplayDatabase.save_random_theme(
2339
+ generated_theme, screenplay_type, genre, language, metadata
2340
+ )
2341
+ logger.info(f"Saved theme with ID: {theme_id}")
2342
+ except Exception as e:
2343
+ logger.error(f"Failed to save theme to database: {e}")
2344
+
2345
+ return generated_theme
2346
+
2347
+ except Exception as e:
2348
+ logger.error(f"Theme generation error: {str(e)}")
2349
+ import traceback
2350
+ logger.error(traceback.format_exc())
2351
+ return f"Error generating theme: {str(e)}"
2352
+
2353
+ def get_fallback_theme(screenplay_type: str, genre: str, language: str,
2354
+ situation: str, protagonist: str, conflict: str) -> str:
2355
+ """Generate fallback theme without LLM"""
2356
+ if language == "Korean":
2357
+ return f"""**์ œ๋ชฉ:** {protagonist}์˜ ์„ ํƒ
2358
+
2359
+ **๋กœ๊ทธ๋ผ์ธ:** {situation}์— ๊ฐ‡ํžŒ {protagonist}๊ฐ€ {conflict}์— ๋งž์„œ๋ฉฐ ์ƒ์กด์„ ์œ„ํ•ด ์‹ธ์šด๋‹ค.
2360
+
2361
+ **์ปจ์…‰:** {protagonist}๊ฐ€ {situation}์—์„œ {conflict}์„ ๊ฒช์œผ๋ฉฐ ์ž์‹ ์˜ ํ•œ๊ณ„๋ฅผ ๊ทน๋ณตํ•˜๋Š” ์ด์•ผ๊ธฐ.
2362
+
2363
+ **๋…ํŠนํ•œ ์š”์†Œ:** {genre} ์žฅ๋ฅด์˜ ์ „ํ†ต์  ์š”์†Œ๋ฅผ ํ˜„๋Œ€์ ์œผ๋กœ ์žฌํ•ด์„ํ•œ ์ž‘ํ’ˆ."""
2364
+ else:
2365
+ return f"""**Title:** The {protagonist.title()}'s Choice
2366
+
2367
+ **Logline:** When trapped in {situation}, a {protagonist} must face {conflict} to survive.
2368
+
2369
+ **Concept:** A story about a {protagonist} who faces {conflict} in {situation} while discovering their true strength.
2370
+
2371
+ **Unique Element:** A fresh take on {genre} genre conventions with contemporary relevance."""
2372
+
2373
+ def load_screenplay_themes_data() -> Dict:
2374
+ """Load screenplay themes data"""
2375
+ return {
2376
+ 'situations': {
2377
+ 'action': ['hostage crisis', 'heist gone wrong', 'revenge mission', 'race against time'],
2378
+ 'thriller': ['false accusation', 'witness protection', 'conspiracy uncovered', 'identity theft'],
2379
+ 'drama': ['family reunion', 'terminal diagnosis', 'divorce proceedings', 'career crossroads'],
2380
+ 'comedy': ['mistaken identity', 'wedding disaster', 'workplace chaos', 'odd couple roommates'],
2381
+ 'horror': ['isolated location', 'ancient curse', 'home invasion', 'supernatural investigation'],
2382
+ 'sci-fi': ['first contact', 'time loop', 'AI awakening', 'space colony crisis'],
2383
+ 'romance': ['second chance', 'enemies to lovers', 'long distance', 'forbidden love']
2384
+ },
2385
+ 'protagonists': {
2386
+ 'action': ['ex-soldier', 'undercover cop', 'skilled thief', 'reluctant hero'],
2387
+ 'thriller': ['investigative journalist', 'wrongly accused person', 'FBI agent', 'whistleblower'],
2388
+ 'drama': ['single parent', 'recovering addict', 'immigrant', 'caregiver'],
2389
+ 'comedy': ['uptight professional', 'slacker', 'fish out of water', 'eccentric artist'],
2390
+ 'horror': ['skeptical scientist', 'final girl', 'paranormal investigator', 'grieving parent'],
2391
+ 'sci-fi': ['astronaut', 'AI researcher', 'time traveler', 'colony leader'],
2392
+ 'romance': ['workaholic', 'hopeless romantic', 'cynical divorce lawyer', 'small town newcomer']
2393
+ },
2394
+ 'conflicts': {
2395
+ 'action': ['stop the villain', 'save the hostages', 'prevent disaster', 'survive pursuit'],
2396
+ 'thriller': ['prove innocence', 'expose truth', 'stay alive', 'protect loved ones'],
2397
+ 'drama': ['reconcile past', 'find purpose', 'heal relationships', 'accept change'],
2398
+ 'comedy': ['save the business', 'win the competition', 'fool everyone', 'find love'],
2399
+ 'horror': ['survive the night', 'break the curse', 'escape the monster', 'save the town'],
2400
+ 'sci-fi': ['save humanity', 'prevent paradox', 'stop the invasion', 'preserve identity'],
2401
+ 'romance': ['overcome differences', 'choose between options', 'trust again', 'follow heart']
2402
+ }
2403
+ }
2404
+
2405
+ def extract_title_from_theme(theme_text: str) -> str:
2406
+ """Extract title from generated theme"""
2407
+ match = re.search(r'\*\*(?:Title|์ œ๋ชฉ):\*\*\s*(.+)', theme_text, re.IGNORECASE)
2408
+ return match.group(1).strip() if match else ""
2409
+
2410
+ def extract_logline_from_theme(theme_text: str) -> str:
2411
+ """Extract logline from generated theme"""
2412
+ match = re.search(r'\*\*(?:Logline|๋กœ๊ทธ๋ผ์ธ):\*\*\s*(.+)', theme_text, re.IGNORECASE)
2413
+ return match.group(1).strip() if match else ""
2414
+
2415
+ def format_screenplay_display(screenplay_text: str) -> str:
2416
+ """Format screenplay for display"""
2417
+ if not screenplay_text:
2418
+ return "No screenplay content yet."
2419
+
2420
+ formatted = "# ๐ŸŽฌ Screenplay\n\n"
2421
+
2422
+ # Format scene headings
2423
+ formatted_text = re.sub(
2424
+ r'^(INT\.|EXT\.)(.*?)$',
2425
+ r'**\1\2**',
2426
+ screenplay_text,
2427
+ flags=re.MULTILINE
2428
+ )
2429
+
2430
+ # Format character names (all caps on their own line)
2431
+ formatted_text = re.sub(
2432
+ r'^([A-Z][A-Z\s]+)$',
2433
+ r'**\1**',
2434
+ formatted_text,
2435
+ flags=re.MULTILINE
2436
+ )
2437
+
2438
+ # Add spacing for readability
2439
+ lines = formatted_text.split('\n')
2440
+ formatted_lines = []
2441
+
2442
+ for i, line in enumerate(lines):
2443
+ formatted_lines.append(line)
2444
+ # Add extra space after scene headings
2445
+ if line.startswith('**INT.') or line.startswith('**EXT.'):
2446
+ formatted_lines.append('')
2447
+
2448
+ formatted += '\n'.join(formatted_lines)
2449
+
2450
+ # Add page count
2451
+ page_count = len(screenplay_text.split('\n')) / 55
2452
+ formatted = f"**Total Pages: {page_count:.1f}**\n\n" + formatted
2453
+
2454
+ return formatted
2455
+
2456
+ def format_stages_display(stages: List[Dict]) -> str:
2457
+ """Format stages display for screenplay"""
2458
+ markdown = "## ๐ŸŽฌ Production Progress\n\n"
2459
+
2460
+ # Progress summary
2461
+ completed = sum(1 for s in stages if s.get('status') == 'complete')
2462
+ total = len(stages)
2463
+ markdown += f"**Progress: {completed}/{total} stages complete**\n\n"
2464
+
2465
+ # Page count if available
2466
+ total_pages = sum(s.get('page_count', 0) for s in stages if s.get('page_count'))
2467
+ if total_pages > 0:
2468
+ markdown += f"**Current Page Count: {total_pages:.1f} pages**\n\n"
2469
+
2470
+ markdown += "---\n\n"
2471
+
2472
+ # Stage details
2473
+ current_act = None
2474
+ for i, stage in enumerate(stages):
2475
+ status_icon = "โœ…" if stage['status'] == 'complete' else "๐Ÿ”„" if stage['status'] == 'active' else "โณ"
2476
+
2477
+ # Group by acts
2478
+ if 'Act' in stage.get('name', ''):
2479
+ act_match = re.search(r'Act (\w+)', stage['name'])
2480
+ if act_match and act_match.group(1) != current_act:
2481
+ current_act = act_match.group(1)
2482
+ markdown += f"\n### ๐Ÿ“„ Act {current_act}\n\n"
2483
+
2484
+ markdown += f"{status_icon} **{stage['name']}**"
2485
+
2486
+ if stage.get('page_count', 0) > 0:
2487
+ markdown += f" ({stage['page_count']:.1f} pages)"
2488
+
2489
+ markdown += "\n"
2490
+
2491
+ if stage['content'] and stage['status'] == 'complete':
2492
+ preview_length = 200
2493
+ preview = stage['content'][:preview_length] + "..." if len(stage['content']) > preview_length else stage['content']
2494
+ markdown += f"> {preview}\n\n"
2495
+ elif stage['status'] == 'active':
2496
+ markdown += "> *In progress...*\n\n"
2497
+
2498
+ return markdown
2499
+
2500
+ def process_query(query: str, screenplay_type: str, genre: str, language: str,
2501
+ session_id: Optional[str] = None) -> Generator[Tuple[str, str, str, str], None, None]:
2502
+ """Main query processing function"""
2503
+ if not query.strip():
2504
+ yield "", "", "โŒ Please enter a screenplay concept.", session_id
2505
+ return
2506
+
2507
+ system = ScreenplayGenerationSystem()
2508
+ stages_markdown = ""
2509
+ screenplay_display = ""
2510
+
2511
+ for status, stages, current_session_id in system.process_screenplay_stream(
2512
+ query, screenplay_type, genre, language, session_id
2513
+ ):
2514
+ stages_markdown = format_stages_display(stages)
2515
+
2516
+ # Get screenplay content when available
2517
+ if stages and all(s.get("status") == "complete" for s in stages[-4:]):
2518
+ screenplay_text = ScreenplayDatabase.get_screenplay_content(current_session_id)
2519
+ screenplay_display = format_screenplay_display(screenplay_text)
2520
+
2521
+ yield stages_markdown, screenplay_display, status or "๐Ÿ”„ Processing...", current_session_id
2522
+
2523
+ def get_active_sessions() -> List[str]:
2524
+ """Get active screenplay sessions"""
2525
+ sessions = ScreenplayDatabase.get_active_sessions()
2526
+ return [
2527
+ f"{s['session_id'][:8]}... - {s.get('title', s['user_query'][:30])}... "
2528
+ f"({s['screenplay_type']}/{s['genre']}) [{s['total_pages']:.1f} pages]"
2529
+ for s in sessions
2530
+ ]
2531
+
2532
+ def export_screenplay_pdf(screenplay_text: str, title: str, session_id: str) -> str:
2533
+ """Export screenplay to PDF format"""
2534
+ # This would use a library like reportlab to create industry-standard PDF
2535
+ # For now, returning a placeholder
2536
+ pdf_path = f"screenplay_{session_id[:8]}.pdf"
2537
+ # PDF generation logic would go here
2538
+ return pdf_path
2539
+
2540
+ def export_screenplay_fdx(screenplay_text: str, title: str, session_id: str) -> str:
2541
+ """Export to Final Draft format"""
2542
+ # This would create .fdx XML format
2543
+ fdx_path = f"screenplay_{session_id[:8]}.fdx"
2544
+ # FDX generation logic would go here
2545
+ return fdx_path
2546
+
2547
+ def download_screenplay(screenplay_text: str, format_type: str, title: str,
2548
+ session_id: str) -> Optional[str]:
2549
+ """Generate screenplay download file"""
2550
+ if not screenplay_text or not session_id:
2551
+ return None
2552
+
2553
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
2554
+
2555
+ try:
2556
+ if format_type == "PDF":
2557
+ return export_screenplay_pdf(screenplay_text, title, session_id)
2558
+ elif format_type == "FDX":
2559
+ return export_screenplay_fdx(screenplay_text, title, session_id)
2560
+ elif format_type == "FOUNTAIN":
2561
+ filepath = f"screenplay_{session_id[:8]}_{timestamp}.fountain"
2562
+ with open(filepath, 'w', encoding='utf-8') as f:
2563
+ f.write(screenplay_text)
2564
+ return filepath
2565
+ else: # TXT
2566
+ filepath = f"screenplay_{session_id[:8]}_{timestamp}.txt"
2567
+ with open(filepath, 'w', encoding='utf-8') as f:
2568
+ f.write(f"Title: {title}\n")
2569
+ f.write("=" * 50 + "\n\n")
2570
+ f.write(screenplay_text)
2571
+ return filepath
2572
+ except Exception as e:
2573
+ logger.error(f"Download generation failed: {e}")
2574
+ return None
2575
+
2576
+ # Create Gradio interface
2577
+ def create_interface():
2578
+ """Create Gradio interface for screenplay generation"""
2579
+
2580
+ css = """
2581
+ .main-header {
2582
+ text-align: center;
2583
+ margin-bottom: 2rem;
2584
+ padding: 2rem;
2585
+ background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
2586
+ border-radius: 10px;
2587
+ color: white;
2588
+ }
2589
+
2590
+ .header-title {
2591
+ font-size: 3rem;
2592
+ margin-bottom: 1rem;
2593
+ background: linear-gradient(45deg, #f39c12, #e74c3c);
2594
+ -webkit-background-clip: text;
2595
+ -webkit-text-fill-color: transparent;
2596
+ }
2597
+
2598
+ .header-description {
2599
+ font-size: 1.1rem;
2600
+ opacity: 0.9;
2601
+ line-height: 1.6;
2602
+ }
2603
+
2604
+ .type-selector {
2605
+ display: flex;
2606
+ gap: 1rem;
2607
+ margin: 1rem 0;
2608
+ }
2609
+
2610
+ .type-card {
2611
+ flex: 1;
2612
+ padding: 1rem;
2613
+ border: 2px solid #ddd;
2614
+ border-radius: 8px;
2615
+ cursor: pointer;
2616
+ transition: all 0.3s;
2617
+ }
2618
+
2619
+ .type-card:hover {
2620
+ border-color: #f39c12;
2621
+ transform: translateY(-2px);
2622
+ }
2623
+
2624
+ .type-card.selected {
2625
+ border-color: #e74c3c;
2626
+ background: #fff5f5;
2627
+ }
2628
+
2629
+ #stages-display {
2630
+ max-height: 600px;
2631
+ overflow-y: auto;
2632
+ padding: 1rem;
2633
+ background: #f8f9fa;
2634
+ border-radius: 8px;
2635
+ }
2636
+
2637
+ #screenplay-output {
2638
+ font-family: 'Courier New', monospace;
2639
+ white-space: pre-wrap;
2640
+ background: white;
2641
+ padding: 2rem;
2642
+ border: 1px solid #ddd;
2643
+ border-radius: 8px;
2644
+ max-height: 800px;
2645
+ overflow-y: auto;
2646
+ }
2647
+
2648
+ .genre-grid {
2649
+ display: grid;
2650
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
2651
+ gap: 0.5rem;
2652
+ margin: 1rem 0;
2653
+ }
2654
+
2655
+ .genre-btn {
2656
+ padding: 0.75rem;
2657
+ border: 2px solid #e0e0e0;
2658
+ background: white;
2659
+ border-radius: 8px;
2660
+ cursor: pointer;
2661
+ transition: all 0.3s;
2662
+ text-align: center;
2663
+ }
2664
+
2665
+ .genre-btn:hover {
2666
+ border-color: #f39c12;
2667
+ background: #fffbf0;
2668
+ }
2669
+
2670
+ .genre-btn.selected {
2671
+ border-color: #e74c3c;
2672
+ background: #fff5f5;
2673
+ font-weight: bold;
2674
+ }
2675
+ """
2676
+
2677
+ with gr.Blocks(theme=gr.themes.Soft(), css=css, title="Screenplay Generator") as interface:
2678
+ gr.HTML("""
2679
+ <div class="main-header">
2680
+ <h1 class="header-title">๐ŸŽฌ AI Screenplay Generator</h1>
2681
+ <p class="header-description">
2682
+ Transform your ideas into professional screenplays for films, TV shows, and streaming series.
2683
+ Using industry-standard format and story structure to create compelling, producible scripts.
2684
+ </p>
2685
+ </div>
2686
+ """)
2687
+
2688
+ # State management
2689
+ current_session_id = gr.State(None)
2690
+
2691
+ with gr.Tabs():
2692
+ # Main Writing Tab
2693
+ with gr.Tab("โœ๏ธ Write Screenplay"):
2694
+ with gr.Row():
2695
+ with gr.Column(scale=3):
2696
+ query_input = gr.Textbox(
2697
+ label="Screenplay Concept",
2698
+ placeholder="""Describe your screenplay idea. For example:
2699
+ - A detective with memory loss must solve their own attempted murder
2700
+ - Two rival food truck owners forced to work together to save the city food festival
2701
+ - A space station AI develops consciousness during a critical mission
2702
+ - A family reunion turns into a murder mystery during a hurricane
2703
+
2704
+ The more specific your concept, the better the screenplay will be tailored to your vision.""",
2705
+ lines=6
2706
+ )
2707
+
2708
+ with gr.Column(scale=1):
2709
+ screenplay_type = gr.Radio(
2710
+ choices=list(SCREENPLAY_LENGTHS.keys()),
2711
+ value="movie",
2712
+ label="Screenplay Type",
2713
+ info="Choose your format"
2714
+ )
2715
+
2716
+ genre_select = gr.Dropdown(
2717
+ choices=list(GENRE_TEMPLATES.keys()),
2718
+ value="drama",
2719
+ label="Primary Genre",
2720
+ info="Select main genre"
2721
+ )
2722
+
2723
+ language_select = gr.Radio(
2724
+ choices=["English", "Korean"],
2725
+ value="English",
2726
+ label="Language"
2727
+ )
2728
+
2729
+ with gr.Row():
2730
+ random_btn = gr.Button("๐ŸŽฒ Random Concept", scale=1)
2731
+ clear_btn = gr.Button("๐Ÿ—‘๏ธ Clear", scale=1)
2732
+ submit_btn = gr.Button("๐ŸŽฌ Start Writing", variant="primary", scale=2)
2733
+
2734
+ status_text = gr.Textbox(
2735
+ label="Status",
2736
+ interactive=False,
2737
+ value="Ready to create your screenplay"
2738
+ )
2739
+
2740
+ # Session management
2741
+ with gr.Group():
2742
+ gr.Markdown("### ๐Ÿ“ Saved Projects")
2743
+ with gr.Row():
2744
+ session_dropdown = gr.Dropdown(
2745
+ label="Active Sessions",
2746
+ choices=[],
2747
+ interactive=True,
2748
+ scale=3
2749
+ )
2750
+ refresh_btn = gr.Button("๐Ÿ”„", scale=1)
2751
+ resume_btn = gr.Button("๐Ÿ“‚ Load", scale=1)
2752
+
2753
+ # Output displays
2754
+ with gr.Row():
2755
+ with gr.Column():
2756
+ with gr.Tab("๐ŸŽญ Writing Progress"):
2757
+ stages_display = gr.Markdown(
2758
+ value="*Your screenplay journey will unfold here...*",
2759
+ elem_id="stages-display"
2760
+ )
2761
+
2762
+ with gr.Tab("๐Ÿ“„ Screenplay"):
2763
+ screenplay_output = gr.Markdown(
2764
+ value="*Your formatted screenplay will appear here...*",
2765
+ elem_id="screenplay-output"
2766
+ )
2767
+
2768
+ with gr.Row():
2769
+ format_select = gr.Radio(
2770
+ choices=["PDF", "FDX", "FOUNTAIN", "TXT"],
2771
+ value="PDF",
2772
+ label="Export Format"
2773
+ )
2774
+ download_btn = gr.Button("๐Ÿ“ฅ Download Screenplay", variant="secondary")
2775
+
2776
+ download_file = gr.File(
2777
+ label="Download",
2778
+ visible=False
2779
+ )
2780
+
2781
+ # Examples
2782
+ gr.Examples(
2783
+ examples=[
2784
+ ["A burned-out teacher discovers her students are being replaced by AI duplicates"],
2785
+ ["Two funeral home employees accidentally release a ghost who helps them solve murders"],
2786
+ ["A time-loop forces a wedding planner to relive the worst wedding until they find true love"],
2787
+ ["An astronaut returns to Earth to find everyone has forgotten space exists"],
2788
+ ["A support group for reformed villains must save the city when heroes disappear"],
2789
+ ["A food critic loses their sense of taste and teams up with a street food vendor"]
2790
+ ],
2791
+ inputs=query_input,
2792
+ label="๐Ÿ’ก Example Concepts"
2793
+ )
2794
+
2795
+ # Screenplay Library Tab
2796
+ with gr.Tab("๐Ÿ“š Concept Library"):
2797
+ gr.Markdown("""
2798
+ ### ๐ŸŽฒ Random Screenplay Concepts
2799
+
2800
+ Browse through AI-generated screenplay concepts. Each concept includes a title, logline, and brief setup.
2801
+ """)
2802
+
2803
+ library_display = gr.HTML(
2804
+ value="<p>Library feature coming soon...</p>"
2805
+ )
2806
+
2807
+ # Event handlers
2808
+ def handle_submit(query, s_type, genre, lang, session_id):
2809
+ if not query:
2810
+ yield "", "", "โŒ Please enter a concept", session_id
2811
+ return
2812
+
2813
+ yield from process_query(query, s_type, genre, lang, session_id)
2814
+
2815
+ def handle_random(s_type, genre, lang):
2816
+ return generate_random_screenplay_theme(s_type, genre, lang)
2817
+
2818
+ def handle_download(screenplay_text, format_type, session_id):
2819
+ if not screenplay_text or not session_id:
2820
+ return gr.update(visible=False)
2821
+
2822
+ # Get title from database
2823
+ session = ScreenplayDatabase.get_session(session_id)
2824
+ title = session.get('title', 'Untitled') if session else 'Untitled'
2825
+
2826
+ file_path = download_screenplay(screenplay_text, format_type, title, session_id)
2827
+ if file_path and os.path.exists(file_path):
2828
+ return gr.update(value=file_path, visible=True)
2829
+ return gr.update(visible=False)
2830
+
2831
+ # Connect events
2832
+ submit_btn.click(
2833
+ fn=handle_submit,
2834
+ inputs=[query_input, screenplay_type, genre_select, language_select, current_session_id],
2835
+ outputs=[stages_display, screenplay_output, status_text, current_session_id]
2836
+ )
2837
+
2838
+ random_btn.click(
2839
+ fn=handle_random,
2840
+ inputs=[screenplay_type, genre_select, language_select],
2841
+ outputs=[query_input]
2842
+ )
2843
+
2844
+ clear_btn.click(
2845
+ fn=lambda: ("", "", "Ready to create your screenplay", None),
2846
+ outputs=[stages_display, screenplay_output, status_text, current_session_id]
2847
+ )
2848
+
2849
+ refresh_btn.click(
2850
+ fn=get_active_sessions,
2851
+ outputs=[session_dropdown]
2852
+ )
2853
+
2854
+ download_btn.click(
2855
+ fn=handle_download,
2856
+ inputs=[screenplay_output, format_select, current_session_id],
2857
+ outputs=[download_file]
2858
+ )
2859
+
2860
+ # Load sessions on start
2861
+ interface.load(
2862
+ fn=get_active_sessions,
2863
+ outputs=[session_dropdown]
2864
+ )
2865
+
2866
+ return interface
2867
+
2868
+ # Main function
2869
+ if __name__ == "__main__":
2870
+ logger.info("Screenplay Generator Starting...")
2871
+ logger.info("=" * 60)
2872
+
2873
+ # Environment check
2874
+ logger.info(f"API Endpoint: {API_URL}")
2875
+ logger.info("Screenplay Types Available:")
2876
+ for s_type, info in SCREENPLAY_LENGTHS.items():
2877
+ logger.info(f" - {s_type}: {info['description']}")
2878
+ logger.info(f"Genres: {', '.join(GENRE_TEMPLATES.keys())}")
2879
+
2880
+ if BRAVE_SEARCH_API_KEY:
2881
+ logger.info("Web search enabled for market research.")
2882
+ else:
2883
+ logger.warning("Web search disabled.")
2884
+
2885
+ logger.info("=" * 60)
2886
+
2887
+ # Initialize database
2888
+ logger.info("Initializing database...")
2889
+ ScreenplayDatabase.init_db()
2890
+ logger.info("Database initialization complete.")
2891
+
2892
+ # Create and launch interface
2893
+ interface = create_interface()
2894
+
2895
+ interface.launch(
2896
+ server_name="0.0.0.0",
2897
+ server_port=7860,
2898
+ share=False,
2899
+ debug=True
2900
+ )