Update app.py
Browse files
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
|
|
|
|
|
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
|
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 |
-
|
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 |
-
|
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 |
-
|
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[
|
1685 |
-
"""Main query processing function
|
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,
|
1699 |
-
stages_markdown = format_stages_display(stages
|
1700 |
|
1701 |
# Get final novel content
|
1702 |
-
|
1703 |
-
|
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) ->
|
1722 |
"""Auto-recover recent session"""
|
1723 |
sessions = NovelDatabase.get_active_sessions()
|
1724 |
if sessions:
|
1725 |
latest_session = sessions[0]
|
1726 |
-
return
|
1727 |
-
return
|
1728 |
|
1729 |
-
def resume_session(session_id: str, language: str) -> Generator[
|
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]
|
1775 |
-
|
1776 |
-
|
1777 |
-
|
1778 |
-
|
1779 |
-
|
1780 |
-
|
1781 |
-
|
1782 |
-
|
1783 |
-
|
1784 |
-
|
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 |
-
|
1798 |
-
|
1799 |
-
|
1800 |
-
|
1801 |
-
|
1802 |
-
|
1803 |
-
|
1804 |
-
|
1805 |
-
|
1806 |
-
|
1807 |
-
|
1808 |
-
|
1809 |
-
|
1810 |
-
|
1811 |
-
|
1812 |
-
|
1813 |
-
|
1814 |
-
|
1815 |
-
|
1816 |
-
|
1817 |
-
|
1818 |
-
|
1819 |
-
|
1820 |
-
|
1821 |
-
|
1822 |
-
|
1823 |
-
|
1824 |
-
|
1825 |
-
|
1826 |
-
|
1827 |
-
|
1828 |
-
|
1829 |
-
|
1830 |
-
|
1831 |
-
|
1832 |
-
|
1833 |
-
|
1834 |
-
|
1835 |
-
|
1836 |
-
|
1837 |
-
|
1838 |
-
|
1839 |
-
|
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 |
-
#
|
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'^\*\*.*\*\*', #
|
2011 |
-
r'^Part\s*\d+.*', #
|
2012 |
-
r'^\d+\.\s+.*:.*', #
|
2013 |
-
r'^---+', #
|
2014 |
-
r'^\s*\[.*\]\s*', #
|
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 |
-
|
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
|
3206 |
-
|
3207 |
-
Click the "Random" button to
|
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 |
-
|
3344 |
-
return
|
3345 |
-
|
3346 |
-
def
|
3347 |
-
"""Handle
|
3348 |
-
|
3349 |
-
|
3350 |
-
|
3351 |
-
|
3352 |
-
|
3353 |
-
|
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=
|
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=
|
3405 |
-
inputs=[session_dropdown
|
|
|
|
|
|
|
|
|
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=
|
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=
|
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
|
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 |
+
)
|