openfree commited on
Commit
aa177b7
·
verified ·
1 Parent(s): 8c93ab4

Update app-backup.py

Browse files
Files changed (1) hide show
  1. app-backup.py +567 -1164
app-backup.py CHANGED
@@ -16,11 +16,11 @@ from contextlib import contextmanager
16
  from dataclasses import dataclass, field
17
  from collections import defaultdict
18
 
19
- # 로깅 설정
20
- logging.basicConfig(level=logging.INFO)
21
  logger = logging.getLogger(__name__)
22
 
23
- # Document export imports
24
  try:
25
  from docx import Document
26
  from docx.shared import Inches, Pt, RGBColor
@@ -31,46 +31,44 @@ except ImportError:
31
  DOCX_AVAILABLE = False
32
  logger.warning("python-docx not installed. DOCX export will be disabled.")
33
 
34
- # 환경 변수에서 토큰 가져오기
35
  FRIENDLI_TOKEN = os.getenv("FRIENDLI_TOKEN", "")
36
  BRAVE_SEARCH_API_KEY = os.getenv("BRAVE_SEARCH_API_KEY", "")
37
  API_URL = "https://api.friendli.ai/dedicated/v1/chat/completions"
38
  MODEL_ID = "dep89a2fld32mcm"
 
39
 
40
- # 환경 변수 검증
41
  if not FRIENDLI_TOKEN:
42
  logger.error("FRIENDLI_TOKEN not set. Application will not work properly.")
43
- FRIENDLI_TOKEN = "dummy_token"
 
44
 
45
  if not BRAVE_SEARCH_API_KEY:
46
  logger.warning("BRAVE_SEARCH_API_KEY not set. Web search features will be disabled.")
47
 
48
- # 전역 변수
49
- conversation_history = []
50
- selected_language = "English"
51
-
52
- # DB 경로
53
- DB_PATH = "novel_sessions.db"
54
  db_lock = threading.Lock()
55
 
56
- # 최적화된 단계 구성 (30단계)
57
  OPTIMIZED_STAGES = [
58
- ("director", "🎬 감독자: 초기 기획"),
59
- ("critic", "📝 비평가: 기획 검토"),
60
  ("director", "🎬 감독자: 수정된 마스터플랜"),
61
  ] + [
62
  (f"writer{i}", f"✍️ 작가 {i}: 초안 (페이지 {(i-1)*3+1}-{i*3})")
63
  for i in range(1, 11)
64
  ] + [
65
- ("critic", f"📝 비평가: 일관성 검토"),
66
  ] + [
67
  (f"writer{i}", f"✍️ 작가 {i}: 수정본 (페이지 {(i-1)*3+1}-{i*3})")
68
  for i in range(1, 11)
69
  ] + [
70
- ("critic", f"📝 비평가: 최종 검토"),
71
  ]
72
 
73
 
 
74
  @dataclass
75
  class CharacterState:
76
  """캐릭터의 현재 상태를 나타내는 데이터 클래스"""
@@ -84,7 +82,6 @@ class CharacterState:
84
  description: str = ""
85
  role: str = ""
86
 
87
-
88
  @dataclass
89
  class PlotPoint:
90
  """플롯 포인트를 나타내는 데이터 클래스"""
@@ -95,7 +92,6 @@ class PlotPoint:
95
  impact_level: int
96
  timestamp: str = ""
97
 
98
-
99
  @dataclass
100
  class TimelineEvent:
101
  """시간선 이벤트를 나타내는 데이터 클래스"""
@@ -106,274 +102,153 @@ class TimelineEvent:
106
  relative_time: str = ""
107
 
108
 
 
109
  class ConsistencyTracker:
110
  """일관성 추적 시스템"""
111
-
112
  def __init__(self):
113
  self.character_states: Dict[str, CharacterState] = {}
114
  self.plot_points: List[PlotPoint] = []
115
  self.timeline_events: List[TimelineEvent] = []
116
  self.locations: Dict[str, str] = {}
117
  self.established_facts: List[str] = []
118
- self.content_hashes: set = set()
119
-
120
  def register_character(self, character: CharacterState):
121
  """새 캐릭터 등록"""
122
  self.character_states[character.name] = character
123
  logger.info(f"Character registered: {character.name}")
124
-
125
  def update_character_state(self, name: str, chapter: int, updates: Dict[str, Any]):
126
  """캐릭터 상태 업데이트"""
127
  if name not in self.character_states:
128
  self.register_character(CharacterState(name=name, last_seen_chapter=chapter))
129
-
130
  char = self.character_states[name]
131
  for key, value in updates.items():
132
  if hasattr(char, key):
133
  setattr(char, key, value)
134
-
135
  char.last_seen_chapter = chapter
136
-
137
  def add_plot_point(self, plot_point: PlotPoint):
138
  """플롯 포인트 추가"""
139
  plot_point.timestamp = datetime.now().isoformat()
140
  self.plot_points.append(plot_point)
141
-
142
- def add_timeline_event(self, event: TimelineEvent):
143
- """시간선 이벤트 추가"""
144
- self.timeline_events.append(event)
145
-
146
- def check_repetition(self, content: str) -> Tuple[bool, str]:
147
- """반복 내용 검사"""
148
- # 내용 해시 생성
149
- content_hash = hashlib.md5(content.encode()).hexdigest()
150
-
151
- if content_hash in self.content_hashes:
152
- return True, "완전 동일한 내용 반복"
153
-
154
- # 문장 수준 반복 검사
155
  sentences = re.split(r'[.!?]+', content)
156
  for sentence in sentences:
157
- if len(sentence.strip()) > 20:
158
- sentence_hash = hashlib.md5(sentence.strip().encode()).hexdigest()
 
159
  if sentence_hash in self.content_hashes:
160
- return True, f"문장 반복: {sentence.strip()[:50]}..."
161
-
162
- # 해시 저장
163
- self.content_hashes.add(content_hash)
 
164
  for sentence in sentences:
165
- if len(sentence.strip()) > 20:
166
- sentence_hash = hashlib.md5(sentence.strip().encode()).hexdigest()
167
- self.content_hashes.add(sentence_hash)
168
-
169
  return False, ""
170
-
171
  def validate_consistency(self, chapter: int, content: str) -> List[str]:
172
  """일관성 검증"""
173
  errors = []
174
-
175
- # 캐릭터 일관성 검사
176
  for char_name, char_state in self.character_states.items():
177
- if char_name.lower() in content.lower():
178
- # 사망한 캐릭터 등장 검사
179
- if not char_state.alive:
180
- errors.append(f"⚠️ 사망한 캐릭터 '{char_name}'이 등장함")
181
-
182
- # 부상 상태 연속성 검사
183
- if char_state.injuries and "완전히 회복" not in content:
184
- recent_injuries = [inj for inj in char_state.injuries if "중상" in inj or "심각" in inj]
185
- if recent_injuries:
186
- errors.append(f"⚠️ '{char_name}'의 부상 상태가 언급되지 않음")
187
-
188
- # 반복 검사
189
- is_repetition, repeat_msg = self.check_repetition(content)
190
  if is_repetition:
191
  errors.append(f"🔄 {repeat_msg}")
192
-
193
- # 시간선 일관성 검사
194
- time_references = re.findall(r'(어제|오늘|내일|지금|방금|곧|나중에|이전에|다음에)', content)
195
- if time_references:
196
- # 최근 시간 참조와 비교
197
- recent_events = [e for e in self.timeline_events if e.chapter >= chapter - 1]
198
- if recent_events and time_references:
199
- # 간단한 시간 일관성 검사
200
- pass # 복잡한 로직은 생략
201
-
202
  return errors
203
-
204
  def get_character_summary(self, chapter: int) -> str:
205
  """현재 챕터 기준 캐릭터 요약"""
206
- summary = "\n=== 캐릭터 현황 ===\n"
207
-
208
- active_chars = [char for char in self.character_states.values()
209
- if char.last_seen_chapter >= chapter - 2]
210
-
211
  for char in active_chars:
212
  status = "생존" if char.alive else "사망"
213
  summary += f"• {char.name}: {status}"
214
- if char.alive:
215
- if char.location:
216
- summary += f" (위치: {char.location})"
217
- if char.injuries:
218
- summary += f" (부상: {', '.join(char.injuries[-1:])})"
219
  summary += "\n"
220
-
221
  return summary
222
-
223
  def get_plot_summary(self, chapter: int) -> str:
224
  """플롯 요약"""
225
- summary = "\n=== 주요 사건 ===\n"
226
-
227
  recent_events = [p for p in self.plot_points if p.chapter >= chapter - 2]
228
- for event in recent_events[-5:]:
229
- summary += f" 챕터 {event.chapter}: {event.description}\n"
230
-
 
231
  return summary
232
 
233
 
234
- class ConsistencyValidator:
235
- """일관성 검증 시스템"""
236
-
237
- def __init__(self, consistency_tracker: ConsistencyTracker):
238
- self.tracker = consistency_tracker
239
-
240
- def validate_writer_content(self, writer_num: int, content: str,
241
- all_previous_content: str) -> List[str]:
242
- """작가 내용 일관성 검증"""
243
- errors = []
244
-
245
- # 기본 일관성 검증
246
- basic_errors = self.tracker.validate_consistency(writer_num, content)
247
- errors.extend(basic_errors)
248
-
249
- # 캐릭터 추출 및 상태 업데이트
250
- characters = self.extract_characters(content)
251
- for char_name in characters:
252
- self.tracker.update_character_state(char_name, writer_num, {})
253
-
254
- # 새로운 사실 추출
255
- new_facts = self.extract_facts(content)
256
- for fact in new_facts:
257
- if fact not in self.tracker.established_facts:
258
- self.tracker.established_facts.append(fact)
259
-
260
- return errors
261
-
262
- def extract_characters(self, content: str) -> List[str]:
263
- """캐릭터 이름 추출"""
264
- # 간단한 캐릭터 추출 로직
265
- potential_names = re.findall(r'\b[A-Z][a-z]+\b', content)
266
- # 일반적인 단어 제외
267
- common_words = {'The', 'This', 'That', 'They', 'Then', 'There', 'When', 'Where', 'What', 'Who', 'How', 'Why'}
268
- characters = [name for name in potential_names if name not in common_words]
269
- return list(set(characters))
270
-
271
- def extract_facts(self, content: str) -> List[str]:
272
- """확립된 사실 추출"""
273
- # 간단한 사실 추출 로직
274
- sentences = re.split(r'[.!?]+', content)
275
- facts = []
276
-
277
- for sentence in sentences:
278
- sentence = sentence.strip()
279
- if len(sentence) > 10:
280
- # 확정적인 표현이 있는 문장들
281
- definitive_patterns = [
282
- r'.*is.*',
283
- r'.*was.*',
284
- r'.*has.*',
285
- r'.*had.*',
286
- r'.*died.*',
287
- r'.*killed.*',
288
- r'.*destroyed.*',
289
- r'.*created.*'
290
- ]
291
-
292
- for pattern in definitive_patterns:
293
- if re.match(pattern, sentence, re.IGNORECASE):
294
- facts.append(sentence)
295
- break
296
-
297
- return facts
298
-
299
-
300
  class WebSearchIntegration:
301
  """웹 검색 기능 (감독자 단계에서만 사용)"""
302
-
303
  def __init__(self):
304
  self.brave_api_key = BRAVE_SEARCH_API_KEY
305
  self.search_url = "https://api.search.brave.com/res/v1/web/search"
306
  self.enabled = bool(self.brave_api_key)
307
-
308
  def search(self, query: str, count: int = 3, language: str = "en") -> List[Dict]:
309
  """웹 검색 수행"""
310
  if not self.enabled:
311
  return []
312
-
313
  headers = {
314
  "Accept": "application/json",
315
  "X-Subscription-Token": self.brave_api_key
316
  }
317
-
318
- search_lang = "ko" if language == "Korean" else "en"
319
-
320
  params = {
321
  "q": query,
322
  "count": count,
323
- "search_lang": search_lang,
324
  "text_decorations": False,
325
  "safesearch": "moderate"
326
  }
327
-
328
  try:
329
  response = requests.get(self.search_url, headers=headers, params=params, timeout=10)
330
- if response.status_code == 200:
331
- results = response.json()
332
- web_results = results.get("web", {}).get("results", [])
333
- logger.info(f"Search successful: Found {len(web_results)} results for '{query}'")
334
- return web_results
335
- else:
336
- logger.error(f"Search API error: {response.status_code}")
337
- return []
338
- except Exception as e:
339
- logger.error(f"Search error: {str(e)}")
340
  return []
341
-
342
- def extract_relevant_info(self, results: List[Dict], max_chars: int = 2000) -> str:
343
  """검색 결과에서 관련 정보 추출"""
344
  if not results:
345
  return ""
346
-
347
  extracted = []
348
  total_chars = 0
349
-
350
  for i, result in enumerate(results[:3], 1):
351
  title = result.get("title", "")
352
  description = result.get("description", "")
353
  url = result.get("url", "")
354
-
355
- info = f"[{i}] {title}\n{description}\n출처: {url}\n"
356
-
357
  if total_chars + len(info) < max_chars:
358
  extracted.append(info)
359
  total_chars += len(info)
360
  else:
361
  break
362
-
363
  return "\n---\n".join(extracted)
364
 
365
 
366
  class NovelDatabase:
367
  """소설 세션 관리 데이터베이스"""
368
-
369
  @staticmethod
370
  def init_db():
371
- """데이터베이스 초기화"""
372
  with sqlite3.connect(DB_PATH) as conn:
373
  conn.execute("PRAGMA journal_mode=WAL")
374
  cursor = conn.cursor()
375
-
376
- # 세션 테이블
377
  cursor.execute('''
378
  CREATE TABLE IF NOT EXISTS sessions (
379
  session_id TEXT PRIMARY KEY,
@@ -387,8 +262,6 @@ class NovelDatabase:
387
  consistency_report TEXT
388
  )
389
  ''')
390
-
391
- # 단계 테이블
392
  cursor.execute('''
393
  CREATE TABLE IF NOT EXISTS stages (
394
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -406,8 +279,6 @@ class NovelDatabase:
406
  UNIQUE(session_id, stage_number)
407
  )
408
  ''')
409
-
410
- # 캐릭터 상태 테이블
411
  cursor.execute('''
412
  CREATE TABLE IF NOT EXISTS character_states (
413
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -423,19 +294,15 @@ class NovelDatabase:
423
  FOREIGN KEY (session_id) REFERENCES sessions(session_id)
424
  )
425
  ''')
426
-
427
- # 인덱스 생성
428
  cursor.execute('CREATE INDEX IF NOT EXISTS idx_session_id ON stages(session_id)')
429
  cursor.execute('CREATE INDEX IF NOT EXISTS idx_stage_number ON stages(stage_number)')
430
  cursor.execute('CREATE INDEX IF NOT EXISTS idx_char_session ON character_states(session_id)')
431
  cursor.execute('CREATE INDEX IF NOT EXISTS idx_session_status ON sessions(status)')
432
-
433
  conn.commit()
434
-
435
  @staticmethod
436
  @contextmanager
437
  def get_db():
438
- """데이터베이스 연결 컨텍스트 매니저"""
439
  with db_lock:
440
  conn = sqlite3.connect(DB_PATH, timeout=30.0)
441
  conn.row_factory = sqlite3.Row
@@ -443,773 +310,422 @@ class NovelDatabase:
443
  yield conn
444
  finally:
445
  conn.close()
446
-
447
  @staticmethod
448
  def create_session(user_query: str, language: str) -> str:
449
- """새 세션 생성"""
450
  session_id = hashlib.md5(f"{user_query}{datetime.now()}".encode()).hexdigest()
451
-
452
  with NovelDatabase.get_db() as conn:
453
- cursor = conn.cursor()
454
- cursor.execute('''
455
- INSERT INTO sessions (session_id, user_query, language)
456
- VALUES (?, ?, ?)
457
- ''', (session_id, user_query, language))
458
  conn.commit()
459
-
460
  return session_id
461
-
462
  @staticmethod
463
- def save_stage(session_id: str, stage_number: int, stage_name: str,
464
- role: str, content: str, status: str = 'complete',
465
  consistency_score: float = 0.0):
466
- """단계 저장"""
467
  word_count = len(content.split()) if content else 0
468
-
469
  with NovelDatabase.get_db() as conn:
470
  cursor = conn.cursor()
471
-
472
  cursor.execute('''
473
  INSERT INTO stages (session_id, stage_number, stage_name, role, content, word_count, status, consistency_score)
474
  VALUES (?, ?, ?, ?, ?, ?, ?, ?)
475
- ON CONFLICT(session_id, stage_number)
476
  DO UPDATE SET content=?, word_count=?, status=?, stage_name=?, consistency_score=?, updated_at=datetime('now')
477
  ''', (session_id, stage_number, stage_name, role, content, word_count, status, consistency_score,
478
  content, word_count, status, stage_name, consistency_score))
479
-
480
- cursor.execute('''
481
- UPDATE sessions
482
- SET updated_at = datetime('now'), current_stage = ?
483
- WHERE session_id = ?
484
- ''', (stage_number, session_id))
485
-
486
  conn.commit()
487
-
488
  @staticmethod
489
  def get_session(session_id: str) -> Optional[Dict]:
490
- """세션 정보 가져오기"""
491
  with NovelDatabase.get_db() as conn:
492
- cursor = conn.cursor()
493
- cursor.execute('SELECT * FROM sessions WHERE session_id = ?', (session_id,))
494
- row = cursor.fetchone()
495
  return dict(row) if row else None
496
-
497
  @staticmethod
498
  def get_latest_active_session() -> Optional[Dict]:
499
- """최근 활성 세션 가져오기"""
500
  with NovelDatabase.get_db() as conn:
501
- cursor = conn.cursor()
502
- cursor.execute('''
503
- SELECT * FROM sessions
504
- WHERE status = 'active'
505
- ORDER BY updated_at DESC
506
- LIMIT 1
507
- ''')
508
- row = cursor.fetchone()
509
  return dict(row) if row else None
510
-
511
  @staticmethod
512
  def get_stages(session_id: str) -> List[Dict]:
513
- """세션의 모든 단계 가져오기"""
514
  with NovelDatabase.get_db() as conn:
515
- cursor = conn.cursor()
516
- cursor.execute('''
517
- SELECT * FROM stages
518
- WHERE session_id = ?
519
- ORDER BY stage_number
520
- ''', (session_id,))
521
- return [dict(row) for row in cursor.fetchall()]
522
-
523
  @staticmethod
524
  def get_writer_content(session_id: str) -> str:
525
- """모든 작가의 최종 내용 가져오기"""
526
  with NovelDatabase.get_db() as conn:
527
- cursor = conn.cursor()
528
-
529
  all_content = []
530
-
531
- # 작가 1-10의 수정본 가져오기
532
  for writer_num in range(1, 11):
533
- cursor.execute('''
534
- SELECT content FROM stages
535
- WHERE session_id = ? AND role = ?
536
- AND stage_name LIKE '%수정본%'
537
- ORDER BY stage_number DESC
538
- LIMIT 1
539
- ''', (session_id, f'writer{writer_num}'))
540
-
541
- row = cursor.fetchone()
542
  if row and row['content']:
543
- content = row['content'].strip()
544
- if content:
545
- all_content.append(content)
546
-
547
  return '\n\n'.join(all_content)
548
-
549
  @staticmethod
550
  def update_final_novel(session_id: str, final_novel: str, consistency_report: str = ""):
551
- """최종 소설 업데이트"""
552
  with NovelDatabase.get_db() as conn:
553
- cursor = conn.cursor()
554
- cursor.execute('''
555
- UPDATE sessions
556
- SET final_novel = ?, status = 'complete', updated_at = datetime('now'), consistency_report = ?
557
- WHERE session_id = ?
558
- ''', (final_novel, consistency_report, session_id))
559
  conn.commit()
560
-
561
  @staticmethod
562
  def get_active_sessions() -> List[Dict]:
563
- """활성 세션 목록 가져오기"""
564
  with NovelDatabase.get_db() as conn:
565
- cursor = conn.cursor()
566
- cursor.execute('''
567
- SELECT session_id, user_query, language, created_at, current_stage
568
- FROM sessions
569
- WHERE status = 'active'
570
- ORDER BY updated_at DESC
571
- LIMIT 10
572
- ''')
573
- return [dict(row) for row in cursor.fetchall()]
574
 
575
 
576
  class NovelWritingSystem:
577
  """최적화된 소설 생성 시스템"""
578
-
579
  def __init__(self):
580
  self.token = FRIENDLI_TOKEN
581
  self.api_url = API_URL
582
  self.model_id = MODEL_ID
583
-
584
- # 핵심 시스템 컴포넌트
585
  self.consistency_tracker = ConsistencyTracker()
586
- self.consistency_validator = ConsistencyValidator(self.consistency_tracker)
587
  self.web_search = WebSearchIntegration()
588
-
589
- # 세션 관리
590
  self.current_session_id = None
591
-
592
- # 데이터베이스 초기화
593
  NovelDatabase.init_db()
594
-
595
  def create_headers(self):
596
  """API 헤더 생성"""
597
- return {
598
- "Authorization": f"Bearer {self.token}",
599
- "Content-Type": "application/json"
600
- }
601
-
602
- def create_director_initial_prompt(self, user_query: str, language: str = "English") -> str:
603
- """감독자 초기 프롬프트 (웹 검색 포함)"""
604
-
605
- # 웹 검색 수행
606
- search_results = ""
607
- if self.web_search.enabled:
608
- search_queries = []
609
- if language == "Korean":
610
- search_queries = [
611
- f"{user_query} 소설 설정",
612
- f"{user_query} 배경 정보",
613
- f"{user_query} 관련 자료"
614
- ]
615
- else:
616
- search_queries = [
617
- f"{user_query} novel setting",
618
- f"{user_query} background information",
619
- f"{user_query} related material"
620
- ]
621
-
622
- for query in search_queries[:2]: # 2개 쿼리만 사용
623
- results = self.web_search.search(query, count=2, language=language)
624
- if results:
625
- search_info = self.web_search.extract_relevant_info(results)
626
- search_results += f"\n### {query}\n{search_info}\n"
627
-
628
- if language == "Korean":
629
- base_prompt = f"""당신은 30페이지 중편 소설을 기획하는 문학 감독자입니다.
630
-
631
- 사용자 주제: {user_query}
632
-
633
- {search_results if search_results else ""}
634
-
635
- 다음 요소들을 포함한 상세한 소설 기획을 작성하세요:
636
-
637
- 1. **주제와 장르 설정**
638
- - 핵심 주제와 메시지
639
- - 장르 및 분위기
640
- - 독자층 고려사항
641
-
642
- 2. **주요 등장인물** (3-5명)
643
- | 이름 | 역할 | 성격 | 배경 | 목표 | 갈등 |
644
- |------|------|------|------|------|------|
645
-
646
- 3. **배경 설정**
647
- - 시공간적 배경
648
- - 사회적/문화적 환경
649
- - 주요 장소들
650
-
651
- 4. **플롯 구조** (10개 파트, 각 3페이지)
652
- | 파트 | 페이지 | 주요 사건 | 긴장도 | 캐릭터 발전 |
653
- |------|--------|-----------|---------|-----------|
654
- | 1 | 1-3 | | | |
655
- | 2 | 4-6 | | | |
656
- | 3 | 7-9 | | | |
657
- | 4 | 10-12 | | | |
658
- | 5 | 13-15 | | | |
659
- | 6 | 16-18 | | | |
660
- | 7 | 19-21 | | | |
661
- | 8 | 22-24 | | | |
662
- | 9 | 25-27 | | | |
663
- | 10 | 28-30 | | | |
664
-
665
- 5. **작가별 지침**
666
- - 각 작가가 담당할 내용과 주의사항
667
- - 일관성 유지를 위한 핵심 설정
668
- - 문체와 톤 가이드라인
669
-
670
- 창의적이고 흥미로운 소설이 될 수 있도록 상세하게 기획하세요."""
671
- else:
672
- base_prompt = f"""You are a literary director planning a 30-page novella.
673
-
674
- User Theme: {user_query}
675
-
676
- {search_results if search_results else ""}
677
-
678
- Create a detailed novel plan including:
679
-
680
- 1. **Theme and Genre Setting**
681
- - Core theme and message
682
- - Genre and atmosphere
683
- - Target audience considerations
684
-
685
- 2. **Main Characters** (3-5 characters)
686
- | Name | Role | Personality | Background | Goal | Conflict |
687
- |------|------|-------------|------------|------|----------|
688
-
689
- 3. **Setting**
690
- - Temporal and spatial background
691
- - Social/cultural environment
692
- - Key locations
693
-
694
- 4. **Plot Structure** (10 parts, 3 pages each)
695
- | Part | Pages | Main Events | Tension | Character Development |
696
- |------|-------|-------------|---------|---------------------|
697
- | 1 | 1-3 | | | |
698
- | 2 | 4-6 | | | |
699
- | 3 | 7-9 | | | |
700
- | 4 | 10-12 | | | |
701
- | 5 | 13-15 | | | |
702
- | 6 | 16-18 | | | |
703
- | 7 | 19-21 | | | |
704
- | 8 | 22-24 | | | |
705
- | 9 | 25-27 | | | |
706
- | 10 | 28-30 | | | |
707
-
708
- 5. **Guidelines for Writers**
709
- - Content and precautions for each writer
710
- - Key settings for consistency
711
- - Style and tone guidelines
712
-
713
- Plan creatively for an engaging novel."""
714
-
715
- return base_prompt
716
-
717
- def create_critic_director_prompt(self, director_plan: str, language: str = "English") -> str:
718
- """비평가의 감독자 기획 검토"""
719
- if language == "Korean":
720
- return f"""당신은 문학 비평가입니다. 감독자의 소설 기획을 일관성 관점에서 검토하세요.
721
-
722
- 감독자 기획:
723
- {director_plan}
724
 
725
- 다음 관점에서 검토하고 개선안을 제시하세요:
726
-
727
- 1. **일관성 잠재 위험**
728
- - 캐릭터 설정의 모순 가능성
729
- - 플롯 전개의 논리적 허점
730
- - 시간선/공간 설정의 문제점
731
-
732
- 2. **캐릭터 연속성**
733
- - 캐릭터의 동기와 행동 일관성
734
- - 관계 설정의 지속성
735
- - 성격 변화의 자연스러움
736
-
737
- 3. **서사 구조 검토**
738
- - 플롯 포인트 간의 연결성
739
- - 긴장 곡선의 적절성
740
- - 결말까지의 논리적 흐름
741
-
742
- 4. **작가별 가이드라인 보완**
743
- - 일관성 유지를 위한 추가 지침
744
- - 주의해야 함정들
745
- - 체크해야 핵심 요소들
746
-
747
- 일관성 유지에 중점을 구체적인 개선안을 제시하세요."""
748
- else:
749
- return f"""You are a literary critic. Review the director's novel plan from a consistency perspective.
750
-
751
- Director's Plan:
752
- {director_plan}
753
-
754
- Review and provide improvements focusing on:
755
-
756
- 1. **Consistency Risk Assessment**
757
- - Potential contradictions in character settings
758
- - Logical gaps in plot development
759
- - Timeline/spatial setting issues
760
-
761
- 2. **Character Continuity**
762
- - Consistency of character motivations and actions
763
- - Relationship sustainability
764
- - Natural character development
765
-
766
- 3. **Narrative Structure Review**
767
- - Connectivity between plot points
768
- - Appropriate tension curve
769
- - Logical flow to conclusion
770
-
771
- 4. **Writer Guidelines Enhancement**
772
- - Additional guidelines for consistency
773
- - Potential pitfalls to avoid
774
- - Key elements to check
775
-
776
- Provide specific improvements focused on maintaining consistency."""
777
-
778
- def create_director_revision_prompt(self, initial_plan: str, critic_feedback: str, language: str = "English") -> str:
779
- """감독자 수정 프롬프트"""
780
- if language == "Korean":
781
- return f"""감독자로서 비평가의 일관성 검토를 반영하여 소설 기획을 수정합니다.
782
-
783
- 초기 기획:
784
- {initial_plan}
785
-
786
- 비평가 피드백:
787
- {critic_feedback}
788
-
789
- 다음을 포함한 수정된 최종 기획을 제시하세요:
790
-
791
- 1. **보완된 캐릭터 설정**
792
- - 일관성 문제가 해결된 캐릭터들
793
- - 명확한 동기와 성격 특성
794
- - 관계 설정의 지속성 확보
795
-
796
- 2. **개선된 플롯 구조**
797
- - 논리적 허점이 수정된 서사
798
- - 자연스러운 사건 전개
799
- - 일관된 시간선과 공간 설정
800
-
801
- 3. **작가별 상세 지침**
802
- - 각 작가가 담당할 구체적 내용
803
- - 일관성 유지를 위한 필수 체크리스트
804
- - 캐릭터 상태 및 설정 정보
805
-
806
- 4. **일관성 관리 시스템**
807
- - 캐릭터 추적 방법
808
- - 플롯 연속성 체크 포인트
809
- - 문체 및 톤 통일 방안
810
-
811
- 10명의 작가가 일관성 있는 소설을 만들 수 있는 완벽한 마스터플랜을 제시하세요."""
812
- else:
813
- return f"""As director, revise the novel plan reflecting the critic's consistency review.
814
-
815
- Initial Plan:
816
- {initial_plan}
817
-
818
- Critic Feedback:
819
- {critic_feedback}
820
-
821
- Present the revised final plan including:
822
-
823
- 1. **Enhanced Character Settings**
824
- - Characters with resolved consistency issues
825
- - Clear motivations and personality traits
826
- - Sustained relationship settings
827
-
828
- 2. **Improved Plot Structure**
829
- - Narrative with logical gaps fixed
830
- - Natural event progression
831
- - Consistent timeline and spatial settings
832
 
833
- 3. **Detailed Guidelines for Writers**
834
- - Specific content for each writer
835
- - Essential checklist for consistency
836
- - Character state and setting information
 
 
 
837
 
838
- 4. **Consistency Management System**
839
- - Character tracking methods
840
- - Plot continuity checkpoints
841
- - Style and tone unification methods
 
842
 
843
- Present a perfect masterplan for 10 writers to create a consistent novel."""
844
-
845
- def create_writer_prompt(self, writer_number: int, director_plan: str, previous_content: str, language: str = "English") -> str:
846
- """작가 프롬프트 생성"""
847
  pages_start = (writer_number - 1) * 3 + 1
848
  pages_end = writer_number * 3
849
 
850
- # 일관성 정보 추가
851
- consistency_info = self.consistency_tracker.get_character_summary(writer_number)
852
- consistency_info += self.consistency_tracker.get_plot_summary(writer_number)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
853
 
854
- if language == "Korean":
855
- return f"""당신은 작가 {writer_number}번입니다. 30페이지 중편 소설의 {pages_start}-{pages_end}페이지를 작성하세요.
856
-
857
- 감독자 마스터플랜:
858
- {director_plan}
859
-
860
- {consistency_info}
861
-
862
- {'이전 내용 요약:' if previous_content else ''}
863
- {previous_content[-1500:] if previous_content else ''}
864
-
865
- **작성 지침:**
866
- 1. **분량**: 정확히 1,400-1,500단어 (약 3페이지)
867
- 2. **일관성**: 이전 내용과 자연스럽게 연결
868
- 3. **캐릭터**: 기존 설정과 상태 유지
869
- 4. **플롯**: 마스터플랜의 해당 부분 충실히 구현
870
- 5. **문체**: 전체 소설의 톤과 일치
871
-
872
- 창의적이고 흥미로운 내용을 작성하되, 일관성을 절대 잃지 마세요."""
873
- else:
874
- return f"""You are Writer #{writer_number}. Write pages {pages_start}-{pages_end} of the 30-page novella.
875
-
876
- Director's Masterplan:
877
- {director_plan}
878
-
879
- {consistency_info}
880
-
881
- {'Previous Content Summary:' if previous_content else ''}
882
- {previous_content[-1500:] if previous_content else ''}
883
-
884
- **Writing Guidelines:**
885
- 1. **Length**: Exactly 1,400-1,500 words (about 3 pages)
886
- 2. **Consistency**: Connect naturally with previous content
887
- 3. **Characters**: Maintain existing settings and states
888
- 4. **Plot**: Faithfully implement the relevant part of the masterplan
889
- 5. **Style**: Match the tone of the entire novel
890
-
891
- Write creatively and engagingly, but never lose consistency."""
892
-
893
- def create_critic_consistency_prompt(self, all_content: str, language: str = "English") -> str:
894
- """비평가 일관성 검토 프롬프트"""
895
- if language == "Korean":
896
- return f"""당신은 일관성 검토 전문 비평가입니다. 지금까지 작성된 모든 내용을 검토하세요.
897
-
898
- 현재까지 작성된 내용:
899
- {all_content[-3000:]} # 최근 3000자만 표시
900
-
901
- 다음 항목들을 철저히 검토하세요:
902
-
903
- 1. **캐릭터 일관성**
904
- - 인물 설정의 일관성
905
- - 성격과 행동의 연속성
906
- - 관계 설정의 지속성
907
-
908
- 2. **플롯 연속성**
909
- - 사건의 논리적 연결
910
- - 시간선의 일관성
911
- - 공간 설정의 지속성
912
-
913
- 3. **반복 내용 검사**
914
- - 동일한 내용의 반복
915
- - 유사한 표현의 과도한 사용
916
- - 중복되는 장면이나 설명
917
-
918
- 4. **설정 충돌**
919
- - 이전에 확립된 설정과의 모순
920
- - 세계관 설정의 일관성
921
- - 기술적/사회적 배경의 연속성
922
-
923
- **검토 결과:**
924
- - 발견된 문제점들을 구체적으로 나열
925
- - 각 문제점에 대한 수정 방향 제시
926
- - 앞으로 주의해야 할 사항들 안내
927
-
928
- 일관성 유지를 위한 구체적이고 실용적인 피드백을 제공하세요."""
929
- else:
930
- return f"""You are a consistency review specialist critic. Review all content written so far.
931
-
932
- Current Content:
933
- {all_content[-3000:]} # Show only recent 3000 characters
934
-
935
- Thoroughly review these items:
936
-
937
- 1. **Character Consistency**
938
- - Character setting consistency
939
- - Personality and behavior continuity
940
- - Relationship sustainability
941
-
942
- 2. **Plot Continuity**
943
- - Logical connection of events
944
- - Timeline consistency
945
- - Spatial setting sustainability
946
 
947
- 3. **Repetition Check**
948
- - Repetition of identical content
949
- - Excessive use of similar expressions
950
- - Duplicate scenes or descriptions
 
 
951
 
952
- 4. **Setting Conflicts**
953
- - Contradictions with previously established settings
954
- - Worldbuilding consistency
955
- - Technical/social background continuity
956
 
957
- **Review Results:**
958
- - Specifically list discovered problems
959
- - Provide correction directions for each problem
960
- - Guide precautions for the future
961
 
962
- Provide specific and practical feedback for maintaining consistency."""
963
-
964
- def create_writer_revision_prompt(self, writer_number: int, initial_content: str,
965
- consistency_feedback: str, language: str = "English") -> str:
966
  """작가 수정 프롬프트"""
967
- if language == "Korean":
968
- return f"""작가 {writer_number}번으로서 일관성 검토 결과를 반영하여 수정하세요.
969
-
970
- 초기 작성 내용:
971
- {initial_content}
972
-
973
- 일관성 검토 피드백:
974
- {consistency_feedback}
975
-
976
- 다음 사항을 반영하여 수정하세요:
977
-
978
- 1. **일관성 문제 수정**
979
- - 지적된 모든 일관성 문제 해결
980
- - 캐릭터 설정상태 정확히 반영
981
- - 플롯 연속성 확보
982
-
983
- 2. **반복 내용 제거**
984
- - 중복되는 표현이나 장면 삭제
985
- - 새로운 표현과 관점으로 대체
986
- - 창의적이고 다양한 문장 구조 사용
987
-
988
- 3. **설정 충돌 해결**
989
- - 이전 설정과의 모순 제거
990
- - 세계관 일관성 유지
991
- - 시간선과 공간 ��정 정확히 반영
992
-
993
- 4. **분량 유지**
994
- - 1,400-1,500단어 정확히 유지
995
- - 수정 과정에서 내용 품질 향상
996
-
997
- 수정된 최종 버전을 제시하세요. 창의성은 유지하되 일관성을 절대 놓치지 마세요."""
998
- else:
999
- return f"""As Writer #{writer_number}, revise reflecting the consistency review results.
1000
-
1001
- Initial Content:
1002
- {initial_content}
1003
-
1004
- Consistency Review Feedback:
1005
- {consistency_feedback}
1006
-
1007
- Revise reflecting these points:
1008
-
1009
- 1. **Fix Consistency Issues**
1010
- - Resolve all pointed consistency problems
1011
- - Accurately reflect character settings and states
1012
- - Ensure plot continuity
1013
-
1014
- 2. **Remove Repetitive Content**
1015
- - Delete duplicate expressions or scenes
1016
- - Replace with new expressions and perspectives
1017
- - Use creative and varied sentence structures
1018
-
1019
- 3. **Resolve Setting Conflicts**
1020
- - Remove contradictions with previous settings
1021
- - Maintain worldbuilding consistency
1022
- - Accurately reflect timeline and spatial settings
1023
-
1024
- 4. **Maintain Length**
1025
- - Keep exactly 1,400-1,500 words
1026
- - Improve content quality during revision
1027
-
1028
- Present the revised final version. Maintain creativity but never miss consistency."""
1029
-
1030
- def create_critic_final_prompt(self, complete_novel: str, language: str = "English") -> str:
1031
- """최종 비평가 검토"""
1032
- if language == "Korean":
1033
- return f"""완성된 소설의 최종 일관성 검토를 수행하세요.
1034
-
1035
- 완성된 소설:
1036
- {complete_novel[-2000:]} # 마지막 2000자 표시
1037
-
1038
- **최종 검토 항목:**
1039
-
1040
- 1. **전체 일관성 평가**
1041
- - 캐릭터 일관성 점수 (1-10)
1042
- - 플롯 연속성 점수 (1-10)
1043
- - 설정 일관성 점수 (1-10)
1044
- - 전반적 통일성 점수 (1-10)
1045
-
1046
- 2. **발견된 문제점**
1047
- - 남아있는 일관성 문제들
1048
- - 반복이나 중복 내용
1049
- - 설정 충돌 사항들
1050
-
1051
- 3. **성공 요소**
1052
- - 잘 유지된 일관성 부분
1053
- - 훌륭한 캐릭터 연속성
1054
- - 효과적인 플롯 전개
1055
-
1056
- 4. **최종 평가**
1057
- - 전체적인 소설의 완성도
1058
- - 일관성 관점에서의 품질
1059
- - 독자에게 미칠 영향
1060
-
1061
- **일관성 보고서**를 작성하여 이 소설의 일관성 수준을 종합적으로 평가하세요."""
1062
- else:
1063
- return f"""Perform final consistency review of the completed novel.
1064
-
1065
- Completed Novel:
1066
- {complete_novel[-2000:]} # Show last 2000 characters
1067
-
1068
- **Final Review Items:**
1069
-
1070
- 1. **Overall Consistency Evaluation**
1071
- - Character consistency score (1-10)
1072
- - Plot continuity score (1-10)
1073
- - Setting consistency score (1-10)
1074
- - Overall unity score (1-10)
1075
-
1076
- 2. **Problems Found**
1077
- - Remaining consistency issues
1078
- - Repetitive or duplicate content
1079
- - Setting conflicts
1080
-
1081
- 3. **Success Elements**
1082
- - Well-maintained consistency parts
1083
- - Excellent character continuity
1084
- - Effective plot development
1085
-
1086
- 4. **Final Assessment**
1087
- - Overall completion of the novel
1088
- - Quality from consistency perspective
1089
- - Impact on readers
1090
-
1091
- Write a **Consistency Report** to comprehensively evaluate this novel's consistency level."""
1092
-
1093
- def call_llm_streaming(self, messages: List[Dict[str, str]], role: str,
1094
- language: str = "English") -> Generator[str, None, None]:
1095
- """LLM 스트리밍 호출"""
1096
  try:
1097
  system_prompts = self.get_system_prompts(language)
1098
-
1099
- full_messages = [
1100
- {"role": "system", "content": system_prompts.get(role, "")},
1101
- *messages
1102
- ]
1103
-
1104
- # 역할별 토큰 할당
1105
- if role.startswith("writer"):
1106
- max_tokens = 10000
1107
- temperature = 0.8
1108
- elif role == "critic":
1109
- max_tokens = 8000
1110
- temperature = 0.6
1111
- else: # director
1112
- max_tokens = 12000
1113
- temperature = 0.7
1114
 
1115
  payload = {
1116
  "model": self.model_id,
1117
  "messages": full_messages,
1118
- "max_tokens": max_tokens,
1119
- "temperature": temperature,
1120
  "top_p": 0.9,
 
 
1121
  "stream": True,
1122
  "stream_options": {"include_usage": True}
1123
  }
1124
 
 
 
 
1125
  response = requests.post(
1126
- self.api_url,
1127
- headers=self.create_headers(),
1128
- json=payload,
1129
- stream=True,
1130
- timeout=120
1131
  )
1132
 
 
1133
  if response.status_code != 200:
1134
- logger.error(f"API error: {response.status_code}")
1135
- yield f" API 오류 ({response.status_code}): {response.text[:200]}"
 
1136
  return
1137
 
 
 
 
1138
  buffer = ""
1139
  total_content = ""
1140
-
 
 
1141
  for line in response.iter_lines():
1142
- if line:
1143
- line = line.decode('utf-8')
1144
- if line.startswith("data: "):
1145
- data = line[6:]
1146
- if data == "[DONE]":
1147
- if buffer:
1148
- yield buffer
1149
- break
1150
-
1151
- try:
1152
- chunk = json.loads(data)
1153
- if "choices" in chunk and chunk["choices"]:
1154
- content = chunk["choices"][0].get("delta", {}).get("content", "")
1155
- if content:
1156
- buffer += content
1157
- total_content += content
1158
-
1159
- if len(buffer) > 100:
1160
- yield buffer
1161
- buffer = ""
1162
- except json.JSONDecodeError:
1163
- continue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1164
 
 
1165
  if buffer:
1166
  yield buffer
 
 
 
 
 
 
 
1167
 
 
 
 
 
 
 
1168
  except Exception as e:
1169
- logger.error(f"스트리밍 오류: {str(e)}")
1170
  yield f"❌ 오류 발생: {str(e)}"
1171
-
 
 
 
 
1172
  def get_system_prompts(self, language: str) -> Dict[str, str]:
1173
- """시스템 프롬프트 생성"""
1174
- if language == "Korean":
1175
- return {
1176
  "director": "당신은 창의적이고 체계적인 소설 기획 전문가입니다. 흥미롭고 일관성 있는 스토리를 설계하세요.",
1177
  "critic": "당신은 일관성 검토 전문 비평가입니다. 캐릭터, 플롯, 설정의 일관성을 철저히 점검하고 개선방안을 제시하세요.",
1178
- "writer1": "당신은 소설의 매력적인 시작을 담당하는 작가입니다. 독자를 사로잡는 도입부를 만드세요.",
1179
- "writer2": "당신은 이야기를 발전시키는 작가입니다. 등장인물과 갈등을 깊이 있게 전개하세요.",
1180
- "writer3": "당신은 갈등을 심화시키는 작가입니다. 긴장감과 흥미를 높여가세요.",
1181
- "writer4": "당신은 전환점을 만드는 작가입니다. 이야기에 새로운 전개를 가져오세요.",
1182
- "writer5": "당신은 중반부를 담당하는 작가입니다. 복잡성과 깊이를 더해가세요.",
1183
- "writer6": "당신은 클라이맥스를 준비하는 작가입니다. 긴장감을 최고조로 끌어올리세요.",
1184
- "writer7": "당신은 클라이맥스를 담당하는 작가입니다. 모든 갈등이 폭발하는 순간을 그려내세요.",
1185
- "writer8": "당신은 해결 과정을 담당하는 작가입니다. 갈등을 해결해나가는 과정을 보여주세요.",
1186
- "writer9": "당신은 결말을 준비하는 작가입니다. 이야기를 자연스럽게 마무리로 이끌어가세요.",
1187
- "writer10": "당신은 완벽한 결말을 만드는 작가입니다. 독자에게 깊은 여운을 남기는 마무리를 하세요."
1188
- }
1189
- else:
1190
- return {
1191
  "director": "You are a creative and systematic novel planning expert. Design engaging and consistent stories.",
1192
  "critic": "You are a consistency review specialist critic. Thoroughly check character, plot, and setting consistency and suggest improvements.",
1193
- "writer1": "You are a writer responsible for the captivating beginning. Create an opening that hooks readers.",
1194
- "writer2": "You are a writer who develops the story. Deeply develop characters and conflicts.",
1195
- "writer3": "You are a writer who intensifies conflicts. Increase tension and interest.",
1196
- "writer4": "You are a writer who creates turning points. Bring new developments to the story.",
1197
- "writer5": "You are a writer responsible for the middle part. Add complexity and depth.",
1198
- "writer6": "You are a writer preparing for the climax. Raise tension to its peak.",
1199
- "writer7": "You are a writer responsible for the climax. Depict the moment when all conflicts explode.",
1200
- "writer8": "You are a writer responsible for the resolution process. Show the process of resolving conflicts.",
1201
- "writer9": "You are a writer preparing for the ending. Lead the story naturally to its conclusion.",
1202
- "writer10": "You are a writer who creates the perfect ending. Create a conclusion that leaves readers with deep resonance."
1203
  }
1204
-
1205
- def process_novel_stream(self, query: str, language: str = "English",
1206
- session_id: Optional[str] = None,
1207
- resume_from_stage: int = 0) -> Generator[Tuple[str, List[Dict[str, str]]], None, None]:
1208
- """소설 생성 스트리밍 프로세스"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1209
  try:
1210
- global conversation_history
1211
-
1212
- # 세션 생성 또는 복구
1213
  if session_id:
1214
  self.current_session_id = session_id
1215
  session = NovelDatabase.get_session(session_id)
@@ -1220,364 +736,251 @@ Write a **Consistency Report** to comprehensively evaluate this novel's consiste
1220
  logger.info(f"Resuming session {session_id} from stage {resume_from_stage}")
1221
  else:
1222
  self.current_session_id = NovelDatabase.create_session(query, language)
1223
- resume_from_stage = 0
1224
  logger.info(f"Created new session: {self.current_session_id}")
1225
-
1226
- # 대화 기록 초기화
1227
- conversation_history = [{
1228
- "role": "human",
1229
- "content": query,
1230
- "timestamp": datetime.now()
1231
- }]
1232
-
1233
- # 기존 단계 로드
1234
  stages = []
1235
  if resume_from_stage > 0:
1236
- existing_stages = NovelDatabase.get_stages(self.current_session_id)
1237
- for stage_data in existing_stages:
1238
- stages.append({
1239
- "name": stage_data['stage_name'],
1240
- "status": stage_data['status'],
1241
- "content": stage_data['content'] or "",
1242
- "consistency_score": stage_data.get('consistency_score', 0.0)
1243
- })
1244
-
1245
- # 단계 처리
1246
  for stage_idx in range(resume_from_stage, len(OPTIMIZED_STAGES)):
1247
  role, stage_name = OPTIMIZED_STAGES[stage_idx]
1248
-
1249
- # 단계 추가
1250
  if stage_idx >= len(stages):
1251
- stages.append({
1252
- "name": stage_name,
1253
- "status": "active",
1254
- "content": "",
1255
- "consistency_score": 0.0
1256
- })
1257
  else:
1258
  stages[stage_idx]["status"] = "active"
1259
-
1260
- yield "", stages
1261
-
1262
- # 프롬프트 생성
1263
  prompt = self.get_stage_prompt(stage_idx, role, query, language, stages)
1264
-
1265
- # 스트리밍 생성
1266
  stage_content = ""
1267
- for chunk in self.call_llm_streaming(
1268
- [{"role": "user", "content": prompt}],
1269
- role,
1270
- language
1271
- ):
1272
  stage_content += chunk
1273
  stages[stage_idx]["content"] = stage_content
1274
- yield "", stages
1275
-
1276
- # 일관성 검증 (작가 단계)
1277
  consistency_score = 0.0
1278
  if role.startswith("writer"):
1279
- writer_num = int(role.replace("writer", ""))
1280
- all_previous = self.get_previous_content(stages, stage_idx)
1281
-
1282
- errors = self.consistency_validator.validate_writer_content(
1283
- writer_num, stage_content, all_previous
1284
- )
1285
-
1286
- consistency_score = max(0, 10 - len(errors))
1287
  stages[stage_idx]["consistency_score"] = consistency_score
1288
-
1289
- if errors:
1290
- logger.warning(f"Writer {writer_num} consistency issues: {errors}")
1291
-
1292
- # 단계 완료
1293
  stages[stage_idx]["status"] = "complete"
1294
-
1295
- # DB 저장
1296
  NovelDatabase.save_stage(
1297
- self.current_session_id,
1298
- stage_idx,
1299
- stage_name,
1300
- role,
1301
- stage_content,
1302
- "complete",
1303
- consistency_score
1304
  )
1305
-
1306
- yield "", stages
1307
-
1308
- # 최종 소설 생성
1309
- complete_novel = NovelDatabase.get_writer_content(self.current_session_id)
1310
-
1311
- # 최종 일관성 보고서 생성
1312
- final_report = self.generate_consistency_report(complete_novel, language)
1313
-
1314
- # 최종 저장
1315
- NovelDatabase.update_final_novel(self.current_session_id, complete_novel, final_report)
1316
-
1317
- # 완료 메시지
1318
- final_message = f"✅ 소설 완성! 총 {len(complete_novel.split())}단어\n\n{final_report}"
1319
- yield final_message, stages
1320
-
1321
  except Exception as e:
1322
- logger.error(f"Error in process_novel_stream: {str(e)}", exc_info=True)
1323
- error_stage = {
1324
- "name": "❌ 오류",
1325
- "status": "error",
1326
- "content": str(e),
1327
- "consistency_score": 0.0
1328
- }
1329
- stages.append(error_stage)
1330
- yield f"오류 발생: {str(e)}", stages
1331
-
1332
- def get_stage_prompt(self, stage_idx: int, role: str, query: str,
1333
- language: str, stages: List[Dict]) -> str:
1334
- """단계별 프롬프트 생성"""
1335
- if stage_idx == 0: # 감독자 초기
1336
  return self.create_director_initial_prompt(query, language)
1337
- elif stage_idx == 1: # 비평가 기획 검토
1338
- return self.create_critic_director_prompt(stages[0]["content"], language)
1339
- elif stage_idx == 2: # 감독자 수정
1340
- return self.create_director_revision_prompt(
1341
- stages[0]["content"], stages[1]["content"], language)
1342
- elif 3 <= stage_idx <= 12: # 작가 초안
 
 
1343
  writer_num = stage_idx - 2
1344
- final_plan = stages[2]["content"]
1345
- previous_content = self.get_previous_content(stages, stage_idx)
1346
- return self.create_writer_prompt(writer_num, final_plan, previous_content, language)
1347
- elif stage_idx == 13: # 일관성 검토
 
1348
  all_content = self.get_all_content(stages, stage_idx)
1349
- return self.create_critic_consistency_prompt(all_content, language)
1350
- elif 14 <= stage_idx <= 23: # 작가 수정
 
1351
  writer_num = stage_idx - 13
1352
  initial_content = stages[2 + writer_num]["content"]
1353
- consistency_feedback = stages[13]["content"]
1354
- return self.create_writer_revision_prompt(
1355
- writer_num, initial_content, consistency_feedback, language)
1356
- elif stage_idx == 24: # 최종 검토
1357
  complete_novel = self.get_all_writer_content(stages)
1358
  return self.create_critic_final_prompt(complete_novel, language)
1359
 
1360
  return ""
1361
-
1362
- def get_previous_content(self, stages: List[Dict], current_stage: int) -> str:
1363
- """이전 내용 가져오기"""
1364
- content = ""
1365
- for i in range(max(0, current_stage - 3), current_stage):
1366
- if i < len(stages) and stages[i]["content"]:
1367
- content += stages[i]["content"] + "\n\n"
1368
- return content
1369
-
 
 
 
 
 
 
 
 
 
1370
  def get_all_content(self, stages: List[Dict], current_stage: int) -> str:
1371
- """모든 내용 가져오기"""
1372
- content = ""
1373
- for i in range(current_stage):
1374
- if i < len(stages) and stages[i]["content"]:
1375
- content += stages[i]["content"] + "\n\n"
1376
- return content
1377
-
1378
  def get_all_writer_content(self, stages: List[Dict]) -> str:
1379
- """모든 작가 내용 가져오기"""
1380
- content = ""
1381
- for i in range(14, 24): # 작가 수정본들
1382
- if i < len(stages) and stages[i]["content"]:
1383
- content += stages[i]["content"] + "\n\n"
1384
- return content
1385
-
1386
  def generate_consistency_report(self, complete_novel: str, language: str) -> str:
1387
- """일관성 보고서 생���"""
1388
- if language == "Korean":
1389
- return f"""📊 일관성 보고서
1390
- - 전체 길이: {len(complete_novel.split())} 단어
1391
- - 캐릭터 수: {len(self.consistency_tracker.character_states)}
1392
- - 플롯 포인트: {len(self.consistency_tracker.plot_points)}
1393
- - 일관성 점수: 양호"""
1394
- else:
1395
- return f"""📊 Consistency Report
1396
- - Total Length: {len(complete_novel.split())} words
1397
- - Characters: {len(self.consistency_tracker.character_states)}
1398
- - Plot Points: {len(self.consistency_tracker.plot_points)}
1399
- - Consistency Score: Good"""
1400
 
1401
 
1402
- # Gradio 인터페이스 함수들
1403
- def process_query(query: str, language: str, session_id: str = None) -> Generator[Tuple[str, str, str, str], None, None]:
1404
- """쿼리 처리 및 업데이트"""
1405
- if not query.strip() and not session_id:
1406
- if language == "Korean":
1407
- yield "", "", "❌ 소설 주제를 입력해주세요.", None
1408
- else:
1409
- yield "", "", "❌ Please enter a novel theme.", None
1410
  return
1411
 
1412
  system = NovelWritingSystem()
 
 
1413
 
1414
- try:
1415
- for final_novel, stages in system.process_novel_stream(query, language, session_id):
1416
- # 단계 표시 형식화
1417
- stages_display = format_stages_display(stages, language)
1418
-
1419
- # 진행률 계산
1420
- completed = sum(1 for s in stages if s.get("status") == "complete")
1421
- total = len(stages)
1422
- progress_percent = (completed / total * 100) if total > 0 else 0
1423
-
1424
- if "✅ 소설 완성!" in str(final_novel):
1425
- status = "✅ 완료! 다운로드 가능합니다."
1426
- else:
1427
- if language == "Korean":
1428
- status = f"🔄 진행중... ({completed}/{total} - {progress_percent:.1f}%)"
1429
- else:
1430
- status = f"🔄 Processing... ({completed}/{total} - {progress_percent:.1f}%)"
1431
-
1432
- yield stages_display, final_novel, status, system.current_session_id
1433
-
1434
- except Exception as e:
1435
- logger.error(f"Error in process_query: {str(e)}", exc_info=True)
1436
- if language == "Korean":
1437
- yield "", "", f"❌ 오류 발생: {str(e)}", None
1438
- else:
1439
- yield "", "", f"❌ Error occurred: {str(e)}", None
1440
-
1441
-
1442
- def format_stages_display(stages: List[Dict[str, str]], language: str) -> str:
1443
- """단계 표시 형식화"""
1444
- display = ""
1445
-
1446
- for idx, stage in enumerate(stages):
1447
- status_icon = "✅" if stage.get("status") == "complete" else ("⏳" if stage.get("status") == "active" else "❌")
1448
 
1449
- # 일관성 점수 표시
1450
- consistency_indicator = ""
1451
- if stage.get("consistency_score", 0) > 0:
1452
- score = stage["consistency_score"]
1453
- if score >= 8:
1454
- consistency_indicator = " 🟢"
1455
- elif score >= 6:
1456
- consistency_indicator = " 🟡"
1457
- else:
1458
- consistency_indicator = " 🔴"
1459
 
1460
- # 활성 단계는 내용 표시, 다른 단계는 상태만 표시
1461
- if stage.get("status") == "active":
1462
- display += f"\n\n{status_icon} **{stage['name']}**{consistency_indicator}\n"
1463
- display += f"```\n{stage.get('content', '')[-800:]}\n```"
1464
- else:
1465
- display += f"\n{status_icon} {stage['name']}{consistency_indicator}"
1466
-
1467
- return display
1468
-
1469
 
1470
- def get_active_sessions(language: str) -> List[Tuple[str, str]]:
1471
  """활성 세션 목록 가져오기"""
1472
- try:
1473
- sessions = NovelDatabase.get_active_sessions()
1474
-
1475
- choices = []
1476
- for session in sessions:
1477
- created = datetime.fromisoformat(session['created_at'])
1478
- date_str = created.strftime("%Y-%m-%d %H:%M")
1479
- query_preview = session['user_query'][:50] + "..." if len(session['user_query']) > 50 else session['user_query']
1480
- label = f"[{date_str}] {query_preview} (Stage {session['current_stage']})"
1481
- choices.append((label, session['session_id']))
1482
-
1483
- return choices
1484
- except Exception as e:
1485
- logger.error(f"Error getting active sessions: {str(e)}")
1486
- return []
1487
 
 
 
 
 
 
 
1488
 
1489
  def resume_session(session_id: str, language: str) -> Generator[Tuple[str, str, str, str], None, None]:
1490
- """세션 재개"""
1491
  if not session_id:
1492
- if language == "Korean":
1493
- yield "", "", "❌ 세션을 선택해주세요.", None
1494
- else:
1495
- yield "", "", "❌ Please select a session.", None
1496
  return
1497
 
1498
- yield from process_query("", language, session_id)
1499
-
 
 
 
 
 
 
 
 
 
1500
 
1501
- def auto_recover_session(language: str) -> Tuple[str, str]:
1502
- """자동 세션 복구"""
 
 
 
 
 
 
1503
  try:
1504
- latest_session = NovelDatabase.get_latest_active_session()
1505
- if latest_session:
1506
- if language == "Korean":
1507
- message = f"✅ 자동 복구: '{latest_session['user_query'][:30]}...' (Stage {latest_session['current_stage']})"
1508
- else:
1509
- message = f"✅ Auto recovered: '{latest_session['user_query'][:30]}...' (Stage {latest_session['current_stage']})"
1510
- return latest_session['session_id'], message
1511
  else:
1512
- return None, ""
1513
  except Exception as e:
1514
- logger.error(f"Error in auto recovery: {str(e)}")
1515
- return None, ""
1516
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1517
 
1518
- def download_novel(novel_text: str, format: str, language: str, session_id: str = None) -> str:
1519
- """소설 다운로드"""
1520
- if not session_id:
1521
- logger.error("No session_id provided for download")
1522
- return None
1523
 
1524
- # 세션 정보 가져오기
1525
- session = NovelDatabase.get_session(session_id)
1526
- if not session:
1527
- logger.error(f"Session not found: {session_id}")
1528
- return None
1529
 
1530
- # 완성된 소설 가져오기
1531
- complete_novel = NovelDatabase.get_writer_content(session_id)
1532
- if not complete_novel:
1533
- logger.error("No complete novel found")
1534
- return None
1535
 
1536
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
1537
- temp_dir = tempfile.gettempdir()
1538
- safe_filename = re.sub(r'[^\w\s-]', '', session['user_query'][:30]).strip()
 
 
1539
 
1540
- if format == "DOCX" and DOCX_AVAILABLE:
1541
- # DOCX 생성
1542
- doc = Document()
1543
-
1544
- # 제목 추가
1545
- title = doc.add_heading('AI 협업 소설' if language == 'Korean' else 'AI Collaborative Novel', 0)
1546
-
1547
- # 메타정보 추가
1548
- doc.add_paragraph(f"주제: {session['user_query']}" if language == 'Korean' else f"Theme: {session['user_query']}")
1549
- doc.add_paragraph(f"생성일: {datetime.now().strftime('%Y-%m-%d')}" if language == 'Korean' else f"Created: {datetime.now().strftime('%Y-%m-%d')}")
1550
- doc.add_paragraph(f"단어 수: {len(complete_novel.split())}" if language == 'Korean' else f"Word Count: {len(complete_novel.split())}")
1551
- doc.add_paragraph()
1552
-
1553
- # 소설 내용 추가
1554
- paragraphs = complete_novel.split('\n\n')
1555
- for para in paragraphs:
1556
- if para.strip():
1557
- doc.add_paragraph(para.strip())
1558
-
1559
- # 파일 저장
1560
- filename = f"Novel_{safe_filename}_{timestamp}.docx"
1561
- filepath = os.path.join(temp_dir, filename)
1562
- doc.save(filepath)
1563
-
1564
- return filepath
1565
- else:
1566
- # TXT 생성
1567
- filename = f"Novel_{safe_filename}_{timestamp}.txt"
1568
- filepath = os.path.join(temp_dir, filename)
1569
-
1570
- with open(filepath, 'w', encoding='utf-8') as f:
1571
- f.write("=" * 60 + "\n")
1572
- f.write(f"AI 협업 소설\n" if language == 'Korean' else f"AI COLLABORATIVE NOVEL\n")
1573
- f.write("=" * 60 + "\n\n")
1574
- f.write(f"주제: {session['user_query']}\n" if language == 'Korean' else f"Theme: {session['user_query']}\n")
1575
- f.write(f"생성일: {datetime.now()}\n" if language == 'Korean' else f"Created: {datetime.now()}\n")
1576
- f.write(f"단어 수: {len(complete_novel.split())}\n" if language == 'Korean' else f"Word Count: {len(complete_novel.split())}\n")
1577
- f.write("=" * 60 + "\n\n")
1578
- f.write(complete_novel)
1579
-
1580
- return filepath
1581
 
1582
 
1583
  # CSS 스타일
 
16
  from dataclasses import dataclass, field
17
  from collections import defaultdict
18
 
19
+ # --- 로깅 설정 ---
20
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
21
  logger = logging.getLogger(__name__)
22
 
23
+ # --- Document export imports ---
24
  try:
25
  from docx import Document
26
  from docx.shared import Inches, Pt, RGBColor
 
31
  DOCX_AVAILABLE = False
32
  logger.warning("python-docx not installed. DOCX export will be disabled.")
33
 
34
+ # --- 환경 변수 상수 ---
35
  FRIENDLI_TOKEN = os.getenv("FRIENDLI_TOKEN", "")
36
  BRAVE_SEARCH_API_KEY = os.getenv("BRAVE_SEARCH_API_KEY", "")
37
  API_URL = "https://api.friendli.ai/dedicated/v1/chat/completions"
38
  MODEL_ID = "dep89a2fld32mcm"
39
+ DB_PATH = "novel_sessions_v2.db"
40
 
41
+ # --- 환경 변수 검증 ---
42
  if not FRIENDLI_TOKEN:
43
  logger.error("FRIENDLI_TOKEN not set. Application will not work properly.")
44
+ # 실제 환경에서는 여기서 프로그램을 종료해야 하지만, 데모를 위해 더미 토큰을 사용합니다.
45
+ FRIENDLI_TOKEN = "dummy_token_for_testing"
46
 
47
  if not BRAVE_SEARCH_API_KEY:
48
  logger.warning("BRAVE_SEARCH_API_KEY not set. Web search features will be disabled.")
49
 
50
+ # --- 전역 변수 ---
 
 
 
 
 
51
  db_lock = threading.Lock()
52
 
53
+ # 최적화된 단계 구성 (25단계로 압축 및 강화)
54
  OPTIMIZED_STAGES = [
55
+ ("director", "🎬 감독자: 초기 기획 (웹 검색 포함)"),
56
+ ("critic", "📝 비평가: 기획 검토 (테마 및 일관성)"),
57
  ("director", "🎬 감독자: 수정된 마스터플랜"),
58
  ] + [
59
  (f"writer{i}", f"✍️ 작가 {i}: 초안 (페이지 {(i-1)*3+1}-{i*3})")
60
  for i in range(1, 11)
61
  ] + [
62
+ ("critic", "📝 비평가: 중간 검토 (일관성 및 테마 유지)"),
63
  ] + [
64
  (f"writer{i}", f"✍️ 작가 {i}: 수정본 (페이지 {(i-1)*3+1}-{i*3})")
65
  for i in range(1, 11)
66
  ] + [
67
+ ("critic", f"📝 비평가: 최종 검토 및 종합 보고서 작성"),
68
  ]
69
 
70
 
71
+ # --- 데이터 클래스 ---
72
  @dataclass
73
  class CharacterState:
74
  """캐릭터의 현재 상태를 나타내는 데이터 클래스"""
 
82
  description: str = ""
83
  role: str = ""
84
 
 
85
  @dataclass
86
  class PlotPoint:
87
  """플롯 포인트를 나타내는 데이터 클래스"""
 
92
  impact_level: int
93
  timestamp: str = ""
94
 
 
95
  @dataclass
96
  class TimelineEvent:
97
  """시간선 이벤트를 나타내는 데이터 클래스"""
 
102
  relative_time: str = ""
103
 
104
 
105
+ # --- 핵심 로직 클래스 ---
106
  class ConsistencyTracker:
107
  """일관성 추적 시스템"""
 
108
  def __init__(self):
109
  self.character_states: Dict[str, CharacterState] = {}
110
  self.plot_points: List[PlotPoint] = []
111
  self.timeline_events: List[TimelineEvent] = []
112
  self.locations: Dict[str, str] = {}
113
  self.established_facts: List[str] = []
114
+ self.content_hashes: Dict[str, int] = {} # 해시와 해당 챕터 번호를 저장
115
+
116
  def register_character(self, character: CharacterState):
117
  """새 캐릭터 등록"""
118
  self.character_states[character.name] = character
119
  logger.info(f"Character registered: {character.name}")
120
+
121
  def update_character_state(self, name: str, chapter: int, updates: Dict[str, Any]):
122
  """캐릭터 상태 업데이트"""
123
  if name not in self.character_states:
124
  self.register_character(CharacterState(name=name, last_seen_chapter=chapter))
 
125
  char = self.character_states[name]
126
  for key, value in updates.items():
127
  if hasattr(char, key):
128
  setattr(char, key, value)
 
129
  char.last_seen_chapter = chapter
130
+
131
  def add_plot_point(self, plot_point: PlotPoint):
132
  """플롯 포인트 추가"""
133
  plot_point.timestamp = datetime.now().isoformat()
134
  self.plot_points.append(plot_point)
135
+
136
+ def check_repetition(self, content: str, current_chapter: int) -> Tuple[bool, str]:
137
+ """향상된 반복 내용 검사"""
 
 
 
 
 
 
 
 
 
 
 
138
  sentences = re.split(r'[.!?]+', content)
139
  for sentence in sentences:
140
+ sentence_strip = sentence.strip()
141
+ if len(sentence_strip) > 20: # 너무 짧은 문장은 무시
142
+ sentence_hash = hashlib.md5(sentence_strip.encode('utf-8')).hexdigest()
143
  if sentence_hash in self.content_hashes:
144
+ previous_chapter = self.content_hashes[sentence_hash]
145
+ # 바로 이전 챕터와의 반복은 허용할 수 있으므로, 2챕터 이상 차이날 때만 오류로 간주
146
+ if current_chapter > previous_chapter + 1:
147
+ return True, f"문장 반복 (챕터 {previous_chapter}과 유사): {sentence_strip[:50]}..."
148
+ # 새 내용의 해시 추가
149
  for sentence in sentences:
150
+ sentence_strip = sentence.strip()
151
+ if len(sentence_strip) > 20:
152
+ sentence_hash = hashlib.md5(sentence_strip.encode('utf-8')).hexdigest()
153
+ self.content_hashes[sentence_hash] = current_chapter
154
  return False, ""
155
+
156
  def validate_consistency(self, chapter: int, content: str) -> List[str]:
157
  """일관성 검증"""
158
  errors = []
159
+ # 사망한 캐릭터 등장 검사
 
160
  for char_name, char_state in self.character_states.items():
161
+ if char_name.lower() in content.lower() and not char_state.alive:
162
+ errors.append(f"⚠️ 사망한 캐릭터 '{char_name}'이(가) 등장했습니다.")
163
+ # 내용 반복 검사
164
+ is_repetition, repeat_msg = self.check_repetition(content, chapter)
 
 
 
 
 
 
 
 
 
165
  if is_repetition:
166
  errors.append(f"🔄 {repeat_msg}")
 
 
 
 
 
 
 
 
 
 
167
  return errors
168
+
169
  def get_character_summary(self, chapter: int) -> str:
170
  """현재 챕터 기준 캐릭터 요약"""
171
+ summary = "\n=== 캐릭터 현황 요약 (이전 2개 챕터 기준) ===\n"
172
+ active_chars = [char for char in self.character_states.values() if char.last_seen_chapter >= chapter - 2]
173
+ if not active_chars:
174
+ return "\n(아직 주요 캐릭터 정보가 없습니다.)\n"
 
175
  for char in active_chars:
176
  status = "생존" if char.alive else "사망"
177
  summary += f"• {char.name}: {status}"
178
+ if char.alive and char.location: summary += f" (위치: {char.location})"
179
+ if char.injuries: summary += f" (부상: {', '.join(char.injuries[-1:])})"
 
 
 
180
  summary += "\n"
 
181
  return summary
182
+
183
  def get_plot_summary(self, chapter: int) -> str:
184
  """플롯 요약"""
185
+ summary = "\n=== 최근 주요 사건 요약 ===\n"
 
186
  recent_events = [p for p in self.plot_points if p.chapter >= chapter - 2]
187
+ if not recent_events:
188
+ return "\n(아직 주요 사건이 없습니다.)\n"
189
+ for event in recent_events[-3:]: # 최근 3개만 표시
190
+ summary += f"• [챕터 {event.chapter}] {event.description}\n"
191
  return summary
192
 
193
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
  class WebSearchIntegration:
195
  """웹 검색 기능 (감독자 단계에서만 사용)"""
 
196
  def __init__(self):
197
  self.brave_api_key = BRAVE_SEARCH_API_KEY
198
  self.search_url = "https://api.search.brave.com/res/v1/web/search"
199
  self.enabled = bool(self.brave_api_key)
200
+
201
  def search(self, query: str, count: int = 3, language: str = "en") -> List[Dict]:
202
  """웹 검색 수행"""
203
  if not self.enabled:
204
  return []
 
205
  headers = {
206
  "Accept": "application/json",
207
  "X-Subscription-Token": self.brave_api_key
208
  }
 
 
 
209
  params = {
210
  "q": query,
211
  "count": count,
212
+ "search_lang": "ko" if language == "Korean" else "en",
213
  "text_decorations": False,
214
  "safesearch": "moderate"
215
  }
 
216
  try:
217
  response = requests.get(self.search_url, headers=headers, params=params, timeout=10)
218
+ response.raise_for_status()
219
+ results = response.json().get("web", {}).get("results", [])
220
+ logger.info(f" 검색 성공: '{query}'에 대해 {len(results)}개 결과 발견")
221
+ return results
222
+ except requests.exceptions.RequestException as e:
223
+ logger.error(f"웹 검색 API 오류: {e}")
 
 
 
 
224
  return []
225
+
226
+ def extract_relevant_info(self, results: List[Dict], max_chars: int = 1500) -> str:
227
  """검색 결과에서 관련 정보 추출"""
228
  if not results:
229
  return ""
 
230
  extracted = []
231
  total_chars = 0
 
232
  for i, result in enumerate(results[:3], 1):
233
  title = result.get("title", "")
234
  description = result.get("description", "")
235
  url = result.get("url", "")
236
+ info = f"[{i}] {title}\n{description}\nSource: {url}\n"
 
 
237
  if total_chars + len(info) < max_chars:
238
  extracted.append(info)
239
  total_chars += len(info)
240
  else:
241
  break
 
242
  return "\n---\n".join(extracted)
243
 
244
 
245
  class NovelDatabase:
246
  """소설 세션 관리 데이터베이스"""
 
247
  @staticmethod
248
  def init_db():
 
249
  with sqlite3.connect(DB_PATH) as conn:
250
  conn.execute("PRAGMA journal_mode=WAL")
251
  cursor = conn.cursor()
 
 
252
  cursor.execute('''
253
  CREATE TABLE IF NOT EXISTS sessions (
254
  session_id TEXT PRIMARY KEY,
 
262
  consistency_report TEXT
263
  )
264
  ''')
 
 
265
  cursor.execute('''
266
  CREATE TABLE IF NOT EXISTS stages (
267
  id INTEGER PRIMARY KEY AUTOINCREMENT,
 
279
  UNIQUE(session_id, stage_number)
280
  )
281
  ''')
 
 
282
  cursor.execute('''
283
  CREATE TABLE IF NOT EXISTS character_states (
284
  id INTEGER PRIMARY KEY AUTOINCREMENT,
 
294
  FOREIGN KEY (session_id) REFERENCES sessions(session_id)
295
  )
296
  ''')
 
 
297
  cursor.execute('CREATE INDEX IF NOT EXISTS idx_session_id ON stages(session_id)')
298
  cursor.execute('CREATE INDEX IF NOT EXISTS idx_stage_number ON stages(stage_number)')
299
  cursor.execute('CREATE INDEX IF NOT EXISTS idx_char_session ON character_states(session_id)')
300
  cursor.execute('CREATE INDEX IF NOT EXISTS idx_session_status ON sessions(status)')
 
301
  conn.commit()
302
+
303
  @staticmethod
304
  @contextmanager
305
  def get_db():
 
306
  with db_lock:
307
  conn = sqlite3.connect(DB_PATH, timeout=30.0)
308
  conn.row_factory = sqlite3.Row
 
310
  yield conn
311
  finally:
312
  conn.close()
313
+
314
  @staticmethod
315
  def create_session(user_query: str, language: str) -> str:
 
316
  session_id = hashlib.md5(f"{user_query}{datetime.now()}".encode()).hexdigest()
 
317
  with NovelDatabase.get_db() as conn:
318
+ conn.cursor().execute(
319
+ 'INSERT INTO sessions (session_id, user_query, language) VALUES (?, ?, ?)',
320
+ (session_id, user_query, language)
321
+ )
 
322
  conn.commit()
 
323
  return session_id
324
+
325
  @staticmethod
326
+ def save_stage(session_id: str, stage_number: int, stage_name: str,
327
+ role: str, content: str, status: str = 'complete',
328
  consistency_score: float = 0.0):
 
329
  word_count = len(content.split()) if content else 0
 
330
  with NovelDatabase.get_db() as conn:
331
  cursor = conn.cursor()
 
332
  cursor.execute('''
333
  INSERT INTO stages (session_id, stage_number, stage_name, role, content, word_count, status, consistency_score)
334
  VALUES (?, ?, ?, ?, ?, ?, ?, ?)
335
+ ON CONFLICT(session_id, stage_number)
336
  DO UPDATE SET content=?, word_count=?, status=?, stage_name=?, consistency_score=?, updated_at=datetime('now')
337
  ''', (session_id, stage_number, stage_name, role, content, word_count, status, consistency_score,
338
  content, word_count, status, stage_name, consistency_score))
339
+ cursor.execute(
340
+ "UPDATE sessions SET updated_at = datetime('now'), current_stage = ? WHERE session_id = ?",
341
+ (stage_number, session_id)
342
+ )
 
 
 
343
  conn.commit()
344
+
345
  @staticmethod
346
  def get_session(session_id: str) -> Optional[Dict]:
 
347
  with NovelDatabase.get_db() as conn:
348
+ row = conn.cursor().execute('SELECT * FROM sessions WHERE session_id = ?', (session_id,)).fetchone()
 
 
349
  return dict(row) if row else None
350
+
351
  @staticmethod
352
  def get_latest_active_session() -> Optional[Dict]:
 
353
  with NovelDatabase.get_db() as conn:
354
+ row = conn.cursor().execute("SELECT * FROM sessions WHERE status = 'active' ORDER BY updated_at DESC LIMIT 1").fetchone()
 
 
 
 
 
 
 
355
  return dict(row) if row else None
356
+
357
  @staticmethod
358
  def get_stages(session_id: str) -> List[Dict]:
 
359
  with NovelDatabase.get_db() as conn:
360
+ rows = conn.cursor().execute('SELECT * FROM stages WHERE session_id = ? ORDER BY stage_number', (session_id,)).fetchall()
361
+ return [dict(row) for row in rows]
362
+
 
 
 
 
 
363
  @staticmethod
364
  def get_writer_content(session_id: str) -> str:
 
365
  with NovelDatabase.get_db() as conn:
 
 
366
  all_content = []
 
 
367
  for writer_num in range(1, 11):
368
+ row = conn.cursor().execute(
369
+ "SELECT content FROM stages WHERE session_id = ? AND role = ? AND stage_name LIKE '%수정본%' ORDER BY stage_number DESC LIMIT 1",
370
+ (session_id, f'writer{writer_num}')
371
+ ).fetchone()
 
 
 
 
 
372
  if row and row['content']:
373
+ all_content.append(row['content'].strip())
 
 
 
374
  return '\n\n'.join(all_content)
375
+
376
  @staticmethod
377
  def update_final_novel(session_id: str, final_novel: str, consistency_report: str = ""):
 
378
  with NovelDatabase.get_db() as conn:
379
+ conn.cursor().execute(
380
+ "UPDATE sessions SET final_novel = ?, status = 'complete', updated_at = datetime('now'), consistency_report = ? WHERE session_id = ?",
381
+ (final_novel, consistency_report, session_id)
382
+ )
 
 
383
  conn.commit()
384
+
385
  @staticmethod
386
  def get_active_sessions() -> List[Dict]:
 
387
  with NovelDatabase.get_db() as conn:
388
+ rows = conn.cursor().execute(
389
+ "SELECT session_id, user_query, language, created_at, current_stage FROM sessions WHERE status = 'active' ORDER BY updated_at DESC LIMIT 10"
390
+ ).fetchall()
391
+ return [dict(row) for row in rows]
 
 
 
 
 
392
 
393
 
394
  class NovelWritingSystem:
395
  """최적화된 소설 생성 시스템"""
 
396
  def __init__(self):
397
  self.token = FRIENDLI_TOKEN
398
  self.api_url = API_URL
399
  self.model_id = MODEL_ID
 
 
400
  self.consistency_tracker = ConsistencyTracker()
 
401
  self.web_search = WebSearchIntegration()
 
 
402
  self.current_session_id = None
 
 
403
  NovelDatabase.init_db()
404
+
405
  def create_headers(self):
406
  """API 헤더 생성"""
407
+ return {"Authorization": f"Bearer {self.token}", "Content-Type": "application/json"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
408
 
409
+ # --- 프롬프트 생성 함수들 (Thematic Guardian 개념 통합) ---
410
+ def create_director_initial_prompt(self, user_query: str, language: str) -> str:
411
+ """감독자 초기 기획 프롬프트 (웹 검색 및 테마 제약 조건 강화)"""
412
+ search_results_str = ""
413
+ if self.web_search.enabled:
414
+ queries = [f"{user_query} novel setting", f"{user_query} background information"]
415
+ search_results = self.web_search.search(queries[0], count=2, language=language)
416
+ if search_results:
417
+ search_results_str = self.web_search.extract_relevant_info(search_results)
418
+
419
+ lang_prompts = {
420
+ "Korean": {
421
+ "title": "당신은 30페이지 분량의 중편 소설을 기획하는 문학 감독자입니다.",
422
+ "user_theme": "사용자 주제",
423
+ "plan_instruction": "다음 요소들을 포함한 상세한 소설 기획을 작성하세요:",
424
+ "theme_section": "1. **주제와 장르 설정**\n - 핵심 주제와 메시지 (사용자 의도 깊이 반영)\n - 장르 및 분위기\n - 독자층 고려사항",
425
+ "char_section": "2. **주요 등장인물** (3-5명)\n | 이름 | 역할 | 성격 | 배경 | 목표 | 갈등 |",
426
+ "setting_section": "3. **배경 설정**\n - 시공간적 배경\n - 사회적/문화적 환경\n - 주요 장소들",
427
+ "plot_section": "4. **플롯 구조** (10개 파트, 각 3페이지 분량)\n | 파트 | 페이지 | 주요 사건 | 긴장도 | ��릭터 발전 |",
428
+ "guideline_section": "5. **작가별 지침**\n - 일관성 유지를 위한 핵심 설정\n - 문체와 톤 가이드라인",
429
+ "constraint_title": "⚠️매우 중요한 지시사항: 핵심 제약 조건⚠️",
430
+ "constraint_body": "이 소설은 **AI로 인해 모든 것이 쉽게 해결되는 긍정적이고 단순한 이야기가 아닙니다.**\n반드시 사용자의 주제인 '{query}'에 담긴 **핵심 감정(예: 불안, 소외감, 상실감, 세대 갈등 등)을 중심으로 서사를 전개해야 합니다.**\nAI나 특정 기술은 편리한 도구가 아니라, 주인공에게 **갈등과 상실감을 안겨주는 핵심 원인**으로 작용해야 합니다.\n이 제약 조건을 절대 벗어나지 마십시오.",
431
+ "final_instruction": "창의적이고 깊이 있는 소설이 수 있도록 상세하게 기획하세요."
432
+ },
433
+ "English": {
434
+ "title": "You are a literary director planning a 30-page novella.",
435
+ "user_theme": "User Theme",
436
+ "plan_instruction": "Create a detailed novel plan including:",
437
+ "theme_section": "1. **Theme and Genre**\n - Core theme and message (Deeply reflect user's intent)\n - Genre and atmosphere",
438
+ "char_section": "2. **Main Characters** (3-5)\n | Name | Role | Personality | Background | Goal | Conflict |",
439
+ "setting_section": "3. **Setting**\n - Time and place\n - Social/cultural environment",
440
+ "plot_section": "4. **Plot Structure** (10 parts, ~3 pages each)\n | Part | Pages | Main Events | Tension | Character Development |",
441
+ "guideline_section": "5. **Writer Guidelines**\n - Key settings for consistency\n - Style and tone guidelines",
442
+ "constraint_title": "⚠️CRITICAL INSTRUCTION: CORE CONSTRAINTS⚠️",
443
+ "constraint_body": "This is **NOT a simple, positive story where AI solves everything.**\nYou must develop the narrative around the core emotions of the user's theme: '{query}' (e.g., anxiety, alienation, loss, generational conflict).\nAI or specific technology should be the **root cause of the protagonist's conflict and loss**, not a convenient tool.\nDo not deviate from this constraint.",
444
+ "final_instruction": "Plan in detail for a creative and profound novel."
445
+ }
446
+ }
447
+ p = lang_prompts[language]
448
+ return f"{p['title']}\n\n{p['user_theme']}: {user_query}\n\n{search_results_str}\n\n{p['plan_instruction']}\n\n{p['theme_section']}\n\n{p['char_section']}\n\n{p['setting_section']}\n\n{p['plot_section']}\n\n{p['guideline_section']}\n\n---\n{p['constraint_title']}\n{p['constraint_body'].format(query=user_query)}\n---\n\n{p['final_instruction']}"
449
+
450
+ def create_critic_director_prompt(self, director_plan: str, user_query: str, language: str) -> str:
451
+ """비평가의 감독자 기획 검토 프롬프트 (테마 일관성 강화)"""
452
+ lang_prompts = {
453
+ "Korean": {
454
+ "title": "당신은 문학 비평가입니다. 감독자의 소설 기획을 '주제 일관성'과 '기술적 일관성' 관점에서 검토하세요.",
455
+ "theme_check": f"**1. 주제 일관성 (가장 중요)**\n - **원래 주제:** '{user_query}'\n - 기획안이 주제의 핵심 감정(불안, 상실감 등)에서 벗어나 긍정적이거나 단순한 방향으로 흐르지 않았습니까?\n - AI나 기술이 갈등의 원인이 아닌, 단순 해결사로 묘사되지 않았습니까?",
456
+ "consistency_check": "**2. 기술적 일관성**\n - 캐릭터 설정의 모순, 플롯의 논리적 허점, 시간선/공간 설정의 문제점을 검토하세요.",
457
+ "instruction": "위 항목들을 중심으로 구체적인 문제점과 개선안을 제시하세요."
458
+ },
459
+ "English": {
460
+ "title": "You are a literary critic. Review the director's plan from the perspectives of 'Thematic Consistency' and 'Technical Consistency'.",
461
+ "theme_check": f"**1. Thematic Consistency (Most Important)**\n - **Original Theme:** '{user_query}'\n - Does the plan drift from the core emotions (e.g., anxiety, loss) towards an overly positive or simplistic narrative?\n - Is AI depicted as a simple problem-solver instead of the root of the conflict?",
462
+ "consistency_check": "**2. Technical Consistency**\n - Review for character contradictions, plot holes, and timeline/setting issues.",
463
+ "instruction": "Provide specific problems and suggestions for improvement based on the above."
464
+ }
465
+ }
466
+ p = lang_prompts[language]
467
+ return f"{p['title']}\n\n**감독자 기획:**\n{director_plan}\n\n---\n**검토 항목:**\n{p['theme_check']}\n\n{p['consistency_check']}\n\n{p['instruction']}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
468
 
469
+ def create_director_revision_prompt(self, initial_plan: str, critic_feedback: str, user_query: str, language: str) -> str:
470
+ """감독자 수정 프롬프트 (테마 제약 조건 재강조)"""
471
+ return f"""감독자로서 비평가의 피드백을 반영하여 소설 기획을 수정합니다.
472
+
473
+ **원래 주제:** {user_query}
474
+ **초기 기획:**\n{initial_plan}
475
+ **비평가 피드백:**\n{critic_feedback}
476
 
477
+ **수정 지침:**
478
+ - 비평가가 지적한 모든 일관성 문제와 주제 이탈 문제를 해결하세요.
479
+ - **핵심 제약 조건**을 다시 한번 상기하고, 소설 전체가 '불안'과 '상실감'의 톤을 유지하도록 플롯을 구체화하세요.
480
+ - 10명의 작가가 혼동 없이 작업할 수 있도록 명확하고 상세한 최종 마스터플랜을 작성하세요.
481
+ """
482
 
483
+ def create_writer_prompt(self, writer_number: int, director_plan: str, previous_content_summary: str, user_query: str, language: str) -> str:
484
+ """작가 프롬프트 (테마 리마인더 포함)"""
 
 
485
  pages_start = (writer_number - 1) * 3 + 1
486
  pages_end = writer_number * 3
487
 
488
+ lang_prompts = {
489
+ "Korean": {
490
+ "title": f"당신은 작가 {writer_number}번입니다. 소설의 {pages_start}-{pages_end} 페이지를 작성하세요.",
491
+ "plan": "감독자 마스터플랜",
492
+ "prev_summary": "이전 내용 요약",
493
+ "guidelines": "**작성 지침:**\n1. **분량**: 1,400-1,500 단어 내외\n2. **연결성**: 요약된 이전 내용과 자연스럽게 연결\n3. **일관성**: 캐릭터 설정과 상태, 플롯 구조를 반드시 따를 것",
494
+ "reminder_title": "⭐ 잊지 마세요 (테마 리마인더)",
495
+ "reminder_body": f"이 소설의 핵심은 '{user_query}'에 담긴 **불안, 소외, 상실감**입니다. 긍정적인 해결을 서두르지 말고, 주인공의 내면 갈등을 심도 있게 묘사하는 데 집중하세요.",
496
+ "final_instruction": "창의적이면서도 주제와 일관성을 절대 잃지 마십시오."
497
+ },
498
+ "English": {
499
+ "title": f"You are Writer #{writer_number}. Write pages {pages_start}-{pages_end} of the novella.",
500
+ "plan": "Director's Masterplan",
501
+ "prev_summary": "Previous Content Summary",
502
+ "guidelines": "**Writing Guidelines:**\n1. **Length**: Approx. 1,400-1,500 words\n2. **Connectivity**: Connect naturally with the summarized previous content.\n3. **Consistency**: Strictly follow character settings, states, and plot structure.",
503
+ "reminder_title": "⭐ REMINDER (THEME)",
504
+ "reminder_body": f"The core of this novel is the **anxiety, alienation, and loss** from the theme '{user_query}'. Do not rush to a positive resolution; focus on deeply describing the protagonist's internal conflict.",
505
+ "final_instruction": "Be creative, but never lose consistency and the core theme."
506
+ }
507
+ }
508
 
509
+ p = lang_prompts[language]
510
+ consistency_info = self.consistency_tracker.get_character_summary(writer_number) + self.consistency_tracker.get_plot_summary(writer_number)
511
+
512
+ return f"{p['title']}\n\n**{p['plan']}:**\n{director_plan}\n\n{consistency_info}\n\n**{p['prev_summary']}:**\n{previous_content_summary}\n\n---\n{p['guidelines']}\n\n**{p['reminder_title']}**\n{p['reminder_body']}\n---\n\n{p['final_instruction']}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
513
 
514
+ def create_critic_consistency_prompt(self, all_content: str, user_query: str, language: str) -> str:
515
+ """비평가 중간 검토 프롬프트 (테마 검토 강화)"""
516
+ return f"""당신은 일관성 검토 전문 비평가입니다. 지금까지 작성된 내용을 검토하세요.
517
+
518
+ **원래 주제:** {user_query}
519
+ **현재까지 작성된 내용 (최근 3000자):**\n{all_content[-3000:]}
520
 
521
+ **검토 항목:**
522
+ 1. **주제 일관성 (가장 중요):** 내용이 원래 주제의 어두운 감정선에서 벗어나지 않았는지 확인하고, 벗어났다면 수정 방향을 제시하세요.
523
+ 2. **기술적 일관성:** 캐릭터, 플롯, 설정의 연속성과 논리적 오류를 찾아내세요.
524
+ 3. **반복 내용:** 의미적으로 중복되는 장면이나 표현이 없는지 확인하세요.
525
 
526
+ **결과:** 발견된 문제점과 구체적인 수정 제안을 목록으로 제시하세요.
527
+ """
 
 
528
 
529
+ def create_writer_revision_prompt(self, writer_number: int, initial_content: str, consistency_feedback: str, language: str) -> str:
 
 
 
530
  """작가 수정 프롬프트"""
531
+ return f"""작가 {writer_number}번으로서 비평가의 피드백을 반영하여 내용을 수정하세요.
532
+
533
+ **초기 작성 내용:**\n{initial_content}
534
+ **비평가 피드백:**\n{consistency_feedback}
535
+
536
+ **수정 지침:**
537
+ - 지적된 모든 주제 이탈 및 일관성 문제를 해결하세요.
538
+ - 분량(1,400-1,500 단어)을 유지하면서 내용의 질을 높이세요.
539
+ - 수정된 최종 버전을 제시하세요.
540
+ """
541
+
542
+ def create_critic_final_prompt(self, complete_novel: str, language: str) -> str:
543
+ """최종 비평가 검토 보고서 작성 프롬프트"""
544
+ return f"""완성된 소설의 최종 일관성 완성도에 대한 종합 보고서를 작성하세요.
545
+
546
+ **완성된 소설 (마지막 2000자):**\n{complete_novel[-2000:]}
547
+
548
+ **보고서 포함 항목:**
549
+ 1. **전체 일관성 평가:** 캐릭터, 플롯, 설정, 주제 유지에 대한 점수(1-10)와 총평.
550
+ 2. **최종 발견된 문제점:** 남아있는 사소한 문제점들.
551
+ 3. **성공 요소:** 특히 잘 유지된 일관성 부분이나 주제 표현이 뛰어난 부분.
552
+ 4. **최종 평가:** 소설의 전반적인 완성도와 독자에게 미칠 영향에 대한 평가.
553
+ """
554
+
555
+ # --- LLM 호출 함수들 ---
556
+ def call_llm_sync(self, messages: List[Dict[str, str]], role: str, language: str) -> str:
557
+ """LLM 동기식 호출 (요약 등 내부용)"""
558
+ full_content = ""
559
+ for chunk in self.call_llm_streaming(messages, role, language):
560
+ full_content += chunk
561
+ if full_content.startswith(""):
562
+ raise Exception(f"LLM Sync Call Failed: {full_content}")
563
+ return full_content
564
+
565
+ def call_llm_streaming(self, messages: List[Dict[str, str]], role: str, language: str) -> Generator[str, None, None]:
566
+ """LLM 스트리밍 호출 (완전한 에러 처리 및 디버깅)"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
567
  try:
568
  system_prompts = self.get_system_prompts(language)
569
+ full_messages = [{"role": "system", "content": system_prompts.get(role, "You are a helpful assistant.")}, *messages]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
570
 
571
  payload = {
572
  "model": self.model_id,
573
  "messages": full_messages,
574
+ "max_tokens": 10000,
575
+ "temperature": 0.75,
576
  "top_p": 0.9,
577
+ "presence_penalty": 0.3,
578
+ "frequency_penalty": 0.2,
579
  "stream": True,
580
  "stream_options": {"include_usage": True}
581
  }
582
 
583
+ logger.info(f"[{role}] API 스트리밍 시작")
584
+
585
+ # API 호출
586
  response = requests.post(
587
+ self.api_url,
588
+ headers=self.create_headers(),
589
+ json=payload,
590
+ stream=True,
591
+ timeout=180
592
  )
593
 
594
+ # 상태 코드 확인
595
  if response.status_code != 200:
596
+ logger.error(f"API 응답 오류: {response.status_code}")
597
+ logger.error(f"응답 내용: {response.text[:500]}")
598
+ yield f"❌ API 오류 (상태 코드: {response.status_code})"
599
  return
600
 
601
+ response.raise_for_status()
602
+
603
+ # 스트리밍 처리
604
  buffer = ""
605
  total_content = ""
606
+ chunk_count = 0
607
+ error_count = 0
608
+
609
  for line in response.iter_lines():
610
+ if not line:
611
+ continue
612
+
613
+ try:
614
+ line_str = line.decode('utf-8').strip()
615
+
616
+ # SSE 형식 확인
617
+ if not line_str.startswith("data: "):
618
+ continue
619
+
620
+ data_str = line_str[6:] # "data: " 제거
621
+
622
+ # 스트림 종료 확인
623
+ if data_str == "[DONE]":
624
+ logger.info(f"[{role}] 스트리밍 완료 - 총 {len(total_content)} 문자")
625
+ break
626
+
627
+ # JSON 파싱
628
+ try:
629
+ data = json.loads(data_str)
630
+ except json.JSONDecodeError:
631
+ logger.warning(f"JSON 파싱 실패: {data_str[:100]}")
632
+ continue
633
+
634
+ # choices 배열 안전하게 확인
635
+ choices = data.get("choices", None)
636
+ if not choices or not isinstance(choices, list) or len(choices) == 0:
637
+ # 에러 응답 확인
638
+ if "error" in data:
639
+ error_msg = data.get("error", {}).get("message", "Unknown error")
640
+ logger.error(f"API 에러: {error_msg}")
641
+ yield f"❌ API 에러: {error_msg}"
642
+ return
643
+ continue
644
+
645
+ # delta에서 content 추출
646
+ delta = choices[0].get("delta", {})
647
+ content = delta.get("content", "")
648
+
649
+ if content:
650
+ buffer += content
651
+ total_content += content
652
+ chunk_count += 1
653
+
654
+ # 100자 또는 줄바꿈마다 yield
655
+ if len(buffer) >= 100 or '\n' in buffer:
656
+ yield buffer
657
+ buffer = ""
658
+ time.sleep(0.01) # UI 업데이트를 위한 짧은 대기
659
+
660
+ except Exception as e:
661
+ error_count += 1
662
+ logger.error(f"청크 처리 오류 #{error_count}: {str(e)}")
663
+ if error_count > 10: # 너무 많은 에러시 중단
664
+ yield f"❌ 스트리밍 중 과도한 오류 발생"
665
+ return
666
+ continue
667
 
668
+ # 남은 버퍼 처리
669
  if buffer:
670
  yield buffer
671
+
672
+ # 결과 확인
673
+ if chunk_count == 0:
674
+ logger.error(f"[{role}] 콘텐츠가 전혀 수신되지 않음")
675
+ yield "❌ API로부터 응답을 받지 못했습니다."
676
+ else:
677
+ logger.info(f"[{role}] 성공적으로 {chunk_count}개 청크, 총 {len(total_content)}자 수신")
678
 
679
+ except requests.exceptions.Timeout:
680
+ logger.error("API 요청 시간 초과")
681
+ yield "❌ API 요청 시간이 초과되었습니다."
682
+ except requests.exceptions.ConnectionError:
683
+ logger.error("API 연결 실패")
684
+ yield "❌ API 서버에 연결할 수 없습니다."
685
  except Exception as e:
686
+ logger.error(f"예기치 않은 오류: {type(e).__name__}: {str(e)}", exc_info=True)
687
  yield f"❌ 오류 발생: {str(e)}"
688
+
689
+
690
+
691
+
692
+
693
  def get_system_prompts(self, language: str) -> Dict[str, str]:
694
+ """역할별 시스템 프롬프트 생성"""
695
+ base_prompts = {
696
+ "Korean": {
697
  "director": "당신은 창의적이고 체계적인 소설 기획 전문가입니다. 흥미롭고 일관성 있는 스토리를 설계하세요.",
698
  "critic": "당신은 일관성 검토 전문 비평가입니다. 캐릭터, 플롯, 설정의 일관성을 철저히 점검하고 개선방안을 제시하세요.",
699
+ "writer_base": "당신은 전문 소설 작가입니다. 주어진 지침에 따라 몰입감 있고 일관성 있는 내용을 작성하세요."
700
+ },
701
+ "English": {
 
 
 
 
 
 
 
 
 
 
702
  "director": "You are a creative and systematic novel planning expert. Design engaging and consistent stories.",
703
  "critic": "You are a consistency review specialist critic. Thoroughly check character, plot, and setting consistency and suggest improvements.",
704
+ "writer_base": "You are a professional novel writer. Write immersive and consistent content according to the given guidelines."
 
 
 
 
 
 
 
 
 
705
  }
706
+ }
707
+
708
+ prompts = base_prompts[language].copy()
709
+
710
+ # 작가별 특수 프롬프트
711
+ if language == "Korean":
712
+ prompts["writer1"] = "당신은 소설의 매력적인 시작을 담당하는 작가입니다. 독자를 사로잡는 도입부를 만드세요."
713
+ prompts["writer10"] = "당신은 완벽한 결말을 만드는 작가입니다. 독자에게 깊은 여운을 남기는 마무리를 하세요."
714
+ else:
715
+ prompts["writer1"] = "You are a writer responsible for the captivating beginning. Create an opening that hooks readers."
716
+ prompts["writer10"] = "You are a writer who creates the perfect ending. Create a conclusion that leaves readers with deep resonance."
717
+
718
+ # writer2-9는 기본 프롬프트 사용
719
+ for i in range(2, 10):
720
+ prompts[f"writer{i}"] = prompts["writer_base"]
721
+
722
+ return prompts
723
+
724
+ # --- 메인 프로세스 ---
725
+ def process_novel_stream(self, query: str, language: str, session_id: Optional[str] = None) -> Generator[Tuple[str, List[Dict[str, Any]], str], None, None]:
726
+ """소설 생성 스트리밍 프로세스 (강화된 로직)"""
727
  try:
728
+ resume_from_stage = 0
 
 
729
  if session_id:
730
  self.current_session_id = session_id
731
  session = NovelDatabase.get_session(session_id)
 
736
  logger.info(f"Resuming session {session_id} from stage {resume_from_stage}")
737
  else:
738
  self.current_session_id = NovelDatabase.create_session(query, language)
 
739
  logger.info(f"Created new session: {self.current_session_id}")
740
+
 
 
 
 
 
 
 
 
741
  stages = []
742
  if resume_from_stage > 0:
743
+ stages = [{
744
+ "name": s['stage_name'], "status": s['status'], "content": s.get('content', ''),
745
+ "consistency_score": s.get('consistency_score', 0.0)
746
+ } for s in NovelDatabase.get_stages(self.current_session_id)]
747
+
 
 
 
 
 
748
  for stage_idx in range(resume_from_stage, len(OPTIMIZED_STAGES)):
749
  role, stage_name = OPTIMIZED_STAGES[stage_idx]
 
 
750
  if stage_idx >= len(stages):
751
+ stages.append({"name": stage_name, "status": "active", "content": "", "consistency_score": 0.0})
 
 
 
 
 
752
  else:
753
  stages[stage_idx]["status"] = "active"
754
+
755
+ yield "", stages, self.current_session_id
 
 
756
  prompt = self.get_stage_prompt(stage_idx, role, query, language, stages)
 
 
757
  stage_content = ""
758
+ for chunk in self.call_llm_streaming([{"role": "user", "content": prompt}], role, language):
 
 
 
 
759
  stage_content += chunk
760
  stages[stage_idx]["content"] = stage_content
761
+ yield "", stages, self.current_session_id
762
+
 
763
  consistency_score = 0.0
764
  if role.startswith("writer"):
765
+ writer_num = int(re.search(r'\d+', role).group())
766
+ all_previous = self.get_all_content(stages, stage_idx)
767
+ errors = self.consistency_tracker.validate_consistency(writer_num, stage_content)
768
+ consistency_score = max(0, 10 - len(errors) * 2)
 
 
 
 
769
  stages[stage_idx]["consistency_score"] = consistency_score
770
+
 
 
 
 
771
  stages[stage_idx]["status"] = "complete"
 
 
772
  NovelDatabase.save_stage(
773
+ self.current_session_id, stage_idx, stage_name, role,
774
+ stage_content, "complete", consistency_score
 
 
 
 
 
775
  )
776
+ yield "", stages, self.current_session_id
777
+
778
+ final_novel = NovelDatabase.get_writer_content(self.current_session_id)
779
+ final_report = self.generate_consistency_report(final_novel, language)
780
+ NovelDatabase.update_final_novel(self.current_session_id, final_novel, final_report)
781
+ yield f"✅ 소설 완성! 총 {len(final_novel.split())}단어", stages, self.current_session_id
782
+
 
 
 
 
 
 
 
 
 
783
  except Exception as e:
784
+ logger.error(f"소설 생성 프로세스 오류: {e}", exc_info=True)
785
+ yield f"❌ 오류 발생: {e}", stages if 'stages' in locals() else [], self.current_session_id
786
+
787
+ def get_stage_prompt(self, stage_idx: int, role: str, query: str, language: str, stages: List[Dict]) -> str:
788
+ """단계별 프롬프트 생성 (요약 기능 및 주제 전달 강화)"""
789
+ if stage_idx == 0:
 
 
 
 
 
 
 
 
790
  return self.create_director_initial_prompt(query, language)
791
+ if stage_idx == 1:
792
+ return self.create_critic_director_prompt(stages[0]["content"], query, language)
793
+ if stage_idx == 2:
794
+ return self.create_director_revision_prompt(stages[0]["content"], stages[1]["content"], query, language)
795
+
796
+ master_plan = stages[2]["content"]
797
+
798
+ if 3 <= stage_idx <= 12: # 작가 초안
799
  writer_num = stage_idx - 2
800
+ previous_content = self.get_all_content(stages, stage_idx)
801
+ summary = self.create_summary(previous_content, language)
802
+ return self.create_writer_prompt(writer_num, master_plan, summary, query, language)
803
+
804
+ if stage_idx == 13: # 비평가 중간 검토
805
  all_content = self.get_all_content(stages, stage_idx)
806
+ return self.create_critic_consistency_prompt(all_content, query, language)
807
+
808
+ if 14 <= stage_idx <= 23: # 작가 수정
809
  writer_num = stage_idx - 13
810
  initial_content = stages[2 + writer_num]["content"]
811
+ feedback = stages[13]["content"]
812
+ return self.create_writer_revision_prompt(writer_num, initial_content, feedback, language)
813
+
814
+ if stage_idx == 24: # 최종 검토
815
  complete_novel = self.get_all_writer_content(stages)
816
  return self.create_critic_final_prompt(complete_novel, language)
817
 
818
  return ""
819
+
820
+ def create_summary(self, content: str, language: str) -> str:
821
+ """LLM을 이용해 이전 내용을 요약"""
822
+ if not content.strip():
823
+ return "이전 내용이 없습니다." if language == "Korean" else "No previous content."
824
+
825
+ prompt_text = "다음 소설 내용을 3~5개의 핵심적인 문장으로 요약해줘. 다음 작가가 이야기를 이어가는 데 필요한 핵심 정보(등장인물의 현재 상황, 감정, 마지막 사건)를 포함해야 해."
826
+ if language != "Korean":
827
+ prompt_text = "Summarize the following novel content in 3-5 key sentences. Include crucial information for the next writer to continue the story (characters' current situation, emotions, and the last major event)."
828
+
829
+ summary_prompt = f"{prompt_text}\n\n---\n{content[-2000:]}"
830
+ try:
831
+ summary = self.call_llm_sync([{"role": "user", "content": summary_prompt}], "critic", language)
832
+ return summary
833
+ except Exception as e:
834
+ logger.error(f"요약 생성 실패: {e}")
835
+ return content[-1000:]
836
+
837
  def get_all_content(self, stages: List[Dict], current_stage: int) -> str:
838
+ """현재까지의 모든 내용 가져오기"""
839
+ return "\n\n".join(s["content"] for i, s in enumerate(stages) if i < current_stage and s["content"])
840
+
 
 
 
 
841
  def get_all_writer_content(self, stages: List[Dict]) -> str:
842
+ """모든 작가 최종 수정본 내용 가져오기"""
843
+ return "\n\n".join(s["content"] for i, s in enumerate(stages) if 14 <= i <= 23 and s["content"])
844
+
 
 
 
 
845
  def generate_consistency_report(self, complete_novel: str, language: str) -> str:
846
+ """최종 보고서 생성 (LLM 호출)"""
847
+ prompt = self.create_critic_final_prompt(complete_novel, language)
848
+ try:
849
+ report = self.call_llm_sync([{"role": "user", "content": prompt}], "critic", language)
850
+ return report
851
+ except Exception as e:
852
+ logger.error(f"최종 보고서 생성 실패: {e}")
853
+ return "보고서 생성 중 오류 발생"
 
 
 
 
 
854
 
855
 
856
+ # --- 유틸리티 함수들 ---
857
+ def process_query(query: str, language: str, session_id: Optional[str] = None) -> Generator[Tuple[str, str, str, str], None, None]:
858
+ """메인 쿼리 처리 함수"""
859
+ if not query.strip():
860
+ yield "", "", "❌ 주제를 입력해주세요.", session_id
 
 
 
861
  return
862
 
863
  system = NovelWritingSystem()
864
+ stages_markdown = ""
865
+ novel_content = ""
866
 
867
+ for status, stages, current_session_id in system.process_novel_stream(query, language, session_id):
868
+ stages_markdown = format_stages_display(stages)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
869
 
870
+ # 최종 소설 내용 가져오기
871
+ if stages and all(s.get("status") == "complete" for s in stages[-10:]):
872
+ novel_content = NovelDatabase.get_writer_content(current_session_id)
873
+ novel_content = format_novel_display(novel_content)
 
 
 
 
 
 
874
 
875
+ yield stages_markdown, novel_content, status or "🔄 처리 중...", current_session_id
 
 
 
 
 
 
 
 
876
 
877
+ def get_active_sessions(language: str) -> List[str]:
878
  """활성 세션 목록 가져오기"""
879
+ sessions = NovelDatabase.get_active_sessions()
880
+ return [f"{s['session_id'][:8]}... - {s['user_query'][:50]}... ({s['created_at']})"
881
+ for s in sessions]
 
 
 
 
 
 
 
 
 
 
 
 
882
 
883
+ def auto_recover_session(language: str) -> Tuple[Optional[str], str]:
884
+ """가장 최근 활성 세션 자동 복구"""
885
+ latest_session = NovelDatabase.get_latest_active_session()
886
+ if latest_session:
887
+ return latest_session['session_id'], f"세션 {latest_session['session_id'][:8]}... 복구됨"
888
+ return None, "복구할 세션이 없습니다."
889
 
890
  def resume_session(session_id: str, language: str) -> Generator[Tuple[str, str, str, str], None, None]:
891
+ """세션 재개 함수"""
892
  if not session_id:
893
+ yield "", "", "❌ 세션 ID가 없습니다.", session_id
 
 
 
894
  return
895
 
896
+ # 드롭다운에서 세션 ID 추출
897
+ if "..." in session_id:
898
+ session_id = session_id.split("...")[0]
899
+
900
+ session = NovelDatabase.get_session(session_id)
901
+ if not session:
902
+ yield "", "", "❌ 세션을 찾을 수 없습니다.", None
903
+ return
904
+
905
+ # process_query를 통해 재개
906
+ yield from process_query(session['user_query'], session['language'], session_id)
907
 
908
+ def download_novel(novel_text: str, format_type: str, language: str, session_id: str) -> Optional[str]:
909
+ """소설 다운로드 파일 생성"""
910
+ if not novel_text or not session_id:
911
+ return None
912
+
913
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
914
+ filename = f"novel_{session_id[:8]}_{timestamp}"
915
+
916
  try:
917
+ if format_type == "DOCX" and DOCX_AVAILABLE:
918
+ return export_to_docx(novel_text, filename, language)
 
 
 
 
 
919
  else:
920
+ return export_to_txt(novel_text, filename)
921
  except Exception as e:
922
+ logger.error(f"파일 생성 실패: {e}")
923
+ return None
924
 
925
+ def format_stages_display(stages: List[Dict]) -> str:
926
+ """단계별 진행 상황 마크다운 포맷팅"""
927
+ markdown = "## 🎬 진행 상황\n\n"
928
+ for i, stage in enumerate(stages):
929
+ status_icon = "✅" if stage['status'] == 'complete' else "🔄" if stage['status'] == 'active' else "⏳"
930
+ markdown += f"{status_icon} **{stage['name']}**"
931
+ if stage.get('consistency_score', 0) > 0:
932
+ markdown += f" (일관성: {stage['consistency_score']:.1f}/10)"
933
+ markdown += "\n"
934
+ if stage['content']:
935
+ preview = stage['content'][:200] + "..." if len(stage['content']) > 200 else stage['content']
936
+ markdown += f"> {preview}\n\n"
937
+ return markdown
938
+
939
+ def format_novel_display(novel_text: str) -> str:
940
+ """소설 내용 마크다운 포맷팅"""
941
+ if not novel_text:
942
+ return "아직 완성된 내용이 없습니다."
943
+
944
+ # 페이지 구분 추가
945
+ pages = novel_text.split('\n\n')
946
+ formatted = "# 📖 완성된 소설\n\n"
947
+
948
+ for i, page in enumerate(pages):
949
+ if page.strip():
950
+ formatted += f"### 페이지 {i+1}\n\n{page}\n\n---\n\n"
951
+
952
+ return formatted
953
 
954
+ def export_to_docx(content: str, filename: str, language: str) -> str:
955
+ """DOCX 파일로 내보내기"""
956
+ doc = Document()
 
 
957
 
958
+ # 제목 추가
959
+ title = doc.add_heading('AI 협업 소설', 0)
960
+ title.alignment = WD_ALIGN_PARAGRAPH.CENTER
 
 
961
 
962
+ # 메타데이터
963
+ doc.add_paragraph(f"생성일: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
964
+ doc.add_paragraph(f"언어: {language}")
965
+ doc.add_page_break()
 
966
 
967
+ # 본문 추가
968
+ paragraphs = content.split('\n\n')
969
+ for para in paragraphs:
970
+ if para.strip():
971
+ doc.add_paragraph(para.strip())
972
 
973
+ # 파일 저장
974
+ filepath = f"{filename}.docx"
975
+ doc.save(filepath)
976
+ return filepath
977
+
978
+ def export_to_txt(content: str, filename: str) -> str:
979
+ """TXT 파일로 내보내기"""
980
+ filepath = f"{filename}.txt"
981
+ with open(filepath, 'w', encoding='utf-8') as f:
982
+ f.write(content)
983
+ return filepath
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
984
 
985
 
986
  # CSS 스타일