Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -211,38 +211,45 @@ class CharacterProfile:
|
|
211 |
|
212 |
# --- Core logic classes ---
|
213 |
class ScreenplayTracker:
|
214 |
-
|
215 |
-
|
216 |
-
|
217 |
-
|
218 |
-
|
219 |
-
|
220 |
-
|
221 |
-
|
222 |
-
|
223 |
-
|
224 |
-
|
225 |
-
|
226 |
-
|
227 |
-
|
228 |
-
|
229 |
-
|
230 |
-
|
231 |
-
|
232 |
-
|
233 |
-
|
234 |
-
|
235 |
-
|
236 |
-
|
237 |
-
|
238 |
-
|
239 |
-
|
240 |
-
|
241 |
-
|
242 |
-
|
243 |
-
|
244 |
-
|
245 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
246 |
|
247 |
class ScreenplayDatabase:
|
248 |
"""Database management for screenplay sessions"""
|
@@ -1622,183 +1629,189 @@ You consider all perspectives: producers, actors, audience.
|
|
1622 |
You provide feedback that's critical yet encouraging."""
|
1623 |
}
|
1624 |
}
|
1625 |
-
|
1626 |
-
return base_prompts.get(language, base_prompts["English"])
|
1627 |
|
1628 |
-
|
1629 |
-
|
1630 |
-
|
1631 |
-
|
1632 |
-
|
1633 |
-
|
1634 |
-
|
1635 |
-
|
1636 |
-
|
1637 |
-
|
1638 |
-
|
1639 |
-
|
1640 |
-
|
1641 |
-
|
1642 |
-
|
1643 |
-
|
1644 |
-
|
1645 |
-
|
1646 |
-
|
1647 |
-
|
1648 |
-
|
1649 |
-
|
1650 |
-
|
1651 |
-
if resume_from_stage > 0:
|
1652 |
-
# Get existing stages from database
|
1653 |
-
db_stages = ScreenplayDatabase.get_stages(self.current_session_id)
|
1654 |
-
stages = [{
|
1655 |
-
"name": s['stage_name'],
|
1656 |
-
"status": s['status'],
|
1657 |
-
"content": s.get('content', ''),
|
1658 |
-
"page_count": s.get('page_count', 0)
|
1659 |
-
} for s in db_stages]
|
1660 |
-
|
1661 |
-
for stage_idx in range(resume_from_stage, len(SCREENPLAY_STAGES)):
|
1662 |
-
role, stage_name = SCREENPLAY_STAGES[stage_idx]
|
1663 |
-
|
1664 |
-
if stage_idx >= len(stages):
|
1665 |
-
stages.append({
|
1666 |
-
"name": stage_name,
|
1667 |
-
"status": "active",
|
1668 |
-
"content": "",
|
1669 |
-
"page_count": 0
|
1670 |
-
})
|
1671 |
-
else:
|
1672 |
-
stages[stage_idx]["status"] = "active"
|
1673 |
-
|
1674 |
-
yield f"🔄 Processing {stage_name}...", stages, self.current_session_id
|
1675 |
-
|
1676 |
-
prompt = self.get_stage_prompt(stage_idx, role, query, screenplay_type,
|
1677 |
-
genre, language, stages)
|
1678 |
-
stage_content = ""
|
1679 |
-
|
1680 |
-
for chunk in self.call_llm_streaming([{"role": "user", "content": prompt}],
|
1681 |
-
role, language):
|
1682 |
-
stage_content += chunk
|
1683 |
-
stages[stage_idx]["content"] = stage_content
|
1684 |
-
if role == "screenwriter":
|
1685 |
-
stages[stage_idx]["page_count"] = len(stage_content.split('\n')) / 55
|
1686 |
-
yield f"🔄 {stage_name} in progress...", stages, self.current_session_id
|
1687 |
-
|
1688 |
-
# Process content based on role
|
1689 |
-
if role == "producer":
|
1690 |
-
self._process_producer_content(stage_content)
|
1691 |
-
elif role == "story_developer":
|
1692 |
-
self._process_story_content(stage_content)
|
1693 |
-
elif role == "character_designer":
|
1694 |
-
self._process_character_content(stage_content)
|
1695 |
-
elif role == "scene_planner":
|
1696 |
-
self._process_scene_content(stage_content)
|
1697 |
-
|
1698 |
-
stages[stage_idx]["status"] = "complete"
|
1699 |
-
ScreenplayDatabase.save_stage(
|
1700 |
-
self.current_session_id, stage_idx, stage_name, role,
|
1701 |
-
stage_content, "complete"
|
1702 |
-
)
|
1703 |
-
|
1704 |
-
yield f"✅ {stage_name} completed", stages, self.current_session_id
|
1705 |
|
1706 |
-
|
1707 |
-
|
1708 |
-
|
1709 |
-
|
1710 |
-
|
1711 |
-
|
1712 |
-
|
1713 |
-
|
1714 |
-
|
1715 |
-
|
|
|
|
|
1716 |
|
1717 |
-
|
1718 |
-
|
1719 |
-
|
1720 |
-
|
1721 |
-
|
1722 |
-
|
1723 |
-
|
1724 |
-
|
1725 |
-
|
1726 |
-
|
1727 |
-
|
1728 |
-
|
1729 |
-
|
1730 |
-
|
1731 |
-
|
1732 |
-
|
1733 |
-
if stage_idx == 2: # Character Designer
|
1734 |
-
return self.create_character_designer_prompt(
|
1735 |
-
stages[0]["content"], stages[1]["content"], genre, language
|
1736 |
-
)
|
1737 |
-
|
1738 |
-
if stage_idx == 3: # Structure Critic
|
1739 |
-
return self.create_critic_structure_prompt(
|
1740 |
-
stages[1]["content"], stages[2]["content"], screenplay_type, genre, language
|
1741 |
-
)
|
1742 |
-
|
1743 |
-
if stage_idx == 4: # Scene Planner
|
1744 |
-
return self.create_scene_planner_prompt(
|
1745 |
-
stages[1]["content"], stages[2]["content"], screenplay_type, genre, language
|
1746 |
-
)
|
1747 |
-
|
1748 |
-
# Screenwriter acts
|
1749 |
-
if role == "screenwriter":
|
1750 |
-
act_mapping = {5: "Act 1", 7: "Act 2A", 9: "Act 2B", 11: "Act 3"}
|
1751 |
-
if stage_idx in act_mapping:
|
1752 |
-
act = act_mapping[stage_idx]
|
1753 |
-
previous_acts = self._get_previous_acts(stages, stage_idx)
|
1754 |
-
return self.create_screenwriter_prompt(
|
1755 |
-
act, stages[4]["content"], stages[2]["content"],
|
1756 |
-
previous_acts, screenplay_type, genre, language
|
1757 |
-
)
|
1758 |
-
|
1759 |
-
# Script doctor reviews
|
1760 |
-
if role == "script_doctor":
|
1761 |
-
act_mapping = {6: "Act 1", 8: "Act 2A", 10: "Act 2B"}
|
1762 |
-
if stage_idx in act_mapping:
|
1763 |
-
act = act_mapping[stage_idx]
|
1764 |
-
act_content = stages[stage_idx-1]["content"]
|
1765 |
-
return self.create_script_doctor_prompt(act_content, act, genre, language)
|
1766 |
-
|
1767 |
-
# Final reviewer
|
1768 |
-
if role == "final_reviewer":
|
1769 |
-
complete_screenplay = ScreenplayDatabase.get_screenplay_content(self.current_session_id)
|
1770 |
-
return self.create_final_reviewer_prompt(
|
1771 |
-
complete_screenplay, screenplay_type, genre, language
|
1772 |
-
)
|
1773 |
-
|
1774 |
-
return ""
|
1775 |
|
1776 |
-
def
|
1777 |
-
|
1778 |
-
|
1779 |
-
|
1780 |
-
|
1781 |
-
|
1782 |
-
|
1783 |
-
|
1784 |
-
|
1785 |
-
|
1786 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1787 |
|
1788 |
def _process_producer_content(self, content: str):
|
1789 |
-
|
1790 |
-
|
1791 |
-
|
1792 |
-
|
1793 |
-
|
1794 |
-
|
1795 |
-
|
1796 |
-
|
1797 |
-
|
1798 |
-
|
1799 |
-
|
1800 |
-
|
1801 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1802 |
|
1803 |
def _process_story_content(self, content: str):
|
1804 |
"""Process story developer output"""
|
|
|
211 |
|
212 |
# --- Core logic classes ---
|
213 |
class ScreenplayTracker:
|
214 |
+
"""Unified screenplay tracker"""
|
215 |
+
def __init__(self):
|
216 |
+
self.screenplay_bible = ScreenplayBible()
|
217 |
+
self.scenes: List[SceneBreakdown] = []
|
218 |
+
self.characters: Dict[str, CharacterProfile] = {}
|
219 |
+
self.page_count = 0
|
220 |
+
self.act_pages = {"1": 0, "2A": 0, "2B": 0, "3": 0}
|
221 |
+
self.dialogue_action_ratio = 0.0
|
222 |
+
|
223 |
+
def add_scene(self, scene: SceneBreakdown):
|
224 |
+
"""Add scene to tracker"""
|
225 |
+
self.scenes.append(scene)
|
226 |
+
self.page_count += scene.page_count
|
227 |
+
|
228 |
+
def add_character(self, character: CharacterProfile):
|
229 |
+
"""Add character to tracker"""
|
230 |
+
self.characters[character.name] = character
|
231 |
+
# Update bible with main characters
|
232 |
+
if character.role == "protagonist":
|
233 |
+
self.screenplay_bible.protagonist = asdict(character)
|
234 |
+
elif character.role == "antagonist":
|
235 |
+
self.screenplay_bible.antagonist = asdict(character)
|
236 |
+
elif character.role == "supporting":
|
237 |
+
self.screenplay_bible.supporting_cast[character.name] = asdict(character)
|
238 |
+
|
239 |
+
def update_bible(self, key: str, value: Any):
|
240 |
+
"""Update screenplay bible"""
|
241 |
+
if hasattr(self.screenplay_bible, key):
|
242 |
+
setattr(self.screenplay_bible, key, value)
|
243 |
+
|
244 |
+
def get_act_page_target(self, act: str, total_pages: int) -> int:
|
245 |
+
"""Get target pages for each act"""
|
246 |
+
if act == "1":
|
247 |
+
return int(total_pages * 0.25)
|
248 |
+
elif act in ["2A", "2B"]:
|
249 |
+
return int(total_pages * 0.25)
|
250 |
+
elif act == "3":
|
251 |
+
return int(total_pages * 0.25)
|
252 |
+
return 0
|
253 |
|
254 |
class ScreenplayDatabase:
|
255 |
"""Database management for screenplay sessions"""
|
|
|
1629 |
You provide feedback that's critical yet encouraging."""
|
1630 |
}
|
1631 |
}
|
|
|
|
|
1632 |
|
1633 |
+
def _parse_character_profile(self, content: str, role: str) -> CharacterProfile:
|
1634 |
+
"""Parse character profile from content"""
|
1635 |
+
# Extract character details using regex or string parsing
|
1636 |
+
name = self._extract_field(content, r"(?:Name|이름)[:\s]*") or f"Character_{role}"
|
1637 |
+
|
1638 |
+
# Extract age with better parsing
|
1639 |
+
age_str = self._extract_field(content, r"(?:Age|나이)[:\s]*") or "30"
|
1640 |
+
# Extract just the number from age string
|
1641 |
+
age_match = re.search(r'\d+', age_str)
|
1642 |
+
age = int(age_match.group()) if age_match else 30
|
1643 |
+
|
1644 |
+
return CharacterProfile(
|
1645 |
+
name=name,
|
1646 |
+
age=age,
|
1647 |
+
role=role,
|
1648 |
+
archetype=self._extract_field(content, r"(?:Archetype|아크타입)[:\s]*") or "",
|
1649 |
+
want=self._extract_field(content, r"(?:WANT|외적 목표)[:\s]*") or "",
|
1650 |
+
need=self._extract_field(content, r"(?:NEED|내적 필요)[:\s]*") or "",
|
1651 |
+
backstory=self._extract_field(content, r"(?:Backstory|백스토리)[:\s]*") or "",
|
1652 |
+
personality=self._extract_personality_traits(content),
|
1653 |
+
speech_pattern=self._extract_field(content, r"(?:Speech Pattern|말투)[:\s]*") or "",
|
1654 |
+
character_arc=self._extract_field(content, r"(?:Arc|아크|Character Arc)[:\s]*") or ""
|
1655 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1656 |
|
1657 |
+
def _extract_field(self, content: str, field_pattern: str) -> Optional[str]:
|
1658 |
+
"""Extract field value from content with improved parsing"""
|
1659 |
+
# More flexible pattern that handles various formats
|
1660 |
+
pattern = rf'{field_pattern}(.+?)(?=\n[A-Z가-힣]|$)'
|
1661 |
+
match = re.search(pattern, content, re.IGNORECASE | re.DOTALL)
|
1662 |
+
if match:
|
1663 |
+
value = match.group(1).strip()
|
1664 |
+
# Remove markdown formatting if present
|
1665 |
+
value = re.sub(r'\*\*', '', value)
|
1666 |
+
value = re.sub(r'^\s*[-•]\s*', '', value)
|
1667 |
+
return value.strip()
|
1668 |
+
return None
|
1669 |
|
1670 |
+
def _extract_personality_traits(self, content: str) -> List[str]:
|
1671 |
+
"""Extract personality traits from content"""
|
1672 |
+
traits = []
|
1673 |
+
# Look for personality section
|
1674 |
+
personality_section = self._extract_field(content, r"(?:Personality|성격)[:\s]*")
|
1675 |
+
if personality_section:
|
1676 |
+
# Extract individual traits (usually listed)
|
1677 |
+
trait_lines = personality_section.split('\n')
|
1678 |
+
for line in trait_lines:
|
1679 |
+
line = line.strip()
|
1680 |
+
if line and not line.endswith(':'):
|
1681 |
+
# Remove list markers
|
1682 |
+
trait = re.sub(r'^\s*[-•*]\s*', '', line)
|
1683 |
+
if trait:
|
1684 |
+
traits.append(trait)
|
1685 |
+
return traits[:5] # Limit to 5 traits
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1686 |
|
1687 |
+
def _process_character_content(self, content: str):
|
1688 |
+
"""Process character designer output with better error handling"""
|
1689 |
+
try:
|
1690 |
+
# Extract protagonist
|
1691 |
+
protagonist_section = self._extract_section(content, r"(?:PROTAGONIST|주인공)")
|
1692 |
+
if protagonist_section:
|
1693 |
+
protagonist = self._parse_character_profile(protagonist_section, "protagonist")
|
1694 |
+
self.screenplay_tracker.add_character(protagonist)
|
1695 |
+
ScreenplayDatabase.save_character(self.current_session_id, protagonist)
|
1696 |
+
|
1697 |
+
# Extract antagonist
|
1698 |
+
antagonist_section = self._extract_section(content, r"(?:ANTAGONIST|적대자)")
|
1699 |
+
if antagonist_section:
|
1700 |
+
antagonist = self._parse_character_profile(antagonist_section, "antagonist")
|
1701 |
+
self.screenplay_tracker.add_character(antagonist)
|
1702 |
+
ScreenplayDatabase.save_character(self.current_session_id, antagonist)
|
1703 |
+
|
1704 |
+
# Extract supporting characters
|
1705 |
+
supporting_section = self._extract_section(content, r"(?:SUPPORTING CAST|조력자들)")
|
1706 |
+
if supporting_section:
|
1707 |
+
# Parse multiple supporting characters
|
1708 |
+
self._parse_supporting_characters(supporting_section)
|
1709 |
+
|
1710 |
+
except Exception as e:
|
1711 |
+
logger.error(f"Error processing character content: {e}")
|
1712 |
+
# Continue with default values rather than failing
|
1713 |
+
|
1714 |
+
def _parse_supporting_characters(self, content: str):
|
1715 |
+
"""Parse supporting characters from content"""
|
1716 |
+
# Split by character markers (numbers or bullets)
|
1717 |
+
char_sections = re.split(r'\n(?:\d+\.|[-•*])\s*', content)
|
1718 |
+
|
1719 |
+
for i, section in enumerate(char_sections[1:], 1): # Skip first empty split
|
1720 |
+
if section.strip():
|
1721 |
+
try:
|
1722 |
+
name = self._extract_field(section, r"(?:Name|이름)[:\s]*") or f"Supporting_{i}"
|
1723 |
+
role = self._extract_field(section, r"(?:Role|역할)[:\s]*") or "supporting"
|
1724 |
+
|
1725 |
+
character = CharacterProfile(
|
1726 |
+
name=name,
|
1727 |
+
age=30, # Default age for supporting characters
|
1728 |
+
role="supporting",
|
1729 |
+
archetype=role,
|
1730 |
+
want="",
|
1731 |
+
need="",
|
1732 |
+
backstory=self._extract_field(section, r"(?:Backstory|백스토리)[:\s]*") or "",
|
1733 |
+
personality=[],
|
1734 |
+
speech_pattern="",
|
1735 |
+
character_arc=""
|
1736 |
+
)
|
1737 |
+
|
1738 |
+
self.screenplay_tracker.add_character(character)
|
1739 |
+
ScreenplayDatabase.save_character(self.current_session_id, character)
|
1740 |
+
|
1741 |
+
except Exception as e:
|
1742 |
+
logger.warning(f"Error parsing supporting character {i}: {e}")
|
1743 |
+
continue
|
1744 |
+
|
1745 |
+
def _extract_section(self, content: str, section_pattern: str) -> str:
|
1746 |
+
"""Extract section from content with improved pattern matching"""
|
1747 |
+
# More flexible section extraction
|
1748 |
+
pattern = rf'{section_pattern}[:\s]*\n?(.*?)(?=\n\n[A-Z가-힣]{{2,}}[:\s]|\n\n\d+\.|$)'
|
1749 |
+
match = re.search(pattern, content, re.IGNORECASE | re.DOTALL)
|
1750 |
+
if match:
|
1751 |
+
return match.group(1).strip()
|
1752 |
+
|
1753 |
+
# Try alternative pattern
|
1754 |
+
pattern2 = rf'{section_pattern}.*?\n((?:.*\n)*?)(?=\n[A-Z가-힣]{{2,}}:|$)'
|
1755 |
+
match2 = re.search(pattern2, content, re.IGNORECASE | re.DOTALL)
|
1756 |
+
if match2:
|
1757 |
+
return match2.group(1).strip()
|
1758 |
+
|
1759 |
+
return ""
|
1760 |
|
1761 |
def _process_producer_content(self, content: str):
|
1762 |
+
"""Process producer output with better extraction"""
|
1763 |
+
try:
|
1764 |
+
# Extract title with various formats
|
1765 |
+
title_patterns = [
|
1766 |
+
r'(?:TITLE|제목)[:\s]*\*?\*?([^\n*]+)\*?\*?',
|
1767 |
+
r'\*\*(?:TITLE|제목)\*\*[:\s]*([^\n]+)',
|
1768 |
+
r'Title[:\s]*([^\n]+)'
|
1769 |
+
]
|
1770 |
+
|
1771 |
+
for pattern in title_patterns:
|
1772 |
+
title_match = re.search(pattern, content, re.IGNORECASE)
|
1773 |
+
if title_match:
|
1774 |
+
self.screenplay_tracker.screenplay_bible.title = title_match.group(1).strip()
|
1775 |
+
break
|
1776 |
+
|
1777 |
+
# Extract logline with various formats
|
1778 |
+
logline_patterns = [
|
1779 |
+
r'(?:LOGLINE|로그라인)[:\s]*\*?\*?([^\n]+)',
|
1780 |
+
r'\*\*(?:LOGLINE|로그라인)\*\*[:\s]*([^\n]+)',
|
1781 |
+
r'Logline[:\s]*([^\n]+)'
|
1782 |
+
]
|
1783 |
+
|
1784 |
+
for pattern in logline_patterns:
|
1785 |
+
logline_match = re.search(pattern, content, re.IGNORECASE | re.DOTALL)
|
1786 |
+
if logline_match:
|
1787 |
+
# Get full logline (might be multi-line)
|
1788 |
+
logline_text = logline_match.group(1).strip()
|
1789 |
+
# Continue reading if it's incomplete
|
1790 |
+
if not logline_text.endswith('.'):
|
1791 |
+
next_lines = content[logline_match.end():].split('\n')
|
1792 |
+
for line in next_lines[:3]: # Check next 3 lines
|
1793 |
+
if line.strip() and not re.match(r'^[A-Z가-힣\d]', line.strip()):
|
1794 |
+
logline_text += ' ' + line.strip()
|
1795 |
+
else:
|
1796 |
+
break
|
1797 |
+
self.screenplay_tracker.screenplay_bible.logline = logline_text
|
1798 |
+
break
|
1799 |
+
|
1800 |
+
# Extract genre
|
1801 |
+
genre_match = re.search(r'(?:Primary Genre|주 장르)[:\s]*([^\n]+)', content, re.IGNORECASE)
|
1802 |
+
if genre_match:
|
1803 |
+
self.screenplay_tracker.screenplay_bible.genre = genre_match.group(1).strip()
|
1804 |
+
|
1805 |
+
# Save to database
|
1806 |
+
ScreenplayDatabase.save_screenplay_bible(self.current_session_id,
|
1807 |
+
self.screenplay_tracker.screenplay_bible)
|
1808 |
+
|
1809 |
+
except Exception as e:
|
1810 |
+
logger.error(f"Error processing producer content: {e}")
|
1811 |
+
|
1812 |
+
|
1813 |
+
|
1814 |
+
|
1815 |
|
1816 |
def _process_story_content(self, content: str):
|
1817 |
"""Process story developer output"""
|