Spaces:
Running
Running
Update app.py
Browse files
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
|
1120 |
word_count = len(all_content.split())
|
1121 |
-
expected_words = 14500 # 30νμ΄μ§ * 500λ¨μ΄
|
1122 |
|
|
|
1123 |
if language == "Korean":
|
1124 |
-
return f"""κ°λ
μλ‘μ λΉνκ°μ μ΅μ’
νκ°λ₯Ό λ°μνμ¬
|
1125 |
|
1126 |
-
|
1127 |
-
{all_content}
|
1128 |
|
1129 |
λΉνκ° μ΅μ’
νκ°:
|
1130 |
-
{critic_final_feedback}
|
1131 |
|
1132 |
-
|
1133 |
|
1134 |
-
#
|
1135 |
-
|
1136 |
-
|
1137 |
-
-
|
1138 |
-
-
|
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 |
-
|
1165 |
else:
|
1166 |
-
return f"""As director, create the final
|
1167 |
|
1168 |
-
|
1169 |
-
{all_content}
|
1170 |
|
1171 |
Critic's Final Evaluation:
|
1172 |
-
{critic_final_feedback}
|
1173 |
|
1174 |
-
|
1175 |
|
1176 |
-
#
|
1177 |
-
|
1178 |
-
|
1179 |
-
-
|
1180 |
-
-
|
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
|
1205 |
|
1206 |
-
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1729 |
|
1730 |
# Save final novel to DB
|
1731 |
-
NovelDatabase.update_final_novel(self.current_session_id,
|
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
|
1951 |
-
if not
|
|
|
1952 |
return None
|
1953 |
|
1954 |
-
#
|
1955 |
-
|
1956 |
-
|
1957 |
-
|
1958 |
-
novel_text = all_writer_content
|
1959 |
-
|
1960 |
-
if not novel_text:
|
1961 |
return None
|
1962 |
|
1963 |
-
#
|
1964 |
-
|
1965 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
|
|
|
|
|
|
|
|
1972 |
|
1973 |
if format == "DOCX" and DOCX_AVAILABLE:
|
1974 |
# Create DOCX
|
1975 |
doc = Document()
|
1976 |
|
1977 |
-
#
|
1978 |
-
|
1979 |
-
|
1980 |
-
|
1981 |
-
|
1982 |
-
|
1983 |
-
|
1984 |
-
|
1985 |
-
|
1986 |
-
|
1987 |
-
|
1988 |
-
|
1989 |
-
|
1990 |
-
|
1991 |
-
|
1992 |
-
|
1993 |
-
|
1994 |
-
|
1995 |
-
|
1996 |
-
|
1997 |
-
|
1998 |
-
|
1999 |
-
|
2000 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
2001 |
doc.add_page_break()
|
2002 |
|
2003 |
-
#
|
2004 |
doc.add_heading('Table of Contents', 1)
|
2005 |
for i in range(1, 11):
|
2006 |
-
doc.add_paragraph(f'
|
2007 |
-
|
2008 |
doc.add_page_break()
|
2009 |
|
2010 |
-
#
|
2011 |
-
|
2012 |
-
|
2013 |
-
|
2014 |
-
|
2015 |
-
|
2016 |
-
|
2017 |
-
|
2018 |
-
|
2019 |
-
|
2020 |
-
|
2021 |
-
|
2022 |
-
|
2023 |
-
|
2024 |
-
|
2025 |
-
|
2026 |
-
|
2027 |
-
|
2028 |
-
|
2029 |
-
|
2030 |
-
|
2031 |
-
|
2032 |
-
if
|
2033 |
-
doc.
|
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
|
2050 |
-
|
2051 |
-
|
2052 |
-
|
2053 |
-
|
2054 |
-
|
2055 |
-
|
2056 |
|
2057 |
# Save
|
2058 |
temp_dir = tempfile.gettempdir()
|
2059 |
-
filename = f"
|
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"
|
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 -
|
2075 |
f.write("="*60 + "\n")
|
2076 |
-
f.write(f"
|
2077 |
-
f.write(f"Total
|
2078 |
-
f.write(f"
|
2079 |
-
f.write(f"
|
|
|
|
|
2080 |
f.write("="*60 + "\n\n")
|
2081 |
|
2082 |
-
|
2083 |
-
|
2084 |
-
|
2085 |
-
|
2086 |
-
|
2087 |
-
|
2088 |
-
|
2089 |
-
|
2090 |
-
|
2091 |
-
|
2092 |
-
|
2093 |
-
|
2094 |
-
|
2095 |
-
|
2096 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
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=
|
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
|