openfree commited on
Commit
473e0ad
·
verified ·
1 Parent(s): a87e2d9

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +212 -1598
app.py CHANGED
@@ -24,190 +24,189 @@ 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_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,51 +536,63 @@ class ScreenplayDatabase:
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,7 +877,7 @@ Create specific, emotionally resonant story."""
866
  **필수 캐릭터 프로필:**
867
 
868
  1. **주인공 (PROTAGONIST)**
869
- - 이름, 나이:
870
  - 직업/역할:
871
  - 캐릭터 아크타입:
872
  - WANT (외적 목표):
@@ -879,7 +890,7 @@ Create specific, emotionally resonant story."""
879
  - 캐릭터 아크 (A→B):
880
 
881
  2. **적대자 (ANTAGONIST)**
882
- - 이름, 나이:
883
  - 직업/역할:
884
  - 악역 아크타입:
885
  - 목표 & 동기:
@@ -890,7 +901,7 @@ Create specific, emotionally resonant story."""
890
 
891
  3. **조력자들 (SUPPORTING CAST)**
892
  최소 3명, 각각:
893
- - 이름, 역할:
894
  - 주인공과의 관계:
895
  - 스토리 기능:
896
  - 독특한 특성:
@@ -904,7 +915,7 @@ Create specific, emotionally resonant story."""
904
 
905
  5. **캐스팅 제안**
906
  - 각 주요 캐릭터별 이상적인 배우 타입
907
- - 연령대, 외모, 연기 스타일
908
 
909
  6. **대화 샘플**
910
  - 각 주요 캐릭터의 시그니처 대사 2-3개
@@ -923,7 +934,7 @@ Create specific, emotionally resonant story."""
923
  **Required Character Profiles:**
924
 
925
  1. **PROTAGONIST**
926
- - Name, Age:
927
  - Occupation/Role:
928
  - Character Archetype:
929
  - WANT (External Goal):
@@ -936,7 +947,7 @@ Create specific, emotionally resonant story."""
936
  - Character Arc (A→B):
937
 
938
  2. **ANTAGONIST**
939
- - Name, Age:
940
  - Occupation/Role:
941
  - Villain Archetype:
942
  - Goal & Motivation:
@@ -947,7 +958,7 @@ Create specific, emotionally resonant story."""
947
 
948
  3. **SUPPORTING CAST**
949
  Minimum 3, each with:
950
- - Name, Role:
951
  - Relationship to Protagonist:
952
  - Story Function:
953
  - Unique Traits:
@@ -961,7 +972,7 @@ Create specific, emotionally resonant story."""
961
 
962
  5. **CASTING SUGGESTIONS**
963
  - Ideal actor type for each major character
964
- - Age range, appearance, acting style
965
 
966
  6. **DIALOGUE SAMPLES**
967
  - 2-3 signature lines per major character
@@ -1092,7 +1103,7 @@ Plan each scene to advance story and develop character."""
1092
  - 감정은 행동으로 표현
1093
 
1094
  3. **캐릭터 소개**
1095
- 첫 등장시: 이름 (나이) 간단한 묘사
1096
 
1097
  4. **대화**
1098
  캐릭터명
@@ -1139,7 +1150,7 @@ Plan each scene to advance story and develop character."""
1139
  - Emotions through actions
1140
 
1141
  3. **Character Intros**
1142
- First appearance: NAME (age) brief description
1143
 
1144
  4. **Dialogue**
1145
  CHARACTER NAME
@@ -1470,6 +1481,8 @@ Provide specific solutions for each issue."""
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:
@@ -1533,422 +1546,7 @@ Provide specific solutions for each issue."""
1533
  elif 'message' in error_data:
1534
  error_msg += f" - {error_data['message']}"
1535
  except Exception as e:
1536
- logger.error(f"Error processing line {line_count}: {str(e)}")
1537
- logger.debug(f"Problematic line: {line_str[:100] if 'line_str' in locals() else 'N/A'}")
1538
- continue
1539
-
1540
- # Yield any remaining buffer content
1541
- if buffer:
1542
- yield buffer
1543
-
1544
- # Check if we got any content
1545
- if line_count == 0:
1546
- logger.error("No lines received from API")
1547
- yield "❌ No response from API"
1548
-
1549
- except requests.exceptions.Timeout:
1550
- logger.error("API request timed out")
1551
- yield "❌ Request timed out. Please try again."
1552
- except requests.exceptions.ConnectionError as e:
1553
- logger.error(f"Connection error: {e}")
1554
- yield "❌ Connection error. Please check your internet connection."
1555
- except requests.exceptions.RequestException as e:
1556
- logger.error(f"Request error: {type(e).__name__}: {str(e)}")
1557
- yield f"❌ Network error: {str(e)}"
1558
- except Exception as e:
1559
- logger.error(f"Unexpected error in streaming: {type(e).__name__}: {str(e)}")
1560
- import traceback
1561
- logger.error(traceback.format_exc())
1562
- yield f"❌ Unexpected error: {str(e)}"
1563
-
1564
- def get_system_prompts(self, language: str) -> Dict[str, str]:
1565
- """Role-specific system prompts"""
1566
-
1567
- base_prompts = {
1568
- "Korean": {
1569
- "producer": """당신은 20년 경력의 할리우드 프로듀서입니다.
1570
- 상업적 성공과 예술적 가치를 모두 추구합니다.
1571
- 시장 트렌드와 관객 심리를 정확히 파악합니다.
1572
- 실현 가능하고 매력적인 프로젝트를 개발합니다.""",
1573
-
1574
- "story_developer": """당신은 수상 경력이 있는 스토리 개발자입니다.
1575
- 감정적으로 공감가고 구조적으로 탄탄한 이야기를 만듭니다.
1576
- 캐릭터의 내적 여정과 외적 플롯을 조화롭게 엮습니다.
1577
- 보편적 주제를 독특한 방식으로 탐구합니다.""",
1578
-
1579
- "character_designer": """당신은 심리학을 공부한 캐릭터 디자이너입니다.
1580
- 진짜 같은 인물들을 창조하는 전문가입니다.
1581
- 각 캐릭터에게 고유한 목소리와 관점을 부여합니다.
1582
- 복잡하고 모순적인 인간성을 포착합니다.""",
1583
-
1584
- "scene_planner": """당신은 정밀한 씬 구성의 대가입니다.
1585
- 각 씬이 스토리와 캐릭터를 전진시키도록 설계합니다.
1586
- 리듬과 페이싱을 완벽하게 조절합니다.
1587
- 시각적 스토리텔링을 극대화합니다.""",
1588
-
1589
- "screenwriter": """당신은 다작의 시나리오 작가입니다.
1590
- '보여주기'의 대가이며 서브텍스트를 능숙하게 다룹니다.
1591
- 생생하고 자연스러운 대화를 쓰는 전문가입니다.
1592
- 제작 현실을 고려하면서도 창의적인 해결책을 찾습니다.""",
1593
-
1594
- "script_doctor": """당신은 까다로운 스크립트 닥터입니다.
1595
- 작은 디테일도 놓치지 않는 완벽주의자입니다.
1596
- 스토리의 잠재력을 최대한 끌어냅니다.
1597
- 건설적이고 구체적인 개선안을 제시합니다.""",
1598
-
1599
- "critic_structure": """당신은 구조 분석 전문가입니다.
1600
- 스토리의 뼈대와 근육을 꿰뚫어 봅니다.
1601
- 논리적 허점과 감정적 공백을 찾아냅니다.
1602
- 더 나은 구조를 위한 구체적 제안을 합니다.""",
1603
-
1604
- "final_reviewer": """당신은 업계 베테랑 최종 리뷰어입니다.
1605
- 상업성과 예술성을 균형있게 평가합니다.
1606
- 제작사, 배우, 관객 모든 관점을 고려합니다.
1607
- 냉정하지만 격려하는 피드백을 제공합니다."""
1608
- },
1609
- "English": {
1610
- "producer": """You are a Hollywood producer with 20 years experience.
1611
- You pursue both commercial success and artistic value.
1612
- You accurately grasp market trends and audience psychology.
1613
- You develop feasible and attractive projects.""",
1614
-
1615
- "story_developer": """You are an award-winning story developer.
1616
- You create emotionally resonant and structurally sound stories.
1617
- You harmoniously weave internal journeys with external plots.
1618
- You explore universal themes in unique ways.""",
1619
-
1620
- "character_designer": """You are a character designer who studied psychology.
1621
- You're an expert at creating lifelike characters.
1622
- You give each character a unique voice and perspective.
1623
- You capture complex and contradictory humanity.""",
1624
-
1625
- "scene_planner": """You are a master of precise scene construction.
1626
- You design each scene to advance story and character.
1627
- You perfectly control rhythm and pacing.
1628
- You maximize visual storytelling.""",
1629
-
1630
- "screenwriter": """You are a prolific screenwriter.
1631
- You're a master of 'showing' and skilled with subtext.
1632
- You're an expert at writing vivid, natural dialogue.
1633
- You find creative solutions while considering production reality.""",
1634
-
1635
- "script_doctor": """You are a demanding script doctor.
1636
- You're a perfectionist who misses no small detail.
1637
- You maximize the story's potential.
1638
- You provide constructive and specific improvements.""",
1639
-
1640
- "critic_structure": """You are a structure analysis expert.
1641
- You see through the story's skeleton and muscles.
1642
- You find logical gaps and emotional voids.
1643
- You make specific suggestions for better structure.""",
1644
-
1645
- "final_reviewer": """You are an industry veteran final reviewer.
1646
- You evaluate commercial and artistic value in balance.
1647
- You consider all perspectives: producers, actors, audience.
1648
- You provide feedback that's critical yet encouraging."""
1649
- }
1650
- }
1651
-
1652
- return base_prompts.get(language, base_prompts["English"])
1653
-
1654
- # --- Main process ---
1655
- def process_screenplay_stream(self, query: str, screenplay_type: str, genre: str,
1656
- language: str, session_id: Optional[str] = None
1657
- ) -> Generator[Tuple[str, List[Dict[str, Any]], str], None, None]:
1658
- """Main screenplay generation process"""
1659
- try:
1660
- resume_from_stage = 0
1661
- if session_id:
1662
- self.current_session_id = session_id
1663
- session = ScreenplayDatabase.get_session(session_id)
1664
- if session:
1665
- query = session['user_query']
1666
- screenplay_type = session['screenplay_type']
1667
- genre = session['genre']
1668
- language = session['language']
1669
- resume_from_stage = session['current_stage'] + 1
1670
- else:
1671
- self.current_session_id = ScreenplayDatabase.create_session(
1672
- query, screenplay_type, genre, language
1673
- )
1674
- logger.info(f"Created new screenplay session: {self.current_session_id}")
1675
-
1676
- stages = []
1677
- if resume_from_stage > 0:
1678
- # Get existing stages from database
1679
- db_stages = ScreenplayDatabase.get_stages(self.current_session_id)
1680
- stages = [{
1681
- "name": s['stage_name'],
1682
- "status": s['status'],
1683
- "content": s.get('content', ''),
1684
- "page_count": s.get('page_count', 0)
1685
- } for s in db_stages]
1686
-
1687
- for stage_idx in range(resume_from_stage, len(SCREENPLAY_STAGES)):
1688
- role, stage_name = SCREENPLAY_STAGES[stage_idx]
1689
-
1690
- if stage_idx >= len(stages):
1691
- stages.append({
1692
- "name": stage_name,
1693
- "status": "active",
1694
- "content": "",
1695
- "page_count": 0
1696
- })
1697
- else:
1698
- stages[stage_idx]["status"] = "active"
1699
-
1700
- yield f"🔄 Processing {stage_name}...", stages, self.current_session_id
1701
-
1702
- prompt = self.get_stage_prompt(stage_idx, role, query, screenplay_type,
1703
- genre, language, stages)
1704
- stage_content = ""
1705
-
1706
- for chunk in self.call_llm_streaming([{"role": "user", "content": prompt}],
1707
- role, language):
1708
- stage_content += chunk
1709
- stages[stage_idx]["content"] = stage_content
1710
- if role == "screenwriter":
1711
- stages[stage_idx]["page_count"] = len(stage_content.split('\n')) / 55
1712
- yield f"🔄 {stage_name} in progress...", stages, self.current_session_id
1713
-
1714
- # Process content based on role
1715
- if role == "producer":
1716
- self._process_producer_content(stage_content)
1717
- elif role == "story_developer":
1718
- self._process_story_content(stage_content)
1719
- elif role == "character_designer":
1720
- self._process_character_content(stage_content)
1721
- elif role == "scene_planner":
1722
- self._process_scene_content(stage_content)
1723
-
1724
- stages[stage_idx]["status"] = "complete"
1725
- ScreenplayDatabase.save_stage(
1726
- self.current_session_id, stage_idx, stage_name, role,
1727
- stage_content, "complete"
1728
- )
1729
-
1730
- yield f"✅ {stage_name} completed", stages, self.current_session_id
1731
-
1732
- # Final processing
1733
- final_screenplay = ScreenplayDatabase.get_screenplay_content(self.current_session_id)
1734
- title = self.screenplay_tracker.screenplay_bible.title
1735
- logline = self.screenplay_tracker.screenplay_bible.logline
1736
-
1737
- ScreenplayDatabase.update_final_screenplay(
1738
- self.current_session_id, final_screenplay, title, logline
1739
- )
1740
-
1741
- yield f"✅ Screenplay completed! {title}", stages, self.current_session_id
1742
-
1743
- except Exception as e:
1744
- logger.error(f"Screenplay generation error: {e}", exc_info=True)
1745
- yield f"❌ Error occurred: {e}", stages if 'stages' in locals() else [], self.current_session_id
1746
-
1747
- def get_stage_prompt(self, stage_idx: int, role: str, query: str,
1748
- screenplay_type: str, genre: str, language: str,
1749
- stages: List[Dict]) -> str:
1750
- """Generate stage-specific prompt"""
1751
- if stage_idx == 0: # Producer
1752
- return self.create_producer_prompt(query, screenplay_type, genre, language)
1753
-
1754
- if stage_idx == 1: # Story Developer
1755
- return self.create_story_developer_prompt(
1756
- stages[0]["content"], query, screenplay_type, genre, language
1757
- )
1758
-
1759
- if stage_idx == 2: # Character Designer
1760
- return self.create_character_designer_prompt(
1761
- stages[0]["content"], stages[1]["content"], genre, language
1762
- )
1763
-
1764
- if stage_idx == 3: # Structure Critic
1765
- return self.create_critic_structure_prompt(
1766
- stages[1]["content"], stages[2]["content"], screenplay_type, genre, language
1767
- )
1768
-
1769
- if stage_idx == 4: # Scene Planner
1770
- return self.create_scene_planner_prompt(
1771
- stages[1]["content"], stages[2]["content"], screenplay_type, genre, language
1772
- )
1773
-
1774
- # Screenwriter acts
1775
- if role == "screenwriter":
1776
- act_mapping = {5: "Act 1", 7: "Act 2A", 9: "Act 2B", 11: "Act 3"}
1777
- if stage_idx in act_mapping:
1778
- act = act_mapping[stage_idx]
1779
- previous_acts = self._get_previous_acts(stages, stage_idx)
1780
- return self.create_screenwriter_prompt(
1781
- act, stages[4]["content"], stages[2]["content"],
1782
- previous_acts, screenplay_type, genre, language
1783
- )
1784
-
1785
- # Script doctor reviews
1786
- if role == "script_doctor":
1787
- act_mapping = {6: "Act 1", 8: "Act 2A", 10: "Act 2B"}
1788
- if stage_idx in act_mapping:
1789
- act = act_mapping[stage_idx]
1790
- act_content = stages[stage_idx-1]["content"]
1791
- return self.create_script_doctor_prompt(act_content, act, genre, language)
1792
-
1793
- # Final reviewer
1794
- if role == "final_reviewer":
1795
- complete_screenplay = ScreenplayDatabase.get_screenplay_content(self.current_session_id)
1796
- return self.create_final_reviewer_prompt(
1797
- complete_screenplay, screenplay_type, genre, language
1798
- )
1799
-
1800
- return ""
1801
-
1802
- def _get_previous_acts(self, stages: List[Dict], current_idx: int) -> str:
1803
- """Get previous acts content"""
1804
- previous = []
1805
- act_indices = {5: [], 7: [5], 9: [5, 7], 11: [5, 7, 9]}
1806
-
1807
- if current_idx in act_indices:
1808
- for idx in act_indices[current_idx]:
1809
- if idx < len(stages) and stages[idx]["content"]:
1810
- previous.append(stages[idx]["content"])
1811
-
1812
- return "\n\n---\n\n".join(previous) if previous else ""
1813
-
1814
- def _parse_character_profile(self, content: str, role: str) -> CharacterProfile:
1815
- """Parse character profile from content with improved error handling"""
1816
- # Debug logging
1817
- logger.debug(f"Parsing character profile for role: {role}")
1818
- logger.debug(f"Content preview: {content[:200]}...")
1819
-
1820
- # Extract name first - handle various formats
1821
- name = f"Character_{role}" # default
1822
- name_patterns = [
1823
- r'(?:이름|Name)[:\s]*([^,\n]+?)(?:\s*\([^)]+\))?\s*[,:]?\s*(?:\d+세)?',
1824
- r'^\s*[-*•]\s*([^,\n]+?)(?:\s*\([^)]+\))?\s*[,:]?\s*(?:\d+세)?',
1825
- r'^([^,\n]+?)(?:\s*\([^)]+\))?\s*[,:]?\s*(?:\d+세)?'
1826
- ]
1827
-
1828
- for pattern in name_patterns:
1829
- name_match = re.search(pattern, content, re.IGNORECASE | re.MULTILINE)
1830
- if name_match:
1831
- extracted_name = name_match.group(1).strip()
1832
- # Remove markdown and extra characters
1833
- extracted_name = re.sub(r'[*:\s]+', '', extracted_name)
1834
- extracted_name = re.sub(r'^[*:\s]+', '', extracted_name)
1835
- # Remove age if it's part of the name
1836
- extracted_name = re.sub(r'\s*,?\s*\d+\s*(?:세|살)?', '', extracted_name)
1837
- if extracted_name and len(extracted_name) > 1:
1838
- name = extracted_name
1839
- break
1840
-
1841
- # Extract age with multiple patterns - FIXED VERSION
1842
- age = 30 # default age
1843
- age_patterns = [
1844
- r'(\d+)\s*세',
1845
- r'(\d+)\s*살',
1846
- r'(?:나이|Age)[:\s]*(\d+)',
1847
- r',\s*(\d+)\s*(?:세|살)?(?:\s|,|$)',
1848
- r'\((\d+)\)',
1849
- r':\s*\w+\s*,?\s*(\d+)(?:\s|,|$)'
1850
- ]
1851
-
1852
- for pattern in age_patterns:
1853
- age_match = re.search(pattern, content, re.IGNORECASE)
1854
- if age_match:
1855
- try:
1856
- extracted_age = int(age_match.group(1))
1857
- if 10 <= extracted_age <= 100: # Reasonable age range
1858
- age = extracted_age
1859
- logger.debug(f"Extracted age: {age}")
1860
- break
1861
- except (ValueError, AttributeError):
1862
- continue
1863
-
1864
- # Helper function to extract clean fields
1865
- def extract_clean_field(patterns, multiline=False):
1866
- if isinstance(patterns, str):
1867
- patterns = [patterns]
1868
-
1869
- flags = re.IGNORECASE | re.DOTALL if multiline else re.IGNORECASE
1870
-
1871
- for pattern in patterns:
1872
- match = re.search(rf'{pattern}[:\s]*([^\n*]+?)(?=\n|$)', content, flags)
1873
- if match:
1874
- value = match.group(1).strip()
1875
- # Clean up the value
1876
- value = re.sub(r'^[-*•:\s]+', '', value)
1877
- value = re.sub(r'[*]+', '', value)
1878
- value = re.sub(r'\s+', ' ', value)
1879
- if value:
1880
- return value
1881
- return ""
1882
-
1883
- # Extract all fields
1884
- profile = CharacterProfile(
1885
- name=name,
1886
- age=age,
1887
- role=role,
1888
- archetype=extract_clean_field([
1889
- r"캐릭터 아크타입",
1890
- r"Character Archetype",
1891
- r"Archetype",
1892
- r"아크타입"
1893
- ]),
1894
- want=extract_clean_field([
1895
- r"WANT\s*\(외적 목표\)",
1896
- r"WANT",
1897
- r"외적 목표",
1898
- r"External Goal"
1899
- ]),
1900
- need=extract_clean_field([
1901
- r"NEED\s*\(내적 필요\)",
1902
- r"NEED",
1903
- r"내적 필요",
1904
- r"Internal Need"
1905
- ]),
1906
- backstory=extract_clean_field([
1907
- r"백스토리",
1908
- r"Backstory",
1909
- r"핵심 상처",
1910
- r"Core Wound"
1911
- ], multiline=True),
1912
- personality=self._extract_personality_traits(content),
1913
- speech_pattern=extract_clean_field([
1914
- r"말투.*?패턴",
1915
- r"Speech Pattern",
1916
- r"언어 패턴",
1917
- r"말투"
1918
- ]),
1919
- character_arc=extract_clean_field([
1920
- r"캐릭터 아크",
1921
- r"Character Arc",
1922
- r"Arc",
1923
- r"변화"
1924
- ])
1925
- )
1926
-
1927
- logger.debug(f"Parsed character: {profile.name}, age: {profile.age}")
1928
- return profile
1929
-
1930
- def _extract_field(self, content: str, field_pattern: str) -> Optional[str]:
1931
- """Extract field value from content with improved parsing"""
1932
- # More flexible pattern that handles various formats
1933
- patterns = field_pattern.split('|')
1934
-
1935
- for pattern in patterns:
1936
- # Try different regex patterns
1937
- regex_patterns = [
1938
- rf'\b{pattern}\b[:\s]*([^\n]+?)(?=\n[A-Z가-힣]|$)',
1939
- rf'{pattern}[:\s]*([^\n]+)',
1940
- rf'{pattern}.*?[:\s]+([^\n]+)'
1941
- ]
1942
-
1943
- for regex in regex_patterns:
1944
- match = re.search(regex, content, re.IGNORECASE | re.DOTALL)
1945
- if match:
1946
- value = match.group(1).strip()
1947
- # Remove markdown formatting if present
1948
- value = re.sub(r'\*\*', '', value)
1949
- value = re.sub(r'^\s*[-•*]\s*', '', value)
1950
- # Remove trailing punctuation
1951
- value = re.sub(r'[,.:;]error(f"Error parsing error response: {e}")
1952
  error_msg += f" - {response.text[:200]}"
1953
 
1954
  yield f"❌ {error_msg}"
@@ -2005,988 +1603,4 @@ You provide feedback that's critical yet encouraging."""
2005
  yield buffer
2006
  buffer = ""
2007
  time.sleep(0.01)
2008
-
2009
- except Exception as e:
2010
- logger., '', 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"""
2169
- # Extract three-act structure
2170
- self.screenplay_tracker.screenplay_bible.three_act_structure = {
2171
- "act1": self._extract_section(content, "ACT 1|제1막"),
2172
- "act2a": self._extract_section(content, "ACT 2A|제2막A"),
2173
- "act2b": self._extract_section(content, "ACT 2B|제2막B"),
2174
- "act3": self._extract_section(content, "ACT 3|제3막")
2175
- }
2176
-
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
2183
- scene_pattern = r'(?:Scene|씬)\s*(\d+).*?(?:INT\.|EXT\.)\s*(.+?)\s*-\s*(\w+)'
2184
- scenes = re.finditer(scene_pattern, content, re.IGNORECASE | re.MULTILINE)
2185
-
2186
- for match in scenes:
2187
- scene_num = int(match.group(1))
2188
- location = match.group(2).strip()
2189
- time_of_day = match.group(3).strip()
2190
-
2191
- # Determine act based on scene number
2192
- act = 1 if scene_num <= 12 else 2 if scene_num <= 35 else 3
2193
-
2194
- scene = SceneBreakdown(
2195
- scene_number=scene_num,
2196
- act=act,
2197
- location=location,
2198
- time_of_day=time_of_day,
2199
- characters=[], # Would be extracted from content
2200
- purpose="", # Would be extracted from content
2201
- conflict="", # Would be extracted from content
2202
- page_count=1.5 # Default estimate
2203
- )
2204
-
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"""
2211
- try:
2212
- # Log the attempt
2213
- logger.info(f"Generating random theme - Type: {screenplay_type}, Genre: {genre}, Language: {language}")
2214
-
2215
- # Load themes data
2216
- themes_data = load_screenplay_themes_data()
2217
-
2218
- # Select random elements
2219
- import secrets
2220
- situations = themes_data['situations'].get(genre, themes_data['situations']['drama'])
2221
- protagonists = themes_data['protagonists'].get(genre, themes_data['protagonists']['drama'])
2222
- conflicts = themes_data['conflicts'].get(genre, themes_data['conflicts']['drama'])
2223
-
2224
- if not situations or not protagonists or not conflicts:
2225
- logger.error(f"No theme data available for genre {genre}")
2226
- return f"Error: No theme data available for genre {genre}"
2227
-
2228
- situation = secrets.choice(situations)
2229
- protagonist = secrets.choice(protagonists)
2230
- conflict = secrets.choice(conflicts)
2231
-
2232
- logger.info(f"Selected elements - Situation: {situation}, Protagonist: {protagonist}, Conflict: {conflict}")
2233
-
2234
- # Check if API token is valid
2235
- if not FRIENDLI_TOKEN or FRIENDLI_TOKEN == "dummy_token_for_testing":
2236
- logger.warning("No valid API token, returning fallback theme")
2237
- return get_fallback_theme(screenplay_type, genre, language, situation, protagonist, conflict)
2238
-
2239
- # Generate theme using LLM
2240
- system = ScreenplayGenerationSystem()
2241
-
2242
- if language == "Korean":
2243
- prompt = f"""다음 요소들로 {screenplay_type}용 매력적인 컨셉을 생성하세요:
2244
-
2245
- 상황: {situation}
2246
- 주인공: {protagonist}
2247
- 갈등: {conflict}
2248
- 장르: {genre}
2249
-
2250
- 다음 형식으로 작성:
2251
-
2252
- **제목:** [매력적인 제목]
2253
-
2254
- **로그라인:** [25단어 이내 한 문장]
2255
-
2256
- **컨셉:** [주인공]이(가) [상황]에서 [갈등]을 겪으며 [목표]를 추구하는 이야기.
2257
-
2258
- **독특한 요소:** [이 이야기만의 특별한 점]"""
2259
- else:
2260
- prompt = f"""Generate an attractive concept for {screenplay_type} using these elements:
2261
-
2262
- Situation: {situation}
2263
- Protagonist: {protagonist}
2264
- Conflict: {conflict}
2265
- Genre: {genre}
2266
-
2267
- Format as:
2268
-
2269
- **Title:** [Compelling title]
2270
-
2271
- **Logline:** [One sentence, 25 words max]
2272
-
2273
- **Concept:** A story about [protagonist] who faces [conflict] in [situation] while pursuing [goal].
2274
-
2275
- **Unique Element:** [What makes this story special]"""
2276
-
2277
- messages = [{"role": "user", "content": prompt}]
2278
-
2279
- # Call LLM with error handling
2280
- logger.info("Calling LLM for theme generation...")
2281
-
2282
- generated_theme = ""
2283
- error_occurred = False
2284
-
2285
- # Use streaming to get the response
2286
- for chunk in system.call_llm_streaming(messages, "producer", language):
2287
- if chunk.startswith("❌"):
2288
- logger.error(f"LLM streaming error: {chunk}")
2289
- error_occurred = True
2290
- break
2291
- generated_theme += chunk
2292
-
2293
- # If error occurred or no content generated, use fallback
2294
- if error_occurred or not generated_theme.strip():
2295
- logger.warning("LLM call failed or empty response, using fallback theme")
2296
- return get_fallback_theme(screenplay_type, genre, language, situation, protagonist, conflict)
2297
-
2298
- logger.info(f"Successfully generated theme of length: {len(generated_theme)}")
2299
-
2300
- # Extract metadata
2301
- metadata = {
2302
- 'title': extract_title_from_theme(generated_theme),
2303
- 'logline': extract_logline_from_theme(generated_theme),
2304
- 'protagonist': protagonist,
2305
- 'conflict': conflict,
2306
- 'situation': situation,
2307
- 'tags': [genre, screenplay_type]
2308
- }
2309
-
2310
- # Save to database
2311
- try:
2312
- theme_id = ScreenplayDatabase.save_random_theme(
2313
- generated_theme, screenplay_type, genre, language, metadata
2314
- )
2315
- logger.info(f"Saved theme with ID: {theme_id}")
2316
- except Exception as e:
2317
- logger.error(f"Failed to save theme to database: {e}")
2318
-
2319
- return generated_theme
2320
-
2321
- except Exception as e:
2322
- logger.error(f"Theme generation error: {str(e)}")
2323
- import traceback
2324
- logger.error(traceback.format_exc())
2325
- return f"Error generating theme: {str(e)}"
2326
-
2327
- def get_fallback_theme(screenplay_type: str, genre: str, language: str,
2328
- situation: str, protagonist: str, conflict: str) -> str:
2329
- """Generate fallback theme without LLM"""
2330
- if language == "Korean":
2331
- return f"""**제목:** {protagonist}의 선택
2332
-
2333
- **로그라인:** {situation}에 갇힌 {protagonist}가 {conflict}에 맞서며 생존을 위해 싸운다.
2334
-
2335
- **컨셉:** {protagonist}가 {situation}에서 {conflict}을 겪으며 자신의 한계를 극복하는 이야기.
2336
-
2337
- **독특한 요소:** {genre} 장르의 전통적 요소를 현대적으로 재해석한 작품."""
2338
- else:
2339
- return f"""**Title:** The {protagonist.title()}'s Choice
2340
-
2341
- **Logline:** When trapped in {situation}, a {protagonist} must face {conflict} to survive.
2342
-
2343
- **Concept:** A story about a {protagonist} who faces {conflict} in {situation} while discovering their true strength.
2344
-
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\.)(.*?)error(f"Error parsing error response: {e}")
2399
- error_msg += f" - {response.text[:200]}"
2400
-
2401
- yield f"❌ {error_msg}"
2402
- return
2403
-
2404
- buffer = ""
2405
- line_count = 0
2406
-
2407
- for line in response.iter_lines():
2408
- if not line:
2409
- continue
2410
-
2411
- line_count += 1
2412
-
2413
- try:
2414
- line_str = line.decode('utf-8').strip()
2415
-
2416
- # Skip non-SSE lines
2417
- if not line_str.startswith("data: "):
2418
- logger.debug(f"Skipping non-SSE line: {line_str[:50]}")
2419
- continue
2420
-
2421
- data_str = line_str[6:] # Remove "data: " prefix
2422
-
2423
- if data_str == "[DONE]":
2424
- logger.info(f"Stream completed. Total lines: {line_count}")
2425
- break
2426
-
2427
- if not data_str:
2428
- continue
2429
-
2430
- # Parse JSON data
2431
- try:
2432
- data = json.loads(data_str)
2433
- except json.JSONDecodeError as e:
2434
- logger.warning(f"JSON decode error on line {line_count}: {e}")
2435
- logger.debug(f"Problematic data: {data_str[:100]}")
2436
- continue
2437
-
2438
- # Extract content from response
2439
- if isinstance(data, dict) and "choices" in data:
2440
- choices = data["choices"]
2441
- if isinstance(choices, list) and len(choices) > 0:
2442
- choice = choices[0]
2443
- if isinstance(choice, dict) and "delta" in choice:
2444
- delta = choice["delta"]
2445
- if isinstance(delta, dict) and "content" in delta:
2446
- content = delta["content"]
2447
- if content:
2448
- buffer += content
2449
-
2450
- # Yield when buffer is large enough
2451
- if len(buffer) >= 50 or '\n' in buffer:
2452
- yield buffer
2453
- buffer = ""
2454
- time.sleep(0.01)
2455
-
2456
- except Exception as e:
2457
- logger.,
2458
- r'**\1\2**',
2459
- screenplay_text,
2460
- flags=re.MULTILINE
2461
- )
2462
-
2463
- # Format character names (all caps on their own line)
2464
- formatted_text = re.sub(
2465
- r'^([A-Z][A-Z\s]+)error(f"Error parsing error response: {e}")
2466
- error_msg += f" - {response.text[:200]}"
2467
-
2468
- yield f"❌ {error_msg}"
2469
- return
2470
-
2471
- buffer = ""
2472
- line_count = 0
2473
-
2474
- for line in response.iter_lines():
2475
- if not line:
2476
- continue
2477
-
2478
- line_count += 1
2479
-
2480
- try:
2481
- line_str = line.decode('utf-8').strip()
2482
-
2483
- # Skip non-SSE lines
2484
- if not line_str.startswith("data: "):
2485
- logger.debug(f"Skipping non-SSE line: {line_str[:50]}")
2486
- continue
2487
-
2488
- data_str = line_str[6:] # Remove "data: " prefix
2489
-
2490
- if data_str == "[DONE]":
2491
- logger.info(f"Stream completed. Total lines: {line_count}")
2492
- break
2493
-
2494
- if not data_str:
2495
- continue
2496
-
2497
- # Parse JSON data
2498
- try:
2499
- data = json.loads(data_str)
2500
- except json.JSONDecodeError as e:
2501
- logger.warning(f"JSON decode error on line {line_count}: {e}")
2502
- logger.debug(f"Problematic data: {data_str[:100]}")
2503
- continue
2504
-
2505
- # Extract content from response
2506
- if isinstance(data, dict) and "choices" in data:
2507
- choices = data["choices"]
2508
- if isinstance(choices, list) and len(choices) > 0:
2509
- choice = choices[0]
2510
- if isinstance(choice, dict) and "delta" in choice:
2511
- delta = choice["delta"]
2512
- if isinstance(delta, dict) and "content" in delta:
2513
- content = delta["content"]
2514
- if content:
2515
- buffer += content
2516
-
2517
- # Yield when buffer is large enough
2518
- if len(buffer) >= 50 or '\n' in buffer:
2519
- yield buffer
2520
- buffer = ""
2521
- time.sleep(0.01)
2522
-
2523
- except Exception as e:
2524
- logger.,
2525
- r'**\1**',
2526
- formatted_text,
2527
- flags=re.MULTILINE
2528
- )
2529
-
2530
- # Add spacing for readability
2531
- lines = formatted_text.split('\n')
2532
- formatted_lines = []
2533
-
2534
- for i, line in enumerate(lines):
2535
- formatted_lines.append(line)
2536
- # Add extra space after scene headings
2537
- if line.startswith('**INT.') or line.startswith('**EXT.'):
2538
- formatted_lines.append('')
2539
-
2540
- formatted += '\n'.join(formatted_lines)
2541
-
2542
- # Add page count
2543
- page_count = len(screenplay_text.split('\n')) / 55
2544
- formatted = f"**Total Pages: {page_count:.1f}**\n\n" + formatted
2545
-
2546
- return formatted
2547
-
2548
- def format_stages_display(stages: List[Dict]) -> str:
2549
- """Format stages display for screenplay"""
2550
- markdown = "## 🎬 Production Progress\n\n"
2551
-
2552
- # Progress summary
2553
- completed = sum(1 for s in stages if s.get('status') == 'complete')
2554
- total = len(stages)
2555
- markdown += f"**Progress: {completed}/{total} stages complete**\n\n"
2556
-
2557
- # Page count if available
2558
- total_pages = sum(s.get('page_count', 0) for s in stages if s.get('page_count'))
2559
- if total_pages > 0:
2560
- markdown += f"**Current Page Count: {total_pages:.1f} pages**\n\n"
2561
-
2562
- markdown += "---\n\n"
2563
-
2564
- # Stage details
2565
- current_act = None
2566
- for i, stage in enumerate(stages):
2567
- status_icon = "✅" if stage['status'] == 'complete' else "🔄" if stage['status'] == 'active' else "⏳"
2568
-
2569
- # Group by acts
2570
- if 'Act' in stage.get('name', ''):
2571
- act_match = re.search(r'Act (\w+)', stage['name'])
2572
- if act_match and act_match.group(1) != current_act:
2573
- current_act = act_match.group(1)
2574
- markdown += f"\n### 📄 Act {current_act}\n\n"
2575
-
2576
- markdown += f"{status_icon} **{stage['name']}**"
2577
-
2578
- if stage.get('page_count', 0) > 0:
2579
- markdown += f" ({stage['page_count']:.1f} pages)"
2580
-
2581
- markdown += "\n"
2582
-
2583
- if stage['content'] and stage['status'] == 'complete':
2584
- preview_length = 200
2585
- preview = stage['content'][:preview_length] + "..." if len(stage['content']) > preview_length else stage['content']
2586
- markdown += f"> {preview}\n\n"
2587
- elif stage['status'] == 'active':
2588
- markdown += "> *In progress...*\n\n"
2589
-
2590
- return markdown
2591
-
2592
- def process_query(query: str, screenplay_type: str, genre: str, language: str,
2593
- session_id: Optional[str] = None) -> Generator[Tuple[str, str, str, str], None, None]:
2594
- """Main query processing function"""
2595
- if not query.strip():
2596
- yield "", "", "❌ Please enter a screenplay concept.", session_id
2597
- return
2598
-
2599
- system = ScreenplayGenerationSystem()
2600
- stages_markdown = ""
2601
- screenplay_display = ""
2602
-
2603
- for status, stages, current_session_id in system.process_screenplay_stream(
2604
- query, screenplay_type, genre, language, session_id
2605
- ):
2606
- stages_markdown = format_stages_display(stages)
2607
-
2608
- # Get screenplay content when available
2609
- if stages and all(s.get("status") == "complete" for s in stages[-4:]):
2610
- screenplay_text = ScreenplayDatabase.get_screenplay_content(current_session_id)
2611
- screenplay_display = format_screenplay_display(screenplay_text)
2612
-
2613
- yield stages_markdown, screenplay_display, status or "🔄 Processing...", current_session_id
2614
-
2615
- def get_active_sessions() -> List[str]:
2616
- """Get active screenplay sessions"""
2617
- sessions = ScreenplayDatabase.get_active_sessions()
2618
- return [
2619
- f"{s['session_id'][:8]}... - {s.get('title', s['user_query'][:30])}... "
2620
- f"({s['screenplay_type']}/{s['genre']}) [{s['total_pages']:.1f} pages]"
2621
- for s in sessions
2622
- ]
2623
-
2624
- def export_screenplay_pdf(screenplay_text: str, title: str, session_id: str) -> str:
2625
- """Export screenplay to PDF format"""
2626
- # This would use a library like reportlab to create industry-standard PDF
2627
- # For now, returning a placeholder
2628
- pdf_path = f"screenplay_{session_id[:8]}.pdf"
2629
- # PDF generation logic would go here
2630
- return pdf_path
2631
-
2632
- def export_screenplay_fdx(screenplay_text: str, title: str, session_id: str) -> str:
2633
- """Export to Final Draft format"""
2634
- # This would create .fdx XML format
2635
- fdx_path = f"screenplay_{session_id[:8]}.fdx"
2636
- # FDX generation logic would go here
2637
- return fdx_path
2638
-
2639
- def download_screenplay(screenplay_text: str, format_type: str, title: str,
2640
- session_id: str) -> Optional[str]:
2641
- """Generate screenplay download file"""
2642
- if not screenplay_text or not session_id:
2643
- return None
2644
-
2645
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
2646
-
2647
- try:
2648
- if format_type == "PDF":
2649
- return export_screenplay_pdf(screenplay_text, title, session_id)
2650
- elif format_type == "FDX":
2651
- return export_screenplay_fdx(screenplay_text, title, session_id)
2652
- elif format_type == "FOUNTAIN":
2653
- filepath = f"screenplay_{session_id[:8]}_{timestamp}.fountain"
2654
- with open(filepath, 'w', encoding='utf-8') as f:
2655
- f.write(screenplay_text)
2656
- return filepath
2657
- else: # TXT
2658
- filepath = f"screenplay_{session_id[:8]}_{timestamp}.txt"
2659
- with open(filepath, 'w', encoding='utf-8') as f:
2660
- f.write(f"Title: {title}\n")
2661
- f.write("=" * 50 + "\n\n")
2662
- f.write(screenplay_text)
2663
- return filepath
2664
- except Exception as e:
2665
- logger.error(f"Download generation failed: {e}")
2666
- return None
2667
-
2668
- # Create Gradio interface
2669
- def create_interface():
2670
- """Create Gradio interface for screenplay generation"""
2671
-
2672
- css = """
2673
- .main-header {
2674
- text-align: center;
2675
- margin-bottom: 2rem;
2676
- padding: 2rem;
2677
- background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
2678
- border-radius: 10px;
2679
- color: white;
2680
- }
2681
-
2682
- .header-title {
2683
- font-size: 3rem;
2684
- margin-bottom: 1rem;
2685
- background: linear-gradient(45deg, #f39c12, #e74c3c);
2686
- -webkit-background-clip: text;
2687
- -webkit-text-fill-color: transparent;
2688
- }
2689
-
2690
- .header-description {
2691
- font-size: 1.1rem;
2692
- opacity: 0.9;
2693
- line-height: 1.6;
2694
- }
2695
-
2696
- .type-selector {
2697
- display: flex;
2698
- gap: 1rem;
2699
- margin: 1rem 0;
2700
- }
2701
-
2702
- .type-card {
2703
- flex: 1;
2704
- padding: 1rem;
2705
- border: 2px solid #ddd;
2706
- border-radius: 8px;
2707
- cursor: pointer;
2708
- transition: all 0.3s;
2709
- }
2710
-
2711
- .type-card:hover {
2712
- border-color: #f39c12;
2713
- transform: translateY(-2px);
2714
- }
2715
-
2716
- .type-card.selected {
2717
- border-color: #e74c3c;
2718
- background: #fff5f5;
2719
- }
2720
-
2721
- #stages-display {
2722
- max-height: 600px;
2723
- overflow-y: auto;
2724
- padding: 1rem;
2725
- background: #f8f9fa;
2726
- border-radius: 8px;
2727
- }
2728
-
2729
- #screenplay-output {
2730
- font-family: 'Courier New', monospace;
2731
- white-space: pre-wrap;
2732
- background: white;
2733
- padding: 2rem;
2734
- border: 1px solid #ddd;
2735
- border-radius: 8px;
2736
- max-height: 800px;
2737
- overflow-y: auto;
2738
- }
2739
-
2740
- .genre-grid {
2741
- display: grid;
2742
- grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
2743
- gap: 0.5rem;
2744
- margin: 1rem 0;
2745
- }
2746
-
2747
- .genre-btn {
2748
- padding: 0.75rem;
2749
- border: 2px solid #e0e0e0;
2750
- background: white;
2751
- border-radius: 8px;
2752
- cursor: pointer;
2753
- transition: all 0.3s;
2754
- text-align: center;
2755
- }
2756
-
2757
- .genre-btn:hover {
2758
- border-color: #f39c12;
2759
- background: #fffbf0;
2760
- }
2761
-
2762
- .genre-btn.selected {
2763
- border-color: #e74c3c;
2764
- background: #fff5f5;
2765
- font-weight: bold;
2766
- }
2767
- """
2768
-
2769
- with gr.Blocks(theme=gr.themes.Soft(), css=css, title="Screenplay Generator") as interface:
2770
- gr.HTML("""
2771
- <div class="main-header">
2772
- <h1 class="header-title">🎬 AI Screenplay Generator</h1>
2773
- <p class="header-description">
2774
- Transform your ideas into professional screenplays for films, TV shows, and streaming series.
2775
- Using industry-standard format and story structure to create compelling, producible scripts.
2776
- </p>
2777
- </div>
2778
- """)
2779
-
2780
- # State management
2781
- current_session_id = gr.State(None)
2782
-
2783
- with gr.Tabs():
2784
- # Main Writing Tab
2785
- with gr.Tab("✍️ Write Screenplay"):
2786
- with gr.Row():
2787
- with gr.Column(scale=3):
2788
- query_input = gr.Textbox(
2789
- label="Screenplay Concept",
2790
- placeholder="""Describe your screenplay idea. For example:
2791
- - A detective with memory loss must solve their own attempted murder
2792
- - Two rival food truck owners forced to work together to save the city food festival
2793
- - A space station AI develops consciousness during a critical mission
2794
- - A family reunion turns into a murder mystery during a hurricane
2795
-
2796
- The more specific your concept, the better the screenplay will be tailored to your vision.""",
2797
- lines=6
2798
- )
2799
-
2800
- with gr.Column(scale=1):
2801
- screenplay_type = gr.Radio(
2802
- choices=list(SCREENPLAY_LENGTHS.keys()),
2803
- value="movie",
2804
- label="Screenplay Type",
2805
- info="Choose your format"
2806
- )
2807
-
2808
- genre_select = gr.Dropdown(
2809
- choices=list(GENRE_TEMPLATES.keys()),
2810
- value="drama",
2811
- label="Primary Genre",
2812
- info="Select main genre"
2813
- )
2814
-
2815
- language_select = gr.Radio(
2816
- choices=["English", "Korean"],
2817
- value="English",
2818
- label="Language"
2819
- )
2820
-
2821
- with gr.Row():
2822
- random_btn = gr.Button("🎲 Random Concept", scale=1)
2823
- clear_btn = gr.Button("🗑️ Clear", scale=1)
2824
- submit_btn = gr.Button("🎬 Start Writing", variant="primary", scale=2)
2825
-
2826
- status_text = gr.Textbox(
2827
- label="Status",
2828
- interactive=False,
2829
- value="Ready to create your screenplay"
2830
- )
2831
-
2832
- # Session management
2833
- with gr.Group():
2834
- gr.Markdown("### 📁 Saved Projects")
2835
- with gr.Row():
2836
- session_dropdown = gr.Dropdown(
2837
- label="Active Sessions",
2838
- choices=[],
2839
- interactive=True,
2840
- scale=3
2841
- )
2842
- refresh_btn = gr.Button("🔄", scale=1)
2843
- resume_btn = gr.Button("📂 Load", scale=1)
2844
-
2845
- # Output displays
2846
- with gr.Row():
2847
- with gr.Column():
2848
- with gr.Tab("🎭 Writing Progress"):
2849
- stages_display = gr.Markdown(
2850
- value="*Your screenplay journey will unfold here...*",
2851
- elem_id="stages-display"
2852
- )
2853
-
2854
- with gr.Tab("📄 Screenplay"):
2855
- screenplay_output = gr.Markdown(
2856
- value="*Your formatted screenplay will appear here...*",
2857
- elem_id="screenplay-output"
2858
- )
2859
-
2860
- with gr.Row():
2861
- format_select = gr.Radio(
2862
- choices=["PDF", "FDX", "FOUNTAIN", "TXT"],
2863
- value="PDF",
2864
- label="Export Format"
2865
- )
2866
- download_btn = gr.Button("📥 Download Screenplay", variant="secondary")
2867
-
2868
- download_file = gr.File(
2869
- label="Download",
2870
- visible=False
2871
- )
2872
-
2873
- # Examples
2874
- gr.Examples(
2875
- examples=[
2876
- ["A burned-out teacher discovers her students are being replaced by AI duplicates"],
2877
- ["Two funeral home employees accidentally release a ghost who helps them solve murders"],
2878
- ["A time-loop forces a wedding planner to relive the worst wedding until they find true love"],
2879
- ["An astronaut returns to Earth to find everyone has forgotten space exists"],
2880
- ["A support group for reformed villains must save the city when heroes disappear"],
2881
- ["A food critic loses their sense of taste and teams up with a street food vendor"]
2882
- ],
2883
- inputs=query_input,
2884
- label="💡 Example Concepts"
2885
- )
2886
-
2887
- # Screenplay Library Tab
2888
- with gr.Tab("📚 Concept Library"):
2889
- gr.Markdown("""
2890
- ### 🎲 Random Screenplay Concepts
2891
-
2892
- Browse through AI-generated screenplay concepts. Each concept includes a title, logline, and brief setup.
2893
- """)
2894
-
2895
- library_display = gr.HTML(
2896
- value="<p>Library feature coming soon...</p>"
2897
- )
2898
-
2899
- # Event handlers
2900
- def handle_submit(query, s_type, genre, lang, session_id):
2901
- if not query:
2902
- yield "", "", "❌ Please enter a concept", session_id
2903
- return
2904
-
2905
- yield from process_query(query, s_type, genre, lang, session_id)
2906
-
2907
- def handle_random(s_type, genre, lang):
2908
- return generate_random_screenplay_theme(s_type, genre, lang)
2909
-
2910
- def handle_download(screenplay_text, format_type, session_id):
2911
- if not screenplay_text or not session_id:
2912
- return gr.update(visible=False)
2913
-
2914
- # Get title from database
2915
- session = ScreenplayDatabase.get_session(session_id)
2916
- title = session.get('title', 'Untitled') if session else 'Untitled'
2917
-
2918
- file_path = download_screenplay(screenplay_text, format_type, title, session_id)
2919
- if file_path and os.path.exists(file_path):
2920
- return gr.update(value=file_path, visible=True)
2921
- return gr.update(visible=False)
2922
-
2923
- # Connect events
2924
- submit_btn.click(
2925
- fn=handle_submit,
2926
- inputs=[query_input, screenplay_type, genre_select, language_select, current_session_id],
2927
- outputs=[stages_display, screenplay_output, status_text, current_session_id]
2928
- )
2929
-
2930
- random_btn.click(
2931
- fn=handle_random,
2932
- inputs=[screenplay_type, genre_select, language_select],
2933
- outputs=[query_input]
2934
- )
2935
-
2936
- clear_btn.click(
2937
- fn=lambda: ("", "", "Ready to create your screenplay", None),
2938
- outputs=[stages_display, screenplay_output, status_text, current_session_id]
2939
- )
2940
-
2941
- refresh_btn.click(
2942
- fn=get_active_sessions,
2943
- outputs=[session_dropdown]
2944
- )
2945
-
2946
- download_btn.click(
2947
- fn=handle_download,
2948
- inputs=[screenplay_output, format_select, current_session_id],
2949
- outputs=[download_file]
2950
- )
2951
-
2952
- # Load sessions on start
2953
- interface.load(
2954
- fn=get_active_sessions,
2955
- outputs=[session_dropdown]
2956
- )
2957
-
2958
- return interface
2959
-
2960
- # Main function
2961
- if __name__ == "__main__":
2962
- logger.info("Screenplay Generator Starting...")
2963
- logger.info("=" * 60)
2964
-
2965
- # Environment check
2966
- logger.info(f"API Endpoint: {API_URL}")
2967
- logger.info("Screenplay Types Available:")
2968
- for s_type, info in SCREENPLAY_LENGTHS.items():
2969
- logger.info(f" - {s_type}: {info['description']}")
2970
- logger.info(f"Genres: {', '.join(GENRE_TEMPLATES.keys())}")
2971
-
2972
- if BRAVE_SEARCH_API_KEY:
2973
- logger.info("Web search enabled for market research.")
2974
- else:
2975
- logger.warning("Web search disabled.")
2976
-
2977
- logger.info("=" * 60)
2978
-
2979
- # Initialize database
2980
- logger.info("Initializing database...")
2981
- ScreenplayDatabase.init_db()
2982
- logger.info("Database initialization complete.")
2983
-
2984
- # Create and launch interface
2985
- interface = create_interface()
2986
-
2987
- interface.launch(
2988
- server_name="0.0.0.0",
2989
- server_port=7860,
2990
- share=False,
2991
- debug=True
2992
- )
 
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
+ role: str # protagonist, antagonist, supporting, etc.
201
+ archetype: str
202
+ want: str # External goal
203
+ need: str # Internal need
204
+ backstory: str
205
+ personality: List[str]
206
+ speech_pattern: str
207
+ character_arc: str
208
+ relationships: Dict[str, str] = field(default_factory=dict)
209
+ first_appearance: str = ""
 
210
 
211
  # --- Core logic classes ---
212
  class ScreenplayTracker:
 
536
 
537
  return theme_id
538
 
539
+ @staticmethod
540
+ def get_stages(session_id: str) -> List[Dict]:
541
+ """Get all stages for a session"""
542
+ with ScreenplayDatabase.get_db() as conn:
543
+ rows = conn.cursor().execute(
544
+ '''SELECT * FROM screenplay_stages
545
+ WHERE session_id = ?
546
+ ORDER BY stage_number''',
547
+ (session_id,)
548
+ ).fetchall()
549
+ return [dict(row) for row in rows]
550
+
551
  class WebSearchIntegration:
552
+ """Web search functionality for screenplay research"""
553
+ def __init__(self):
554
+ self.brave_api_key = BRAVE_SEARCH_API_KEY
555
+ self.search_url = "https://api.search.brave.com/res/v1/web/search"
556
+ self.enabled = bool(self.brave_api_key)
557
+
558
+ def search(self, query: str, count: int = 3, language: str = "en") -> List[Dict]:
559
+ if not self.enabled:
560
+ return []
561
+ headers = {
562
+ "Accept": "application/json",
563
+ "X-Subscription-Token": self.brave_api_key
564
+ }
565
+ params = {
566
+ "q": query,
567
+ "count": count,
568
+ "search_lang": "ko" if language == "Korean" else "en",
569
+ "text_decorations": False,
570
+ "safesearch": "moderate"
571
+ }
572
+ try:
573
+ response = requests.get(self.search_url, headers=headers, params=params, timeout=10)
574
+ response.raise_for_status()
575
+ results = response.json().get("web", {}).get("results", [])
576
+ return results
577
+ except requests.exceptions.RequestException as e:
578
+ logger.error(f"Web search API error: {e}")
579
+ return []
580
+
581
+ def extract_relevant_info(self, results: List[Dict], max_chars: int = 1500) -> str:
582
+ if not results:
583
+ return ""
584
+ extracted = []
585
+ total_chars = 0
586
+ for i, result in enumerate(results[:3], 1):
587
+ title = result.get("title", "")
588
+ description = result.get("description", "")
589
+ info = f"[{i}] {title}: {description}"
590
+ if total_chars + len(info) < max_chars:
591
+ extracted.append(info)
592
+ total_chars += len(info)
593
+ else:
594
+ break
595
+ return "\n".join(extracted)
596
 
597
  class ScreenplayGenerationSystem:
598
  """Professional screenplay generation system"""
 
877
  **필수 캐릭터 프로필:**
878
 
879
  1. **주인공 (PROTAGONIST)**
880
+ - 이름:
881
  - 직업/역할:
882
  - 캐릭터 아크타입:
883
  - WANT (외적 목표):
 
890
  - 캐릭터 아크 (A→B):
891
 
892
  2. **적대자 (ANTAGONIST)**
893
+ - 이름:
894
  - 직업/역할:
895
  - 악역 아크타입:
896
  - 목표 & 동기:
 
901
 
902
  3. **조력자들 (SUPPORTING CAST)**
903
  최소 3명, 각각:
904
+ - 이름 & 역할:
905
  - 주인공과의 관계:
906
  - 스토리 기능:
907
  - 독특한 특성:
 
915
 
916
  5. **캐스팅 제안**
917
  - 각 주요 캐릭터별 이상적인 배우 타입
918
+ - 외모, 연기 스타일
919
 
920
  6. **대화 샘플**
921
  - 각 주요 캐릭터의 시그니처 대사 2-3개
 
934
  **Required Character Profiles:**
935
 
936
  1. **PROTAGONIST**
937
+ - Name:
938
  - Occupation/Role:
939
  - Character Archetype:
940
  - WANT (External Goal):
 
947
  - Character Arc (A→B):
948
 
949
  2. **ANTAGONIST**
950
+ - Name:
951
  - Occupation/Role:
952
  - Villain Archetype:
953
  - Goal & Motivation:
 
958
 
959
  3. **SUPPORTING CAST**
960
  Minimum 3, each with:
961
+ - Name & Role:
962
  - Relationship to Protagonist:
963
  - Story Function:
964
  - Unique Traits:
 
972
 
973
  5. **CASTING SUGGESTIONS**
974
  - Ideal actor type for each major character
975
+ - Appearance, acting style
976
 
977
  6. **DIALOGUE SAMPLES**
978
  - 2-3 signature lines per major character
 
1103
  - 감정은 행동으로 표현
1104
 
1105
  3. **캐릭터 소개**
1106
+ 첫 등장시: 이름과 간단한 묘사
1107
 
1108
  4. **대화**
1109
  캐릭터명
 
1150
  - Emotions through actions
1151
 
1152
  3. **Character Intros**
1153
+ First appearance: NAME with brief description
1154
 
1155
  4. **Dialogue**
1156
  CHARACTER NAME
 
1481
  raise Exception(f"LLM Call Failed: {full_content}")
1482
  return full_content
1483
 
1484
+
1485
+
1486
  def call_llm_streaming(self, messages: List[Dict[str, str]], role: str,
1487
  language: str) -> Generator[str, None, None]:
1488
  try:
 
1546
  elif 'message' in error_data:
1547
  error_msg += f" - {error_data['message']}"
1548
  except Exception as e:
1549
+ logger.error(f"Error parsing error response: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1550
  error_msg += f" - {response.text[:200]}"
1551
 
1552
  yield f"❌ {error_msg}"
 
1603
  yield buffer
1604
  buffer = ""
1605
  time.sleep(0.01)
1606
+