Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -441,52 +441,6 @@ class NovelDatabase:
|
|
441 |
except:
|
442 |
# Fallback to SQLite default format
|
443 |
return datetime.strptime(datetime_str, "%Y-%m-%d %H:%M:%S")
|
444 |
-
|
445 |
-
@staticmethod
|
446 |
-
def verify_novel_content(session_id: str) -> Dict[str, Any]:
|
447 |
-
"""μΈμ
μ μ 체 μμ€ λ΄μ© κ²μ¦"""
|
448 |
-
with NovelDatabase.get_db() as conn:
|
449 |
-
cursor = conn.cursor()
|
450 |
-
|
451 |
-
# λͺ¨λ μκ° μμ λ³Έ νμΈ
|
452 |
-
cursor.execute(f'''
|
453 |
-
SELECT stage_number, stage_name, LENGTH(content) as content_length, word_count
|
454 |
-
FROM stages
|
455 |
-
WHERE session_id = ? AND stage_number IN ({','.join(map(str, WRITER_REVISION_STAGES))})
|
456 |
-
ORDER BY stage_number
|
457 |
-
''', (session_id,))
|
458 |
-
|
459 |
-
results = []
|
460 |
-
total_length = 0
|
461 |
-
total_words = 0
|
462 |
-
|
463 |
-
for row in cursor.fetchall():
|
464 |
-
results.append({
|
465 |
-
'stage': row['stage_number'],
|
466 |
-
'name': row['stage_name'],
|
467 |
-
'length': row['content_length'] or 0,
|
468 |
-
'words': row['word_count'] or 0
|
469 |
-
})
|
470 |
-
total_length += row['content_length'] or 0
|
471 |
-
total_words += row['word_count'] or 0
|
472 |
-
|
473 |
-
# μ΅μ’
μμ€ νμΈ
|
474 |
-
cursor.execute('''
|
475 |
-
SELECT LENGTH(final_novel) as final_length
|
476 |
-
FROM sessions
|
477 |
-
WHERE session_id = ?
|
478 |
-
''', (session_id,))
|
479 |
-
|
480 |
-
final_row = cursor.fetchone()
|
481 |
-
final_length = final_row['final_length'] if final_row else 0
|
482 |
-
|
483 |
-
return {
|
484 |
-
'writer_stages': results,
|
485 |
-
'total_writer_content': total_length,
|
486 |
-
'total_words': total_words,
|
487 |
-
'final_novel_length': final_length,
|
488 |
-
'expected_words': 14500 # 10 μκ° * 1450 νκ·
|
489 |
-
}
|
490 |
|
491 |
class NovelWritingSystem:
|
492 |
def __init__(self):
|
@@ -1024,149 +978,6 @@ Write a revision reflecting:
|
|
1024 |
Present the revised final version. Never use page markers.
|
1025 |
You MUST maintain 1,400-1,500 words."""
|
1026 |
|
1027 |
-
def create_critic_final_prompt(self, all_content: str, director_plan: str, language: str = "English") -> str:
|
1028 |
-
"""Final critic evaluation of complete novel"""
|
1029 |
-
content_preview = all_content[:3000] + "\n...\n" + all_content[-3000:] if len(all_content) > 6000 else all_content
|
1030 |
-
|
1031 |
-
if language == "Korean":
|
1032 |
-
return f"""μ 체 30νμ΄μ§ μμ€μ μ΅μ’
νκ°ν©λλ€.
|
1033 |
-
|
1034 |
-
κ°λ
μμ λ§μ€ν°νλ:
|
1035 |
-
{director_plan}
|
1036 |
-
|
1037 |
-
μμ±λ μ 체 μμ€ (미리보기):
|
1038 |
-
{content_preview}
|
1039 |
-
|
1040 |
-
μ΄ λΆλ: {len(all_content.split())} λ¨μ΄
|
1041 |
-
|
1042 |
-
μ’
ν©μ μΈ νκ°μ μ΅μ’
κ°μ μ μμ μ μνμΈμ:
|
1043 |
-
|
1044 |
-
1. **μ 체μ μμ±λ νκ°**
|
1045 |
-
| νλͺ© | μ μ(10μ ) | νκ° | κ°μ νμ |
|
1046 |
-
|------|-----------|------|----------|
|
1047 |
-
| νλ‘― μΌκ΄μ± | | | |
|
1048 |
-
| μΈλ¬Ό λ°μ | | | |
|
1049 |
-
| μ£Όμ μ λ¬ | | | |
|
1050 |
-
| 문체 ν΅μΌμ± | | | |
|
1051 |
-
| λ
μ λͺ°μ
λ | | | |
|
1052 |
-
|
1053 |
-
2. **κ°μ λΆμ**
|
1054 |
-
- κ°μ₯ ν¨κ³Όμ μΈ λΆλΆ
|
1055 |
-
- λ°μ΄λ μ₯λ©΄μ΄λ λν
|
1056 |
-
- μ±κ³΅μ μΈ μΈλ¬Ό λ¬μ¬
|
1057 |
-
|
1058 |
-
3. **μ½μ λ° κ°μ μ **
|
1059 |
-
- μ 체μ νλ¦μ λ¬Έμ
|
1060 |
-
- λ―Έν΄κ²° νλ‘―
|
1061 |
-
- μΊλ¦ν° μΌκ΄μ± μ΄μ
|
1062 |
-
- νμ΄μ± λ¬Έμ
|
1063 |
-
|
1064 |
-
4. **ννΈλ³ μ°κ²°μ±**
|
1065 |
-
| μ°κ²°λΆ | μμ°μ€λ¬μ | λ¬Έμ μ | κ°μ μ μ |
|
1066 |
-
|--------|-----------|--------|----------|
|
1067 |
-
|
1068 |
-
5. **μ΅μ’
κΆκ³ μ¬ν**
|
1069 |
-
- μ¦μ μμ μ΄ νμν μ€λ μ€λ₯
|
1070 |
-
- μ 체μ νμ§ ν₯μμ μν μ μ
|
1071 |
-
- μΆν κ°λ₯μ± νκ°
|
1072 |
-
|
1073 |
-
κ°λ
μκ° μ΅μ’
μμ ν μ μλλ‘ κ΅¬μ²΄μ μ΄κ³ μ€ν κ°λ₯ν νΌλλ°±μ μ 곡νμΈμ."""
|
1074 |
-
else:
|
1075 |
-
return f"""Final evaluation of the complete 30-page novel.
|
1076 |
-
|
1077 |
-
Director's Masterplan:
|
1078 |
-
{director_plan}
|
1079 |
-
|
1080 |
-
Complete Novel (Preview):
|
1081 |
-
{content_preview}
|
1082 |
-
|
1083 |
-
Total length: {len(all_content.split())} words
|
1084 |
-
|
1085 |
-
Provide comprehensive evaluation and final improvement suggestions:
|
1086 |
-
|
1087 |
-
1. **Overall Completion Assessment**
|
1088 |
-
| Item | Score(10) | Evaluation | Improvement Needed |
|
1089 |
-
|------|-----------|------------|-------------------|
|
1090 |
-
| Plot Consistency | | | |
|
1091 |
-
| Character Development | | | |
|
1092 |
-
| Theme Delivery | | | |
|
1093 |
-
| Style Unity | | | |
|
1094 |
-
| Reader Engagement | | | |
|
1095 |
-
|
1096 |
-
2. **Strength Analysis**
|
1097 |
-
- Most effective parts
|
1098 |
-
- Outstanding scenes or dialogue
|
1099 |
-
- Successful character portrayal
|
1100 |
-
|
1101 |
-
3. **Weaknesses and Improvements**
|
1102 |
-
- Overall flow issues
|
1103 |
-
- Unresolved plots
|
1104 |
-
- Character consistency issues
|
1105 |
-
- Pacing problems
|
1106 |
-
|
1107 |
-
4. **Part Connectivity**
|
1108 |
-
| Connection | Smoothness | Issues | Suggestions |
|
1109 |
-
|------------|------------|--------|-------------|
|
1110 |
-
|
1111 |
-
5. **Final Recommendations**
|
1112 |
-
- Critical errors needing immediate fix
|
1113 |
-
- Suggestions for overall quality improvement
|
1114 |
-
- Publication readiness assessment
|
1115 |
-
|
1116 |
-
Provide specific and actionable feedback for the director's final revision."""
|
1117 |
-
|
1118 |
-
def create_director_final_prompt(self, all_content: str, critic_final_feedback: str, language: str = "English") -> str:
|
1119 |
-
"""Director's final compilation - λ©νλ°μ΄ν°λ§ μμ±"""
|
1120 |
-
word_count = len(all_content.split())
|
1121 |
-
|
1122 |
-
# μ 체 λ΄μ©μ ν¬ν¨νμ§ μκ³ λ©νλ°μ΄ν°λ§ μμ²
|
1123 |
-
if language == "Korean":
|
1124 |
-
return f"""κ°λ
μλ‘μ λΉνκ°μ μ΅μ’
νκ°λ₯Ό λ°μνμ¬ μμ€μ λ©νλ°μ΄ν°λ₯Ό μμ±ν©λλ€.
|
1125 |
-
|
1126 |
-
μ΄ λ¨μ΄ μ: {word_count}λ¨μ΄
|
1127 |
-
|
1128 |
-
λΉνκ° μ΅μ’
νκ°:
|
1129 |
-
{critic_final_feedback[:1000]}
|
1130 |
-
|
1131 |
-
λ€μ μ λ³΄λ§ κ°λ¨ν μ 곡νμΈμ:
|
1132 |
-
|
1133 |
-
# μμ€ μ 보
|
1134 |
-
- μ λͺ©: [μμ€ μ λͺ©]
|
1135 |
-
- μ₯λ₯΄: [μ₯λ₯΄]
|
1136 |
-
- μ£Όμ : [ν΅μ¬ μ£Όμ ]
|
1137 |
-
- ν μ€ μμ½: [κ°λ¨ν μμ½]
|
1138 |
-
|
1139 |
-
## λ±μ₯μΈλ¬Ό μκ°
|
1140 |
-
[μ£Όμ μΈλ¬Ό 3-4λͺ
μ κ°λ¨ν μκ° - κ° 2-3μ€]
|
1141 |
-
|
1142 |
-
## μκ°μ λ§
|
1143 |
-
[μνμ λν κ°λ¨ν ν΄μ€ - 5μ€ μ΄λ΄]
|
1144 |
-
|
1145 |
-
μ£Όμ: λ³Έλ¬Έμ μμ±νμ§ λ§μΈμ. λ©νλ°μ΄ν°λ§ μ 곡νμΈμ."""
|
1146 |
-
else:
|
1147 |
-
return f"""As director, create metadata for the final novel reflecting the critic's evaluation.
|
1148 |
-
|
1149 |
-
Total word count: {word_count} words
|
1150 |
-
|
1151 |
-
Critic's Final Evaluation:
|
1152 |
-
{critic_final_feedback[:1000]}
|
1153 |
-
|
1154 |
-
Provide only the following information briefly:
|
1155 |
-
|
1156 |
-
# Novel Information
|
1157 |
-
- Title: [Novel Title]
|
1158 |
-
- Genre: [Genre]
|
1159 |
-
- Theme: [Core Theme]
|
1160 |
-
- One-line summary: [Brief summary]
|
1161 |
-
|
1162 |
-
## Character Introduction
|
1163 |
-
[Brief introduction of 3-4 main characters - 2-3 lines each]
|
1164 |
-
|
1165 |
-
## Author's Note
|
1166 |
-
[Brief commentary about the work - within 5 lines]
|
1167 |
-
|
1168 |
-
Note: Do not write the main text. Provide metadata only."""
|
1169 |
-
|
1170 |
def simulate_streaming(self, text: str, role: str) -> Generator[str, None, None]:
|
1171 |
"""Simulate streaming in test mode"""
|
1172 |
words = text.split()
|
@@ -1223,10 +1034,6 @@ Note: Do not write the main text. Provide metadata only."""
|
|
1223 |
max_tokens = 10000 # μΆ©λΆν ν ν°
|
1224 |
temperature = 0.8
|
1225 |
top_p = 0.95
|
1226 |
-
elif role == "director" and ("μ΅μ’
" in str(messages) or "final" in str(messages)):
|
1227 |
-
max_tokens = 30000 # μ΅μ’
ν΅ν©μ μν μΆ©λΆν ν ν°
|
1228 |
-
temperature = 0.6
|
1229 |
-
top_p = 0.9
|
1230 |
else:
|
1231 |
max_tokens = 8000
|
1232 |
temperature = 0.6
|
@@ -1558,7 +1365,7 @@ Seoyeon leaned back in her chair and closed her eyes for a moment. Fatigue penet
|
|
1558 |
def process_novel_stream(self, query: str, language: str = "English",
|
1559 |
session_id: Optional[str] = None,
|
1560 |
resume_from_stage: int = 0) -> Generator[Tuple[str, List[Dict[str, str]]], None, None]:
|
1561 |
-
"""Process novel writing with streaming updates
|
1562 |
try:
|
1563 |
global conversation_history
|
1564 |
|
@@ -1594,7 +1401,7 @@ Seoyeon leaned back in her chair and closed her eyes for a moment. Fatigue penet
|
|
1594 |
"content": stage_data['content'] or ""
|
1595 |
})
|
1596 |
|
1597 |
-
# Define all stages for 10 writers
|
1598 |
stage_definitions = [
|
1599 |
("director", f"π¬ {'κ°λ
μ: μ΄κΈ° κΈ°ν' if language == 'Korean' else 'Director: Initial Planning'}"),
|
1600 |
("critic", f"π {'λΉνκ°: κΈ°ν κ²ν ' if language == 'Korean' else 'Critic: Plan Review'}"),
|
@@ -1609,10 +1416,7 @@ Seoyeon leaned back in her chair and closed her eyes for a moment. Fatigue penet
|
|
1609 |
(f"writer{writer_num}", f"βοΈ {'μμ±μ' if language == 'Korean' else 'Writer'} {writer_num}: {'μμ λ³Έ' if language == 'Korean' else 'Revision'}")
|
1610 |
])
|
1611 |
|
1612 |
-
|
1613 |
-
("critic", f"π {'λΉνκ°: μ΅μ’
νκ°' if language == 'Korean' else 'Critic: Final Evaluation'}"),
|
1614 |
-
("director", f"π¬ {'κ°λ
μ: μ΅μ’
μμ±λ³Έ' if language == 'Korean' else 'Director: Final Version'}")
|
1615 |
-
])
|
1616 |
|
1617 |
# Store total stages for get_stage_prompt
|
1618 |
self.total_stages = len(stage_definitions)
|
@@ -1686,23 +1490,15 @@ Seoyeon leaned back in her chair and closed her eyes for a moment. Fatigue penet
|
|
1686 |
if verification['total_words'] < 12000:
|
1687 |
logger.error(f"Final novel too short! Only {verification['total_words']} words")
|
1688 |
|
1689 |
-
# Get final novel from last stage
|
1690 |
-
final_novel_metadata = stages[-1]["content"] if stages else ""
|
1691 |
-
|
1692 |
# Get complete novel from DB
|
1693 |
complete_novel = NovelDatabase.get_all_writer_content(self.current_session_id)
|
1694 |
|
1695 |
-
# Combine metadata and complete content for display
|
1696 |
-
if final_novel_metadata and complete_novel:
|
1697 |
-
final_novel = f"{final_novel_metadata}\n\n---\n\n## Complete Novel\n\n{complete_novel}"
|
1698 |
-
else:
|
1699 |
-
final_novel = complete_novel or final_novel_metadata
|
1700 |
-
|
1701 |
# Save final novel to DB
|
1702 |
NovelDatabase.update_final_novel(self.current_session_id, complete_novel)
|
1703 |
|
1704 |
-
# Final yield
|
1705 |
-
|
|
|
1706 |
|
1707 |
except Exception as e:
|
1708 |
logger.error(f"Error in process_novel_stream: {str(e)}", exc_info=True)
|
@@ -1763,43 +1559,24 @@ Seoyeon leaned back in her chair and closed her eyes for a moment. Fatigue penet
|
|
1763 |
language
|
1764 |
)
|
1765 |
|
1766 |
-
# Critic stages
|
1767 |
elif role == "critic":
|
1768 |
final_plan = stages[2]["content"]
|
1769 |
|
1770 |
-
# Final evaluation
|
1771 |
-
if "μ΅μ’
" in stages[stage_idx]["name"] or "Final" in stages[stage_idx]["name"]:
|
1772 |
-
# Get all writer content from DB
|
1773 |
-
all_writer_content = NovelDatabase.get_all_writer_content(self.current_session_id)
|
1774 |
-
logger.info(f"Final evaluation with {len(all_writer_content)} characters of content")
|
1775 |
-
return self.create_critic_final_prompt(all_writer_content, final_plan, language)
|
1776 |
-
|
1777 |
# Writer review
|
1778 |
-
|
1779 |
-
|
1780 |
-
|
1781 |
-
|
1782 |
-
|
1783 |
-
|
1784 |
-
|
1785 |
-
|
1786 |
-
|
1787 |
-
|
1788 |
-
|
1789 |
-
|
1790 |
-
|
1791 |
-
)
|
1792 |
-
|
1793 |
-
# Director final - DBμμ λͺ¨λ μκ° λ΄μ© κ°μ Έμ€κΈ°
|
1794 |
-
elif stage_idx == self.total_stages - 1:
|
1795 |
-
critic_final_idx = stage_idx - 1
|
1796 |
-
all_writer_content = NovelDatabase.get_all_writer_content(self.current_session_id)
|
1797 |
-
logger.info(f"Final director compilation with {len(all_writer_content)} characters of content")
|
1798 |
-
return self.create_director_final_prompt(
|
1799 |
-
all_writer_content,
|
1800 |
-
stages[critic_final_idx]["content"],
|
1801 |
-
language
|
1802 |
-
)
|
1803 |
|
1804 |
return ""
|
1805 |
|
@@ -1833,13 +1610,13 @@ def process_query(query: str, language: str, session_id: str = None) -> Generato
|
|
1833 |
total = len(stages)
|
1834 |
progress_percent = (completed / total * 100) if total > 0 else 0
|
1835 |
|
1836 |
-
if
|
|
|
|
|
1837 |
if language == "Korean":
|
1838 |
status = f"π μ§νμ€... ({completed}/{total} - {progress_percent:.1f}%)"
|
1839 |
else:
|
1840 |
status = f"π Processing... ({completed}/{total} - {progress_percent:.1f}%)"
|
1841 |
-
else:
|
1842 |
-
status = "β
Complete!"
|
1843 |
|
1844 |
yield stages_display, final_novel, status, recovery_status
|
1845 |
|
@@ -1889,165 +1666,6 @@ def get_active_sessions(language: str) -> List[Tuple[str, str]]:
|
|
1889 |
logger.error(f"Error getting active sessions: {str(e)}", exc_info=True)
|
1890 |
return []
|
1891 |
|
1892 |
-
def get_chapter_content(session_id: str, chapter: str) -> str:
|
1893 |
-
"""Get content of a specific chapter"""
|
1894 |
-
if not session_id or not chapter:
|
1895 |
-
return "No session or chapter selected"
|
1896 |
-
|
1897 |
-
try:
|
1898 |
-
chapter_num = int(chapter.split()[-1])
|
1899 |
-
|
1900 |
-
with NovelDatabase.get_db() as conn:
|
1901 |
-
cursor = conn.cursor()
|
1902 |
-
# Get the writer's revision (final version)
|
1903 |
-
cursor.execute('''
|
1904 |
-
SELECT content, word_count, updated_at
|
1905 |
-
FROM stages
|
1906 |
-
WHERE session_id = ?
|
1907 |
-
AND role = ?
|
1908 |
-
AND stage_name LIKE '%Revision%'
|
1909 |
-
ORDER BY stage_number DESC
|
1910 |
-
LIMIT 1
|
1911 |
-
''', (session_id, f'writer{chapter_num}'))
|
1912 |
-
|
1913 |
-
row = cursor.fetchone()
|
1914 |
-
if row and row['content']:
|
1915 |
-
content = row['content']
|
1916 |
-
word_count = row['word_count'] or len(content.split())
|
1917 |
-
updated = row['updated_at']
|
1918 |
-
|
1919 |
-
# νμ΄μ§ λ§ν¬ μ κ±°
|
1920 |
-
content = re.sub(r'\[(?:νμ΄μ§|Page|page)\s*\d+\]', '', content)
|
1921 |
-
content = re.sub(r'(?:νμ΄μ§|Page)\s*\d+:', '', content)
|
1922 |
-
|
1923 |
-
return f"""## Chapter {chapter_num} (Pages {(chapter_num-1)*3+1}-{chapter_num*3})
|
1924 |
-
|
1925 |
-
**Word Count:** {word_count:,} words
|
1926 |
-
**Last Updated:** {updated}
|
1927 |
-
|
1928 |
-
---
|
1929 |
-
|
1930 |
-
{content}"""
|
1931 |
-
else:
|
1932 |
-
return f"Chapter {chapter_num} not found or not yet written"
|
1933 |
-
except Exception as e:
|
1934 |
-
logger.error(f"Error getting chapter content: {str(e)}")
|
1935 |
-
return f"Error: {str(e)}"
|
1936 |
-
|
1937 |
-
def get_db_sessions_detailed() -> List[Tuple[str, str]]:
|
1938 |
-
"""Get detailed list of all sessions for DB explorer"""
|
1939 |
-
try:
|
1940 |
-
with NovelDatabase.get_db() as conn:
|
1941 |
-
cursor = conn.cursor()
|
1942 |
-
cursor.execute('''
|
1943 |
-
SELECT
|
1944 |
-
s.session_id,
|
1945 |
-
s.user_query,
|
1946 |
-
s.language,
|
1947 |
-
s.created_at,
|
1948 |
-
s.updated_at,
|
1949 |
-
s.status,
|
1950 |
-
s.current_stage,
|
1951 |
-
COUNT(st.id) as total_stages,
|
1952 |
-
SUM(CASE WHEN st.status = 'complete' THEN 1 ELSE 0 END) as completed_stages
|
1953 |
-
FROM sessions s
|
1954 |
-
LEFT JOIN stages st ON s.session_id = st.session_id
|
1955 |
-
GROUP BY s.session_id
|
1956 |
-
ORDER BY s.updated_at DESC
|
1957 |
-
''')
|
1958 |
-
|
1959 |
-
sessions = cursor.fetchall()
|
1960 |
-
|
1961 |
-
choices = []
|
1962 |
-
for session in sessions:
|
1963 |
-
created = NovelDatabase.parse_datetime(session['created_at'])
|
1964 |
-
updated = NovelDatabase.parse_datetime(session['updated_at'])
|
1965 |
-
query_preview = session['user_query'][:40] + "..." if len(session['user_query']) > 40 else session['user_query']
|
1966 |
-
|
1967 |
-
label = (f"[{session['status'].upper()}] {query_preview} | "
|
1968 |
-
f"Created: {created.strftime('%Y-%m-%d %H:%M')} | "
|
1969 |
-
f"Progress: {session['completed_stages']}/{session['total_stages']}")
|
1970 |
-
|
1971 |
-
choices.append((label, session['session_id']))
|
1972 |
-
|
1973 |
-
return choices
|
1974 |
-
except Exception as e:
|
1975 |
-
logger.error(f"Error getting detailed sessions: {str(e)}")
|
1976 |
-
return []
|
1977 |
-
|
1978 |
-
def get_session_details(session_id: str) -> str:
|
1979 |
-
"""Get detailed information about a session"""
|
1980 |
-
if not session_id:
|
1981 |
-
return "No session selected"
|
1982 |
-
|
1983 |
-
try:
|
1984 |
-
session = NovelDatabase.get_session(session_id)
|
1985 |
-
if not session:
|
1986 |
-
return "Session not found"
|
1987 |
-
|
1988 |
-
# Get stages information
|
1989 |
-
stages = NovelDatabase.get_stages(session_id)
|
1990 |
-
|
1991 |
-
# Calculate statistics
|
1992 |
-
total_words = 0
|
1993 |
-
completed_writers = 0
|
1994 |
-
writer_stats = []
|
1995 |
-
|
1996 |
-
for stage in stages:
|
1997 |
-
if stage['role'].startswith('writer') and 'Revision' in stage['stage_name']:
|
1998 |
-
completed_writers += 1
|
1999 |
-
word_count = stage['word_count'] or 0
|
2000 |
-
total_words += word_count
|
2001 |
-
writer_num = int(stage['role'].replace('writer', ''))
|
2002 |
-
writer_stats.append(f"- Writer {writer_num}: {word_count:,} words")
|
2003 |
-
|
2004 |
-
# Build detailed info
|
2005 |
-
info = f"""# Session Details
|
2006 |
-
|
2007 |
-
**Session ID:** `{session_id}`
|
2008 |
-
**Theme:** {session['user_query']}
|
2009 |
-
**Language:** {session['language']}
|
2010 |
-
**Status:** {session['status']}
|
2011 |
-
**Created:** {session['created_at']}
|
2012 |
-
**Last Updated:** {session['updated_at']}
|
2013 |
-
|
2014 |
-
## Progress
|
2015 |
-
- **Current Stage:** {session['current_stage']}
|
2016 |
-
- **Completed Writers:** {completed_writers}/10
|
2017 |
-
- **Total Words:** {total_words:,}
|
2018 |
-
- **Estimated Pages:** ~{total_words/500:.0f}
|
2019 |
-
|
2020 |
-
## Writer Statistics
|
2021 |
-
{"".join(writer_stats) if writer_stats else "No writers completed yet"}
|
2022 |
-
|
2023 |
-
## Search History
|
2024 |
-
"""
|
2025 |
-
|
2026 |
-
# Add search history
|
2027 |
-
with NovelDatabase.get_db() as conn:
|
2028 |
-
cursor = conn.cursor()
|
2029 |
-
cursor.execute('''
|
2030 |
-
SELECT role, query, created_at
|
2031 |
-
FROM search_history
|
2032 |
-
WHERE session_id = ?
|
2033 |
-
ORDER BY created_at DESC
|
2034 |
-
LIMIT 10
|
2035 |
-
''', (session_id,))
|
2036 |
-
|
2037 |
-
searches = cursor.fetchall()
|
2038 |
-
if searches:
|
2039 |
-
info += "\nRecent searches:\n"
|
2040 |
-
for search in searches:
|
2041 |
-
info += f"- [{search['role']}] {search['query']} ({search['created_at']})\n"
|
2042 |
-
else:
|
2043 |
-
info += "\nNo search history available."
|
2044 |
-
|
2045 |
-
return info
|
2046 |
-
|
2047 |
-
except Exception as e:
|
2048 |
-
logger.error(f"Error getting session details: {str(e)}")
|
2049 |
-
return f"Error: {str(e)}"
|
2050 |
-
|
2051 |
def resume_session(session_id: str, language: str) -> Generator[Tuple[str, str, str], None, None]:
|
2052 |
"""Resume an existing session"""
|
2053 |
if not session_id:
|
@@ -2088,100 +1706,98 @@ def download_novel(novel_text: str, format: str, language: str, session_id: str
|
|
2088 |
logger.error(f"Session not found: {session_id}")
|
2089 |
return None
|
2090 |
|
2091 |
-
# λͺ¨λ
|
2092 |
-
|
2093 |
-
|
2094 |
-
# μ΅μ’
κ°λ
μμ λ©νλ°μ΄ν° κ°μ Έμ€κΈ°
|
2095 |
-
metadata = ""
|
2096 |
-
with NovelDatabase.get_db() as conn:
|
2097 |
-
cursor = conn.cursor()
|
2098 |
-
cursor.execute('''
|
2099 |
-
SELECT content FROM stages
|
2100 |
-
WHERE session_id = ? AND role = 'director' AND stage_name LIKE '%Final%'
|
2101 |
-
ORDER BY stage_number DESC LIMIT 1
|
2102 |
-
''', (session_id,))
|
2103 |
-
row = cursor.fetchone()
|
2104 |
-
if row and row['content']:
|
2105 |
-
metadata = row['content']
|
2106 |
|
2107 |
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
2108 |
|
2109 |
-
# ν΅κ³ μ 보
|
2110 |
-
word_count = len(all_writer_content.split())
|
2111 |
-
char_count = len(all_writer_content)
|
2112 |
-
|
2113 |
-
logger.info(f"Exporting novel: {word_count} words, {char_count} characters from DB")
|
2114 |
-
|
2115 |
if format == "DOCX" and DOCX_AVAILABLE:
|
2116 |
# Create DOCX
|
2117 |
doc = Document()
|
2118 |
|
2119 |
-
#
|
2120 |
-
|
2121 |
-
|
2122 |
-
|
2123 |
-
|
2124 |
-
|
2125 |
-
level = len(line.split()[0].strip('#'))
|
2126 |
-
text = line.lstrip('#').strip()
|
2127 |
-
if text and level <= 2: # μ λͺ©κ³Ό μ£Όμ μΉμ
λ§
|
2128 |
-
doc.add_heading(text, level)
|
2129 |
-
elif line.strip() and not line.startswith('['):
|
2130 |
-
doc.add_paragraph(line)
|
2131 |
-
|
2132 |
-
doc.add_page_break()
|
2133 |
-
else:
|
2134 |
-
# κΈ°λ³Έ μ λͺ©
|
2135 |
-
title_para = doc.add_paragraph()
|
2136 |
-
title_run = title_para.add_run('AI Collaborative Novel')
|
2137 |
-
title_run.font.size = Pt(24)
|
2138 |
-
title_run.font.bold = True
|
2139 |
-
title_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
2140 |
-
doc.add_page_break()
|
2141 |
|
2142 |
-
|
2143 |
-
doc.
|
2144 |
-
|
2145 |
-
doc.add_paragraph(
|
2146 |
-
|
2147 |
-
|
2148 |
-
|
2149 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
2150 |
|
2151 |
-
# λͺ©μ°¨
|
2152 |
-
doc.add_heading('Table of Contents', 1)
|
2153 |
-
for i in range(1, 11):
|
2154 |
-
doc.add_paragraph(f'Chapter {i}: Pages {(i-1)*3+1}-{i*3}')
|
2155 |
doc.add_page_break()
|
2156 |
|
2157 |
-
#
|
2158 |
-
|
2159 |
-
|
2160 |
|
|
|
|
|
2161 |
for stage in stages:
|
2162 |
if stage['role'].startswith('writer') and 'Revision' in stage['stage_name']:
|
2163 |
-
|
2164 |
-
|
2165 |
-
# μ±ν° ν€λ
|
2166 |
-
doc.add_heading(f'Chapter {writer_num}', 1)
|
2167 |
-
doc.add_paragraph(f'Pages {(writer_num-1)*3+1}-{writer_num*3}')
|
2168 |
-
doc.add_paragraph(f'Word Count: {stage["word_count"] or "N/A"}')
|
2169 |
-
doc.add_paragraph()
|
2170 |
-
|
2171 |
-
# μκ° λ΄μ©
|
2172 |
content = stage['content'] or ""
|
2173 |
# νμ΄μ§ λ§ν¬ μ κ±°
|
2174 |
content = re.sub(r'\[(?:νμ΄μ§|Page|page)\s*\d+\]', '', content)
|
2175 |
content = re.sub(r'(?:νμ΄μ§|Page)\s*\d+:', '', content)
|
|
|
2176 |
|
2177 |
-
|
2178 |
-
|
2179 |
-
|
2180 |
-
|
2181 |
-
|
2182 |
-
|
2183 |
-
|
2184 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2185 |
|
2186 |
# νμ΄μ§ μ€μ
|
2187 |
for section in doc.sections:
|
@@ -2194,16 +1810,18 @@ def download_novel(novel_text: str, format: str, language: str, session_id: str
|
|
2194 |
|
2195 |
# Save
|
2196 |
temp_dir = tempfile.gettempdir()
|
2197 |
-
|
|
|
2198 |
filepath = os.path.join(temp_dir, filename)
|
2199 |
doc.save(filepath)
|
2200 |
|
2201 |
-
logger.info(f"DOCX saved successfully: {filepath}")
|
2202 |
return filepath
|
2203 |
else:
|
2204 |
# TXT format
|
2205 |
temp_dir = tempfile.gettempdir()
|
2206 |
-
|
|
|
2207 |
filepath = os.path.join(temp_dir, filename)
|
2208 |
|
2209 |
with open(filepath, 'w', encoding='utf-8') as f:
|
@@ -2211,40 +1829,39 @@ def download_novel(novel_text: str, format: str, language: str, session_id: str
|
|
2211 |
f.write("AI COLLABORATIVE NOVEL - COMPLETE VERSION\n")
|
2212 |
f.write("="*60 + "\n")
|
2213 |
f.write(f"Theme: {session['user_query']}\n")
|
2214 |
-
f.write(f"
|
2215 |
-
f.write(f"
|
2216 |
-
f.write(f"Total Pages: ~{word_count/500:.0f}\n")
|
2217 |
-
f.write(f"Export Date: {datetime.now()}\n")
|
2218 |
-
f.write(f"Session ID: {session_id}\n")
|
2219 |
f.write("="*60 + "\n\n")
|
2220 |
|
2221 |
-
|
2222 |
-
|
2223 |
-
f.write("=== METADATA ===\n")
|
2224 |
-
f.write(metadata)
|
2225 |
-
f.write("\n\n" + "="*60 + "\n\n")
|
2226 |
-
|
2227 |
-
# κ° μκ°μ μ΅μ’
λ³Έ
|
2228 |
-
stages = NovelDatabase.get_stages(session_id)
|
2229 |
-
writer_num = 0
|
2230 |
|
|
|
2231 |
for stage in stages:
|
2232 |
if stage['role'].startswith('writer') and 'Revision' in stage['stage_name']:
|
2233 |
-
|
2234 |
-
f.write(f"\n{'='*40}\n")
|
2235 |
-
f.write(f"CHAPTER {writer_num} (Pages {(writer_num-1)*3+1}-{writer_num*3})\n")
|
2236 |
-
f.write(f"Word Count: {stage['word_count'] or 'N/A'}\n")
|
2237 |
-
f.write(f"{'='*40}\n\n")
|
2238 |
-
|
2239 |
content = stage['content'] or ""
|
2240 |
# νμ΄μ§ λ§ν¬ μ κ±°
|
2241 |
content = re.sub(r'\[(?:νμ΄μ§|Page|page)\s*\d+\]', '', content)
|
2242 |
content = re.sub(r'(?:νμ΄μ§|Page)\s*\d+:', '', content)
|
|
|
2243 |
|
2244 |
-
|
2245 |
-
|
2246 |
-
|
2247 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2248 |
return filepath
|
2249 |
|
2250 |
# Custom CSS
|
@@ -2294,21 +1911,10 @@ custom_css = """
|
|
2294 |
padding: 30px;
|
2295 |
border-radius: 12px;
|
2296 |
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
2297 |
-
max-height:
|
2298 |
overflow-y: auto;
|
2299 |
}
|
2300 |
|
2301 |
-
#chapter-content {
|
2302 |
-
background-color: rgba(255, 255, 255, 0.95);
|
2303 |
-
padding: 30px;
|
2304 |
-
border-radius: 12px;
|
2305 |
-
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
2306 |
-
max-height: 800px;
|
2307 |
-
overflow-y: auto;
|
2308 |
-
font-family: Georgia, serif;
|
2309 |
-
line-height: 1.8;
|
2310 |
-
}
|
2311 |
-
|
2312 |
.download-section {
|
2313 |
background-color: rgba(255, 255, 255, 0.9);
|
2314 |
padding: 15px;
|
@@ -2339,12 +1945,12 @@ def create_interface():
|
|
2339 |
AI Collaborative Novel Generation - 10 Writers Edition
|
2340 |
</h3>
|
2341 |
<p style="font-size: 1.1em; color: #ddd; max-width: 800px; margin: 0 auto;">
|
2342 |
-
Enter a theme or prompt, and watch as
|
2343 |
-
The system includes 1 Director, 1 Critic, and 10 Writers
|
2344 |
<br><br>
|
2345 |
<span class="search-indicator">π Web search enabled</span> |
|
2346 |
-
<span class="auto-save-indicator">πΎ Auto-save
|
2347 |
-
<span style="color: #FF9800;">β»οΈ
|
2348 |
</p>
|
2349 |
</div>
|
2350 |
""")
|
@@ -2404,7 +2010,7 @@ def create_interface():
|
|
2404 |
|
2405 |
with gr.Tab("π Final Novel / μ΅μ’
μμ€"):
|
2406 |
novel_output = gr.Markdown(
|
2407 |
-
value="",
|
2408 |
elem_id="novel-output"
|
2409 |
)
|
2410 |
|
@@ -2416,42 +2022,12 @@ def create_interface():
|
|
2416 |
value="DOCX" if DOCX_AVAILABLE else "TXT",
|
2417 |
label="Format / νμ"
|
2418 |
)
|
2419 |
-
download_btn = gr.Button("β¬οΈ Download
|
2420 |
|
2421 |
download_file = gr.File(
|
2422 |
label="Downloaded File / λ€μ΄λ‘λλ νμΌ",
|
2423 |
visible=False
|
2424 |
)
|
2425 |
-
|
2426 |
-
with gr.Tab("π Chapter View / μ±ν°λ³ 보기"):
|
2427 |
-
chapter_select = gr.Dropdown(
|
2428 |
-
label="Select Chapter / μ±ν° μ ν",
|
2429 |
-
choices=[f"Chapter {i}" for i in range(1, 11)],
|
2430 |
-
value="Chapter 1"
|
2431 |
-
)
|
2432 |
-
chapter_content = gr.Markdown(
|
2433 |
-
value="Select a chapter to view its content / μ±ν°λ₯Ό μ ννμ¬ λ΄μ©μ νμΈνμΈμ",
|
2434 |
-
elem_id="chapter-content"
|
2435 |
-
)
|
2436 |
-
|
2437 |
-
with gr.Tab("π Database Explorer / DB νμ"):
|
2438 |
-
with gr.Row():
|
2439 |
-
db_session_select = gr.Dropdown(
|
2440 |
-
label="Select Session / μΈμ
μ ν",
|
2441 |
-
choices=[],
|
2442 |
-
interactive=True
|
2443 |
-
)
|
2444 |
-
db_refresh_btn = gr.Button("π Refresh / μλ‘κ³ μΉ¨")
|
2445 |
-
|
2446 |
-
db_info = gr.Markdown(
|
2447 |
-
value="Select a session to view details / μΈμ
μ μ ννμ¬ μμΈ μ 보λ₯Ό νμΈνμΈμ"
|
2448 |
-
)
|
2449 |
-
|
2450 |
-
db_export_btn = gr.Button("π€ Export Selected Session / μ νλ μΈμ
λ΄λ³΄λ΄κΈ°", visible=False)
|
2451 |
-
db_export_file = gr.File(
|
2452 |
-
label="Exported File / λ΄λ³΄λΈ νμΌ",
|
2453 |
-
visible=False
|
2454 |
-
)
|
2455 |
|
2456 |
# Hidden state for novel text
|
2457 |
novel_text_state = gr.State("")
|
@@ -2484,29 +2060,6 @@ def create_interface():
|
|
2484 |
session_id, message = auto_recover_session(language)
|
2485 |
return session_id
|
2486 |
|
2487 |
-
def update_chapter_content(chapter, session_id):
|
2488 |
-
if session_id:
|
2489 |
-
return get_chapter_content(session_id, chapter)
|
2490 |
-
else:
|
2491 |
-
return "Please complete or select a session first / λ¨Όμ μΈμ
μ μλ£νκ±°λ μ ννμΈμ"
|
2492 |
-
|
2493 |
-
def refresh_db_sessions():
|
2494 |
-
sessions = get_db_sessions_detailed()
|
2495 |
-
return gr.update(choices=sessions)
|
2496 |
-
|
2497 |
-
def show_session_details(session_id):
|
2498 |
-
if session_id:
|
2499 |
-
return get_session_details(session_id), gr.update(visible=True)
|
2500 |
-
else:
|
2501 |
-
return "Select a session to view details", gr.update(visible=False)
|
2502 |
-
|
2503 |
-
def export_db_session(session_id, format_type):
|
2504 |
-
if session_id:
|
2505 |
-
file_path = download_novel("", format_type, "English", session_id)
|
2506 |
-
if file_path:
|
2507 |
-
return gr.update(value=file_path, visible=True)
|
2508 |
-
return gr.update(visible=False)
|
2509 |
-
|
2510 |
# Connect event handlers
|
2511 |
submit_btn.click(
|
2512 |
fn=process_query,
|
@@ -2514,7 +2067,7 @@ def create_interface():
|
|
2514 |
outputs=[stages_display, novel_output, status_text]
|
2515 |
)
|
2516 |
|
2517 |
-
# Update novel text state and
|
2518 |
novel_output.change(
|
2519 |
fn=lambda x: x,
|
2520 |
inputs=[novel_output],
|
@@ -2567,47 +2120,14 @@ def create_interface():
|
|
2567 |
outputs=[download_file]
|
2568 |
)
|
2569 |
|
2570 |
-
# Chapter view handlers
|
2571 |
-
chapter_select.change(
|
2572 |
-
fn=update_chapter_content,
|
2573 |
-
inputs=[chapter_select, current_session_id],
|
2574 |
-
outputs=[chapter_content]
|
2575 |
-
)
|
2576 |
-
|
2577 |
-
# Update chapter content when session changes
|
2578 |
-
current_session_id.change(
|
2579 |
-
fn=update_chapter_content,
|
2580 |
-
inputs=[chapter_select, current_session_id],
|
2581 |
-
outputs=[chapter_content]
|
2582 |
-
)
|
2583 |
-
|
2584 |
-
# DB Explorer handlers
|
2585 |
-
db_refresh_btn.click(
|
2586 |
-
fn=refresh_db_sessions,
|
2587 |
-
outputs=[db_session_select]
|
2588 |
-
)
|
2589 |
-
|
2590 |
-
db_session_select.change(
|
2591 |
-
fn=show_session_details,
|
2592 |
-
inputs=[db_session_select],
|
2593 |
-
outputs=[db_info, db_export_btn]
|
2594 |
-
)
|
2595 |
-
|
2596 |
-
db_export_btn.click(
|
2597 |
-
fn=lambda session_id: export_db_session(session_id, "DOCX"),
|
2598 |
-
inputs=[db_session_select],
|
2599 |
-
outputs=[db_export_file]
|
2600 |
-
)
|
2601 |
-
|
2602 |
# Load sessions on startup
|
2603 |
def on_load():
|
2604 |
sessions = refresh_sessions()
|
2605 |
-
|
2606 |
-
return sessions, db_sessions
|
2607 |
|
2608 |
interface.load(
|
2609 |
fn=on_load,
|
2610 |
-
outputs=[session_dropdown
|
2611 |
)
|
2612 |
|
2613 |
return interface
|
|
|
441 |
except:
|
442 |
# Fallback to SQLite default format
|
443 |
return datetime.strptime(datetime_str, "%Y-%m-%d %H:%M:%S")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
444 |
|
445 |
class NovelWritingSystem:
|
446 |
def __init__(self):
|
|
|
978 |
Present the revised final version. Never use page markers.
|
979 |
You MUST maintain 1,400-1,500 words."""
|
980 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
981 |
def simulate_streaming(self, text: str, role: str) -> Generator[str, None, None]:
|
982 |
"""Simulate streaming in test mode"""
|
983 |
words = text.split()
|
|
|
1034 |
max_tokens = 10000 # μΆ©λΆν ν ν°
|
1035 |
temperature = 0.8
|
1036 |
top_p = 0.95
|
|
|
|
|
|
|
|
|
1037 |
else:
|
1038 |
max_tokens = 8000
|
1039 |
temperature = 0.6
|
|
|
1365 |
def process_novel_stream(self, query: str, language: str = "English",
|
1366 |
session_id: Optional[str] = None,
|
1367 |
resume_from_stage: int = 0) -> Generator[Tuple[str, List[Dict[str, str]]], None, None]:
|
1368 |
+
"""Process novel writing with streaming updates - μ΅μ’
Director/Critic μ κ±°"""
|
1369 |
try:
|
1370 |
global conversation_history
|
1371 |
|
|
|
1401 |
"content": stage_data['content'] or ""
|
1402 |
})
|
1403 |
|
1404 |
+
# Define all stages for 10 writers (μ΅μ’
νκ° μ κ±°)
|
1405 |
stage_definitions = [
|
1406 |
("director", f"π¬ {'κ°λ
μ: μ΄κΈ° κΈ°ν' if language == 'Korean' else 'Director: Initial Planning'}"),
|
1407 |
("critic", f"π {'λΉνκ°: κΈ°ν κ²ν ' if language == 'Korean' else 'Critic: Plan Review'}"),
|
|
|
1416 |
(f"writer{writer_num}", f"βοΈ {'μμ±μ' if language == 'Korean' else 'Writer'} {writer_num}: {'μμ λ³Έ' if language == 'Korean' else 'Revision'}")
|
1417 |
])
|
1418 |
|
1419 |
+
# μ΅μ’
Directorμ Critic λ¨κ³ μ κ±°
|
|
|
|
|
|
|
1420 |
|
1421 |
# Store total stages for get_stage_prompt
|
1422 |
self.total_stages = len(stage_definitions)
|
|
|
1490 |
if verification['total_words'] < 12000:
|
1491 |
logger.error(f"Final novel too short! Only {verification['total_words']} words")
|
1492 |
|
|
|
|
|
|
|
1493 |
# Get complete novel from DB
|
1494 |
complete_novel = NovelDatabase.get_all_writer_content(self.current_session_id)
|
1495 |
|
|
|
|
|
|
|
|
|
|
|
|
|
1496 |
# Save final novel to DB
|
1497 |
NovelDatabase.update_final_novel(self.current_session_id, complete_novel)
|
1498 |
|
1499 |
+
# Final yield - νλ©΄μλ μλ£ λ©μμ§λ§ νμ
|
1500 |
+
final_message = f"β
Novel complete! {len(complete_novel.split())} words total. Click Download to save."
|
1501 |
+
yield final_message, stages
|
1502 |
|
1503 |
except Exception as e:
|
1504 |
logger.error(f"Error in process_novel_stream: {str(e)}", exc_info=True)
|
|
|
1559 |
language
|
1560 |
)
|
1561 |
|
1562 |
+
# Critic stages for writers
|
1563 |
elif role == "critic":
|
1564 |
final_plan = stages[2]["content"]
|
1565 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1566 |
# Writer review
|
1567 |
+
# Find which writer we're reviewing
|
1568 |
+
for i in range(1, 11):
|
1569 |
+
if f"μμ±μ {i}" in stages[stage_idx]["name"] or f"Writer {i}" in stages[stage_idx]["name"]:
|
1570 |
+
writer_content_idx = stage_idx - 1
|
1571 |
+
# Get previous writers' content from DB
|
1572 |
+
previous_content = NovelDatabase.get_all_writer_content(self.current_session_id)
|
1573 |
+
return self.create_critic_writer_prompt(
|
1574 |
+
i,
|
1575 |
+
stages[writer_content_idx]["content"],
|
1576 |
+
final_plan,
|
1577 |
+
previous_content,
|
1578 |
+
language
|
1579 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1580 |
|
1581 |
return ""
|
1582 |
|
|
|
1610 |
total = len(stages)
|
1611 |
progress_percent = (completed / total * 100) if total > 0 else 0
|
1612 |
|
1613 |
+
if "β
Novel complete!" in str(final_novel):
|
1614 |
+
status = "β
Complete! Ready to download."
|
1615 |
+
else:
|
1616 |
if language == "Korean":
|
1617 |
status = f"π μ§νμ€... ({completed}/{total} - {progress_percent:.1f}%)"
|
1618 |
else:
|
1619 |
status = f"π Processing... ({completed}/{total} - {progress_percent:.1f}%)"
|
|
|
|
|
1620 |
|
1621 |
yield stages_display, final_novel, status, recovery_status
|
1622 |
|
|
|
1666 |
logger.error(f"Error getting active sessions: {str(e)}", exc_info=True)
|
1667 |
return []
|
1668 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1669 |
def resume_session(session_id: str, language: str) -> Generator[Tuple[str, str, str], None, None]:
|
1670 |
"""Resume an existing session"""
|
1671 |
if not session_id:
|
|
|
1706 |
logger.error(f"Session not found: {session_id}")
|
1707 |
return None
|
1708 |
|
1709 |
+
# DBμμ λͺ¨λ μ€ν
μ΄μ§ κ°μ Έμ€κΈ°
|
1710 |
+
stages = NovelDatabase.get_stages(session_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1711 |
|
1712 |
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
1713 |
|
|
|
|
|
|
|
|
|
|
|
|
|
1714 |
if format == "DOCX" and DOCX_AVAILABLE:
|
1715 |
# Create DOCX
|
1716 |
doc = Document()
|
1717 |
|
1718 |
+
# μ λͺ© νμ΄μ§
|
1719 |
+
title_para = doc.add_paragraph()
|
1720 |
+
title_run = title_para.add_run('AI Collaborative Novel')
|
1721 |
+
title_run.font.size = Pt(24)
|
1722 |
+
title_run.font.bold = True
|
1723 |
+
title_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1724 |
|
1725 |
+
doc.add_paragraph()
|
1726 |
+
doc.add_paragraph()
|
1727 |
+
|
1728 |
+
theme_para = doc.add_paragraph()
|
1729 |
+
theme_run = theme_para.add_run(f'Theme: {session["user_query"]}')
|
1730 |
+
theme_run.font.size = Pt(14)
|
1731 |
+
theme_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
1732 |
+
|
1733 |
+
doc.add_paragraph()
|
1734 |
+
|
1735 |
+
date_para = doc.add_paragraph()
|
1736 |
+
date_run = date_para.add_run(f'Created: {datetime.now().strftime("%Y-%m-%d")}')
|
1737 |
+
date_run.font.size = Pt(12)
|
1738 |
+
date_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
1739 |
|
|
|
|
|
|
|
|
|
1740 |
doc.add_page_break()
|
1741 |
|
1742 |
+
# μ 체 ν΅κ³
|
1743 |
+
total_words = 0
|
1744 |
+
writer_count = 0
|
1745 |
|
1746 |
+
# κ° μκ°μ μμ λ³Έλ§ μμ§
|
1747 |
+
writer_contents = []
|
1748 |
for stage in stages:
|
1749 |
if stage['role'].startswith('writer') and 'Revision' in stage['stage_name']:
|
1750 |
+
writer_count += 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1751 |
content = stage['content'] or ""
|
1752 |
# νμ΄μ§ λ§ν¬ μ κ±°
|
1753 |
content = re.sub(r'\[(?:νμ΄μ§|Page|page)\s*\d+\]', '', content)
|
1754 |
content = re.sub(r'(?:νμ΄μ§|Page)\s*\d+:', '', content)
|
1755 |
+
content = content.strip()
|
1756 |
|
1757 |
+
if content:
|
1758 |
+
word_count = stage['word_count'] or len(content.split())
|
1759 |
+
total_words += word_count
|
1760 |
+
writer_contents.append({
|
1761 |
+
'writer_num': writer_count,
|
1762 |
+
'content': content,
|
1763 |
+
'word_count': word_count
|
1764 |
+
})
|
1765 |
+
|
1766 |
+
# ν΅κ³ νμ΄μ§
|
1767 |
+
doc.add_heading('Novel Statistics', 1)
|
1768 |
+
doc.add_paragraph(f'Total Writers: {writer_count}')
|
1769 |
+
doc.add_paragraph(f'Total Words: {total_words:,}')
|
1770 |
+
doc.add_paragraph(f'Estimated Pages: ~{total_words/500:.0f}')
|
1771 |
+
doc.add_paragraph(f'Language: {session["language"]}')
|
1772 |
+
doc.add_page_break()
|
1773 |
+
|
1774 |
+
# λͺ©μ°¨
|
1775 |
+
doc.add_heading('Table of Contents', 1)
|
1776 |
+
for i in range(1, writer_count + 1):
|
1777 |
+
doc.add_paragraph(f'Chapter {i}: Pages {(i-1)*3+1}-{i*3}')
|
1778 |
+
doc.add_page_break()
|
1779 |
+
|
1780 |
+
# κ° μκ°μ λ΄μ© μΆκ°
|
1781 |
+
for writer_data in writer_contents:
|
1782 |
+
writer_num = writer_data['writer_num']
|
1783 |
+
content = writer_data['content']
|
1784 |
+
word_count = writer_data['word_count']
|
1785 |
+
|
1786 |
+
# μ±ν° ν€λ
|
1787 |
+
doc.add_heading(f'Chapter {writer_num}', 1)
|
1788 |
+
doc.add_paragraph(f'Pages {(writer_num-1)*3+1}-{writer_num*3}')
|
1789 |
+
doc.add_paragraph(f'Word Count: {word_count:,}')
|
1790 |
+
doc.add_paragraph()
|
1791 |
+
|
1792 |
+
# μκ° λ΄μ© μΆκ°
|
1793 |
+
paragraphs = content.split('\n\n')
|
1794 |
+
for para_text in paragraphs:
|
1795 |
+
if para_text.strip():
|
1796 |
+
para = doc.add_paragraph(para_text.strip())
|
1797 |
+
para.style.font.size = Pt(11)
|
1798 |
+
|
1799 |
+
if writer_num < writer_count: # λ§μ§λ§ μκ° νμλ νμ΄μ§ κ΅¬λΆ μμ
|
1800 |
+
doc.add_page_break()
|
1801 |
|
1802 |
# νμ΄μ§ μ€μ
|
1803 |
for section in doc.sections:
|
|
|
1810 |
|
1811 |
# Save
|
1812 |
temp_dir = tempfile.gettempdir()
|
1813 |
+
safe_filename = re.sub(r'[^\w\s-]', '', session['user_query'][:30]).strip()
|
1814 |
+
filename = f"Novel_Complete_{safe_filename}_{timestamp}.docx"
|
1815 |
filepath = os.path.join(temp_dir, filename)
|
1816 |
doc.save(filepath)
|
1817 |
|
1818 |
+
logger.info(f"DOCX saved successfully: {filepath} ({total_words} words)")
|
1819 |
return filepath
|
1820 |
else:
|
1821 |
# TXT format
|
1822 |
temp_dir = tempfile.gettempdir()
|
1823 |
+
safe_filename = re.sub(r'[^\w\s-]', '', session['user_query'][:30]).strip()
|
1824 |
+
filename = f"Novel_Complete_{safe_filename}_{timestamp}.txt"
|
1825 |
filepath = os.path.join(temp_dir, filename)
|
1826 |
|
1827 |
with open(filepath, 'w', encoding='utf-8') as f:
|
|
|
1829 |
f.write("AI COLLABORATIVE NOVEL - COMPLETE VERSION\n")
|
1830 |
f.write("="*60 + "\n")
|
1831 |
f.write(f"Theme: {session['user_query']}\n")
|
1832 |
+
f.write(f"Language: {session['language']}\n")
|
1833 |
+
f.write(f"Created: {datetime.now()}\n")
|
|
|
|
|
|
|
1834 |
f.write("="*60 + "\n\n")
|
1835 |
|
1836 |
+
total_words = 0
|
1837 |
+
writer_count = 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1838 |
|
1839 |
+
# κ° μκ°μ μμ λ³Έλ§ μΆλ ₯
|
1840 |
for stage in stages:
|
1841 |
if stage['role'].startswith('writer') and 'Revision' in stage['stage_name']:
|
1842 |
+
writer_count += 1
|
|
|
|
|
|
|
|
|
|
|
1843 |
content = stage['content'] or ""
|
1844 |
# νμ΄μ§ λ§ν¬ μ κ±°
|
1845 |
content = re.sub(r'\[(?:νμ΄μ§|Page|page)\s*\d+\]', '', content)
|
1846 |
content = re.sub(r'(?:νμ΄μ§|Page)\s*\d+:', '', content)
|
1847 |
+
content = content.strip()
|
1848 |
|
1849 |
+
if content:
|
1850 |
+
word_count = stage['word_count'] or len(content.split())
|
1851 |
+
total_words += word_count
|
1852 |
+
|
1853 |
+
f.write(f"\n{'='*40}\n")
|
1854 |
+
f.write(f"CHAPTER {writer_count} (Pages {(writer_count-1)*3+1}-{writer_count*3})\n")
|
1855 |
+
f.write(f"Word Count: {word_count:,}\n")
|
1856 |
+
f.write(f"{'='*40}\n\n")
|
1857 |
+
f.write(content)
|
1858 |
+
f.write("\n\n")
|
1859 |
+
|
1860 |
+
f.write(f"\n{'='*60}\n")
|
1861 |
+
f.write(f"TOTAL: {writer_count} writers, {total_words:,} words\n")
|
1862 |
+
f.write(f"{'='*60}\n")
|
1863 |
+
|
1864 |
+
logger.info(f"TXT saved successfully: {filepath} ({total_words} words)")
|
1865 |
return filepath
|
1866 |
|
1867 |
# Custom CSS
|
|
|
1911 |
padding: 30px;
|
1912 |
border-radius: 12px;
|
1913 |
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
1914 |
+
max-height: 400px;
|
1915 |
overflow-y: auto;
|
1916 |
}
|
1917 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1918 |
.download-section {
|
1919 |
background-color: rgba(255, 255, 255, 0.9);
|
1920 |
padding: 15px;
|
|
|
1945 |
AI Collaborative Novel Generation - 10 Writers Edition
|
1946 |
</h3>
|
1947 |
<p style="font-size: 1.1em; color: #ddd; max-width: 800px; margin: 0 auto;">
|
1948 |
+
Enter a theme or prompt, and watch as AI agents collaborate to create a complete 30-page novella.
|
1949 |
+
The system includes 1 Director, 1 Critic, and 10 Writers working in harmony.
|
1950 |
<br><br>
|
1951 |
<span class="search-indicator">π Web search enabled</span> |
|
1952 |
+
<span class="auto-save-indicator">πΎ Auto-save to database</span> |
|
1953 |
+
<span style="color: #FF9800;">β»οΈ Resume anytime</span>
|
1954 |
</p>
|
1955 |
</div>
|
1956 |
""")
|
|
|
2010 |
|
2011 |
with gr.Tab("π Final Novel / μ΅μ’
μμ€"):
|
2012 |
novel_output = gr.Markdown(
|
2013 |
+
value="Complete novel will be available for download after all writers finish.",
|
2014 |
elem_id="novel-output"
|
2015 |
)
|
2016 |
|
|
|
2022 |
value="DOCX" if DOCX_AVAILABLE else "TXT",
|
2023 |
label="Format / νμ"
|
2024 |
)
|
2025 |
+
download_btn = gr.Button("β¬οΈ Download Complete Novel / μμ±λ μμ€ λ€μ΄λ‘λ", variant="secondary")
|
2026 |
|
2027 |
download_file = gr.File(
|
2028 |
label="Downloaded File / λ€μ΄λ‘λλ νμΌ",
|
2029 |
visible=False
|
2030 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2031 |
|
2032 |
# Hidden state for novel text
|
2033 |
novel_text_state = gr.State("")
|
|
|
2060 |
session_id, message = auto_recover_session(language)
|
2061 |
return session_id
|
2062 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2063 |
# Connect event handlers
|
2064 |
submit_btn.click(
|
2065 |
fn=process_query,
|
|
|
2067 |
outputs=[stages_display, novel_output, status_text]
|
2068 |
)
|
2069 |
|
2070 |
+
# Update novel text state and session ID when novel output changes
|
2071 |
novel_output.change(
|
2072 |
fn=lambda x: x,
|
2073 |
inputs=[novel_output],
|
|
|
2120 |
outputs=[download_file]
|
2121 |
)
|
2122 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2123 |
# Load sessions on startup
|
2124 |
def on_load():
|
2125 |
sessions = refresh_sessions()
|
2126 |
+
return sessions
|
|
|
2127 |
|
2128 |
interface.load(
|
2129 |
fn=on_load,
|
2130 |
+
outputs=[session_dropdown]
|
2131 |
)
|
2132 |
|
2133 |
return interface
|