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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +431 -180
app.py CHANGED
@@ -1116,94 +1116,56 @@ Provide comprehensive evaluation and final improvement suggestions:
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 and polish - λͺ¨λ“  μž‘κ°€ λ‚΄μš© 포함"""
1120
  word_count = len(all_content.split())
1121
- expected_words = 14500 # 30νŽ˜μ΄μ§€ * 500단어
1122
 
 
1123
  if language == "Korean":
1124
- return f"""κ°λ…μžλ‘œμ„œ λΉ„ν‰κ°€μ˜ μ΅œμ’… 평가λ₯Ό λ°˜μ˜ν•˜μ—¬ 완성본을 μ œμž‘ν•©λ‹ˆλ‹€.
1125
 
1126
- 전체 μž‘κ°€λ“€μ˜ μž‘ν’ˆ (30νŽ˜μ΄μ§€ 전체, {word_count}단어):
1127
- {all_content}
1128
 
1129
  비평가 μ΅œμ’… 평가:
1130
- {critic_final_feedback}
1131
 
1132
- λ‹€μŒμ„ ν¬ν•¨ν•œ μ΅œμ’… 완성본을 μ œμ‹œν•˜μ„Έμš”:
1133
 
1134
- # [μ†Œμ„€ 제λͺ©]
1135
-
1136
- ## μž‘ν’ˆ 정보
1137
- - μž₯λ₯΄:
1138
- - λΆ„λŸ‰: 30νŽ˜μ΄μ§€ ({word_count}단어)
1139
- - 주제:
1140
- - ν•œ 쀄 μš”μ•½:
1141
 
1142
  ## λ“±μž₯인물 μ†Œκ°œ
1143
- [μ£Όμš” μΈλ¬Όλ“€μ˜ κ°„λ‹¨ν•œ μ†Œκ°œ]
1144
-
1145
- ---
1146
-
1147
- ## λ³Έλ¬Έ
1148
-
1149
- [10λͺ…μ˜ μž‘κ°€κ°€ μž‘μ„±ν•œ 전체 30νŽ˜μ΄μ§€ λ‚΄μš©μ„ λ‹€μŒ κΈ°μ€€μœΌλ‘œ 톡합]
1150
- 1. μ€‘λŒ€ 였λ₯˜ μˆ˜μ • μ™„λ£Œ
1151
- 2. 파트 κ°„ μ—°κ²° λ§€λ„λŸ½κ²Œ μ‘°μ •
1152
- 3. 문체와 톀 톡일
1153
- 4. μ΅œμ’… 퇴고 및 윀문
1154
- 5. νŽ˜μ΄μ§€ ꡬ뢄 ν‘œμ‹œ μ™„μ „ 제거
1155
- 6. μžμ—°μŠ€λŸ¬μš΄ νλ¦„μœΌλ‘œ μž¬κ΅¬μ„±
1156
-
1157
- [전체 30νŽ˜μ΄μ§€ λΆ„λŸ‰μ˜ μ™„μ„±λœ μ†Œμ„€ λ³Έλ¬Έ]
1158
-
1159
- ---
1160
 
1161
  ## μž‘κ°€μ˜ 말
1162
- [μž‘ν’ˆμ— λŒ€ν•œ κ°„λ‹¨ν•œ ν•΄μ„€μ΄λ‚˜ μ˜λ„]
1163
 
1164
- λͺ¨λ“  μž‘κ°€μ˜ κΈ°μ—¬λ₯Ό ν†΅ν•©ν•œ μ™„μ „ν•œ 30νŽ˜μ΄μ§€ μ†Œμ„€μ„ μ œμ‹œν•˜μ„Έμš”."""
1165
  else:
1166
- return f"""As director, create the final version reflecting the critic's final evaluation.
1167
 
1168
- Complete Writers' Work (Full 30 pages, {word_count} words):
1169
- {all_content}
1170
 
1171
  Critic's Final Evaluation:
1172
- {critic_final_feedback}
1173
 
1174
- Present the final version including:
1175
 
1176
- # [Novel Title]
1177
-
1178
- ## Work Information
1179
- - Genre:
1180
- - Length: 30 pages ({word_count} words)
1181
- - Theme:
1182
- - One-line summary:
1183
 
1184
  ## Character Introduction
1185
- [Brief introduction of main characters]
1186
-
1187
- ---
1188
-
1189
- ## Main Text
1190
-
1191
- [Integrate all 30 pages written by 10 writers with these criteria]
1192
- 1. Critical errors corrected
1193
- 2. Smooth transitions between parts
1194
- 3. Unified style and tone
1195
- 4. Final editing and polishing
1196
- 5. Complete removal of page markers
1197
- 6. Reorganized for natural flow
1198
-
1199
- [Complete 30-page novel text]
1200
-
1201
- ---
1202
 
1203
  ## Author's Note
1204
- [Brief commentary or intention about the work]
1205
 
1206
- Present a complete 30-page novel integrating all writers' contributions."""
1207
 
1208
  def simulate_streaming(self, text: str, role: str) -> Generator[str, None, None]:
1209
  """Simulate streaming in test mode"""
@@ -1725,10 +1687,19 @@ Seoyeon leaned back in her chair and closed her eyes for a moment. Fatigue penet
1725
  logger.error(f"Final novel too short! Only {verification['total_words']} words")
1726
 
1727
  # Get final novel from last stage
1728
- final_novel = stages[-1]["content"] if stages else ""
 
 
 
 
 
 
 
 
 
1729
 
1730
  # Save final novel to DB
1731
- NovelDatabase.update_final_novel(self.current_session_id, final_novel)
1732
 
1733
  # Final yield
1734
  yield final_novel, stages
@@ -1918,6 +1889,165 @@ def get_active_sessions(language: str) -> List[Tuple[str, str]]:
1918
  logger.error(f"Error getting active sessions: {str(e)}", exc_info=True)
1919
  return []
1920
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1921
  def resume_session(session_id: str, language: str) -> Generator[Tuple[str, str, str], None, None]:
1922
  """Resume an existing session"""
1923
  if not session_id:
@@ -1947,155 +2077,174 @@ def auto_recover_session(language: str) -> Tuple[str, str]:
1947
  return None, ""
1948
 
1949
  def download_novel(novel_text: str, format: str, language: str, session_id: str = None) -> str:
1950
- """Download novel in specified format - enhanced for 10 writers"""
1951
- if not novel_text and not session_id:
 
1952
  return None
1953
 
1954
- # If session_id provided, get all writer content
1955
- if session_id:
1956
- all_writer_content = NovelDatabase.get_all_writer_content(session_id)
1957
- if all_writer_content:
1958
- novel_text = all_writer_content
1959
-
1960
- if not novel_text:
1961
  return None
1962
 
1963
- # νŽ˜μ΄μ§€ 마크 제거
1964
- novel_text = re.sub(r'\[(?:νŽ˜μ΄μ§€|Page|page)\s*\d+\]', '', novel_text)
1965
- novel_text = re.sub(r'(?:νŽ˜μ΄μ§€|Page)\s*\d+:', '', novel_text)
 
 
 
 
 
 
 
 
 
 
 
 
1966
 
1967
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
1968
- word_count = len(novel_text.split())
1969
- char_count = len(novel_text)
1970
 
1971
- logger.info(f"Exporting novel: {word_count} words, {char_count} characters")
 
 
 
 
1972
 
1973
  if format == "DOCX" and DOCX_AVAILABLE:
1974
  # Create DOCX
1975
  doc = Document()
1976
 
1977
- # Title page
1978
- title_para = doc.add_paragraph()
1979
- title_run = title_para.add_run('AI Collaborative Novel')
1980
- title_run.font.size = Pt(24)
1981
- title_run.font.bold = True
1982
- title_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
1983
-
1984
- doc.add_paragraph()
1985
-
1986
- # λ¬Έμ„œ 정보 μΆ”κ°€
1987
- info_para = doc.add_paragraph()
1988
- info_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
1989
- info_run = info_para.add_run(f'10 Writers Collaboration\n')
1990
- info_run.font.size = Pt(14)
1991
-
1992
- stats_para = doc.add_paragraph()
1993
- stats_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
1994
- stats_run = stats_para.add_run(
1995
- f'Total words: {word_count:,}\n'
1996
- f'Total pages: ~{word_count/500:.0f}\n'
1997
- f'Export date: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}'
1998
- )
1999
- stats_run.font.size = Pt(12)
2000
-
 
 
 
 
 
 
2001
  doc.add_page_break()
2002
 
2003
- # Table of Contents
2004
  doc.add_heading('Table of Contents', 1)
2005
  for i in range(1, 11):
2006
- doc.add_paragraph(f'Writer {i}: Pages {(i-1)*3+1}-{i*3}')
2007
-
2008
  doc.add_page_break()
2009
 
2010
- # Parse and add content with writer sections
2011
- if session_id:
2012
- # Get individual writer contents from DB
2013
- stages = NovelDatabase.get_stages(session_id)
2014
- writer_num = 0
2015
-
2016
- for stage in stages:
2017
- if stage['role'].startswith('writer') and 'Revision' in stage['stage_name']:
2018
- writer_num += 1
2019
- # Add writer header
2020
- doc.add_heading(f'Writer {writer_num}', 1)
2021
- doc.add_paragraph(f'Pages {(writer_num-1)*3+1}-{writer_num*3}')
2022
- doc.add_paragraph()
2023
-
2024
- # Add writer content
2025
- content = stage['content'] or ""
2026
- paragraphs = content.split('\n\n')
2027
- for para_text in paragraphs:
2028
- if para_text.strip():
2029
- para = doc.add_paragraph(para_text)
2030
- para.style.font.size = Pt(11)
2031
-
2032
- if writer_num < 10: # Don't add page break after last writer
2033
- doc.add_page_break()
2034
- else:
2035
- # If no session_id, add content as is
2036
- lines = novel_text.split('\n')
2037
- for line in lines:
2038
- if line.startswith('#'):
2039
- level = min(len(line.split()[0].strip('#')), 9)
2040
- text = line.lstrip('#').strip()
2041
- if text:
2042
- doc.add_heading(text, level)
2043
- elif line.strip():
2044
- para = doc.add_paragraph(line)
2045
- if hasattr(para.style, 'font'):
2046
  para.style.font.size = Pt(11)
 
 
 
2047
 
2048
  # νŽ˜μ΄μ§€ μ„€μ •
2049
- section = doc.sections[0]
2050
- section.page_height = Inches(11)
2051
- section.page_width = Inches(8.5)
2052
- section.left_margin = Inches(1)
2053
- section.right_margin = Inches(1)
2054
- section.top_margin = Inches(1)
2055
- section.bottom_margin = Inches(1)
2056
 
2057
  # Save
2058
  temp_dir = tempfile.gettempdir()
2059
- filename = f"Novel_10Writers_{timestamp}_{word_count}words.docx"
2060
  filepath = os.path.join(temp_dir, filename)
2061
  doc.save(filepath)
2062
 
2063
- logger.info(f"DOCX saved: {filepath}")
2064
  return filepath
2065
  else:
2066
  # TXT format
2067
  temp_dir = tempfile.gettempdir()
2068
- filename = f"Novel_10Writers_{timestamp}_{word_count}words.txt"
2069
  filepath = os.path.join(temp_dir, filename)
2070
 
2071
- # 파일 μ‹œμž‘ 뢀뢄에 정보 μΆ”κ°€
2072
  with open(filepath, 'w', encoding='utf-8') as f:
2073
  f.write("="*60 + "\n")
2074
- f.write("AI COLLABORATIVE NOVEL - 10 WRITERS\n")
2075
  f.write("="*60 + "\n")
2076
- f.write(f"Total words: {word_count:,}\n")
2077
- f.write(f"Total characters: {char_count:,}\n")
2078
- f.write(f"Estimated pages: {word_count/500:.1f}\n")
2079
- f.write(f"Export date: {datetime.now()}\n")
 
 
2080
  f.write("="*60 + "\n\n")
2081
 
2082
- if session_id:
2083
- # Get individual writer contents
2084
- stages = NovelDatabase.get_stages(session_id)
2085
- writer_num = 0
2086
-
2087
- for stage in stages:
2088
- if stage['role'].startswith('writer') and 'Revision' in stage['stage_name']:
2089
- writer_num += 1
2090
- f.write(f"\n{'='*40}\n")
2091
- f.write(f"WRITER {writer_num} (Pages {(writer_num-1)*3+1}-{writer_num*3})\n")
2092
- f.write(f"{'='*40}\n\n")
2093
- f.write(stage['content'] or "")
2094
- f.write("\n\n")
2095
- else:
2096
- f.write(novel_text)
 
 
 
 
 
 
 
 
 
 
2097
 
2098
- logger.info(f"TXT saved: {filepath}")
2099
  return filepath
2100
 
2101
  # Custom CSS
@@ -2149,6 +2298,17 @@ custom_css = """
2149
  overflow-y: auto;
2150
  }
2151
 
 
 
 
 
 
 
 
 
 
 
 
2152
  .download-section {
2153
  background-color: rgba(255, 255, 255, 0.9);
2154
  padding: 15px;
@@ -2262,6 +2422,36 @@ def create_interface():
2262
  label="Downloaded File / λ‹€μš΄λ‘œλ“œλœ 파일",
2263
  visible=False
2264
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2265
 
2266
  # Hidden state for novel text
2267
  novel_text_state = gr.State("")
@@ -2294,13 +2484,37 @@ def create_interface():
2294
  session_id, message = auto_recover_session(language)
2295
  return session_id
2296
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2297
  submit_btn.click(
2298
  fn=process_query,
2299
  inputs=[query_input, language_select, current_session_id],
2300
  outputs=[stages_display, novel_output, status_text]
2301
  )
2302
 
2303
- # Update novel text state when novel output changes
2304
  novel_output.change(
2305
  fn=lambda x: x,
2306
  inputs=[novel_output],
@@ -2338,7 +2552,7 @@ def create_interface():
2338
  )
2339
 
2340
  def handle_download(format_type, language, session_id, novel_text):
2341
- if not novel_text and not session_id:
2342
  return gr.update(visible=False)
2343
 
2344
  file_path = download_novel(novel_text, format_type, language, session_id)
@@ -2353,10 +2567,47 @@ def create_interface():
2353
  outputs=[download_file]
2354
  )
2355
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2356
  # Load sessions on startup
 
 
 
 
 
2357
  interface.load(
2358
- fn=refresh_sessions,
2359
- outputs=[session_dropdown]
2360
  )
2361
 
2362
  return interface
 
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"""
 
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
 
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:
 
2077
  return None, ""
2078
 
2079
  def download_novel(novel_text: str, format: str, language: str, session_id: str = None) -> str:
2080
+ """Download novel - DBμ—μ„œ 직접 μž‘κ°€ λ‚΄μš©μ„ κ°€μ Έμ™€μ„œ 톡합"""
2081
+ if not session_id:
2082
+ logger.error("No session_id provided for download")
2083
  return None
2084
 
2085
+ # DBμ—μ„œ μ„Έμ…˜ 정보 κ°€μ Έμ˜€κΈ°
2086
+ session = NovelDatabase.get_session(session_id)
2087
+ if not session:
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:
2188
+ section.page_height = Inches(11)
2189
+ section.page_width = Inches(8.5)
2190
+ section.left_margin = Inches(1)
2191
+ section.right_margin = Inches(1)
2192
+ section.top_margin = Inches(1)
2193
+ section.bottom_margin = Inches(1)
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:
2210
  f.write("="*60 + "\n")
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
 
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;
 
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
  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,
2513
  inputs=[query_input, language_select, current_session_id],
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],
 
2552
  )
2553
 
2554
  def handle_download(format_type, language, session_id, novel_text):
2555
+ if not session_id:
2556
  return gr.update(visible=False)
2557
 
2558
  file_path = download_novel(novel_text, format_type, language, session_id)
 
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