Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -30,11 +30,16 @@ except ImportError:
|
|
30 |
logger.warning("python-docx not installed. DOCX export will be disabled.")
|
31 |
|
32 |
# 환경 변수에서 토큰 가져오기
|
33 |
-
FRIENDLI_TOKEN = os.getenv("FRIENDLI_TOKEN", "
|
34 |
API_URL = "https://api.friendli.ai/dedicated/v1/chat/completions"
|
35 |
MODEL_ID = "dep89a2fld32mcm"
|
36 |
TEST_MODE = os.getenv("TEST_MODE", "false").lower() == "true"
|
37 |
|
|
|
|
|
|
|
|
|
|
|
38 |
# 전역 변수
|
39 |
conversation_history = []
|
40 |
selected_language = "English" # 기본 언어
|
@@ -43,13 +48,20 @@ selected_language = "English" # 기본 언어
|
|
43 |
DB_PATH = "novel_sessions.db"
|
44 |
db_lock = threading.Lock()
|
45 |
|
|
|
|
|
|
|
|
|
46 |
class NovelDatabase:
|
47 |
"""Novel session management database"""
|
48 |
|
49 |
@staticmethod
|
50 |
def init_db():
|
51 |
-
"""Initialize database tables"""
|
52 |
with sqlite3.connect(DB_PATH) as conn:
|
|
|
|
|
|
|
53 |
cursor = conn.cursor()
|
54 |
|
55 |
# Sessions table
|
@@ -58,8 +70,8 @@ class NovelDatabase:
|
|
58 |
session_id TEXT PRIMARY KEY,
|
59 |
user_query TEXT NOT NULL,
|
60 |
language TEXT NOT NULL,
|
61 |
-
created_at
|
62 |
-
updated_at
|
63 |
status TEXT DEFAULT 'active',
|
64 |
current_stage INTEGER DEFAULT 0,
|
65 |
final_novel TEXT
|
@@ -76,8 +88,9 @@ class NovelDatabase:
|
|
76 |
role TEXT NOT NULL,
|
77 |
content TEXT,
|
78 |
status TEXT DEFAULT 'pending',
|
79 |
-
created_at
|
80 |
-
FOREIGN KEY (session_id) REFERENCES sessions(session_id)
|
|
|
81 |
)
|
82 |
''')
|
83 |
|
@@ -90,9 +103,9 @@ class NovelDatabase:
|
|
90 |
@staticmethod
|
91 |
@contextmanager
|
92 |
def get_db():
|
93 |
-
"""Database connection context manager"""
|
94 |
with db_lock:
|
95 |
-
conn = sqlite3.connect(DB_PATH)
|
96 |
conn.row_factory = sqlite3.Row
|
97 |
try:
|
98 |
yield conn
|
@@ -121,32 +134,19 @@ class NovelDatabase:
|
|
121 |
with NovelDatabase.get_db() as conn:
|
122 |
cursor = conn.cursor()
|
123 |
|
124 |
-
#
|
125 |
cursor.execute('''
|
126 |
-
|
127 |
-
|
128 |
-
|
129 |
-
|
130 |
-
|
131 |
-
|
132 |
-
if existing:
|
133 |
-
# Update existing stage
|
134 |
-
cursor.execute('''
|
135 |
-
UPDATE stages
|
136 |
-
SET content = ?, status = ?, stage_name = ?
|
137 |
-
WHERE session_id = ? AND stage_number = ?
|
138 |
-
''', (content, status, stage_name, session_id, stage_number))
|
139 |
-
else:
|
140 |
-
# Insert new stage
|
141 |
-
cursor.execute('''
|
142 |
-
INSERT INTO stages (session_id, stage_number, stage_name, role, content, status)
|
143 |
-
VALUES (?, ?, ?, ?, ?, ?)
|
144 |
-
''', (session_id, stage_number, stage_name, role, content, status))
|
145 |
|
146 |
# Update session
|
147 |
cursor.execute('''
|
148 |
UPDATE sessions
|
149 |
-
SET updated_at =
|
150 |
WHERE session_id = ?
|
151 |
''', (stage_number, session_id))
|
152 |
|
@@ -179,11 +179,8 @@ class NovelDatabase:
|
|
179 |
with NovelDatabase.get_db() as conn:
|
180 |
cursor = conn.cursor()
|
181 |
|
182 |
-
# 작가 수정본만 가져오기 (stage_number 5, 8, 11, 14, 17, 20, 23, 26, 29, 32)
|
183 |
-
writer_revision_stages = [5, 8, 11, 14, 17, 20, 23, 26, 29, 32]
|
184 |
-
|
185 |
all_content = []
|
186 |
-
for stage_num in
|
187 |
cursor.execute('''
|
188 |
SELECT content, stage_name FROM stages
|
189 |
WHERE session_id = ? AND stage_number = ?
|
@@ -212,7 +209,7 @@ class NovelDatabase:
|
|
212 |
cursor = conn.cursor()
|
213 |
cursor.execute('''
|
214 |
UPDATE sessions
|
215 |
-
SET final_novel = ?, status = 'complete', updated_at =
|
216 |
WHERE session_id = ?
|
217 |
''', (final_novel, session_id))
|
218 |
conn.commit()
|
@@ -231,22 +228,35 @@ class NovelDatabase:
|
|
231 |
LIMIT 10
|
232 |
''')
|
233 |
return cursor.fetchall()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
234 |
|
235 |
class NovelWritingSystem:
|
236 |
def __init__(self):
|
237 |
self.token = FRIENDLI_TOKEN
|
238 |
self.api_url = API_URL
|
239 |
self.model_id = MODEL_ID
|
240 |
-
self.test_mode = TEST_MODE or
|
241 |
|
242 |
if self.test_mode:
|
243 |
-
logger.warning("Running in test mode.")
|
|
|
|
|
244 |
|
245 |
# Initialize database
|
246 |
NovelDatabase.init_db()
|
247 |
|
248 |
# Session management
|
249 |
self.current_session_id = None
|
|
|
250 |
|
251 |
def create_headers(self):
|
252 |
"""API 헤더 생성"""
|
@@ -931,7 +941,8 @@ Present a complete 50-page novel integrating all writers' contributions."""
|
|
931 |
content = chunk["choices"][0].get("delta", {}).get("content", "")
|
932 |
if content:
|
933 |
buffer += content
|
934 |
-
|
|
|
935 |
yield buffer
|
936 |
buffer = ""
|
937 |
except json.JSONDecodeError:
|
@@ -1175,6 +1186,9 @@ Present a complete 50-page novel integrating all writers' contributions."""
|
|
1175 |
("director", f"🎬 {'감독자: 최종 완성본' if language == 'Korean' else 'Director: Final Version'}")
|
1176 |
])
|
1177 |
|
|
|
|
|
|
|
1178 |
# Process stages starting from resume point
|
1179 |
for stage_idx in range(resume_from_stage, len(stage_definitions)):
|
1180 |
role, stage_name = stage_definitions[stage_idx]
|
@@ -1229,7 +1243,7 @@ Present a complete 50-page novel integrating all writers' contributions."""
|
|
1229 |
yield final_novel, stages
|
1230 |
|
1231 |
except Exception as e:
|
1232 |
-
logger.error(f"Error in process_novel_stream: {str(e)}")
|
1233 |
|
1234 |
# Save error state to DB
|
1235 |
if self.current_session_id:
|
@@ -1315,7 +1329,7 @@ Present a complete 50-page novel integrating all writers' contributions."""
|
|
1315 |
)
|
1316 |
|
1317 |
# Director final - DB에서 모든 작가 내용 가져오기
|
1318 |
-
elif stage_idx ==
|
1319 |
critic_final_idx = stage_idx - 1
|
1320 |
all_writer_content = NovelDatabase.get_all_writer_content(self.current_session_id)
|
1321 |
logger.info(f"Final director compilation with {len(all_writer_content)} characters of content")
|
@@ -1349,7 +1363,7 @@ def process_query(query: str, language: str, session_id: str = None) -> Generato
|
|
1349 |
yield stages_display, final_novel, status
|
1350 |
|
1351 |
except Exception as e:
|
1352 |
-
logger.error(f"Error in process_query: {str(e)}")
|
1353 |
if language == "Korean":
|
1354 |
yield "", "", f"❌ 오류 발생: {str(e)}"
|
1355 |
else:
|
@@ -1378,7 +1392,7 @@ def get_active_sessions(language: str) -> List[Tuple[str, str]]:
|
|
1378 |
|
1379 |
choices = []
|
1380 |
for session in sessions:
|
1381 |
-
created =
|
1382 |
date_str = created.strftime("%Y-%m-%d %H:%M")
|
1383 |
query_preview = session['user_query'][:50] + "..." if len(session['user_query']) > 50 else session['user_query']
|
1384 |
label = f"[{date_str}] {query_preview} (Stage {session['current_stage']})"
|
@@ -1386,7 +1400,7 @@ def get_active_sessions(language: str) -> List[Tuple[str, str]]:
|
|
1386 |
|
1387 |
return choices
|
1388 |
except Exception as e:
|
1389 |
-
logger.error(f"Error getting active sessions: {str(e)}")
|
1390 |
return []
|
1391 |
|
1392 |
def resume_session(session_id: str, language: str) -> Generator[Tuple[str, str, str], None, None]:
|
@@ -1416,9 +1430,11 @@ def download_novel(novel_text: str, format: str, language: str) -> str:
|
|
1416 |
lines = novel_text.split('\n')
|
1417 |
for line in lines:
|
1418 |
if line.startswith('#'):
|
1419 |
-
level
|
|
|
1420 |
text = line.lstrip('#').strip()
|
1421 |
-
|
|
|
1422 |
elif line.strip():
|
1423 |
doc.add_paragraph(line)
|
1424 |
|
@@ -1613,7 +1629,7 @@ def create_interface():
|
|
1613 |
sessions = get_active_sessions("English")
|
1614 |
return gr.update(choices=sessions)
|
1615 |
except Exception as e:
|
1616 |
-
logger.error(f"Error refreshing sessions: {str(e)}")
|
1617 |
return gr.update(choices=[])
|
1618 |
|
1619 |
submit_btn.click(
|
@@ -1674,6 +1690,12 @@ def create_interface():
|
|
1674 |
if __name__ == "__main__":
|
1675 |
logger.info("Starting SOMA Novel Writing System...")
|
1676 |
|
|
|
|
|
|
|
|
|
|
|
|
|
1677 |
# Initialize database on startup
|
1678 |
logger.info("Initializing database...")
|
1679 |
NovelDatabase.init_db()
|
|
|
30 |
logger.warning("python-docx not installed. DOCX export will be disabled.")
|
31 |
|
32 |
# 환경 변수에서 토큰 가져오기
|
33 |
+
FRIENDLI_TOKEN = os.getenv("FRIENDLI_TOKEN", "")
|
34 |
API_URL = "https://api.friendli.ai/dedicated/v1/chat/completions"
|
35 |
MODEL_ID = "dep89a2fld32mcm"
|
36 |
TEST_MODE = os.getenv("TEST_MODE", "false").lower() == "true"
|
37 |
|
38 |
+
# 환경 변수 검증
|
39 |
+
if not FRIENDLI_TOKEN and not TEST_MODE:
|
40 |
+
logger.warning("FRIENDLI_TOKEN not set and TEST_MODE is false. Application will run in test mode.")
|
41 |
+
TEST_MODE = True
|
42 |
+
|
43 |
# 전역 변수
|
44 |
conversation_history = []
|
45 |
selected_language = "English" # 기본 언어
|
|
|
48 |
DB_PATH = "novel_sessions.db"
|
49 |
db_lock = threading.Lock()
|
50 |
|
51 |
+
# Stage 번호 상수
|
52 |
+
WRITER_DRAFT_STAGES = [3, 6, 9, 12, 15, 18, 21, 24, 27, 30] # 작가 초안
|
53 |
+
WRITER_REVISION_STAGES = [5, 8, 11, 14, 17, 20, 23, 26, 29, 32] # 작가 수정본
|
54 |
+
|
55 |
class NovelDatabase:
|
56 |
"""Novel session management database"""
|
57 |
|
58 |
@staticmethod
|
59 |
def init_db():
|
60 |
+
"""Initialize database tables with WAL mode for better concurrency"""
|
61 |
with sqlite3.connect(DB_PATH) as conn:
|
62 |
+
# Enable WAL mode for better concurrent access
|
63 |
+
conn.execute("PRAGMA journal_mode=WAL")
|
64 |
+
|
65 |
cursor = conn.cursor()
|
66 |
|
67 |
# Sessions table
|
|
|
70 |
session_id TEXT PRIMARY KEY,
|
71 |
user_query TEXT NOT NULL,
|
72 |
language TEXT NOT NULL,
|
73 |
+
created_at TEXT DEFAULT (datetime('now')),
|
74 |
+
updated_at TEXT DEFAULT (datetime('now')),
|
75 |
status TEXT DEFAULT 'active',
|
76 |
current_stage INTEGER DEFAULT 0,
|
77 |
final_novel TEXT
|
|
|
88 |
role TEXT NOT NULL,
|
89 |
content TEXT,
|
90 |
status TEXT DEFAULT 'pending',
|
91 |
+
created_at TEXT DEFAULT (datetime('now')),
|
92 |
+
FOREIGN KEY (session_id) REFERENCES sessions(session_id),
|
93 |
+
UNIQUE(session_id, stage_number)
|
94 |
)
|
95 |
''')
|
96 |
|
|
|
103 |
@staticmethod
|
104 |
@contextmanager
|
105 |
def get_db():
|
106 |
+
"""Database connection context manager with timeout"""
|
107 |
with db_lock:
|
108 |
+
conn = sqlite3.connect(DB_PATH, timeout=30.0)
|
109 |
conn.row_factory = sqlite3.Row
|
110 |
try:
|
111 |
yield conn
|
|
|
134 |
with NovelDatabase.get_db() as conn:
|
135 |
cursor = conn.cursor()
|
136 |
|
137 |
+
# UPSERT operation
|
138 |
cursor.execute('''
|
139 |
+
INSERT INTO stages (session_id, stage_number, stage_name, role, content, status)
|
140 |
+
VALUES (?, ?, ?, ?, ?, ?)
|
141 |
+
ON CONFLICT(session_id, stage_number)
|
142 |
+
DO UPDATE SET content=?, status=?, stage_name=?, updated_at=datetime('now')
|
143 |
+
''', (session_id, stage_number, stage_name, role, content, status,
|
144 |
+
content, status, stage_name))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
145 |
|
146 |
# Update session
|
147 |
cursor.execute('''
|
148 |
UPDATE sessions
|
149 |
+
SET updated_at = datetime('now'), current_stage = ?
|
150 |
WHERE session_id = ?
|
151 |
''', (stage_number, session_id))
|
152 |
|
|
|
179 |
with NovelDatabase.get_db() as conn:
|
180 |
cursor = conn.cursor()
|
181 |
|
|
|
|
|
|
|
182 |
all_content = []
|
183 |
+
for stage_num in WRITER_REVISION_STAGES:
|
184 |
cursor.execute('''
|
185 |
SELECT content, stage_name FROM stages
|
186 |
WHERE session_id = ? AND stage_number = ?
|
|
|
209 |
cursor = conn.cursor()
|
210 |
cursor.execute('''
|
211 |
UPDATE sessions
|
212 |
+
SET final_novel = ?, status = 'complete', updated_at = datetime('now')
|
213 |
WHERE session_id = ?
|
214 |
''', (final_novel, session_id))
|
215 |
conn.commit()
|
|
|
228 |
LIMIT 10
|
229 |
''')
|
230 |
return cursor.fetchall()
|
231 |
+
|
232 |
+
@staticmethod
|
233 |
+
def parse_datetime(datetime_str: str) -> datetime:
|
234 |
+
"""Parse SQLite datetime string safely"""
|
235 |
+
try:
|
236 |
+
# Try ISO format first
|
237 |
+
return datetime.fromisoformat(datetime_str)
|
238 |
+
except:
|
239 |
+
# Fallback to SQLite default format
|
240 |
+
return datetime.strptime(datetime_str, "%Y-%m-%d %H:%M:%S")
|
241 |
|
242 |
class NovelWritingSystem:
|
243 |
def __init__(self):
|
244 |
self.token = FRIENDLI_TOKEN
|
245 |
self.api_url = API_URL
|
246 |
self.model_id = MODEL_ID
|
247 |
+
self.test_mode = TEST_MODE or not self.token
|
248 |
|
249 |
if self.test_mode:
|
250 |
+
logger.warning("Running in test mode - no actual API calls will be made.")
|
251 |
+
else:
|
252 |
+
logger.info("Running in production mode with API calls enabled.")
|
253 |
|
254 |
# Initialize database
|
255 |
NovelDatabase.init_db()
|
256 |
|
257 |
# Session management
|
258 |
self.current_session_id = None
|
259 |
+
self.total_stages = 0 # Will be set in process_novel_stream
|
260 |
|
261 |
def create_headers(self):
|
262 |
"""API 헤더 생성"""
|
|
|
941 |
content = chunk["choices"][0].get("delta", {}).get("content", "")
|
942 |
if content:
|
943 |
buffer += content
|
944 |
+
# 더 큰 버퍼로 수정 (긴 단어 대응)
|
945 |
+
if len(buffer) > 100 or '\n\n' in buffer:
|
946 |
yield buffer
|
947 |
buffer = ""
|
948 |
except json.JSONDecodeError:
|
|
|
1186 |
("director", f"🎬 {'감독자: 최종 완성본' if language == 'Korean' else 'Director: Final Version'}")
|
1187 |
])
|
1188 |
|
1189 |
+
# Store total stages for get_stage_prompt
|
1190 |
+
self.total_stages = len(stage_definitions)
|
1191 |
+
|
1192 |
# Process stages starting from resume point
|
1193 |
for stage_idx in range(resume_from_stage, len(stage_definitions)):
|
1194 |
role, stage_name = stage_definitions[stage_idx]
|
|
|
1243 |
yield final_novel, stages
|
1244 |
|
1245 |
except Exception as e:
|
1246 |
+
logger.error(f"Error in process_novel_stream: {str(e)}", exc_info=True)
|
1247 |
|
1248 |
# Save error state to DB
|
1249 |
if self.current_session_id:
|
|
|
1329 |
)
|
1330 |
|
1331 |
# Director final - DB에서 모든 작가 내용 가져오기
|
1332 |
+
elif stage_idx == self.total_stages - 1: # Fixed: using self.total_stages
|
1333 |
critic_final_idx = stage_idx - 1
|
1334 |
all_writer_content = NovelDatabase.get_all_writer_content(self.current_session_id)
|
1335 |
logger.info(f"Final director compilation with {len(all_writer_content)} characters of content")
|
|
|
1363 |
yield stages_display, final_novel, status
|
1364 |
|
1365 |
except Exception as e:
|
1366 |
+
logger.error(f"Error in process_query: {str(e)}", exc_info=True)
|
1367 |
if language == "Korean":
|
1368 |
yield "", "", f"❌ 오류 발생: {str(e)}"
|
1369 |
else:
|
|
|
1392 |
|
1393 |
choices = []
|
1394 |
for session in sessions:
|
1395 |
+
created = NovelDatabase.parse_datetime(session['created_at'])
|
1396 |
date_str = created.strftime("%Y-%m-%d %H:%M")
|
1397 |
query_preview = session['user_query'][:50] + "..." if len(session['user_query']) > 50 else session['user_query']
|
1398 |
label = f"[{date_str}] {query_preview} (Stage {session['current_stage']})"
|
|
|
1400 |
|
1401 |
return choices
|
1402 |
except Exception as e:
|
1403 |
+
logger.error(f"Error getting active sessions: {str(e)}", exc_info=True)
|
1404 |
return []
|
1405 |
|
1406 |
def resume_session(session_id: str, language: str) -> Generator[Tuple[str, str, str], None, None]:
|
|
|
1430 |
lines = novel_text.split('\n')
|
1431 |
for line in lines:
|
1432 |
if line.startswith('#'):
|
1433 |
+
# Safe heading level extraction
|
1434 |
+
level = min(len(line.split()[0].strip('#')), 9)
|
1435 |
text = line.lstrip('#').strip()
|
1436 |
+
if text:
|
1437 |
+
doc.add_heading(text, level)
|
1438 |
elif line.strip():
|
1439 |
doc.add_paragraph(line)
|
1440 |
|
|
|
1629 |
sessions = get_active_sessions("English")
|
1630 |
return gr.update(choices=sessions)
|
1631 |
except Exception as e:
|
1632 |
+
logger.error(f"Error refreshing sessions: {str(e)}", exc_info=True)
|
1633 |
return gr.update(choices=[])
|
1634 |
|
1635 |
submit_btn.click(
|
|
|
1690 |
if __name__ == "__main__":
|
1691 |
logger.info("Starting SOMA Novel Writing System...")
|
1692 |
|
1693 |
+
# Check environment
|
1694 |
+
if TEST_MODE:
|
1695 |
+
logger.warning("Running in TEST MODE - no actual API calls will be made")
|
1696 |
+
else:
|
1697 |
+
logger.info(f"Running in PRODUCTION MODE with API endpoint: {API_URL}")
|
1698 |
+
|
1699 |
# Initialize database on startup
|
1700 |
logger.info("Initializing database...")
|
1701 |
NovelDatabase.init_db()
|