openfree commited on
Commit
f1a53c8
ยท
verified ยท
1 Parent(s): 188ca92

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +510 -186
app.py CHANGED
@@ -4,11 +4,15 @@ import json
4
  import requests
5
  from datetime import datetime
6
  import time
7
- from typing import List, Dict, Any, Generator, Tuple
8
  import logging
9
  import re
10
  import tempfile
11
  from pathlib import Path
 
 
 
 
12
 
13
  # ๋กœ๊น… ์„ค์ •
14
  logging.basicConfig(level=logging.INFO)
@@ -36,6 +40,186 @@ conversation_history = []
36
  selected_language = "English" # ๊ธฐ๋ณธ ์–ธ์–ด
37
  novel_context = {} # ์†Œ์„ค ์ปจํ…์ŠคํŠธ ์ €์žฅ
38
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  class NovelWritingSystem:
40
  def __init__(self):
41
  self.token = FRIENDLI_TOKEN
@@ -46,6 +230,13 @@ class NovelWritingSystem:
46
  if self.test_mode:
47
  logger.warning("Running in test mode.")
48
 
 
 
 
 
 
 
 
49
  # ์†Œ์„ค ์ž‘์„ฑ ์ง„ํ–‰ ์ƒํƒœ
50
  self.novel_state = {
51
  "theme": "",
@@ -1258,11 +1449,25 @@ Last page content from Writer {writer_num}..."""
1258
 
1259
  return test_responses.get(role, "Test response.")
1260
 
1261
- def process_novel_stream(self, query: str, language: str = "English") -> Generator[Tuple[str, List[Dict[str, str]]], None, None]:
 
 
1262
  """Process novel writing with streaming updates"""
1263
  try:
1264
  global conversation_history, novel_context
1265
 
 
 
 
 
 
 
 
 
 
 
 
 
1266
  # Initialize conversation
1267
  conversation_history = [{
1268
  "role": "human",
@@ -1270,203 +1475,116 @@ Last page content from Writer {writer_num}..."""
1270
  "timestamp": datetime.now()
1271
  }]
1272
 
 
1273
  stages = []
1274
- accumulated_content = ""
1275
-
1276
- # 1. Director Initial Planning
1277
- stages.append({
1278
- "name": f"๐ŸŽฌ {'๊ฐ๋…์ž: ์ดˆ๊ธฐ ๊ธฐํš' if language == 'Korean' else 'Director: Initial Planning'}",
1279
- "status": "active",
1280
- "content": ""
1281
- })
1282
- yield "", stages
1283
-
1284
- director_prompt = self.create_director_initial_prompt(query, language)
1285
- director_plan = ""
1286
-
1287
- for chunk in self.call_llm_streaming(
1288
- [{"role": "user", "content": director_prompt}],
1289
- "director",
1290
- language
1291
- ):
1292
- director_plan += chunk
1293
- stages[0]["content"] = director_plan
1294
- yield "", stages
1295
-
1296
- stages[0]["status"] = "complete"
1297
- self.novel_state["theme"] = director_plan
1298
-
1299
- # 2. Critic Review of Plan
1300
- stages.append({
1301
- "name": f"๐Ÿ“ {'๋น„ํ‰๊ฐ€: ๊ธฐํš ๊ฒ€ํ† ' if language == 'Korean' else 'Critic: Plan Review'}",
1302
- "status": "active",
1303
- "content": ""
1304
- })
1305
- yield "", stages
1306
-
1307
- critic_prompt = self.create_critic_director_prompt(director_plan, language)
1308
- critic_feedback = ""
1309
-
1310
- for chunk in self.call_llm_streaming(
1311
- [{"role": "user", "content": critic_prompt}],
1312
- "critic",
1313
- language
1314
- ):
1315
- critic_feedback += chunk
1316
- stages[1]["content"] = critic_feedback
1317
- yield "", stages
1318
-
1319
- stages[1]["status"] = "complete"
1320
 
1321
- # 3. Director Revision
1322
- stages.append({
1323
- "name": f"๐ŸŽฌ {'๊ฐ๋…์ž: ์ˆ˜์ •๋œ ๋งˆ์Šคํ„ฐํ”Œ๋žœ' if language == 'Korean' else 'Director: Revised Masterplan'}",
1324
- "status": "active",
1325
- "content": ""
1326
- })
1327
- yield "", stages
1328
 
1329
- revision_prompt = self.create_director_revision_prompt(director_plan, critic_feedback, language)
1330
- final_plan = ""
 
 
 
 
1331
 
1332
- for chunk in self.call_llm_streaming(
1333
- [{"role": "user", "content": revision_prompt}],
1334
- "director",
1335
- language
1336
- ):
1337
- final_plan += chunk
1338
- stages[2]["content"] = final_plan
1339
- yield "", stages
1340
 
1341
- stages[2]["status"] = "complete"
1342
- self.novel_state["plot_outline"] = final_plan
 
 
1343
 
1344
- # 4-23. Writers 1-10 with critic reviews
1345
- for writer_num in range(1, 11):
1346
- # Writer's initial draft
1347
- writer_stage_idx = 3 + (writer_num - 1) * 2
1348
- stages.append({
1349
- "name": f"โœ๏ธ {'์ž‘์„ฑ์ž' if language == 'Korean' else 'Writer'} {writer_num}: {'์ดˆ์•ˆ' if language == 'Korean' else 'Draft'}",
1350
- "status": "active",
1351
- "content": ""
1352
- })
1353
- yield "", stages
1354
 
1355
- writer_prompt = self.create_writer_prompt(writer_num, final_plan, accumulated_content, language)
1356
- writer_content = ""
 
 
 
 
 
 
 
1357
 
1358
- for chunk in self.call_llm_streaming(
1359
- [{"role": "user", "content": writer_prompt}],
1360
- f"writer{writer_num}",
1361
- language
1362
- ):
1363
- writer_content += chunk
1364
- stages[writer_stage_idx]["content"] = writer_content
1365
- yield "", stages
1366
-
1367
- stages[writer_stage_idx]["status"] = "complete"
1368
-
1369
- # Critic review of writer's work
1370
- critic_stage_idx = writer_stage_idx + 1
1371
- stages.append({
1372
- "name": f"๐Ÿ“ {'๋น„ํ‰๊ฐ€: ์ž‘์„ฑ์ž' if language == 'Korean' else 'Critic: Writer'} {writer_num} {'๊ฒ€ํ† ' if language == 'Korean' else 'Review'}",
1373
- "status": "active",
1374
- "content": ""
1375
- })
1376
  yield "", stages
1377
 
1378
- critic_writer_prompt = self.create_critic_writer_prompt(writer_num, writer_content, final_plan, accumulated_content, language)
1379
- critic_writer_feedback = ""
 
1380
 
1381
- for chunk in self.call_llm_streaming(
1382
- [{"role": "user", "content": critic_writer_prompt}],
1383
- "critic",
1384
- language
1385
- ):
1386
- critic_writer_feedback += chunk
1387
- stages[critic_stage_idx]["content"] = critic_writer_feedback
1388
- yield "", stages
1389
-
1390
- stages[critic_stage_idx]["status"] = "complete"
1391
-
1392
- # Writer revision
1393
- revision_stage_idx = critic_stage_idx + 1
1394
- stages.append({
1395
- "name": f"โœ๏ธ {'์ž‘์„ฑ์ž' if language == 'Korean' else 'Writer'} {writer_num}: {'์ˆ˜์ •๋ณธ' if language == 'Korean' else 'Revision'}",
1396
- "status": "active",
1397
- "content": ""
1398
- })
1399
- yield "", stages
1400
-
1401
- writer_revision_prompt = self.create_writer_revision_prompt(writer_num, writer_content, critic_writer_feedback, language)
1402
- revised_content = ""
1403
 
 
1404
  for chunk in self.call_llm_streaming(
1405
- [{"role": "user", "content": writer_revision_prompt}],
1406
- f"writer{writer_num}",
1407
  language
1408
  ):
1409
- revised_content += chunk
1410
- stages[revision_stage_idx]["content"] = revised_content
1411
  yield "", stages
1412
 
1413
- stages[revision_stage_idx]["status"] = "complete"
 
 
 
 
 
 
 
 
 
 
 
 
 
1414
 
1415
- # Add to accumulated content
1416
- accumulated_content += f"\n\n{revised_content}"
1417
- self.novel_state[f"writer{writer_num}_final"] = revised_content
1418
-
1419
- # 24. Critic Final Evaluation
1420
- final_critic_idx = len(stages)
1421
- stages.append({
1422
- "name": f"๐Ÿ“ {'๋น„ํ‰๊ฐ€: ์ตœ์ข… ํ‰๊ฐ€' if language == 'Korean' else 'Critic: Final Evaluation'}",
1423
- "status": "active",
1424
- "content": ""
1425
- })
1426
- yield "", stages
1427
-
1428
- critic_final_prompt = self.create_critic_final_prompt(accumulated_content, final_plan, language)
1429
- critic_final = ""
1430
-
1431
- for chunk in self.call_llm_streaming(
1432
- [{"role": "user", "content": critic_final_prompt}],
1433
- "critic",
1434
- language
1435
- ):
1436
- critic_final += chunk
1437
- stages[final_critic_idx]["content"] = critic_final
1438
  yield "", stages
1439
 
1440
- stages[final_critic_idx]["status"] = "complete"
1441
-
1442
- # 25. Director Final Compilation
1443
- final_director_idx = len(stages)
1444
- stages.append({
1445
- "name": f"๐ŸŽฌ {'๊ฐ๋…์ž: ์ตœ์ข… ์™„์„ฑ๋ณธ' if language == 'Korean' else 'Director: Final Version'}",
1446
- "status": "active",
1447
- "content": ""
1448
- })
1449
- yield "", stages
1450
 
1451
- director_final_prompt = self.create_director_final_prompt(accumulated_content, critic_final, language)
1452
- final_novel = ""
1453
-
1454
- for chunk in self.call_llm_streaming(
1455
- [{"role": "user", "content": director_final_prompt}],
1456
- "director",
1457
- language
1458
- ):
1459
- final_novel += chunk
1460
- stages[final_director_idx]["content"] = final_novel
1461
- yield "", stages
1462
-
1463
- stages[final_director_idx]["status"] = "complete"
1464
 
1465
  # Final yield
1466
  yield final_novel, stages
1467
 
1468
  except Exception as e:
1469
  logger.error(f"Error in process_novel_stream: {str(e)}")
 
 
 
 
 
 
 
 
 
 
 
 
1470
  error_stage = {
1471
  "name": "โŒ Error",
1472
  "status": "error",
@@ -1474,11 +1592,80 @@ Last page content from Writer {writer_num}..."""
1474
  }
1475
  stages.append(error_stage)
1476
  yield f"Error occurred: {str(e)}", stages
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1477
 
1478
  # Gradio Interface Functions
1479
- def process_query(query: str, language: str) -> Generator[Tuple[str, str, str], None, None]:
1480
  """Process query and yield updates"""
1481
- if not query.strip():
1482
  if language == "Korean":
1483
  yield "", "", "โŒ ์†Œ์„ค ์ฃผ์ œ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”."
1484
  else:
@@ -1488,9 +1675,9 @@ def process_query(query: str, language: str) -> Generator[Tuple[str, str, str],
1488
  system = NovelWritingSystem()
1489
 
1490
  try:
1491
- for final_novel, stages in system.process_novel_stream(query, language):
1492
- # Format stages for display
1493
- stages_html = format_stages_html(stages, language)
1494
 
1495
  status = "๐Ÿ”„ Processing..." if not final_novel else "โœ… Complete!"
1496
 
@@ -1503,16 +1690,19 @@ def process_query(query: str, language: str) -> Generator[Tuple[str, str, str],
1503
  else:
1504
  yield "", "", f"โŒ Error occurred: {str(e)}"
1505
 
1506
- def format_stages_html(stages: List[Dict[str, str]], language: str) -> str:
1507
- """Format stages into HTML"""
1508
- html = '<div class="stages-container">'
 
 
1509
 
1510
- for stage in stages:
1511
  status_icon = "โœ…" if stage.get("status") == "complete" else ("โณ" if stage.get("status") == "active" else "โŒ")
1512
 
1513
  # Create collapsible section for each stage
 
1514
  html += f'''
1515
- <details class="stage-section" {"open" if stage.get("status") == "active" else ""}>
1516
  <summary class="stage-header">
1517
  <span class="status-icon">{status_icon}</span>
1518
  <span class="stage-name">{stage["name"]}</span>
@@ -1524,8 +1714,58 @@ def format_stages_html(stages: List[Dict[str, str]], language: str) -> str:
1524
  '''
1525
 
1526
  html += '</div>'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1527
  return html
1528
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1529
  def download_novel(novel_text: str, format: str, language: str) -> str:
1530
  """Download novel in specified format"""
1531
  if not novel_text:
@@ -1565,7 +1805,7 @@ def download_novel(novel_text: str, format: str, language: str) -> str:
1565
 
1566
  return filepath
1567
 
1568
- # Custom CSS
1569
  custom_css = """
1570
  .gradio-container {
1571
  background: linear-gradient(135deg, #1e3c72, #2a5298);
@@ -1579,6 +1819,13 @@ custom_css = """
1579
  background-color: rgba(255, 255, 255, 0.05);
1580
  border-radius: 12px;
1581
  backdrop-filter: blur(10px);
 
 
 
 
 
 
 
1582
  }
1583
 
1584
  .stage-section {
@@ -1622,6 +1869,7 @@ custom_css = """
1622
  color: #e0e0e0;
1623
  font-family: 'Courier New', monospace;
1624
  line-height: 1.6;
 
1625
  }
1626
 
1627
  #novel-output {
@@ -1657,6 +1905,27 @@ custom_css = """
1657
  border-radius: 8px;
1658
  margin-top: 20px;
1659
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1660
  """
1661
 
1662
  # Create Gradio Interface
@@ -1673,10 +1942,15 @@ def create_interface():
1673
  <p style="font-size: 1.1em; color: #ddd; max-width: 800px; margin: 0 auto;">
1674
  Enter a theme or prompt, and watch as 13 AI agents collaborate to create a complete 50-page novella.
1675
  The system includes 1 Director, 1 Critic, and 10 Writers working in harmony.
 
1676
  </p>
1677
  </div>
1678
  """)
1679
 
 
 
 
 
1680
  with gr.Row():
1681
  with gr.Column(scale=1):
1682
  with gr.Group(elem_classes=["input-section"]):
@@ -1702,6 +1976,24 @@ def create_interface():
1702
  value="๐Ÿ”„ Ready"
1703
  )
1704
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1705
  gr.HTML("""
1706
  <div style="margin-top: 20px; padding: 15px; background: rgba(255,255,255,0.1); border-radius: 8px; color: white;">
1707
  <h4>Writing Process:</h4>
@@ -1714,6 +2006,7 @@ def create_interface():
1714
  <li>Writers revise based on feedback</li>
1715
  <li>Final compilation by Director</li>
1716
  </ol>
 
1717
  </div>
1718
  """)
1719
 
@@ -1766,9 +2059,13 @@ def create_interface():
1766
  def update_novel_state(process, novel, status):
1767
  return process, novel, status, novel
1768
 
 
 
 
 
1769
  submit_btn.click(
1770
  fn=process_query,
1771
- inputs=[query_input, language_select],
1772
  outputs=[process_display, novel_output, status_text]
1773
  ).then(
1774
  fn=update_novel_state,
@@ -1776,9 +2073,30 @@ def create_interface():
1776
  outputs=[process_display, novel_output, status_text, novel_text_state]
1777
  )
1778
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1779
  clear_btn.click(
1780
- fn=lambda: ("", "", "๐Ÿ”„ Ready", ""),
1781
- outputs=[process_display, novel_output, status_text, novel_text_state]
 
 
 
 
 
 
1782
  )
1783
 
1784
  def handle_download(novel_text, format_type, language):
@@ -1796,6 +2114,12 @@ def create_interface():
1796
  inputs=[novel_text_state, format_select, language_select],
1797
  outputs=[download_file]
1798
  )
 
 
 
 
 
 
1799
 
1800
  return interface
1801
 
 
4
  import requests
5
  from datetime import datetime
6
  import time
7
+ from typing import List, Dict, Any, Generator, Tuple, Optional
8
  import logging
9
  import re
10
  import tempfile
11
  from pathlib import Path
12
+ import sqlite3
13
+ import hashlib
14
+ import threading
15
+ from contextlib import contextmanager
16
 
17
  # ๋กœ๊น… ์„ค์ •
18
  logging.basicConfig(level=logging.INFO)
 
40
  selected_language = "English" # ๊ธฐ๋ณธ ์–ธ์–ด
41
  novel_context = {} # ์†Œ์„ค ์ปจํ…์ŠคํŠธ ์ €์žฅ
42
 
43
+ # DB ๊ฒฝ๋กœ
44
+ DB_PATH = "novel_sessions.db"
45
+ db_lock = threading.Lock()
46
+
47
+ class NovelDatabase:
48
+ """Novel session management database"""
49
+
50
+ @staticmethod
51
+ def init_db():
52
+ """Initialize database tables"""
53
+ with sqlite3.connect(DB_PATH) as conn:
54
+ cursor = conn.cursor()
55
+
56
+ # Sessions table
57
+ cursor.execute('''
58
+ CREATE TABLE IF NOT EXISTS sessions (
59
+ session_id TEXT PRIMARY KEY,
60
+ user_query TEXT NOT NULL,
61
+ language TEXT NOT NULL,
62
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
63
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
64
+ status TEXT DEFAULT 'active',
65
+ current_stage INTEGER DEFAULT 0,
66
+ final_novel TEXT
67
+ )
68
+ ''')
69
+
70
+ # Stages table
71
+ cursor.execute('''
72
+ CREATE TABLE IF NOT EXISTS stages (
73
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
74
+ session_id TEXT NOT NULL,
75
+ stage_number INTEGER NOT NULL,
76
+ stage_name TEXT NOT NULL,
77
+ role TEXT NOT NULL,
78
+ content TEXT,
79
+ status TEXT DEFAULT 'pending',
80
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
81
+ FOREIGN KEY (session_id) REFERENCES sessions(session_id)
82
+ )
83
+ ''')
84
+
85
+ # Create indices
86
+ cursor.execute('CREATE INDEX IF NOT EXISTS idx_session_id ON stages(session_id)')
87
+ cursor.execute('CREATE INDEX IF NOT EXISTS idx_stage_number ON stages(stage_number)')
88
+
89
+ conn.commit()
90
+
91
+ @staticmethod
92
+ @contextmanager
93
+ def get_db():
94
+ """Database connection context manager"""
95
+ with db_lock:
96
+ conn = sqlite3.connect(DB_PATH)
97
+ conn.row_factory = sqlite3.Row
98
+ try:
99
+ yield conn
100
+ finally:
101
+ conn.close()
102
+
103
+ @staticmethod
104
+ def create_session(user_query: str, language: str) -> str:
105
+ """Create new session"""
106
+ session_id = hashlib.md5(f"{user_query}{datetime.now()}".encode()).hexdigest()
107
+
108
+ with NovelDatabase.get_db() as conn:
109
+ cursor = conn.cursor()
110
+ cursor.execute('''
111
+ INSERT INTO sessions (session_id, user_query, language)
112
+ VALUES (?, ?, ?)
113
+ ''', (session_id, user_query, language))
114
+ conn.commit()
115
+
116
+ return session_id
117
+
118
+ @staticmethod
119
+ def save_stage(session_id: str, stage_number: int, stage_name: str,
120
+ role: str, content: str, status: str = 'complete'):
121
+ """Save stage content"""
122
+ with NovelDatabase.get_db() as conn:
123
+ cursor = conn.cursor()
124
+
125
+ # Check if stage exists
126
+ cursor.execute('''
127
+ SELECT id FROM stages
128
+ WHERE session_id = ? AND stage_number = ?
129
+ ''', (session_id, stage_number))
130
+
131
+ existing = cursor.fetchone()
132
+
133
+ if existing:
134
+ # Update existing stage
135
+ cursor.execute('''
136
+ UPDATE stages
137
+ SET content = ?, status = ?, stage_name = ?
138
+ WHERE session_id = ? AND stage_number = ?
139
+ ''', (content, status, stage_name, session_id, stage_number))
140
+ else:
141
+ # Insert new stage
142
+ cursor.execute('''
143
+ INSERT INTO stages (session_id, stage_number, stage_name, role, content, status)
144
+ VALUES (?, ?, ?, ?, ?, ?)
145
+ ''', (session_id, stage_number, stage_name, role, content, status))
146
+
147
+ # Update session
148
+ cursor.execute('''
149
+ UPDATE sessions
150
+ SET updated_at = CURRENT_TIMESTAMP, current_stage = ?
151
+ WHERE session_id = ?
152
+ ''', (stage_number, session_id))
153
+
154
+ conn.commit()
155
+
156
+ @staticmethod
157
+ def get_session(session_id: str) -> Optional[Dict]:
158
+ """Get session info"""
159
+ with NovelDatabase.get_db() as conn:
160
+ cursor = conn.cursor()
161
+ cursor.execute('SELECT * FROM sessions WHERE session_id = ?', (session_id,))
162
+ return cursor.fetchone()
163
+
164
+ @staticmethod
165
+ def get_stages(session_id: str) -> List[Dict]:
166
+ """Get all stages for a session"""
167
+ with NovelDatabase.get_db() as conn:
168
+ cursor = conn.cursor()
169
+ cursor.execute('''
170
+ SELECT * FROM stages
171
+ WHERE session_id = ?
172
+ ORDER BY stage_number
173
+ ''', (session_id,))
174
+ return cursor.fetchall()
175
+
176
+ @staticmethod
177
+ def get_completed_content(session_id: str, up_to_stage: int) -> str:
178
+ """Get all completed content up to specified stage"""
179
+ with NovelDatabase.get_db() as conn:
180
+ cursor = conn.cursor()
181
+ cursor.execute('''
182
+ SELECT content FROM stages
183
+ WHERE session_id = ?
184
+ AND stage_number < ?
185
+ AND status = 'complete'
186
+ AND role LIKE 'writer%'
187
+ ORDER BY stage_number
188
+ ''', (session_id, up_to_stage))
189
+
190
+ contents = []
191
+ for row in cursor.fetchall():
192
+ if row['content']:
193
+ contents.append(row['content'])
194
+
195
+ return '\n\n'.join(contents)
196
+
197
+ @staticmethod
198
+ def update_final_novel(session_id: str, final_novel: str):
199
+ """Update final novel content"""
200
+ with NovelDatabase.get_db() as conn:
201
+ cursor = conn.cursor()
202
+ cursor.execute('''
203
+ UPDATE sessions
204
+ SET final_novel = ?, status = 'complete', updated_at = CURRENT_TIMESTAMP
205
+ WHERE session_id = ?
206
+ ''', (final_novel, session_id))
207
+ conn.commit()
208
+
209
+ @staticmethod
210
+ def get_active_sessions() -> List[Dict]:
211
+ """Get all active sessions"""
212
+ with NovelDatabase.get_db() as conn:
213
+ cursor = conn.cursor()
214
+ cursor.execute('''
215
+ SELECT session_id, user_query, language, created_at, current_stage
216
+ FROM sessions
217
+ WHERE status = 'active'
218
+ ORDER BY updated_at DESC
219
+ LIMIT 10
220
+ ''')
221
+ return cursor.fetchall()
222
+
223
  class NovelWritingSystem:
224
  def __init__(self):
225
  self.token = FRIENDLI_TOKEN
 
230
  if self.test_mode:
231
  logger.warning("Running in test mode.")
232
 
233
+ # Initialize database
234
+ NovelDatabase.init_db()
235
+
236
+ # Session management
237
+ self.current_session_id = None
238
+ self.auto_scroll = False # Auto-scroll control
239
+
240
  # ์†Œ์„ค ์ž‘์„ฑ ์ง„ํ–‰ ์ƒํƒœ
241
  self.novel_state = {
242
  "theme": "",
 
1449
 
1450
  return test_responses.get(role, "Test response.")
1451
 
1452
+ def process_novel_stream(self, query: str, language: str = "English",
1453
+ session_id: Optional[str] = None,
1454
+ resume_from_stage: int = 0) -> Generator[Tuple[str, List[Dict[str, str]]], None, None]:
1455
  """Process novel writing with streaming updates"""
1456
  try:
1457
  global conversation_history, novel_context
1458
 
1459
+ # Create or resume session
1460
+ if session_id:
1461
+ self.current_session_id = session_id
1462
+ session = NovelDatabase.get_session(session_id)
1463
+ if session:
1464
+ query = session['user_query']
1465
+ language = session['language']
1466
+ resume_from_stage = session['current_stage'] + 1
1467
+ else:
1468
+ self.current_session_id = NovelDatabase.create_session(query, language)
1469
+ resume_from_stage = 0
1470
+
1471
  # Initialize conversation
1472
  conversation_history = [{
1473
  "role": "human",
 
1475
  "timestamp": datetime.now()
1476
  }]
1477
 
1478
+ # Load existing stages if resuming
1479
  stages = []
1480
+ if resume_from_stage > 0:
1481
+ existing_stages = NovelDatabase.get_stages(self.current_session_id)
1482
+ for stage_data in existing_stages:
1483
+ stages.append({
1484
+ "name": stage_data['stage_name'],
1485
+ "status": stage_data['status'],
1486
+ "content": stage_data['content'] or ""
1487
+ })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1488
 
1489
+ accumulated_content = ""
1490
+ if resume_from_stage > 0:
1491
+ accumulated_content = NovelDatabase.get_completed_content(
1492
+ self.current_session_id,
1493
+ resume_from_stage
1494
+ )
 
1495
 
1496
+ # Define all stages
1497
+ stage_definitions = [
1498
+ ("director", f"๐ŸŽฌ {'๊ฐ๋…์ž: ์ดˆ๊ธฐ ๊ธฐํš' if language == 'Korean' else 'Director: Initial Planning'}"),
1499
+ ("critic", f"๐Ÿ“ {'๋น„ํ‰๊ฐ€: ๊ธฐํš ๊ฒ€ํ† ' if language == 'Korean' else 'Critic: Plan Review'}"),
1500
+ ("director", f"๐ŸŽฌ {'๊ฐ๋…์ž: ์ˆ˜์ •๋œ ๋งˆ์Šคํ„ฐํ”Œ๋žœ' if language == 'Korean' else 'Director: Revised Masterplan'}"),
1501
+ ]
1502
 
1503
+ # Add writer stages
1504
+ for writer_num in range(1, 11):
1505
+ stage_definitions.extend([
1506
+ (f"writer{writer_num}", f"โœ๏ธ {'์ž‘์„ฑ์ž' if language == 'Korean' else 'Writer'} {writer_num}: {'์ดˆ์•ˆ' if language == 'Korean' else 'Draft'}"),
1507
+ ("critic", f"๐Ÿ“ {'๋น„ํ‰๊ฐ€: ์ž‘์„ฑ์ž' if language == 'Korean' else 'Critic: Writer'} {writer_num} {'๊ฒ€ํ† ' if language == 'Korean' else 'Review'}"),
1508
+ (f"writer{writer_num}", f"โœ๏ธ {'์ž‘์„ฑ์ž' if language == 'Korean' else 'Writer'} {writer_num}: {'์ˆ˜์ •๋ณธ' if language == 'Korean' else 'Revision'}")
1509
+ ])
 
1510
 
1511
+ stage_definitions.extend([
1512
+ ("critic", f"๐Ÿ“ {'๋น„ํ‰๊ฐ€: ์ตœ์ข… ํ‰๊ฐ€' if language == 'Korean' else 'Critic: Final Evaluation'}"),
1513
+ ("director", f"๐ŸŽฌ {'๊ฐ๋…์ž: ์ตœ์ข… ์™„์„ฑ๋ณธ' if language == 'Korean' else 'Director: Final Version'}")
1514
+ ])
1515
 
1516
+ # Process stages starting from resume point
1517
+ for stage_idx in range(resume_from_stage, len(stage_definitions)):
1518
+ role, stage_name = stage_definitions[stage_idx]
 
 
 
 
 
 
 
1519
 
1520
+ # Add stage if not already present
1521
+ if stage_idx >= len(stages):
1522
+ stages.append({
1523
+ "name": stage_name,
1524
+ "status": "active",
1525
+ "content": ""
1526
+ })
1527
+ else:
1528
+ stages[stage_idx]["status"] = "active"
1529
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1530
  yield "", stages
1531
 
1532
+ # Get appropriate prompt based on stage
1533
+ prompt = self.get_stage_prompt(stage_idx, role, query, language,
1534
+ stages, accumulated_content)
1535
 
1536
+ stage_content = ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1537
 
1538
+ # Stream content generation
1539
  for chunk in self.call_llm_streaming(
1540
+ [{"role": "user", "content": prompt}],
1541
+ role,
1542
  language
1543
  ):
1544
+ stage_content += chunk
1545
+ stages[stage_idx]["content"] = stage_content
1546
  yield "", stages
1547
 
1548
+ # Mark stage complete and save to DB
1549
+ stages[stage_idx]["status"] = "complete"
1550
+ NovelDatabase.save_stage(
1551
+ self.current_session_id,
1552
+ stage_idx,
1553
+ stage_name,
1554
+ role,
1555
+ stage_content,
1556
+ "complete"
1557
+ )
1558
+
1559
+ # Accumulate writer content
1560
+ if role.startswith("writer") and "์ˆ˜์ •๋ณธ" in stage_name or "Revision" in stage_name:
1561
+ accumulated_content += f"\n\n{stage_content}"
1562
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1563
  yield "", stages
1564
 
1565
+ # Get final novel from last stage
1566
+ final_novel = stages[-1]["content"] if stages else ""
 
 
 
 
 
 
 
 
1567
 
1568
+ # Save final novel to DB
1569
+ NovelDatabase.update_final_novel(self.current_session_id, final_novel)
 
 
 
 
 
 
 
 
 
 
 
1570
 
1571
  # Final yield
1572
  yield final_novel, stages
1573
 
1574
  except Exception as e:
1575
  logger.error(f"Error in process_novel_stream: {str(e)}")
1576
+
1577
+ # Save error state to DB
1578
+ if self.current_session_id:
1579
+ NovelDatabase.save_stage(
1580
+ self.current_session_id,
1581
+ stage_idx if 'stage_idx' in locals() else 0,
1582
+ "Error",
1583
+ "error",
1584
+ str(e),
1585
+ "error"
1586
+ )
1587
+
1588
  error_stage = {
1589
  "name": "โŒ Error",
1590
  "status": "error",
 
1592
  }
1593
  stages.append(error_stage)
1594
  yield f"Error occurred: {str(e)}", stages
1595
+
1596
+ def get_stage_prompt(self, stage_idx: int, role: str, query: str,
1597
+ language: str, stages: List[Dict],
1598
+ accumulated_content: str) -> str:
1599
+ """Get appropriate prompt for each stage"""
1600
+ # Stage 0: Director Initial
1601
+ if stage_idx == 0:
1602
+ return self.create_director_initial_prompt(query, language)
1603
+
1604
+ # Stage 1: Critic reviews Director's plan
1605
+ elif stage_idx == 1:
1606
+ return self.create_critic_director_prompt(stages[0]["content"], language)
1607
+
1608
+ # Stage 2: Director revision
1609
+ elif stage_idx == 2:
1610
+ return self.create_director_revision_prompt(
1611
+ stages[0]["content"], stages[1]["content"], language)
1612
+
1613
+ # Writer stages
1614
+ elif role.startswith("writer"):
1615
+ writer_num = int(role.replace("writer", ""))
1616
+ final_plan = stages[2]["content"] # Director's final plan
1617
+
1618
+ # Initial draft or revision?
1619
+ if "์ดˆ์•ˆ" in stages[stage_idx]["name"] or "Draft" in stages[stage_idx]["name"]:
1620
+ return self.create_writer_prompt(writer_num, final_plan, accumulated_content, language)
1621
+ else: # Revision
1622
+ # Find the initial draft and critic feedback
1623
+ initial_draft_idx = stage_idx - 2
1624
+ critic_feedback_idx = stage_idx - 1
1625
+ return self.create_writer_revision_prompt(
1626
+ writer_num,
1627
+ stages[initial_draft_idx]["content"],
1628
+ stages[critic_feedback_idx]["content"],
1629
+ language
1630
+ )
1631
+
1632
+ # Critic stages
1633
+ elif role == "critic":
1634
+ final_plan = stages[2]["content"]
1635
+
1636
+ # Final evaluation
1637
+ if "์ตœ์ข…" in stages[stage_idx]["name"] or "Final" in stages[stage_idx]["name"]:
1638
+ return self.create_critic_final_prompt(accumulated_content, final_plan, language)
1639
+
1640
+ # Writer review
1641
+ else:
1642
+ # Find which writer we're reviewing
1643
+ for i in range(1, 11):
1644
+ if f"์ž‘์„ฑ์ž {i}" in stages[stage_idx]["name"] or f"Writer {i}" in stages[stage_idx]["name"]:
1645
+ writer_content_idx = stage_idx - 1
1646
+ return self.create_critic_writer_prompt(
1647
+ i,
1648
+ stages[writer_content_idx]["content"],
1649
+ final_plan,
1650
+ accumulated_content,
1651
+ language
1652
+ )
1653
+
1654
+ # Director final
1655
+ elif stage_idx == len(stages) - 1:
1656
+ critic_final_idx = stage_idx - 1
1657
+ return self.create_director_final_prompt(
1658
+ accumulated_content,
1659
+ stages[critic_final_idx]["content"],
1660
+ language
1661
+ )
1662
+
1663
+ return ""
1664
 
1665
  # Gradio Interface Functions
1666
+ def process_query(query: str, language: str, session_id: str = None) -> Generator[Tuple[str, str, str], None, None]:
1667
  """Process query and yield updates"""
1668
+ if not query.strip() and not session_id:
1669
  if language == "Korean":
1670
  yield "", "", "โŒ ์†Œ์„ค ์ฃผ์ œ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”."
1671
  else:
 
1675
  system = NovelWritingSystem()
1676
 
1677
  try:
1678
+ for final_novel, stages in system.process_novel_stream(query, language, session_id):
1679
+ # Format stages for display with scroll control
1680
+ stages_html = format_stages_html(stages, language, system.auto_scroll)
1681
 
1682
  status = "๐Ÿ”„ Processing..." if not final_novel else "โœ… Complete!"
1683
 
 
1690
  else:
1691
  yield "", "", f"โŒ Error occurred: {str(e)}"
1692
 
1693
+ def format_stages_html(stages: List[Dict[str, str]], language: str, auto_scroll: bool = False) -> str:
1694
+ """Format stages into HTML with scroll control"""
1695
+ scroll_behavior = "auto" if auto_scroll else "manual"
1696
+
1697
+ html = f'<div class="stages-container" data-scroll="{scroll_behavior}">'
1698
 
1699
+ for idx, stage in enumerate(stages):
1700
  status_icon = "โœ…" if stage.get("status") == "complete" else ("โณ" if stage.get("status") == "active" else "โŒ")
1701
 
1702
  # Create collapsible section for each stage
1703
+ is_active = stage.get("status") == "active"
1704
  html += f'''
1705
+ <details class="stage-section" {"open" if is_active else ""} id="stage-{idx}">
1706
  <summary class="stage-header">
1707
  <span class="status-icon">{status_icon}</span>
1708
  <span class="stage-name">{stage["name"]}</span>
 
1714
  '''
1715
 
1716
  html += '</div>'
1717
+
1718
+ # Add JavaScript for scroll control
1719
+ if not auto_scroll:
1720
+ html += '''
1721
+ <script>
1722
+ // Prevent auto-scrolling when content updates
1723
+ const container = document.querySelector('.stages-container[data-scroll="manual"]');
1724
+ if (container) {
1725
+ let userScrolled = false;
1726
+ container.addEventListener('scroll', () => {
1727
+ userScrolled = true;
1728
+ });
1729
+
1730
+ // Override scroll behavior on content update
1731
+ const observer = new MutationObserver(() => {
1732
+ if (!userScrolled) {
1733
+ // Don't auto-scroll
1734
+ }
1735
+ });
1736
+ observer.observe(container, { childList: true, subtree: true });
1737
+ }
1738
+ </script>
1739
+ '''
1740
+
1741
  return html
1742
 
1743
+ def get_active_sessions(language: str) -> List[Tuple[str, str, str]]:
1744
+ """Get list of active sessions"""
1745
+ sessions = NovelDatabase.get_active_sessions()
1746
+
1747
+ choices = []
1748
+ for session in sessions:
1749
+ created = datetime.fromisoformat(session['created_at'])
1750
+ date_str = created.strftime("%Y-%m-%d %H:%M")
1751
+ query_preview = session['user_query'][:50] + "..." if len(session['user_query']) > 50 else session['user_query']
1752
+ label = f"[{date_str}] {query_preview} (Stage {session['current_stage']})"
1753
+ choices.append((label, session['session_id']))
1754
+
1755
+ return choices
1756
+
1757
+ def resume_session(session_id: str, language: str) -> Generator[Tuple[str, str, str], None, None]:
1758
+ """Resume an existing session"""
1759
+ if not session_id:
1760
+ return
1761
+
1762
+ # Process with existing session ID
1763
+ yield from process_query("", language, session_id)
1764
+
1765
+ def toggle_auto_scroll(current_state: bool) -> bool:
1766
+ """Toggle auto-scroll state"""
1767
+ return not current_state
1768
+
1769
  def download_novel(novel_text: str, format: str, language: str) -> str:
1770
  """Download novel in specified format"""
1771
  if not novel_text:
 
1805
 
1806
  return filepath
1807
 
1808
+ # Custom CSS with improved scroll handling
1809
  custom_css = """
1810
  .gradio-container {
1811
  background: linear-gradient(135deg, #1e3c72, #2a5298);
 
1819
  background-color: rgba(255, 255, 255, 0.05);
1820
  border-radius: 12px;
1821
  backdrop-filter: blur(10px);
1822
+ scroll-behavior: smooth;
1823
+ position: relative;
1824
+ }
1825
+
1826
+ /* Disable auto-scroll when user has scrolled */
1827
+ .stages-container[data-scroll="manual"] {
1828
+ scroll-behavior: auto;
1829
  }
1830
 
1831
  .stage-section {
 
1869
  color: #e0e0e0;
1870
  font-family: 'Courier New', monospace;
1871
  line-height: 1.6;
1872
+ margin: 0;
1873
  }
1874
 
1875
  #novel-output {
 
1905
  border-radius: 8px;
1906
  margin-top: 20px;
1907
  }
1908
+
1909
+ .session-section {
1910
+ background-color: rgba(255, 255, 255, 0.1);
1911
+ backdrop-filter: blur(10px);
1912
+ padding: 15px;
1913
+ border-radius: 8px;
1914
+ margin-top: 20px;
1915
+ color: white;
1916
+ }
1917
+
1918
+ /* Scroll position indicator */
1919
+ .scroll-indicator {
1920
+ position: absolute;
1921
+ right: 5px;
1922
+ top: 5px;
1923
+ background: rgba(255, 255, 255, 0.2);
1924
+ padding: 5px 10px;
1925
+ border-radius: 15px;
1926
+ font-size: 0.8em;
1927
+ color: white;
1928
+ }
1929
  """
1930
 
1931
  # Create Gradio Interface
 
1942
  <p style="font-size: 1.1em; color: #ddd; max-width: 800px; margin: 0 auto;">
1943
  Enter a theme or prompt, and watch as 13 AI agents collaborate to create a complete 50-page novella.
1944
  The system includes 1 Director, 1 Critic, and 10 Writers working in harmony.
1945
+ All progress is automatically saved and can be resumed anytime.
1946
  </p>
1947
  </div>
1948
  """)
1949
 
1950
+ # State management
1951
+ auto_scroll_state = gr.State(False)
1952
+ current_session_id = gr.State(None)
1953
+
1954
  with gr.Row():
1955
  with gr.Column(scale=1):
1956
  with gr.Group(elem_classes=["input-section"]):
 
1976
  value="๐Ÿ”„ Ready"
1977
  )
1978
 
1979
+ # Session management
1980
+ with gr.Group(elem_classes=["session-section"]):
1981
+ gr.Markdown("### ๐Ÿ’พ Resume Previous Session / ์ด์ „ ์„ธ์…˜ ์žฌ๊ฐœ")
1982
+ session_dropdown = gr.Dropdown(
1983
+ label="Select Session / ์„ธ์…˜ ์„ ํƒ",
1984
+ choices=[],
1985
+ interactive=True
1986
+ )
1987
+ with gr.Row():
1988
+ refresh_btn = gr.Button("๐Ÿ”„ Refresh / ์ƒˆ๋กœ๊ณ ์นจ", scale=1)
1989
+ resume_btn = gr.Button("โ–ถ๏ธ Resume / ์žฌ๊ฐœ", variant="secondary", scale=1)
1990
+
1991
+ # Scroll control
1992
+ auto_scroll_checkbox = gr.Checkbox(
1993
+ label="Auto-scroll / ์ž๋™ ์Šคํฌ๋กค",
1994
+ value=False
1995
+ )
1996
+
1997
  gr.HTML("""
1998
  <div style="margin-top: 20px; padding: 15px; background: rgba(255,255,255,0.1); border-radius: 8px; color: white;">
1999
  <h4>Writing Process:</h4>
 
2006
  <li>Writers revise based on feedback</li>
2007
  <li>Final compilation by Director</li>
2008
  </ol>
2009
+ <p style="margin-top: 10px;"><strong>Note:</strong> All progress is automatically saved to database.</p>
2010
  </div>
2011
  """)
2012
 
 
2059
  def update_novel_state(process, novel, status):
2060
  return process, novel, status, novel
2061
 
2062
+ def refresh_sessions():
2063
+ sessions = get_active_sessions("English")
2064
+ return gr.update(choices=sessions)
2065
+
2066
  submit_btn.click(
2067
  fn=process_query,
2068
+ inputs=[query_input, language_select, current_session_id],
2069
  outputs=[process_display, novel_output, status_text]
2070
  ).then(
2071
  fn=update_novel_state,
 
2073
  outputs=[process_display, novel_output, status_text, novel_text_state]
2074
  )
2075
 
2076
+ resume_btn.click(
2077
+ fn=lambda x: x,
2078
+ inputs=[session_dropdown],
2079
+ outputs=[current_session_id]
2080
+ ).then(
2081
+ fn=resume_session,
2082
+ inputs=[current_session_id, language_select],
2083
+ outputs=[process_display, novel_output, status_text]
2084
+ )
2085
+
2086
+ refresh_btn.click(
2087
+ fn=refresh_sessions,
2088
+ outputs=[session_dropdown]
2089
+ )
2090
+
2091
  clear_btn.click(
2092
+ fn=lambda: ("", "", "๐Ÿ”„ Ready", "", None),
2093
+ outputs=[process_display, novel_output, status_text, novel_text_state, current_session_id]
2094
+ )
2095
+
2096
+ auto_scroll_checkbox.change(
2097
+ fn=toggle_auto_scroll,
2098
+ inputs=[auto_scroll_state],
2099
+ outputs=[auto_scroll_state]
2100
  )
2101
 
2102
  def handle_download(novel_text, format_type, language):
 
2114
  inputs=[novel_text_state, format_select, language_select],
2115
  outputs=[download_file]
2116
  )
2117
+
2118
+ # Load sessions on startup
2119
+ interface.load(
2120
+ fn=refresh_sessions,
2121
+ outputs=[session_dropdown]
2122
+ )
2123
 
2124
  return interface
2125