openfree commited on
Commit
1a0c884
·
verified ·
1 Parent(s): 844d841

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +126 -766
app.py CHANGED
@@ -4,15 +4,20 @@ import json
4
  import requests
5
  from datetime import datetime
6
  import time
7
- from typing import List, Dict, Any, Generator, Optional
8
  import logging
9
  import re
 
 
10
  import sqlite3
 
11
  import threading
12
  from contextlib import contextmanager
13
  from dataclasses import dataclass, field, asdict
14
  from collections import defaultdict
15
- import secrets
 
 
16
 
17
  # --- Logging setup ---
18
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
@@ -93,6 +98,7 @@ class StoryBible:
93
  plot_points: List[Dict[str, Any]] = field(default_factory=list)
94
  themes: List[str] = field(default_factory=list)
95
  symbols: Dict[str, List[str]] = field(default_factory=dict)
 
96
  opening_sentence: str = ""
97
 
98
  @dataclass
@@ -249,18 +255,6 @@ class NovelDatabase:
249
  )
250
  ''')
251
 
252
- # Random themes table
253
- cursor.execute('''
254
- CREATE TABLE IF NOT EXISTS random_themes (
255
- id INTEGER PRIMARY KEY AUTOINCREMENT,
256
- theme_text TEXT NOT NULL,
257
- language TEXT NOT NULL,
258
- created_at TEXT DEFAULT (datetime('now')),
259
- used_count INTEGER DEFAULT 0,
260
- last_used TEXT
261
- )
262
- ''')
263
-
264
  conn.commit()
265
 
266
  @staticmethod
@@ -276,7 +270,6 @@ class NovelDatabase:
276
 
277
  @staticmethod
278
  def create_session(user_query: str, language: str) -> str:
279
- import hashlib
280
  session_id = hashlib.md5(f"{user_query}{datetime.now()}".encode()).hexdigest()
281
  with NovelDatabase.get_db() as conn:
282
  conn.cursor().execute(
@@ -454,48 +447,6 @@ class NovelDatabase:
454
  ).fetchone()
455
  return row['total_words'] if row and row['total_words'] else 0
456
 
457
- @staticmethod
458
- def save_random_theme(theme_text: str, language: str) -> int:
459
- """Save random theme to database"""
460
- with NovelDatabase.get_db() as conn:
461
- cursor = conn.cursor()
462
- cursor.execute(
463
- 'INSERT INTO random_themes (theme_text, language) VALUES (?, ?)',
464
- (theme_text, language)
465
- )
466
- conn.commit()
467
- return cursor.lastrowid
468
-
469
- @staticmethod
470
- def get_random_themes(language: str = None, limit: int = 50) -> List[Dict]:
471
- """Get random themes from database"""
472
- with NovelDatabase.get_db() as conn:
473
- if language:
474
- query = '''SELECT * FROM random_themes
475
- WHERE language = ?
476
- ORDER BY created_at DESC
477
- LIMIT ?'''
478
- rows = conn.cursor().execute(query, (language, limit)).fetchall()
479
- else:
480
- query = '''SELECT * FROM random_themes
481
- ORDER BY created_at DESC
482
- LIMIT ?'''
483
- rows = conn.cursor().execute(query, (limit,)).fetchall()
484
- return [dict(row) for row in rows]
485
-
486
- @staticmethod
487
- def update_theme_usage(theme_id: int):
488
- """Update theme usage count and last used time"""
489
- with NovelDatabase.get_db() as conn:
490
- conn.cursor().execute(
491
- '''UPDATE random_themes
492
- SET used_count = used_count + 1,
493
- last_used = datetime('now')
494
- WHERE id = ?''',
495
- (theme_id,)
496
- )
497
- conn.commit()
498
-
499
  class WebSearchIntegration:
500
  """Web search functionality"""
501
  def __init__(self):
@@ -872,6 +823,7 @@ Provide concrete, innovative plan."""
872
  **Characters:** {', '.join(story_bible.characters.keys()) if story_bible.characters else 'TBD'}
873
  **Key Symbols:** {', '.join(story_bible.symbols.keys()) if story_bible.symbols else 'TBD'}
874
  **Themes:** {', '.join(story_bible.themes[:3]) if story_bible.themes else 'TBD'}
 
875
  """
876
 
877
  # Previous content summary
@@ -1365,8 +1317,8 @@ Provide specific and actionable revision instructions."""
1365
 
1366
  # --- Main process ---
1367
  def process_novel_stream(self, query: str, language: str,
1368
- session_id: Optional[str] = None) -> Generator[List[Dict[str, Any]], None, None]:
1369
- """Single writer novel generation process with enhanced progress tracking"""
1370
  try:
1371
  resume_from_stage = 0
1372
  if session_id:
@@ -1390,10 +1342,8 @@ Provide specific and actionable revision instructions."""
1390
  "status": s['status'],
1391
  "content": s.get('content', ''),
1392
  "word_count": s.get('word_count', 0),
1393
- "momentum": s.get('narrative_momentum', 0.0),
1394
- "role": s.get('role', ''),
1395
- "stage_number": i
1396
- } for i, s in enumerate(NovelDatabase.get_stages(self.current_session_id))]
1397
 
1398
  total_words = NovelDatabase.get_total_words(self.current_session_id)
1399
 
@@ -1405,39 +1355,12 @@ Provide specific and actionable revision instructions."""
1405
  "status": "active",
1406
  "content": "",
1407
  "word_count": 0,
1408
- "momentum": 0.0,
1409
- "role": role,
1410
- "stage_number": stage_idx,
1411
- "progress_detail": {
1412
- "current_role": role,
1413
- "phase": self._get_stage_phase(stage_idx),
1414
- "part_number": self._get_part_number(stage_idx),
1415
- "action": self._get_stage_action(role, stage_idx)
1416
- }
1417
  })
1418
  else:
1419
  stages[stage_idx]["status"] = "active"
1420
- stages[stage_idx]["progress_detail"] = {
1421
- "current_role": role,
1422
- "phase": self._get_stage_phase(stage_idx),
1423
- "part_number": self._get_part_number(stage_idx),
1424
- "action": self._get_stage_action(role, stage_idx)
1425
- }
1426
 
1427
- # Enhanced progress info
1428
- progress_info = {
1429
- "session_id": self.current_session_id,
1430
- "current_stage": stage_idx,
1431
- "total_stages": len(UNIFIED_STAGES),
1432
- "current_role": role,
1433
- "stage_name": stage_name,
1434
- "total_words": total_words,
1435
- "target_words": TARGET_WORDS,
1436
- "percentage": (stage_idx / len(UNIFIED_STAGES)) * 100,
1437
- "status": f"🔄 {self._get_role_emoji(role)} {self._get_role_name(role)} is {self._get_stage_action(role, stage_idx)}..."
1438
- }
1439
-
1440
- yield stages, progress_info
1441
 
1442
  prompt = self.get_stage_prompt(stage_idx, role, query, language, stages)
1443
  stage_content = ""
@@ -1446,10 +1369,7 @@ Provide specific and actionable revision instructions."""
1446
  stage_content += chunk
1447
  stages[stage_idx]["content"] = stage_content
1448
  stages[stage_idx]["word_count"] = len(stage_content.split())
1449
-
1450
- # Update progress during streaming
1451
- progress_info["status"] = f"🔄 {self._get_role_emoji(role)} {self._get_role_name(role)} is {self._get_stage_action(role, stage_idx)}... ({stages[stage_idx]['word_count']} words)"
1452
- yield stages, progress_info
1453
 
1454
  # Content processing and tracking
1455
  if role == "writer":
@@ -1474,11 +1394,7 @@ Provide specific and actionable revision instructions."""
1474
 
1475
  NovelDatabase.save_narrative_tracker(self.current_session_id, self.narrative_tracker)
1476
  total_words = NovelDatabase.get_total_words(self.current_session_id)
1477
-
1478
- # Update progress after completion
1479
- progress_info["total_words"] = total_words
1480
- progress_info["status"] = f"✅ {self._get_role_emoji(role)} {self._get_role_name(role)} completed {self._get_stage_action(role, stage_idx, completed=True)}"
1481
- yield stages, progress_info
1482
 
1483
  # Final processing
1484
  final_novel = NovelDatabase.get_writer_content(self.current_session_id)
@@ -1486,91 +1402,11 @@ Provide specific and actionable revision instructions."""
1486
  final_report = self.generate_literary_report(final_novel, final_word_count, language)
1487
 
1488
  NovelDatabase.update_final_novel(self.current_session_id, final_novel, final_report)
1489
-
1490
- # Final progress info
1491
- final_progress = {
1492
- "session_id": self.current_session_id,
1493
- "current_stage": len(UNIFIED_STAGES),
1494
- "total_stages": len(UNIFIED_STAGES),
1495
- "current_role": "complete",
1496
- "stage_name": "Novel Completed",
1497
- "total_words": final_word_count,
1498
- "target_words": TARGET_WORDS,
1499
- "percentage": 100,
1500
- "status": f"✅ Novel completed! Total {final_word_count:,} words"
1501
- }
1502
- yield stages, final_progress
1503
 
1504
  except Exception as e:
1505
  logger.error(f"Novel generation process error: {e}", exc_info=True)
1506
- error_progress = {
1507
- "session_id": self.current_session_id,
1508
- "status": f"❌ Error occurred: {e}",
1509
- "error": True
1510
- }
1511
- yield stages if 'stages' in locals() else [], error_progress
1512
-
1513
- def _get_role_emoji(self, role: str) -> str:
1514
- """Get emoji for role"""
1515
- emoji_map = {
1516
- "director": "🎬",
1517
- "writer": "✍️",
1518
- "critic_director": "🔍",
1519
- "critic_final": "📊"
1520
- }
1521
- if role.startswith("critic_part"):
1522
- return "📝"
1523
- return emoji_map.get(role, "🔄")
1524
-
1525
- def _get_role_name(self, role: str) -> str:
1526
- """Get display name for role"""
1527
- role_names = {
1528
- "director": "Director",
1529
- "writer": "Writer",
1530
- "critic_director": "Structure Critic",
1531
- "critic_final": "Final Critic"
1532
- }
1533
- if role.startswith("critic_part"):
1534
- part_num = role.replace("critic_part", "")
1535
- return f"Part {part_num} Critic"
1536
- return role_names.get(role, role.title())
1537
-
1538
- def _get_stage_action(self, role: str, stage_idx: int, completed: bool = False) -> str:
1539
- """Get action description for stage"""
1540
- if completed:
1541
- action_map = {
1542
- "director": "narrative planning" if stage_idx < 3 else "final master plan",
1543
- "writer": "writing" if "Revision" not in UNIFIED_STAGES[stage_idx][1] else "revision",
1544
- "critic_director": "structure review",
1545
- "critic_final": "comprehensive evaluation"
1546
- }
1547
- else:
1548
- action_map = {
1549
- "director": "planning narrative structure" if stage_idx < 3 else "finalizing master plan",
1550
- "writer": "writing" if "Revision" not in UNIFIED_STAGES[stage_idx][1] else "revising based on critique",
1551
- "critic_director": "reviewing narrative structure",
1552
- "critic_final": "evaluating complete novel"
1553
- }
1554
-
1555
- if role.startswith("critic_part"):
1556
- if completed:
1557
- return "part review"
1558
- else:
1559
- return "reviewing part for improvements"
1560
-
1561
- return action_map.get(role, "processing")
1562
-
1563
- def _get_stage_phase(self, stage_idx: int) -> str:
1564
- """Get phase description for stage"""
1565
- if stage_idx < 3:
1566
- return "Planning Phase"
1567
- elif stage_idx < len(UNIFIED_STAGES) - 1:
1568
- part_num = self._get_part_number(stage_idx)
1569
- if part_num:
1570
- return f"Part {part_num} Phase"
1571
- return "Writing Phase"
1572
- else:
1573
- return "Final Evaluation Phase"
1574
 
1575
  def get_stage_prompt(self, stage_idx: int, role: str, query: str,
1576
  language: str, stages: List[Dict]) -> str:
@@ -1681,36 +1517,25 @@ Present concrete and executable final plan."""
1681
 
1682
 
1683
  # --- Utility functions ---
1684
- def process_query(query: str, language: str, session_id: Optional[str] = None) -> Generator[Dict[str, Any], None, None]:
1685
- """Main query processing function with enhanced progress tracking"""
1686
  if not query.strip():
1687
- yield {
1688
- "stages_markdown": "",
1689
- "novel_content": "",
1690
- "status": "❌ Please enter a theme.",
1691
- "session_id": session_id,
1692
- "progress_info": {"error": True, "status": "❌ Please enter a theme."}
1693
- }
1694
  return
1695
 
1696
  system = UnifiedLiterarySystem()
 
 
1697
 
1698
- for stages, progress_info in system.process_novel_stream(query, language, session_id):
1699
- stages_markdown = format_stages_display(stages, progress_info)
1700
 
1701
  # Get final novel content
1702
- novel_content = ""
1703
- if progress_info.get("percentage", 0) == 100:
1704
- novel_content = NovelDatabase.get_writer_content(progress_info["session_id"])
1705
  novel_content = format_novel_display(novel_content)
1706
 
1707
- yield {
1708
- "stages_markdown": stages_markdown,
1709
- "novel_content": novel_content,
1710
- "status": progress_info.get("status", "🔄 Processing..."),
1711
- "session_id": progress_info.get("session_id", session_id),
1712
- "progress_info": progress_info
1713
- }
1714
 
1715
  def get_active_sessions(language: str) -> List[str]:
1716
  """Get active session list"""
@@ -1718,24 +1543,18 @@ def get_active_sessions(language: str) -> List[str]:
1718
  return [f"{s['session_id'][:8]}... - {s['user_query'][:50]}... ({s['created_at']}) [{s['total_words']:,} words]"
1719
  for s in sessions]
1720
 
1721
- def auto_recover_session(language: str) -> Dict[str, Any]:
1722
  """Auto-recover recent session"""
1723
  sessions = NovelDatabase.get_active_sessions()
1724
  if sessions:
1725
  latest_session = sessions[0]
1726
- return {"session_id": latest_session['session_id'], "message": f"Session {latest_session['session_id'][:8]}... recovered"}
1727
- return {"session_id": None, "message": "No session to recover."}
1728
 
1729
- def resume_session(session_id: str, language: str) -> Generator[Dict[str, Any], None, None]:
1730
  """Resume session"""
1731
  if not session_id:
1732
- yield {
1733
- "stages_markdown": "",
1734
- "novel_content": "",
1735
- "status": "❌ No session ID.",
1736
- "session_id": None,
1737
- "progress_info": {"error": True, "status": "❌ No session ID."}
1738
- }
1739
  return
1740
 
1741
  if "..." in session_id:
@@ -1743,13 +1562,7 @@ def resume_session(session_id: str, language: str) -> Generator[Dict[str, Any],
1743
 
1744
  session = NovelDatabase.get_session(session_id)
1745
  if not session:
1746
- yield {
1747
- "stages_markdown": "",
1748
- "novel_content": "",
1749
- "status": "❌ Session not found.",
1750
- "session_id": None,
1751
- "progress_info": {"error": True, "status": "❌ Session not found."}
1752
- }
1753
  return
1754
 
1755
  yield from process_query(session['user_query'], session['language'], session_id)
@@ -1771,144 +1584,61 @@ def download_novel(novel_text: str, format_type: str, language: str, session_id:
1771
  logger.error(f"File generation failed: {e}")
1772
  return None
1773
 
1774
- def format_stages_display(stages: List[Dict], progress_info: Dict) -> str:
1775
- """Stage progress display with enhanced role identification"""
1776
- markdown = "## 🎬 Progress Status\n\n"
1777
-
1778
- # Overall progress bar
1779
- percentage = progress_info.get("percentage", 0)
1780
- progress_bar = create_progress_bar(percentage)
1781
- markdown += f"{progress_bar}\n\n"
1782
-
1783
- # Current status with role identification
1784
- current_role = progress_info.get("current_role", "")
1785
- current_action = progress_info.get("status", "")
1786
- markdown += f"### 🎯 Current Status\n"
1787
- markdown += f"**{current_action}**\n\n"
1788
-
1789
- # Statistics
1790
- total_words = progress_info.get("total_words", 0)
1791
- target_words = progress_info.get("target_words", TARGET_WORDS)
1792
- markdown += f"**Total Word Count: {total_words:,} / {target_words:,}** ({(total_words/target_words*100):.1f}%)\n"
1793
-
1794
- # Calculate completed parts
1795
- completed_parts = sum(1 for s in stages
1796
  if 'Revision' in s.get('name', '') and s.get('status') == 'complete')
1797
- markdown += f"**Completed Parts: {completed_parts} / 10**\n"
1798
-
1799
- # Average narrative momentum
1800
- momentum_scores = [s.get('momentum', 0) for s in stages if s.get('momentum', 0) > 0]
1801
- if momentum_scores:
1802
- avg_momentum = sum(momentum_scores) / len(momentum_scores)
1803
- markdown += f"**Average Narrative Momentum: {avg_momentum:.1f} / 10**\n"
1804
-
1805
- markdown += "\n---\n\n"
1806
-
1807
- # Stage details with role identification
1808
- current_phase = ""
1809
- for stage in stages:
1810
- # Phase headers
1811
- stage_phase = get_stage_phase_from_name(stage.get('name', ''))
1812
- if stage_phase != current_phase:
1813
- current_phase = stage_phase
1814
- markdown += f"\n### 📚 {current_phase}\n\n"
1815
-
1816
- # Stage status
1817
- status_icon = "✅" if stage['status'] == 'complete' else "🔄" if stage['status'] == 'active' else "⏳"
1818
- role_emoji = get_role_emoji_from_name(stage.get('name', ''))
1819
-
1820
- # Stage line with role identification
1821
- markdown += f"{status_icon} {role_emoji} **{stage['name']}**"
1822
-
1823
- # Add role tag
1824
- role = stage.get('role', '')
1825
- if role:
1826
- role_display = get_role_display_name(role)
1827
- markdown += f" `[{role_display}]`"
1828
-
1829
- # Word count and momentum
1830
- if stage.get('word_count', 0) > 0:
1831
- markdown += f" ({stage['word_count']:,} words)"
1832
-
1833
- if stage.get('momentum', 0) > 0:
1834
- markdown += f" [Momentum: {stage['momentum']:.1f}/10]"
1835
-
1836
- markdown += "\n"
1837
-
1838
- # Content preview
1839
- if stage['content'] and stage['status'] == 'complete':
1840
- # Adjust preview length by role
1841
- preview_length = 300 if 'writer' in stage.get('role', '') else 200
1842
- preview = stage['content'][:preview_length] + "..." if len(stage['content']) > preview_length else stage['content']
1843
- markdown += f"> {preview}\n\n"
1844
- elif stage['status'] == 'active':
1845
- # Show what the current role is doing
1846
- role_action = get_role_action_description(stage.get('role', ''), stage.get('name', ''))
1847
- markdown += f"> *{role_action}...*\n\n"
1848
-
1849
- return markdown
1850
-
1851
- def create_progress_bar(percentage: float) -> str:
1852
- """Create visual progress bar"""
1853
- filled = int(percentage / 5) # 20 blocks total
1854
- empty = 20 - filled
1855
- bar = "█" * filled + "░" * empty
1856
- return f"Progress: [{bar}] {percentage:.1f}%"
1857
-
1858
- def get_stage_phase_from_name(stage_name: str) -> str:
1859
- """Determine phase from stage name"""
1860
- if "Director" in stage_name and "Final Master Plan" not in stage_name:
1861
- return "Planning Phase"
1862
- elif "Final Master Plan" in stage_name:
1863
- return "Master Plan Finalization"
1864
- elif "Final Critic" in stage_name:
1865
- return "Final Evaluation Phase"
1866
- elif match := re.search(r'Part (\d+)', stage_name):
1867
- part_num = int(match.group(1))
1868
- return f"Part {part_num}: {NARRATIVE_PHASES[part_num-1]}"
1869
- return "Processing Phase"
1870
-
1871
- def get_role_emoji_from_name(stage_name: str) -> str:
1872
- """Get emoji based on stage name"""
1873
- if "Director" in stage_name:
1874
- return "🎬"
1875
- elif "Writer" in stage_name:
1876
- return "✍️"
1877
- elif "Final Critic" in stage_name:
1878
- return "📊"
1879
- elif "Critic" in stage_name:
1880
- return "📝"
1881
- return "🔄"
1882
-
1883
- def get_role_display_name(role: str) -> str:
1884
- """Get user-friendly role name"""
1885
- role_map = {
1886
- "director": "Director",
1887
- "writer": "Writer",
1888
- "critic_director": "Structure Critic",
1889
- "critic_final": "Final Evaluator"
1890
- }
1891
- if role.startswith("critic_part"):
1892
- part_num = role.replace("critic_part", "")
1893
- return f"Part {part_num} Critic"
1894
- return role_map.get(role, role.title())
1895
-
1896
- def get_role_action_description(role: str, stage_name: str) -> str:
1897
- """Get detailed action description for active role"""
1898
- actions = {
1899
- "director": "Planning narrative structure and character arcs",
1900
- "writer": "Crafting literary prose with philosophical depth",
1901
- "critic_director": "Analyzing structural coherence and feasibility",
1902
- "critic_final": "Evaluating literary achievement and impact"
1903
- }
1904
-
1905
- if role.startswith("critic_part"):
1906
- return "Reviewing for continuity, depth, and literary quality"
1907
-
1908
- if "Revision" in stage_name:
1909
- return "Incorporating critique to enhance literary quality"
1910
-
1911
- return actions.get(role, "Processing content")
1912
 
1913
  def format_novel_display(novel_text: str) -> str:
1914
  """Display novel content - Enhanced part separation"""
@@ -1959,9 +1689,10 @@ def export_to_docx(content: str, filename: str, language: str, session_id: str)
1959
  # Generate title from session info
1960
  session = NovelDatabase.get_session(session_id)
1961
 
1962
- # Generate title based on theme and content
1963
  def generate_title(user_query: str, content_preview: str) -> str:
1964
  """Generate title based on theme and content"""
 
1965
  if len(user_query) < 20:
1966
  return user_query
1967
  else:
@@ -2007,12 +1738,14 @@ def export_to_docx(content: str, filename: str, language: str, session_id: str)
2007
  # Remove part titles/numbers patterns
2008
  patterns_to_remove = [
2009
  r'^#{1,6}\s+.*', # Markdown headers
2010
- r'^\*\*.*\*\*', # Bold text **text**
2011
- r'^Part\s*\d+.*', # "Part 1 …" format
2012
- r'^\d+\.\s+.*:.*', # "1. Title: …" format
2013
- r'^---+', # Dividers
2014
- r'^\s*\[.*\]\s*', # Bracket labels
2015
  ]
 
 
2016
 
2017
  lines = text.split('\n')
2018
  cleaned_lines = []
@@ -2096,120 +1829,7 @@ def export_to_txt(content: str, filename: str) -> str:
2096
 
2097
  return filepath
2098
 
2099
- def generate_random_theme(language="English", save_to_db=True):
2100
- """Generate a realistic and relatable novel theme"""
2101
- try:
2102
- # Realistic theme components
2103
- themes = {
2104
- "Korean": {
2105
- "situations": [
2106
- "아버지의 장례식에서 만난 낯선 사람들",
2107
- "30년 만에 돌아온 고향의 변화",
2108
- "은퇴 후 시작된 예상치 못한 일상",
2109
- "딸이 남긴 일기장을 읽는 어머니",
2110
- "폐업을 앞둔 오래된 책방의 마지막 날들",
2111
- "치매가 시작된 남편을 돌보는 아내",
2112
- "이혼 후 혼자 키우는 아이와의 관계",
2113
- "형제간의 유산 분쟁과 가족의 의미"
2114
- ],
2115
- "conflicts": [
2116
- "평생 숨겨온 비밀의 무게",
2117
- "변화를 받아들이는 것의 어려움",
2118
- "과거와 화해하려는 시도",
2119
- "사랑과 의무 사이의 갈등",
2120
- "세대 간 가치관의 충돌",
2121
- "정체성에 대한 혼란과 탐색",
2122
- "용서와 화해의 가능성",
2123
- "삶의 의미를 재정의하는 과정"
2124
- ],
2125
- "questions": [
2126
- "진정한 가족의 의미는 무엇인가?",
2127
- "우리는 과거로부터 자유로울 수 있는가?",
2128
- "변화 속에서 변하지 않는 것은 무엇인가?",
2129
- "사랑은 모든 것을 극복할 수 있는가?",
2130
- "우리가 남기고 가는 것은 무엇인가?",
2131
- "용서란 무엇이며 어떻게 가능한가?",
2132
- "나이가 든다는 것의 진정한 의미는?",
2133
- "행복의 조건은 무엇인가?"
2134
- ]
2135
- },
2136
- "English": {
2137
- "situations": [
2138
- "strangers met at father's funeral",
2139
- "returning home after thirty years away",
2140
- "unexpected daily life after retirement",
2141
- "mother reading daughter's left diary",
2142
- "last days of an old bookstore closing",
2143
- "wife caring for husband with early dementia",
2144
- "single parent after difficult divorce",
2145
- "inheritance dispute between siblings"
2146
- ],
2147
- "conflicts": [
2148
- "weight of lifelong hidden secrets",
2149
- "difficulty of accepting change",
2150
- "attempting reconciliation with past",
2151
- "conflict between love and duty",
2152
- "generational clash of values",
2153
- "confusion and search for identity",
2154
- "possibility of forgiveness and healing",
2155
- "process of redefining life's meaning"
2156
- ],
2157
- "questions": [
2158
- "What truly makes a family?",
2159
- "Can we ever be free from our past?",
2160
- "What remains unchanged amid change?",
2161
- "Can love overcome everything?",
2162
- "What legacy do we leave behind?",
2163
- "What is forgiveness and how is it possible?",
2164
- "What does it truly mean to grow old?",
2165
- "What are the conditions for happiness?"
2166
- ]
2167
- }
2168
- }
2169
-
2170
- # Select language-appropriate components
2171
- lang_themes = themes.get(language, themes["English"])
2172
-
2173
- situation = secrets.choice(lang_themes["situations"])
2174
- conflict = secrets.choice(lang_themes["conflicts"])
2175
- question = secrets.choice(lang_themes["questions"])
2176
-
2177
- # Create natural theme description
2178
- if language == "Korean":
2179
- theme = f"""{situation}
2180
-
2181
- 이 상황에서 주인공은 {conflict}에 직면하게 됩니다.
2182
- 결국 이 이야기는 "{question}"라는 질문에 대한 하나의 대답을 찾아가는 여정이 될 것입니다.
2183
-
2184
- 인간의 보편적인 감정과 현대 사회의 구체적인 현실이 교차하는 지점에서,
2185
- 우리 모두가 한 번쯤 마주하게 되는 삶의 본질적인 문제들을 탐구합니다."""
2186
- else:
2187
- theme = f"""{situation}
2188
-
2189
- In this situation, the protagonist faces {conflict}.
2190
- Ultimately, this story becomes a journey to find one answer to the question: "{question}"
2191
-
2192
- At the intersection of universal human emotions and concrete modern realities,
2193
- it explores the essential life questions we all face at some point."""
2194
-
2195
- # Save to database if requested
2196
- if save_to_db:
2197
- NovelDatabase.save_random_theme(theme, language)
2198
-
2199
- return theme
2200
-
2201
- except Exception as e:
2202
- logger.error(f"Theme generation error: {str(e)}")
2203
- # Fallback themes
2204
- fallback = {
2205
- "Korean": "은퇴한 교사가 옛 제자의 편지를 받고 시작되는 과거와의 화해",
2206
- "English": "A retired teacher's reconciliation with the past after receiving a letter from a former student"
2207
- }
2208
- theme = fallback.get(language, fallback["English"])
2209
- if save_to_db:
2210
- NovelDatabase.save_random_theme(theme, language)
2211
- return theme
2212
-
2213
  # CSS styles - Writer's Study Theme (Light & Warm)
2214
  custom_css = """
2215
  /* Global container - Light paper background */
@@ -2384,112 +2004,6 @@ custom_css = """
2384
  color: #5a453a;
2385
  }
2386
 
2387
- /* Progress display enhancements */
2388
- #stages-display code {
2389
- background: #f5f0e6;
2390
- padding: 2px 6px;
2391
- border-radius: 4px;
2392
- font-family: 'Courier New', monospace;
2393
- color: #5a453a;
2394
- font-size: 0.9em;
2395
- }
2396
-
2397
- /* Random theme library styles */
2398
- #theme-library {
2399
- background: linear-gradient(to bottom, #ffffff 0%, #fdfcfa 100%);
2400
- padding: 25px;
2401
- border-radius: 16px;
2402
- box-shadow: 0 8px 25px rgba(139, 69, 19, 0.08),
2403
- inset 0 1px 3px rgba(255, 255, 255, 0.9);
2404
- color: #3d2914;
2405
- border: 1px solid #e8dcc6;
2406
- }
2407
-
2408
- .theme-card {
2409
- background: linear-gradient(145deg, #faf8f5 0%, #f5f0e8 100%);
2410
- border: 1px solid #e0d0b8;
2411
- border-radius: 12px;
2412
- padding: 18px;
2413
- margin: 10px;
2414
- box-shadow: 0 3px 10px rgba(139, 69, 19, 0.08);
2415
- transition: all 0.3s ease;
2416
- cursor: pointer;
2417
- min-height: 150px;
2418
- display: flex;
2419
- flex-direction: column;
2420
- gap: 10px;
2421
- }
2422
-
2423
- .theme-card:hover {
2424
- transform: translateY(-3px);
2425
- box-shadow: 0 5px 15px rgba(139, 69, 19, 0.15);
2426
- background: linear-gradient(145deg, #fcfaf8 0%, #f7f2ea 100%);
2427
- }
2428
-
2429
- .theme-card-header {
2430
- display: flex;
2431
- justify-content: space-between;
2432
- align-items: center;
2433
- margin-bottom: 8px;
2434
- padding-bottom: 8px;
2435
- border-bottom: 1px solid #e0d0b8;
2436
- }
2437
-
2438
- .theme-card-date {
2439
- font-size: 0.75em;
2440
- color: #8b6239;
2441
- font-style: italic;
2442
- }
2443
-
2444
- .theme-card-lang {
2445
- font-size: 0.75em;
2446
- background: #d4a574;
2447
- color: white;
2448
- padding: 2px 8px;
2449
- border-radius: 4px;
2450
- font-weight: 600;
2451
- }
2452
-
2453
- .theme-card-content {
2454
- font-size: 0.85em;
2455
- line-height: 1.5;
2456
- color: #5a453a;
2457
- overflow: hidden;
2458
- text-overflow: ellipsis;
2459
- display: -webkit-box;
2460
- -webkit-line-clamp: 6;
2461
- -webkit-box-orient: vertical;
2462
- }
2463
-
2464
- .theme-card-footer {
2465
- margin-top: auto;
2466
- padding-top: 8px;
2467
- border-top: 1px solid #f0e6d6;
2468
- display: flex;
2469
- justify-content: space-between;
2470
- align-items: center;
2471
- }
2472
-
2473
- .theme-card-usage {
2474
- font-size: 0.7em;
2475
- color: #8b6239;
2476
- }
2477
-
2478
- .theme-card-copy {
2479
- font-size: 0.75em;
2480
- background: #f5f0e6;
2481
- border: 1px solid #d4c4b0;
2482
- padding: 2px 8px;
2483
- border-radius: 4px;
2484
- cursor: pointer;
2485
- transition: all 0.2s ease;
2486
- }
2487
-
2488
- .theme-card-copy:hover {
2489
- background: #d4a574;
2490
- color: white;
2491
- }
2492
-
2493
  /* Download section - Book binding style */
2494
  .download-section {
2495
  background: linear-gradient(145deg, #faf6f0 0%, #f5efe6 100%);
@@ -3084,99 +2598,8 @@ def process_generated_theme(self, theme_text: str, language: str) -> str:
3084
  summary += f"{theme_elements.get('exploration', '')}"
3085
 
3086
  return summary.strip()
3087
-
3088
-
3089
- def format_theme_card(theme_data: Dict) -> str:
3090
- """Format theme data as HTML card"""
3091
- from html import escape
3092
-
3093
- theme_id = theme_data.get('id', 0)
3094
- theme_text = escape(theme_data.get('theme_text', ''))
3095
- language = theme_data.get('language', 'English')
3096
- created_at = theme_data.get('created_at', '')
3097
- used_count = theme_data.get('used_count', 0)
3098
-
3099
- # Format date
3100
- try:
3101
- date_obj = datetime.strptime(created_at, '%Y-%m-%d %H:%M:%S')
3102
- formatted_date = date_obj.strftime('%Y.%m.%d')
3103
- except:
3104
- formatted_date = created_at[:10] if created_at else 'Unknown'
3105
-
3106
- # Truncate text for card display
3107
- lines = theme_text.split('\n')
3108
- truncated_text = '\n'.join(lines[:6]) if len(lines) > 6 else theme_text
3109
- if len(truncated_text) > 300:
3110
- truncated_text = truncated_text[:297] + '...'
3111
-
3112
- return f"""
3113
- <div class="theme-card" data-theme-id="{theme_id}" data-theme-text="{theme_text}">
3114
- <div class="theme-card-header">
3115
- <span class="theme-card-date">{formatted_date}</span>
3116
- <span class="theme-card-lang">{language[:2].upper()}</span>
3117
- </div>
3118
- <div class="theme-card-content">
3119
- {escape(truncated_text).replace('\n', '<br>')}
3120
- </div>
3121
- <div class="theme-card-footer">
3122
- <span class="theme-card-usage">Used {used_count} times</span>
3123
- <button class="theme-card-copy" onclick="copyTheme({theme_id})">Copy</button>
3124
- </div>
3125
- </div>
3126
- """
3127
-
3128
- def format_theme_library(themes: List[Dict]) -> str:
3129
- """Format theme library as grid"""
3130
- if not themes:
3131
- return """
3132
- <div style="text-align: center; padding: 40px; color: #8b6239;">
3133
- <p style="font-size: 1.2em; margin-bottom: 20px;">📚 No themes in the library yet</p>
3134
- <p>Click the "Random Theme" button to generate and save themes!</p>
3135
- </div>
3136
- """
3137
-
3138
- # Create grid HTML
3139
- cards_html = ''.join([format_theme_card(theme) for theme in themes])
3140
-
3141
- return f"""
3142
- <div style="padding: 20px;">
3143
- <h3 style="color: #2c1810; font-family: 'Playfair Display', serif; margin-bottom: 20px;">
3144
- 📚 Random Theme Library ({len(themes)} themes)
3145
- </h3>
3146
- <div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 15px;">
3147
- {cards_html}
3148
- </div>
3149
- </div>
3150
-
3151
- <script>
3152
- function copyTheme(themeId) {{
3153
- const card = document.querySelector(`[data-theme-id="${{themeId}}"]`);
3154
- if (card) {{
3155
- const themeText = card.getAttribute('data-theme-text');
3156
- const textArea = document.getElementById('theme_input').querySelector('textarea');
3157
- if (textArea) {{
3158
- textArea.value = themeText;
3159
- textArea.dispatchEvent(new Event('input', {{ bubbles: true }}));
3160
-
3161
- // Visual feedback
3162
- const copyBtn = card.querySelector('.theme-card-copy');
3163
- const originalText = copyBtn.textContent;
3164
- copyBtn.textContent = '✓ Copied';
3165
- copyBtn.style.background = '#d4a574';
3166
- copyBtn.style.color = 'white';
3167
-
3168
- setTimeout(() => {{
3169
- copyBtn.textContent = originalText;
3170
- copyBtn.style.background = '';
3171
- copyBtn.style.color = '';
3172
- }}, 2000);
3173
- }}
3174
- }}
3175
- }}
3176
- </script>
3177
- """
3178
-
3179
-
3180
  # Create Gradio interface - Writer's Study Theme
3181
  def create_interface():
3182
  # Using Soft theme with safe color options
@@ -3202,9 +2625,9 @@ def create_interface():
3202
  </p>
3203
 
3204
  <div class="progress-note">
3205
- 🎲 <strong>Novel Theme Random Generator:</strong> This system can generate unique, realistic novel themes based on universal human experiences.
3206
- Each generated theme explores profound questions about family, identity, aging, and the human condition.
3207
- Click the "Random" button to discover meaningful story possibilities!
3208
  </div>
3209
 
3210
  <div class="warning-note">
@@ -3294,20 +2717,6 @@ def create_interface():
3294
  visible=False,
3295
  elem_id="download_file"
3296
  )
3297
-
3298
- with gr.Tab("🎲 Random Theme Library", elem_id="theme_library_tab"):
3299
- theme_library_display = gr.HTML(
3300
- value=format_theme_library([]),
3301
- elem_id="theme-library"
3302
- )
3303
- with gr.Row():
3304
- theme_filter = gr.Radio(
3305
- choices=["All", "English", "Korean"],
3306
- value="All",
3307
- label="Filter by Language",
3308
- elem_id="theme_filter"
3309
- )
3310
- refresh_library_btn = gr.Button("🔄 Refresh Library", variant="secondary")
3311
 
3312
  # Hidden state
3313
  novel_text_state = gr.State("")
@@ -3340,56 +2749,22 @@ def create_interface():
3340
  return gr.update(choices=[])
3341
 
3342
  def handle_auto_recover(language):
3343
- result = auto_recover_session(language)
3344
- return result["session_id"], result["message"]
3345
-
3346
- def handle_submit(query, language, session_id):
3347
- """Handle submit with proper return values"""
3348
- for result in process_query(query, language, session_id):
3349
- yield (
3350
- result["stages_markdown"],
3351
- result["novel_content"],
3352
- result["status"],
3353
- result["session_id"]
3354
- )
3355
-
3356
- def handle_resume(dropdown_value, language):
3357
- """Handle resume with proper return values"""
3358
- if not dropdown_value:
3359
- yield "", "", "❌ No session selected.", None
3360
- return
3361
-
3362
- session_id = dropdown_value.split("...")[0] if "..." in dropdown_value else dropdown_value
3363
-
3364
- for result in resume_session(session_id, language):
3365
- yield (
3366
- result["stages_markdown"],
3367
- result["novel_content"],
3368
- result["status"],
3369
- result["session_id"]
3370
- )
3371
-
3372
- def refresh_theme_library(filter_lang):
3373
- """Refresh theme library display"""
3374
- try:
3375
- if filter_lang == "All":
3376
- themes = NovelDatabase.get_random_themes(limit=50)
3377
- else:
3378
- themes = NovelDatabase.get_random_themes(language=filter_lang, limit=50)
3379
- return format_theme_library(themes)
3380
- except Exception as e:
3381
- logger.error(f"Theme library refresh error: {str(e)}")
3382
- return "<p>Error loading theme library</p>"
3383
-
3384
- def handle_random_theme_with_save(language):
3385
- """Generate random theme and save to database"""
3386
- theme = generate_random_theme(language, save_to_db=True)
3387
- logger.info(f"Generated and saved theme: {theme[:100]}...")
3388
  return theme
3389
 
3390
  # Event connections
3391
  submit_btn.click(
3392
- fn=handle_submit,
3393
  inputs=[query_input, language_select, current_session_id],
3394
  outputs=[stages_display, novel_output, status_text, current_session_id]
3395
  )
@@ -3401,8 +2776,12 @@ def create_interface():
3401
  )
3402
 
3403
  resume_btn.click(
3404
- fn=handle_resume,
3405
- inputs=[session_dropdown, language_select],
 
 
 
 
3406
  outputs=[stages_display, novel_output, status_text, current_session_id]
3407
  )
3408
 
@@ -3411,7 +2790,7 @@ def create_interface():
3411
  inputs=[language_select],
3412
  outputs=[current_session_id, status_text]
3413
  ).then(
3414
- fn=handle_resume,
3415
  inputs=[current_session_id, language_select],
3416
  outputs=[stages_display, novel_output, status_text, current_session_id]
3417
  )
@@ -3427,26 +2806,10 @@ def create_interface():
3427
  )
3428
 
3429
  random_btn.click(
3430
- fn=handle_random_theme_with_save,
3431
  inputs=[language_select],
3432
  outputs=[query_input],
3433
  queue=False
3434
- ).then(
3435
- fn=lambda filter_lang: refresh_theme_library(filter_lang),
3436
- inputs=[theme_filter],
3437
- outputs=[theme_library_display]
3438
- )
3439
-
3440
- refresh_library_btn.click(
3441
- fn=refresh_theme_library,
3442
- inputs=[theme_filter],
3443
- outputs=[theme_library_display]
3444
- )
3445
-
3446
- theme_filter.change(
3447
- fn=refresh_theme_library,
3448
- inputs=[theme_filter],
3449
- outputs=[theme_library_display]
3450
  )
3451
 
3452
  def handle_download(format_type, language, session_id, novel_text):
@@ -3465,13 +2828,10 @@ def create_interface():
3465
  outputs=[download_file]
3466
  )
3467
 
3468
- # Load sessions and theme library on start
3469
  interface.load(
3470
  fn=refresh_sessions,
3471
  outputs=[session_dropdown]
3472
- ).then(
3473
- fn=lambda: refresh_theme_library("All"),
3474
- outputs=[theme_library_display]
3475
  )
3476
 
3477
  return interface
@@ -3512,4 +2872,4 @@ if __name__ == "__main__":
3512
  server_port=7860,
3513
  share=False,
3514
  debug=True
3515
- )
 
4
  import requests
5
  from datetime import datetime
6
  import time
7
+ from typing import List, Dict, Any, Generator, Tuple, Optional, Set
8
  import logging
9
  import re
10
+ import tempfile
11
+ from pathlib import Path
12
  import sqlite3
13
+ import hashlib
14
  import threading
15
  from contextlib import contextmanager
16
  from dataclasses import dataclass, field, asdict
17
  from collections import defaultdict
18
+ import json
19
+ from pathlib import Path
20
+ import random
21
 
22
  # --- Logging setup ---
23
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
 
98
  plot_points: List[Dict[str, Any]] = field(default_factory=list)
99
  themes: List[str] = field(default_factory=list)
100
  symbols: Dict[str, List[str]] = field(default_factory=dict)
101
+ style_guide: Dict[str, str] = field(default_factory=dict)
102
  opening_sentence: str = ""
103
 
104
  @dataclass
 
255
  )
256
  ''')
257
 
 
 
 
 
 
 
 
 
 
 
 
 
258
  conn.commit()
259
 
260
  @staticmethod
 
270
 
271
  @staticmethod
272
  def create_session(user_query: str, language: str) -> str:
 
273
  session_id = hashlib.md5(f"{user_query}{datetime.now()}".encode()).hexdigest()
274
  with NovelDatabase.get_db() as conn:
275
  conn.cursor().execute(
 
447
  ).fetchone()
448
  return row['total_words'] if row and row['total_words'] else 0
449
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
450
  class WebSearchIntegration:
451
  """Web search functionality"""
452
  def __init__(self):
 
823
  **Characters:** {', '.join(story_bible.characters.keys()) if story_bible.characters else 'TBD'}
824
  **Key Symbols:** {', '.join(story_bible.symbols.keys()) if story_bible.symbols else 'TBD'}
825
  **Themes:** {', '.join(story_bible.themes[:3]) if story_bible.themes else 'TBD'}
826
+ **Style:** {story_bible.style_guide.get('voice', 'N/A')}
827
  """
828
 
829
  # Previous content summary
 
1317
 
1318
  # --- Main process ---
1319
  def process_novel_stream(self, query: str, language: str,
1320
+ session_id: Optional[str] = None) -> Generator[Tuple[str, List[Dict[str, Any]], str], None, None]:
1321
+ """Single writer novel generation process"""
1322
  try:
1323
  resume_from_stage = 0
1324
  if session_id:
 
1342
  "status": s['status'],
1343
  "content": s.get('content', ''),
1344
  "word_count": s.get('word_count', 0),
1345
+ "momentum": s.get('narrative_momentum', 0.0)
1346
+ } for s in NovelDatabase.get_stages(self.current_session_id)]
 
 
1347
 
1348
  total_words = NovelDatabase.get_total_words(self.current_session_id)
1349
 
 
1355
  "status": "active",
1356
  "content": "",
1357
  "word_count": 0,
1358
+ "momentum": 0.0
 
 
 
 
 
 
 
 
1359
  })
1360
  else:
1361
  stages[stage_idx]["status"] = "active"
 
 
 
 
 
 
1362
 
1363
+ yield f"🔄 Processing... (Current {total_words:,} words)", stages, self.current_session_id
 
 
 
 
 
 
 
 
 
 
 
 
 
1364
 
1365
  prompt = self.get_stage_prompt(stage_idx, role, query, language, stages)
1366
  stage_content = ""
 
1369
  stage_content += chunk
1370
  stages[stage_idx]["content"] = stage_content
1371
  stages[stage_idx]["word_count"] = len(stage_content.split())
1372
+ yield f"🔄 {stage_name} writing... ({total_words + stages[stage_idx]['word_count']:,} words)", stages, self.current_session_id
 
 
 
1373
 
1374
  # Content processing and tracking
1375
  if role == "writer":
 
1394
 
1395
  NovelDatabase.save_narrative_tracker(self.current_session_id, self.narrative_tracker)
1396
  total_words = NovelDatabase.get_total_words(self.current_session_id)
1397
+ yield f"✅ {stage_name} completed (Total {total_words:,} words)", stages, self.current_session_id
 
 
 
 
1398
 
1399
  # Final processing
1400
  final_novel = NovelDatabase.get_writer_content(self.current_session_id)
 
1402
  final_report = self.generate_literary_report(final_novel, final_word_count, language)
1403
 
1404
  NovelDatabase.update_final_novel(self.current_session_id, final_novel, final_report)
1405
+ yield f"✅ Novel completed! Total {final_word_count:,} words", stages, self.current_session_id
 
 
 
 
 
 
 
 
 
 
 
 
 
1406
 
1407
  except Exception as e:
1408
  logger.error(f"Novel generation process error: {e}", exc_info=True)
1409
+ yield f"❌ Error occurred: {e}", stages if 'stages' in locals() else [], self.current_session_id
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1410
 
1411
  def get_stage_prompt(self, stage_idx: int, role: str, query: str,
1412
  language: str, stages: List[Dict]) -> str:
 
1517
 
1518
 
1519
  # --- Utility functions ---
1520
+ def process_query(query: str, language: str, session_id: Optional[str] = None) -> Generator[Tuple[str, str, str, str], None, None]:
1521
+ """Main query processing function"""
1522
  if not query.strip():
1523
+ yield "", "", "❌ Please enter a theme.", session_id
 
 
 
 
 
 
1524
  return
1525
 
1526
  system = UnifiedLiterarySystem()
1527
+ stages_markdown = ""
1528
+ novel_content = ""
1529
 
1530
+ for status, stages, current_session_id in system.process_novel_stream(query, language, session_id):
1531
+ stages_markdown = format_stages_display(stages)
1532
 
1533
  # Get final novel content
1534
+ if stages and all(s.get("status") == "complete" for s in stages[-10:]):
1535
+ novel_content = NovelDatabase.get_writer_content(current_session_id)
 
1536
  novel_content = format_novel_display(novel_content)
1537
 
1538
+ yield stages_markdown, novel_content, status or "🔄 Processing...", current_session_id
 
 
 
 
 
 
1539
 
1540
  def get_active_sessions(language: str) -> List[str]:
1541
  """Get active session list"""
 
1543
  return [f"{s['session_id'][:8]}... - {s['user_query'][:50]}... ({s['created_at']}) [{s['total_words']:,} words]"
1544
  for s in sessions]
1545
 
1546
+ def auto_recover_session(language: str) -> Tuple[Optional[str], str]:
1547
  """Auto-recover recent session"""
1548
  sessions = NovelDatabase.get_active_sessions()
1549
  if sessions:
1550
  latest_session = sessions[0]
1551
+ return latest_session['session_id'], f"Session {latest_session['session_id'][:8]}... recovered"
1552
+ return None, "No session to recover."
1553
 
1554
+ def resume_session(session_id: str, language: str) -> Generator[Tuple[str, str, str, str], None, None]:
1555
  """Resume session"""
1556
  if not session_id:
1557
+ yield "", "", "❌ No session ID.", session_id
 
 
 
 
 
 
1558
  return
1559
 
1560
  if "..." in session_id:
 
1562
 
1563
  session = NovelDatabase.get_session(session_id)
1564
  if not session:
1565
+ yield "", "", "❌ Session not found.", None
 
 
 
 
 
 
1566
  return
1567
 
1568
  yield from process_query(session['user_query'], session['language'], session_id)
 
1584
  logger.error(f"File generation failed: {e}")
1585
  return None
1586
 
1587
+ def format_stages_display(stages: List[Dict]) -> str:
1588
+ """Stage progress display - For single writer system"""
1589
+ markdown = "## 🎬 Progress Status\n\n"
1590
+
1591
+ # Calculate total word count (writer stages only)
1592
+ total_words = sum(s.get('word_count', 0) for s in stages
1593
+ if s.get('name', '').startswith('✍️ Writer:') and 'Revision' in s.get('name', ''))
1594
+ markdown += f"**Total Word Count: {total_words:,} / {TARGET_WORDS:,}**\n\n"
1595
+
1596
+ # Progress summary
1597
+ completed_parts = sum(1 for s in stages
 
 
 
 
 
 
 
 
 
 
 
1598
  if 'Revision' in s.get('name', '') and s.get('status') == 'complete')
1599
+ markdown += f"**Completed Parts: {completed_parts} / 10**\n\n"
1600
+
1601
+ # Average narrative momentum
1602
+ momentum_scores = [s.get('momentum', 0) for s in stages if s.get('momentum', 0) > 0]
1603
+ if momentum_scores:
1604
+ avg_momentum = sum(momentum_scores) / len(momentum_scores)
1605
+ markdown += f"**Average Narrative Momentum: {avg_momentum:.1f} / 10**\n\n"
1606
+
1607
+ markdown += "---\n\n"
1608
+
1609
+ # Display each stage
1610
+ current_part = 0
1611
+ for i, stage in enumerate(stages):
1612
+ status_icon = "✅" if stage['status'] == 'complete' else "🔄" if stage['status'] == 'active' else "⏳"
1613
+
1614
+ # Add part divider
1615
+ if 'Part' in stage.get('name', '') and 'Critic' not in stage.get('name', ''):
1616
+ part_match = re.search(r'Part (\d+)', stage['name'])
1617
+ if part_match:
1618
+ new_part = int(part_match.group(1))
1619
+ if new_part != current_part:
1620
+ current_part = new_part
1621
+ markdown += f"\n### 📚 Part {current_part}\n\n"
1622
+
1623
+ markdown += f"{status_icon} **{stage['name']}**"
1624
+
1625
+ if stage.get('word_count', 0) > 0:
1626
+ markdown += f" ({stage['word_count']:,} words)"
1627
+
1628
+ if stage.get('momentum', 0) > 0:
1629
+ markdown += f" [Momentum: {stage['momentum']:.1f}/10]"
1630
+
1631
+ markdown += "\n"
1632
+
1633
+ if stage['content'] and stage['status'] == 'complete':
1634
+ # Adjust preview length by role
1635
+ preview_length = 300 if 'writer' in stage.get('name', '').lower() else 200
1636
+ preview = stage['content'][:preview_length] + "..." if len(stage['content']) > preview_length else stage['content']
1637
+ markdown += f"> {preview}\n\n"
1638
+ elif stage['status'] == 'active':
1639
+ markdown += "> *Writing...*\n\n"
1640
+
1641
+ return markdown
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1642
 
1643
  def format_novel_display(novel_text: str) -> str:
1644
  """Display novel content - Enhanced part separation"""
 
1689
  # Generate title from session info
1690
  session = NovelDatabase.get_session(session_id)
1691
 
1692
+ # Title generation function
1693
  def generate_title(user_query: str, content_preview: str) -> str:
1694
  """Generate title based on theme and content"""
1695
+ # Simple rule-based title generation (could use LLM)
1696
  if len(user_query) < 20:
1697
  return user_query
1698
  else:
 
1738
  # Remove part titles/numbers patterns
1739
  patterns_to_remove = [
1740
  r'^#{1,6}\s+.*', # Markdown headers
1741
+ r'^\*\*.*\*\*', # 굵은 글씨 **text**
1742
+ r'^Part\s*\d+.*', # Part 1 …” 형식
1743
+ r'^\d+\.\s+.*:.*', # 1. 제목: …” 형식
1744
+ r'^---+', # 구분선
1745
+ r'^\s*\[.*\]\s*', # 대괄호 라벨
1746
  ]
1747
+
1748
+
1749
 
1750
  lines = text.split('\n')
1751
  cleaned_lines = []
 
1829
 
1830
  return filepath
1831
 
1832
+ # CSS styles
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1833
  # CSS styles - Writer's Study Theme (Light & Warm)
1834
  custom_css = """
1835
  /* Global container - Light paper background */
 
2004
  color: #5a453a;
2005
  }
2006
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2007
  /* Download section - Book binding style */
2008
  .download-section {
2009
  background: linear-gradient(145deg, #faf6f0 0%, #f5efe6 100%);
 
2598
  summary += f"{theme_elements.get('exploration', '')}"
2599
 
2600
  return summary.strip()
2601
+
2602
+ # Create Gradio interface
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2603
  # Create Gradio interface - Writer's Study Theme
2604
  def create_interface():
2605
  # Using Soft theme with safe color options
 
2625
  </p>
2626
 
2627
  <div class="progress-note">
2628
+ 🎲 <strong>Novel Theme Random Generator:</strong> This system can generate up to approximately 170 quadrillion (1.7 × 10¹⁷) unique novel themes.
2629
+ Even writing 100 novels per day, it would take 4.6 million years to exhaust all combinations.
2630
+ Click the "Random" button to explore infinite creative possibilities!
2631
  </div>
2632
 
2633
  <div class="warning-note">
 
2717
  visible=False,
2718
  elem_id="download_file"
2719
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2720
 
2721
  # Hidden state
2722
  novel_text_state = gr.State("")
 
2749
  return gr.update(choices=[])
2750
 
2751
  def handle_auto_recover(language):
2752
+ session_id, message = auto_recover_session(language)
2753
+ return session_id, message
2754
+
2755
+ def handle_random_theme(language):
2756
+ """Handle random theme generation with language support"""
2757
+ import time
2758
+ import datetime
2759
+ time.sleep(0.05)
2760
+ logger.info(f"Random theme requested at {datetime.datetime.now()}")
2761
+ theme = generate_random_theme(language)
2762
+ logger.info(f"Generated theme: {theme[:100]}...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2763
  return theme
2764
 
2765
  # Event connections
2766
  submit_btn.click(
2767
+ fn=process_query,
2768
  inputs=[query_input, language_select, current_session_id],
2769
  outputs=[stages_display, novel_output, status_text, current_session_id]
2770
  )
 
2776
  )
2777
 
2778
  resume_btn.click(
2779
+ fn=lambda x: x.split("...")[0] if x and "..." in x else x,
2780
+ inputs=[session_dropdown],
2781
+ outputs=[current_session_id]
2782
+ ).then(
2783
+ fn=resume_session,
2784
+ inputs=[current_session_id, language_select],
2785
  outputs=[stages_display, novel_output, status_text, current_session_id]
2786
  )
2787
 
 
2790
  inputs=[language_select],
2791
  outputs=[current_session_id, status_text]
2792
  ).then(
2793
+ fn=resume_session,
2794
  inputs=[current_session_id, language_select],
2795
  outputs=[stages_display, novel_output, status_text, current_session_id]
2796
  )
 
2806
  )
2807
 
2808
  random_btn.click(
2809
+ fn=lambda lang: generate_random_theme(lang),
2810
  inputs=[language_select],
2811
  outputs=[query_input],
2812
  queue=False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2813
  )
2814
 
2815
  def handle_download(format_type, language, session_id, novel_text):
 
2828
  outputs=[download_file]
2829
  )
2830
 
2831
+ # Load sessions on start
2832
  interface.load(
2833
  fn=refresh_sessions,
2834
  outputs=[session_dropdown]
 
 
 
2835
  )
2836
 
2837
  return interface
 
2872
  server_port=7860,
2873
  share=False,
2874
  debug=True
2875
+ )