openfree commited on
Commit
e649258
Β·
verified Β·
1 Parent(s): 0473b36

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +134 -614
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 and web search"""
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
- stage_definitions.extend([
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
- yield final_novel, stages
 
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
- else:
1779
- # Find which writer we're reviewing
1780
- for i in range(1, 11):
1781
- if f"μž‘μ„±μž {i}" in stages[stage_idx]["name"] or f"Writer {i}" in stages[stage_idx]["name"]:
1782
- writer_content_idx = stage_idx - 1
1783
- # Get previous writers' content from DB
1784
- previous_content = NovelDatabase.get_all_writer_content(self.current_session_id)
1785
- return self.create_critic_writer_prompt(
1786
- i,
1787
- stages[writer_content_idx]["content"],
1788
- final_plan,
1789
- previous_content,
1790
- language
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 not final_novel:
 
 
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
- all_writer_content = NovelDatabase.get_all_writer_content(session_id)
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
- if metadata:
2121
- # 메타데이터 νŒŒμ‹± 및 μΆ”κ°€
2122
- lines = metadata.split('\n')
2123
- for line in lines:
2124
- if line.startswith('#'):
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.add_heading('Novel Statistics', 1)
2144
- doc.add_paragraph(f'Total Writers: 10')
2145
- doc.add_paragraph(f'Total Words: {word_count:,}')
2146
- doc.add_paragraph(f'Total Pages: ~{word_count/500:.0f}')
2147
- doc.add_paragraph(f'Export Date: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}')
2148
- doc.add_paragraph(f'Session ID: {session_id[:8]}...')
2149
- doc.add_page_break()
 
 
 
 
 
 
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
- stages = NovelDatabase.get_stages(session_id)
2159
- writer_num = 0
2160
 
 
 
2161
  for stage in stages:
2162
  if stage['role'].startswith('writer') and 'Revision' in stage['stage_name']:
2163
- writer_num += 1
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
- paragraphs = content.split('\n\n')
2178
- for para_text in paragraphs:
2179
- if para_text.strip():
2180
- para = doc.add_paragraph(para_text)
2181
- para.style.font.size = Pt(11)
2182
-
2183
- if writer_num < 10: # λ§ˆμ§€λ§‰ μž‘κ°€ ν›„μ—λŠ” νŽ˜μ΄μ§€ ꡬ뢄 μ—†μŒ
2184
- doc.add_page_break()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- filename = f"Novel_Complete_{session['user_query'][:20].replace(' ', '_')}_{timestamp}.docx"
 
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
- filename = f"Novel_Complete_{session['user_query'][:20].replace(' ', '_')}_{timestamp}.txt"
 
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"Total Writers: 10\n")
2215
- f.write(f"Total Words: {word_count:,}\n")
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
- if metadata:
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
- writer_num += 1
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
- f.write(content)
2245
- f.write("\n\n")
2246
-
2247
- logger.info(f"TXT saved successfully: {filepath}")
 
 
 
 
 
 
 
 
 
 
 
 
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: 800px;
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 13 AI agents collaborate to create a complete 30-page novella.
2343
- The system includes 1 Director, 1 Critic, and 10 Writers (each writing 3 pages) working in harmony.
2344
  <br><br>
2345
  <span class="search-indicator">πŸ” Web search enabled</span> |
2346
- <span class="auto-save-indicator">πŸ’Ύ Auto-save enabled</span> |
2347
- <span style="color: #FF9800;">♻️ Auto-recovery on refresh</span>
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 All 10 Writers / 10λͺ… μž‘κ°€ 전체 λ‹€μš΄λ‘œλ“œ", variant="secondary")
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 chapter dropdown when novel output changes
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
- db_sessions = refresh_db_sessions()
2606
- return sessions, db_sessions
2607
 
2608
  interface.load(
2609
  fn=on_load,
2610
- outputs=[session_dropdown, db_session_select]
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