openfree commited on
Commit
05b5c02
·
verified ·
1 Parent(s): cba6a14

Delete app.py

Browse files
Files changed (1) hide show
  1. app.py +0 -2306
app.py DELETED
@@ -1,2306 +0,0 @@
1
- import gradio as gr
2
- import os
3
- import json
4
- import requests
5
- from datetime import datetime
6
- import time
7
- from typing import List, Dict, Any, Generator, Tuple, Optional, Set
8
- import logging
9
- import re
10
- import tempfile
11
- from pathlib import Path
12
- import sqlite3
13
- import uuid # MD5 대신 UUID 사용
14
- import threading
15
- from contextlib import contextmanager
16
- from dataclasses import dataclass, field, asdict
17
- from collections import defaultdict
18
-
19
- # --- Logging setup ---
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, Mm
27
- from docx.enum.text import WD_ALIGN_PARAGRAPH
28
- from docx.enum.style import WD_STYLE_TYPE
29
- from docx.oxml.ns import qn
30
- from docx.oxml import OxmlElement
31
- DOCX_AVAILABLE = True
32
- except ImportError:
33
- DOCX_AVAILABLE = False
34
- logger.warning("python-docx not installed. DOCX export will be disabled.")
35
-
36
- # --- Environment variables and constants ---
37
- FRIENDLI_TOKEN = os.getenv("FRIENDLI_TOKEN", "")
38
- BRAVE_SEARCH_API_KEY = os.getenv("BRAVE_SEARCH_API_KEY", "")
39
- API_URL = "https://api.friendli.ai/dedicated/v1/chat/completions"
40
- MODEL_ID = "dep89a2fld32mcm"
41
- DB_PATH = "novel_sessions_v6.db"
42
-
43
- # Target word count settings
44
- TARGET_WORDS = 8000 # Safety margin
45
- MIN_WORDS_PER_PART = 800 # Minimum words per part
46
-
47
- # --- Environment validation (보안 개선) ---
48
- if not FRIENDLI_TOKEN:
49
- logger.error("FRIENDLI_TOKEN not set. Application will not work properly.")
50
- FRIENDLI_TOKEN = "dummy_token_for_testing"
51
- else:
52
- # 토큰 마스킹
53
- masked_token = FRIENDLI_TOKEN[:8] + "*" * (len(FRIENDLI_TOKEN) - 8)
54
- logger.info(f"FRIENDLI_TOKEN: {masked_token}")
55
-
56
- if not BRAVE_SEARCH_API_KEY:
57
- logger.warning("BRAVE_SEARCH_API_KEY not set. Web search features will be disabled.")
58
- else:
59
- # API 키 마스킹
60
- masked_key = BRAVE_SEARCH_API_KEY[:4] + "*" * (len(BRAVE_SEARCH_API_KEY) - 4)
61
- logger.info(f"BRAVE_SEARCH_API_KEY: {masked_key}")
62
-
63
- # --- Global variables ---
64
- db_lock = threading.Lock()
65
- db_write_lock = threading.Lock() # 쓰기 전용 락 추가
66
-
67
- # Narrative phases definition
68
- NARRATIVE_PHASES = [
69
- "Introduction: Daily Life and Cracks",
70
- "Development 1: Rising Anxiety",
71
- "Development 2: External Shock",
72
- "Development 3: Deepening Internal Conflict",
73
- "Climax 1: Peak of Crisis",
74
- "Climax 2: Moment of Choice",
75
- "Falling Action 1: Consequences and Aftermath",
76
- "Falling Action 2: New Recognition",
77
- "Resolution 1: Changed Daily Life",
78
- "Resolution 2: Open Questions"
79
- ]
80
-
81
- # Stage configuration - Single writer system
82
- UNIFIED_STAGES = [
83
- ("director", "🎬 Director: Integrated Narrative Structure Planning"),
84
- ("critic_director", "📝 Critic: Deep Review of Narrative Structure"),
85
- ("director", "🎬 Director: Final Master Plan"),
86
- ] + [
87
- item for i in range(1, 11)
88
- for item in [
89
- ("writer", f"✍️ Writer: Part {i} - {NARRATIVE_PHASES[i-1]}"),
90
- (f"critic_part{i}", f"📝 Part {i} Critic: Immediate Review and Revision Request"),
91
- ("writer", f"✍️ Writer: Part {i} Revision")
92
- ]
93
- ] + [
94
- ("critic_final", "📝 Final Critic: Comprehensive Evaluation and Literary Achievement"),
95
- ]
96
-
97
- # --- Data classes ---
98
- @dataclass
99
- class StoryBible:
100
- """Story bible for maintaining narrative consistency"""
101
- characters: Dict[str, Dict[str, Any]] = field(default_factory=dict)
102
- settings: Dict[str, str] = field(default_factory=dict)
103
- timeline: List[Dict[str, Any]] = field(default_factory=list)
104
- plot_points: List[Dict[str, Any]] = field(default_factory=list)
105
- themes: List[str] = field(default_factory=list)
106
- symbols: Dict[str, List[str]] = field(default_factory=dict)
107
- style_guide: Dict[str, str] = field(default_factory=dict)
108
- opening_sentence: str = ""
109
-
110
- @dataclass
111
- class PartCritique:
112
- """Critique content for each part"""
113
- part_number: int
114
- continuity_issues: List[str] = field(default_factory=list)
115
- character_consistency: List[str] = field(default_factory=list)
116
- plot_progression: List[str] = field(default_factory=list)
117
- thematic_alignment: List[str] = field(default_factory=list)
118
- technical_issues: List[str] = field(default_factory=list)
119
- strengths: List[str] = field(default_factory=list)
120
- required_changes: List[str] = field(default_factory=list)
121
- literary_quality: List[str] = field(default_factory=list)
122
-
123
- # --- Core logic classes ---
124
- class UnifiedNarrativeTracker:
125
- """Unified narrative tracker for single writer system"""
126
- def __init__(self):
127
- self.story_bible = StoryBible()
128
- self.part_critiques: Dict[int, PartCritique] = {}
129
- self.accumulated_content: List[str] = []
130
- self.word_count_by_part: Dict[int, int] = {}
131
- self.revision_history: Dict[int, List[str]] = defaultdict(list)
132
- self.causal_chains: List[Dict[str, Any]] = []
133
- self.narrative_momentum: float = 0.0
134
-
135
- def update_story_bible(self, element_type: str, key: str, value: Any):
136
- """Update story bible"""
137
- if element_type == "character":
138
- self.story_bible.characters[key] = value
139
- elif element_type == "setting":
140
- self.story_bible.settings[key] = value
141
- elif element_type == "timeline":
142
- self.story_bible.timeline.append({"event": key, "details": value})
143
- elif element_type == "theme":
144
- if key not in self.story_bible.themes:
145
- self.story_bible.themes.append(key)
146
- elif element_type == "symbol":
147
- if key not in self.story_bible.symbols:
148
- self.story_bible.symbols[key] = []
149
- self.story_bible.symbols[key].append(value)
150
-
151
- def add_part_critique(self, part_number: int, critique: PartCritique):
152
- """Add part critique"""
153
- self.part_critiques[part_number] = critique
154
-
155
- def check_continuity(self, current_part: int, new_content: str) -> List[str]:
156
- """Check continuity"""
157
- issues = []
158
-
159
- # Character consistency check
160
- for char_name, char_data in self.story_bible.characters.items():
161
- if char_name in new_content:
162
- if "traits" in char_data:
163
- for trait in char_data["traits"]:
164
- if trait.get("abandoned", False):
165
- issues.append(f"{char_name}'s abandoned trait '{trait['name']}' reappears")
166
-
167
- # Timeline consistency check
168
- if len(self.story_bible.timeline) > 0:
169
- last_event = self.story_bible.timeline[-1]
170
-
171
- # Causality check
172
- if current_part > 1 and not any(kw in new_content for kw in
173
- ['because', 'therefore', 'thus', 'hence', 'consequently']):
174
- issues.append("Unclear causality with previous part")
175
-
176
- return issues
177
-
178
- def calculate_narrative_momentum(self, part_number: int, content: str) -> float:
179
- """Calculate narrative momentum"""
180
- momentum = 5.0
181
-
182
- # New elements introduced
183
- new_elements = len(set(content.split()) - set(' '.join(self.accumulated_content).split()))
184
- if new_elements > 100:
185
- momentum += 2.0
186
-
187
- # Conflict escalation
188
- tension_words = ['crisis', 'conflict', 'tension', 'struggle', 'dilemma']
189
- if any(word in content.lower() for word in tension_words):
190
- momentum += 1.5
191
-
192
- # Causal clarity
193
- causal_words = ['because', 'therefore', 'thus', 'consequently', 'hence']
194
- causal_count = sum(1 for word in causal_words if word in content.lower())
195
- momentum += min(causal_count * 0.5, 2.0)
196
-
197
- # Repetition penalty (조정된 임계값)
198
- if part_number > 1:
199
- prev_content = self.accumulated_content[-1] if self.accumulated_content else ""
200
- overlap = len(set(content.split()) & set(prev_content.split()))
201
- if overlap > len(content.split()) * 0.4: # 30%에서 40%로 조정
202
- momentum -= 2.0
203
-
204
- return max(0.0, min(10.0, momentum))
205
-
206
- class NovelDatabase:
207
- """Database management - Improved for better concurrency"""
208
- @staticmethod
209
- def init_db():
210
- with sqlite3.connect(DB_PATH) as conn:
211
- conn.execute("PRAGMA journal_mode=WAL")
212
- conn.execute("PRAGMA synchronous=NORMAL") # 성능 향상
213
- conn.execute("PRAGMA cache_size=10000") # 캐시 크기 증가
214
- cursor = conn.cursor()
215
-
216
- # Main sessions table
217
- cursor.execute('''
218
- CREATE TABLE IF NOT EXISTS sessions (
219
- session_id TEXT PRIMARY KEY,
220
- user_query TEXT NOT NULL,
221
- language TEXT NOT NULL,
222
- created_at TEXT DEFAULT (datetime('now')),
223
- updated_at TEXT DEFAULT (datetime('now')),
224
- status TEXT DEFAULT 'active',
225
- current_stage INTEGER DEFAULT 0,
226
- final_novel TEXT,
227
- literary_report TEXT,
228
- total_words INTEGER DEFAULT 0,
229
- story_bible TEXT,
230
- narrative_tracker TEXT,
231
- opening_sentence TEXT
232
- )
233
- ''')
234
-
235
- # Stages table
236
- cursor.execute('''
237
- CREATE TABLE IF NOT EXISTS stages (
238
- id INTEGER PRIMARY KEY AUTOINCREMENT,
239
- session_id TEXT NOT NULL,
240
- stage_number INTEGER NOT NULL,
241
- stage_name TEXT NOT NULL,
242
- role TEXT NOT NULL,
243
- content TEXT,
244
- word_count INTEGER DEFAULT 0,
245
- status TEXT DEFAULT 'pending',
246
- narrative_momentum REAL DEFAULT 0.0,
247
- created_at TEXT DEFAULT (datetime('now')),
248
- updated_at TEXT DEFAULT (datetime('now')),
249
- FOREIGN KEY (session_id) REFERENCES sessions(session_id),
250
- UNIQUE(session_id, stage_number)
251
- )
252
- ''')
253
-
254
- # Critiques table
255
- cursor.execute('''
256
- CREATE TABLE IF NOT EXISTS critiques (
257
- id INTEGER PRIMARY KEY AUTOINCREMENT,
258
- session_id TEXT NOT NULL,
259
- part_number INTEGER NOT NULL,
260
- critique_data TEXT,
261
- created_at TEXT DEFAULT (datetime('now')),
262
- FOREIGN KEY (session_id) REFERENCES sessions(session_id)
263
- )
264
- ''')
265
-
266
- # Create indexes for better performance
267
- cursor.execute('CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status)')
268
- cursor.execute('CREATE INDEX IF NOT EXISTS idx_stages_session ON stages(session_id)')
269
-
270
- conn.commit()
271
-
272
- @staticmethod
273
- @contextmanager
274
- def get_db():
275
- """읽기 전용 연결 - 락 없이"""
276
- conn = sqlite3.connect(DB_PATH, timeout=30.0)
277
- conn.row_factory = sqlite3.Row
278
- conn.execute("PRAGMA journal_mode=WAL")
279
- try:
280
- yield conn
281
- finally:
282
- conn.close()
283
-
284
- @staticmethod
285
- @contextmanager
286
- def get_db_write():
287
- """쓰기 연결 - 쓰기 락 사용"""
288
- with db_write_lock:
289
- conn = sqlite3.connect(DB_PATH, timeout=30.0)
290
- conn.row_factory = sqlite3.Row
291
- conn.execute("PRAGMA journal_mode=WAL")
292
- try:
293
- yield conn
294
- finally:
295
- conn.close()
296
-
297
- @staticmethod
298
- def create_session(user_query: str, language: str) -> str:
299
- # UUID 사용으로 보안 개선
300
- session_id = str(uuid.uuid4())
301
- with NovelDatabase.get_db_write() as conn:
302
- conn.cursor().execute(
303
- 'INSERT INTO sessions (session_id, user_query, language) VALUES (?, ?, ?)',
304
- (session_id, user_query, language)
305
- )
306
- conn.commit()
307
- logger.info(f"Created new session: {session_id[:8]}...") # 로그에는 일부만
308
- return session_id
309
-
310
- @staticmethod
311
- def save_stage(session_id: str, stage_number: int, stage_name: str,
312
- role: str, content: str, status: str = 'complete',
313
- narrative_momentum: float = 0.0):
314
- word_count = len(content.split()) if content else 0
315
- with NovelDatabase.get_db_write() as conn:
316
- cursor = conn.cursor()
317
- cursor.execute('''
318
- INSERT INTO stages (session_id, stage_number, stage_name, role, content,
319
- word_count, status, narrative_momentum)
320
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
321
- ON CONFLICT(session_id, stage_number)
322
- DO UPDATE SET content=?, word_count=?, status=?, stage_name=?,
323
- narrative_momentum=?, updated_at=datetime('now')
324
- ''', (session_id, stage_number, stage_name, role, content, word_count,
325
- status, narrative_momentum, content, word_count, status, stage_name,
326
- narrative_momentum))
327
-
328
- # Update total word count
329
- cursor.execute('''
330
- UPDATE sessions
331
- SET total_words = (
332
- SELECT SUM(word_count)
333
- FROM stages
334
- WHERE session_id = ? AND role = 'writer' AND content IS NOT NULL
335
- ),
336
- updated_at = datetime('now'),
337
- current_stage = ?
338
- WHERE session_id = ?
339
- ''', (session_id, stage_number, session_id))
340
-
341
- conn.commit()
342
-
343
- @staticmethod
344
- def save_critique(session_id: str, part_number: int, critique: PartCritique):
345
- """Save critique"""
346
- with NovelDatabase.get_db_write() as conn:
347
- critique_json = json.dumps(asdict(critique))
348
- conn.cursor().execute(
349
- 'INSERT INTO critiques (session_id, part_number, critique_data) VALUES (?, ?, ?)',
350
- (session_id, part_number, critique_json)
351
- )
352
- conn.commit()
353
-
354
- @staticmethod
355
- def save_opening_sentence(session_id: str, opening_sentence: str):
356
- """Save opening sentence"""
357
- with NovelDatabase.get_db_write() as conn:
358
- conn.cursor().execute(
359
- 'UPDATE sessions SET opening_sentence = ? WHERE session_id = ?',
360
- (opening_sentence, session_id)
361
- )
362
- conn.commit()
363
-
364
- @staticmethod
365
- def get_writer_content(session_id: str) -> str:
366
- """Get writer content - Integrate all revisions"""
367
- with NovelDatabase.get_db() as conn: # 읽기 전용
368
- rows = conn.cursor().execute('''
369
- SELECT content FROM stages
370
- WHERE session_id = ? AND role = 'writer'
371
- AND stage_name LIKE '%Revision%'
372
- ORDER BY stage_number
373
- ''', (session_id,)).fetchall()
374
-
375
- if rows:
376
- return '\n\n'.join(row['content'] for row in rows if row['content'])
377
- else:
378
- # If no revisions, use drafts
379
- rows = conn.cursor().execute('''
380
- SELECT content FROM stages
381
- WHERE session_id = ? AND role = 'writer'
382
- AND stage_name NOT LIKE '%Revision%'
383
- ORDER BY stage_number
384
- ''', (session_id,)).fetchall()
385
- return '\n\n'.join(row['content'] for row in rows if row['content'])
386
-
387
- @staticmethod
388
- def save_narrative_tracker(session_id: str, tracker: UnifiedNarrativeTracker):
389
- """Save unified narrative tracker"""
390
- with NovelDatabase.get_db_write() as conn:
391
- tracker_data = json.dumps({
392
- 'story_bible': asdict(tracker.story_bible),
393
- 'part_critiques': {k: asdict(v) for k, v in tracker.part_critiques.items()},
394
- 'word_count_by_part': tracker.word_count_by_part,
395
- 'causal_chains': tracker.causal_chains,
396
- 'narrative_momentum': tracker.narrative_momentum,
397
- 'accumulated_content': tracker.accumulated_content # 세션 복원을 위해 추가
398
- })
399
- conn.cursor().execute(
400
- 'UPDATE sessions SET narrative_tracker = ? WHERE session_id = ?',
401
- (tracker_data, session_id)
402
- )
403
- conn.commit()
404
-
405
- @staticmethod
406
- def load_narrative_tracker(session_id: str) -> Optional[UnifiedNarrativeTracker]:
407
- """Load unified narrative tracker - 세션 복원 개선"""
408
- with NovelDatabase.get_db() as conn: # 읽기 전용
409
- row = conn.cursor().execute(
410
- 'SELECT narrative_tracker FROM sessions WHERE session_id = ?',
411
- (session_id,)
412
- ).fetchone()
413
-
414
- if row and row['narrative_tracker']:
415
- data = json.loads(row['narrative_tracker'])
416
- tracker = UnifiedNarrativeTracker()
417
-
418
- # Restore story bible
419
- bible_data = data.get('story_bible', {})
420
- tracker.story_bible = StoryBible(**bible_data)
421
-
422
- # Restore critiques
423
- for part_num, critique_data in data.get('part_critiques', {}).items():
424
- tracker.part_critiques[int(part_num)] = PartCritique(**critique_data)
425
-
426
- tracker.word_count_by_part = data.get('word_count_by_part', {})
427
- tracker.causal_chains = data.get('causal_chains', [])
428
- tracker.narrative_momentum = data.get('narrative_momentum', 0.0)
429
-
430
- # 세션 복원 시 accumulated_content 복원
431
- tracker.accumulated_content = data.get('accumulated_content', [])
432
-
433
- return tracker
434
- return None
435
-
436
- # Maintain existing methods
437
- @staticmethod
438
- def get_session(session_id: str) -> Optional[Dict]:
439
- with NovelDatabase.get_db() as conn: # 읽기 전용
440
- row = conn.cursor().execute('SELECT * FROM sessions WHERE session_id = ?',
441
- (session_id,)).fetchone()
442
- return dict(row) if row else None
443
-
444
- @staticmethod
445
- def get_stages(session_id: str) -> List[Dict]:
446
- with NovelDatabase.get_db() as conn: # 읽기 전용
447
- rows = conn.cursor().execute(
448
- 'SELECT * FROM stages WHERE session_id = ? ORDER BY stage_number',
449
- (session_id,)
450
- ).fetchall()
451
- return [dict(row) for row in rows]
452
-
453
- @staticmethod
454
- def update_final_novel(session_id: str, final_novel: str, literary_report: str = ""):
455
- with NovelDatabase.get_db_write() as conn:
456
- conn.cursor().execute(
457
- '''UPDATE sessions SET final_novel = ?, status = 'complete',
458
- updated_at = datetime('now'), literary_report = ? WHERE session_id = ?''',
459
- (final_novel, literary_report, session_id)
460
- )
461
- conn.commit()
462
-
463
- @staticmethod
464
- def get_active_sessions() -> List[Dict]:
465
- with NovelDatabase.get_db() as conn: # 읽기 전용
466
- rows = conn.cursor().execute(
467
- '''SELECT session_id, user_query, language, created_at, current_stage, total_words
468
- FROM sessions WHERE status = 'active' ORDER BY updated_at DESC LIMIT 10'''
469
- ).fetchall()
470
- return [dict(row) for row in rows]
471
-
472
- @staticmethod
473
- def get_total_words(session_id: str) -> int:
474
- """Get total word count"""
475
- with NovelDatabase.get_db() as conn: # 읽기 전용
476
- row = conn.cursor().execute(
477
- 'SELECT total_words FROM sessions WHERE session_id = ?',
478
- (session_id,)
479
- ).fetchone()
480
- return row['total_words'] if row and row['total_words'] else 0
481
-
482
-
483
-
484
- class WebSearchIntegration:
485
- """Web search functionality"""
486
- def __init__(self):
487
- self.brave_api_key = BRAVE_SEARCH_API_KEY
488
- self.search_url = "https://api.search.brave.com/res/v1/web/search"
489
- self.enabled = bool(self.brave_api_key)
490
-
491
- def search(self, query: str, count: int = 3, language: str = "en") -> List[Dict]:
492
- if not self.enabled:
493
- return []
494
- headers = {
495
- "Accept": "application/json",
496
- "X-Subscription-Token": self.brave_api_key
497
- }
498
- params = {
499
- "q": query,
500
- "count": count,
501
- "search_lang": "ko" if language == "Korean" else "en",
502
- "text_decorations": False,
503
- "safesearch": "moderate"
504
- }
505
- try:
506
- response = requests.get(self.search_url, headers=headers, params=params, timeout=10)
507
- response.raise_for_status()
508
- results = response.json().get("web", {}).get("results", [])
509
- return results
510
- except requests.exceptions.RequestException as e:
511
- logger.error(f"Web search API error: {e}")
512
- return []
513
-
514
- def extract_relevant_info(self, results: List[Dict], max_chars: int = 1500) -> str:
515
- if not results:
516
- return ""
517
- extracted = []
518
- total_chars = 0
519
- for i, result in enumerate(results[:3], 1):
520
- title = result.get("title", "")
521
- description = result.get("description", "")
522
- info = f"[{i}] {title}: {description}"
523
- if total_chars + len(info) < max_chars:
524
- extracted.append(info)
525
- total_chars += len(info)
526
- else:
527
- break
528
- return "\n".join(extracted)
529
-
530
-
531
- class UnifiedLiterarySystem:
532
- """Single writer progressive literary novel generation system"""
533
- def __init__(self):
534
- self.token = FRIENDLI_TOKEN
535
- self.api_url = API_URL
536
- self.model_id = MODEL_ID
537
- self.narrative_tracker = UnifiedNarrativeTracker()
538
- self.web_search = WebSearchIntegration()
539
- self.current_session_id = None
540
- NovelDatabase.init_db()
541
-
542
- def create_headers(self):
543
- return {"Authorization": f"Bearer {self.token}", "Content-Type": "application/json"}
544
-
545
- # --- Prompt generation functions ---
546
- def augment_query(self, user_query: str, language: str) -> str:
547
- """Augment prompt"""
548
- if len(user_query.split()) < 15:
549
- augmented_template = {
550
- "Korean": f"""'{user_query}'
551
-
552
- **서사 구조 핵심:**
553
- - 10개 파트가 하나의 통합된 이야기를 구성
554
- - 각 파트는 이전 파트의 필연적 결과
555
- - 인물의 명확한 변화 궤적 (A → B → C)
556
- - 중심 갈등의 점진적 고조와 해결
557
- - 강렬한 중심 상징의 의미 변화""",
558
-
559
- "English": f"""'{user_query}'
560
-
561
- **Narrative Structure Core:**
562
- - 10 parts forming one integrated story
563
- - Each part as inevitable result of previous
564
- - Clear character transformation arc (A → B → C)
565
- - Progressive escalation and resolution of central conflict
566
- - Evolving meaning of powerful central symbol"""
567
- }
568
- return augmented_template.get(language, user_query)
569
- return user_query
570
-
571
- def generate_powerful_opening(self, user_query: str, language: str) -> str:
572
- """Generate powerful opening sentence matching the theme"""
573
-
574
- opening_prompt = {
575
- "Korean": f"""주제: {user_query}
576
-
577
- 이 주제에 대한 강렬하고 잊을 수 없는 첫문장을 생성하세요.
578
-
579
- **첫문장 작성 원칙:**
580
- 1. 즉각적인 긴장감이나 궁금증 유발
581
- 2. 평범하지 않은 시각이나 상황 제시
582
- 3. 감각적이고 구체적인 이미지
583
- 4. 철학적 질문이나 역설적 진술
584
- 5. 시간과 공간의 독특한 설정
585
-
586
- **훌륭한 첫문장의 예시 패턴:**
587
- - "그가 죽은 날, ..." (충격적 사건)
588
- - "모든 것이 끝났다고 생각한 순간..." (반전 예고)
589
- - "세상에서 가장 [형용사]한 [명사]는..." (독특한 정의)
590
- - "[구체적 행동]하는 것만으로도..." (일상의 재해석)
591
-
592
- 단 하나의 문장만 제시하세요.""",
593
-
594
- "English": f"""Theme: {user_query}
595
-
596
- Generate an unforgettable opening sentence for this theme.
597
-
598
- **Opening Sentence Principles:**
599
- 1. Immediate tension or curiosity
600
- 2. Unusual perspective or situation
601
- 3. Sensory and specific imagery
602
- 4. Philosophical question or paradox
603
- 5. Unique temporal/spatial setting
604
-
605
- **Great Opening Patterns:**
606
- - "The day he died, ..." (shocking event)
607
- - "At the moment everything seemed over..." (reversal hint)
608
- - "The most [adjective] [noun] in the world..." (unique definition)
609
- - "Just by [specific action]..." (reinterpretation of ordinary)
610
-
611
- Provide only one sentence."""
612
- }
613
-
614
- messages = [{"role": "user", "content": opening_prompt.get(language, opening_prompt["Korean"])}]
615
- opening = self.call_llm_sync(messages, "writer", language)
616
- return opening.strip()
617
-
618
- def create_director_initial_prompt(self, user_query: str, language: str) -> str:
619
- """Director initial planning - Enhanced version"""
620
- augmented_query = self.augment_query(user_query, language)
621
-
622
- # Generate opening sentence
623
- opening_sentence = self.generate_powerful_opening(user_query, language)
624
- self.narrative_tracker.story_bible.opening_sentence = opening_sentence
625
- if self.current_session_id:
626
- NovelDatabase.save_opening_sentence(self.current_session_id, opening_sentence)
627
-
628
- search_results_str = ""
629
- if self.web_search.enabled:
630
- short_query = user_query[:50] if len(user_query) > 50 else user_query
631
- queries = [
632
- f"{short_query} philosophical meaning",
633
- f"human existence meaning {short_query}",
634
- f"{short_query} literary works"
635
- ]
636
- for q in queries[:2]:
637
- try:
638
- results = self.web_search.search(q, count=2, language=language)
639
- if results:
640
- search_results_str += self.web_search.extract_relevant_info(results) + "\n"
641
- except Exception as e:
642
- logger.warning(f"Search failed: {str(e)}")
643
-
644
- lang_prompts = {
645
- "Korean": f"""노벨문학상 수준의 철학적 깊이를 지닌 중편소설(8,000단어)을 기획하세요.
646
-
647
- **주제:** {augmented_query}
648
-
649
- **필수 첫문장:** {opening_sentence}
650
-
651
- **참고 자료:**
652
- {search_results_str if search_results_str else "N/A"}
653
-
654
- **필수 문학적 요소:**
655
-
656
- 1. **철학적 탐구**
657
- - 현대인의 실존적 고뇌 (소외, 정체성, 의미 상실)
658
- - 디지털 시대의 인간 조건
659
- - 자본주의 사회의 모순과 개인의 선택
660
- - 죽음, 사랑, 자유에 대한 새로운 성찰
661
-
662
- 2. **사회적 메시지**
663
- - 계급, 젠더, 세대 간 갈등
664
- - 환경 위기와 인간의 책임
665
- - 기술 발전과 인간성의 충돌
666
- - 현대 민주주의의 위기와 개인의 역할
667
-
668
- 3. **문학적 수사 장치**
669
- - 중심 은유: [구체적 사물/현상] → [추상적 의미]
670
- - 반복되는 모티프: [이미지/행동] (최소 5회 변주)
671
- - 대조법: [A vs B]의 지속적 긴장
672
- - 상징적 공간: [구체적 장소]가 의미하는 것
673
- - 시간의 주관적 흐름 (회상, 예감, 정지)
674
-
675
- 4. **통합된 10파트 구조**
676
- 각 파트별 핵심:
677
- - 파트 1: 첫문장으로 시작, 일상 속 균열 → 철학적 질문 제기
678
- - 파트 2-3: 외부 사건 → 내적 성찰 심화
679
- - 파트 4-5: 사회적 갈등 → 개인적 딜레마
680
- - 파트 6-7: 위기의 정점 → 실존적 선택
681
- - 파트 8-9: 선택의 결과 → 새로운 인식
682
- - 파트 10: 변화된 세계관 → 열린 질문
683
-
684
- 5. **문체 지침**
685
- - 시적 산문체: 일상 언어와 은유의 균형
686
- - 의식의 흐름과 객관적 묘사의 교차
687
- - 짧고 강렬한 문장과 성찰적 긴 문장의 리듬
688
- - 감각적 디테일로 추상적 개념 구현
689
-
690
- 구체적이고 혁신적인 계획을 제시하세요.""",
691
-
692
- "English": f"""Plan a philosophically profound novella (8,000 words) worthy of Nobel Prize.
693
-
694
- **Theme:** {augmented_query}
695
-
696
- **Required Opening:** {opening_sentence}
697
-
698
- **Reference:**
699
- {search_results_str if search_results_str else "N/A"}
700
-
701
- **Essential Literary Elements:**
702
-
703
- 1. **Philosophical Exploration**
704
- - Modern existential anguish (alienation, identity, loss of meaning)
705
- - Human condition in digital age
706
- - Capitalist contradictions and individual choice
707
- - New reflections on death, love, freedom
708
-
709
- 2. **Social Message**
710
- - Class, gender, generational conflicts
711
- - Environmental crisis and human responsibility
712
- - Technology vs humanity collision
713
- - Modern democracy crisis and individual role
714
-
715
- 3. **Literary Devices**
716
- - Central metaphor: [concrete object/phenomenon] → [abstract meaning]
717
- - Recurring motif: [image/action] (minimum 5 variations)
718
- - Contrast: sustained tension of [A vs B]
719
- - Symbolic space: what [specific place] means
720
- - Subjective time flow (flashback, premonition, pause)
721
-
722
- 4. **Integrated 10-Part Structure**
723
- Each part's core:
724
- - Part 1: Start with opening sentence, daily cracks → philosophical questions
725
- - Part 2-3: External events → deepening introspection
726
- - Part 4-5: Social conflict → personal dilemma
727
- - Part 6-7: Crisis peak → existential choice
728
- - Part 8-9: Choice consequences → new recognition
729
- - Part 10: Changed worldview → open questions
730
-
731
- 5. **Style Guidelines**
732
- - Poetic prose: balance of everyday language and metaphor
733
- - Stream of consciousness crossing with objective description
734
- - Rhythm of short intense sentences and reflective long ones
735
- - Abstract concepts through sensory details
736
-
737
- Provide concrete, innovative plan."""
738
- }
739
-
740
- return lang_prompts.get(language, lang_prompts["Korean"])
741
-
742
- def create_critic_director_prompt(self, director_plan: str, user_query: str, language: str) -> str:
743
- """Director plan deep review - Enhanced version"""
744
- lang_prompts = {
745
- "Korean": f"""서사 구조 전문가로서 이 기획을 심층 분석하세요.
746
-
747
- **원 주제:** {user_query}
748
-
749
- **감독자 기획:**
750
- {director_plan}
751
-
752
- **심층 검토 항목:**
753
-
754
- 1. **인과관계 검증**
755
- 각 파트 간 연결을 검토하고 논리적 비약을 찾으세요:
756
- - 파트 1→2: [연결성 평가]
757
- - 파트 2→3: [연결성 평가]
758
- (모든 연결 지점 검토)
759
-
760
- 2. **철학적 깊이 평가**
761
- - 제시된 철학적 주제가 충분히 깊은가?
762
- - 현대적 관련성이 있는가?
763
- - 독창적 통찰이 있는가?
764
-
765
- 3. **문학적 장치의 효과성**
766
- - 은유와 상징이 유기적으로 작동하는가?
767
- - 과도하거나 부족하지 않은가?
768
- - 주제와 긴밀히 연결되는가?
769
-
770
- 4. **캐릭터 아크 실현 가능성**
771
- - 변화가 충분히 점진적인가?
772
- - 각 단계의 동기가 명확한가?
773
- - 심리적 신뢰성이 있는가?
774
-
775
- 5. **8,000단어 실현 가능성**
776
- - 각 파트가 800단어를 유지할 수 있는가?
777
- - 늘어지거나 압축되는 부분은 없는가?
778
-
779
- **필수 개선사항을 구체적으로 제시하세요.**""",
780
-
781
- "English": f"""As narrative structure expert, deeply analyze this plan.
782
-
783
- **Original Theme:** {user_query}
784
-
785
- **Director's Plan:**
786
- {director_plan}
787
-
788
- **Deep Review Items:**
789
-
790
- 1. **Causality Verification**
791
- Review connections between parts, find logical leaps:
792
- - Part 1→2: [Connection assessment]
793
- - Part 2→3: [Connection assessment]
794
- (Review all connection points)
795
-
796
- 2. **Philosophical Depth Assessment**
797
- - Is philosophical theme deep enough?
798
- - Contemporary relevance?
799
- - Original insights?
800
-
801
- 3. **Literary Device Effectiveness**
802
- - Do metaphors and symbols work organically?
803
- - Not excessive or insufficient?
804
- - Tightly connected to theme?
805
-
806
- 4. **Character Arc Feasibility**
807
- - Is change sufficiently gradual?
808
- - Are motivations clear at each stage?
809
- - Psychological credibility?
810
-
811
- 5. **8,000-word Feasibility**
812
- - Can each part sustain 800 words?
813
- - Any dragging or compressed sections?
814
-
815
- **Provide specific required improvements.**"""
816
- }
817
-
818
- return lang_prompts.get(language, lang_prompts["Korean"])
819
-
820
- def create_writer_prompt(self, part_number: int, master_plan: str,
821
- accumulated_content: str, story_bible: StoryBible,
822
- language: str) -> str:
823
- """Single writer prompt - Enhanced version"""
824
-
825
- phase_name = NARRATIVE_PHASES[part_number-1]
826
- target_words = MIN_WORDS_PER_PART
827
-
828
- # Part-specific instructions
829
- philosophical_focus = {
830
- 1: "Introduce existential anxiety through daily cracks",
831
- 2: "First collision between individual and society",
832
- 3: "Self-recognition through encounter with others",
833
- 4: "Shaking beliefs and clashing values",
834
- 5: "Weight of choice and paradox of freedom",
835
- 6: "Test of humanity in extreme situations",
836
- 7: "Weight of consequences and responsibility",
837
- 8: "Self-rediscovery through others' gaze",
838
- 9: "Reconciliation with the irreconcilable",
839
- 10: "New life possibilities and unresolved questions"
840
- }
841
-
842
- literary_techniques = {
843
- 1: "Introducing objective correlative",
844
- 2: "Contrapuntal narration",
845
- 3: "Stream of consciousness",
846
- 4: "Subtle shifts in perspective",
847
- 5: "Aesthetics of silence and omission",
848
- 6: "Subjective transformation of time",
849
- 7: "Intersection of multiple viewpoints",
850
- 8: "Subversion of metaphor",
851
- 9: "Reinterpretation of archetypal images",
852
- 10: "Multi-layered open ending"
853
- }
854
-
855
- # Story bible summary
856
- bible_summary = f"""
857
- **Characters:** {', '.join(story_bible.characters.keys()) if story_bible.characters else 'TBD'}
858
- **Key Symbols:** {', '.join(story_bible.symbols.keys()) if story_bible.symbols else 'TBD'}
859
- **Themes:** {', '.join(story_bible.themes[:3]) if story_bible.themes else 'TBD'}
860
- **Style:** {story_bible.style_guide.get('voice', 'N/A')}
861
- """
862
-
863
- # Previous content summary
864
- prev_content = ""
865
- if accumulated_content:
866
- prev_parts = accumulated_content.split('\n\n')
867
- if len(prev_parts) >= 1:
868
- prev_content = prev_parts[-1][-2000:] # Last 2000 chars of previous part
869
-
870
- lang_prompts = {
871
- "Korean": f"""당신은 현대 문학의 최전선에 선 작가입니다.
872
- **현재: 파트 {part_number} - {phase_name}**
873
-
874
- {"**필수 첫문장:** " + story_bible.opening_sentence if part_number == 1 and story_bible.opening_sentence else ""}
875
-
876
- **이번 파트의 철학적 초점:** {philosophical_focus[part_number]}
877
- **핵심 문학 기법:** {literary_techniques[part_number]}
878
-
879
- **전체 계획:**
880
- {master_plan}
881
-
882
- **스토리 바이블:**
883
- {bible_summary}
884
-
885
- **직전 내용:**
886
- {prev_content if prev_content else "첫 파트입니다"}
887
-
888
- **파트 {part_number} 작성 지침:**
889
-
890
- 1. **분량:** {target_words}-900 단어 (필수)
891
-
892
- 2. **문학적 수사 요구사항:**
893
- - 최소 3개의 독창적 은유/직유
894
- - 1개 이상의 상징적 이미지 심화
895
- - 감각적 묘사와 추상적 사유의 융합
896
- - 리듬감 있는 문장 구성 (장단의 변주)
897
-
898
- 3. **현대적 고뇌 표현:**
899
- - 디지털 시대의 소외감
900
- - 자본주의적 삶의 부조리
901
- - 관계의 표면성과 진정성 갈망
902
- - 의미 추구와 무의미의 직면
903
-
904
- 4. **사회적 메시지 내재화:**
905
- - 직접적 주장이 아닌 상황과 인물을 통한 암시
906
- - 개인의 고통과 사회 구조의 연결
907
- - 미시적 일상과 거시적 문제의 교차
908
-
909
- 5. **서사적 추진력:**
910
- - 이전 파트의 필연적 결과로 시작
911
- - 새로운 갈등 층위 추가
912
- - 다음 파트를 향한 긴장감 조성
913
-
914
- **문학적 금기:**
915
- - 진부한 표현이나 상투적 은유
916
- - 감정의 직접적 설명
917
- - 도덕적 판단이나 교훈
918
- - 인위적인 해결이나 위안
919
-
920
- 파트 {part_number}를 깊이 있는 문학적 성취로 만드세요.""",
921
-
922
- "English": f"""You are a writer at the forefront of contemporary literature.
923
- **Current: Part {part_number} - {phase_name}**
924
-
925
- {"**Required Opening:** " + story_bible.opening_sentence if part_number == 1 and story_bible.opening_sentence else ""}
926
-
927
- **Philosophical Focus:** {philosophical_focus[part_number]}
928
- **Core Literary Technique:** {literary_techniques[part_number]}
929
-
930
- **Master Plan:**
931
- {master_plan}
932
-
933
- **Story Bible:**
934
- {bible_summary}
935
-
936
- **Previous Content:**
937
- {prev_content if prev_content else "This is the first part"}
938
-
939
- **Part {part_number} Guidelines:**
940
-
941
- 1. **Length:** {target_words}-900 words (mandatory)
942
-
943
- 2. **Literary Device Requirements:**
944
- - Minimum 3 original metaphors/similes
945
- - Deepen at least 1 symbolic image
946
- - Fusion of sensory description and abstract thought
947
- - Rhythmic sentence composition (variation of long/short)
948
-
949
- 3. **Modern Anguish Expression:**
950
- - Digital age alienation
951
- - Absurdity of capitalist life
952
- - Surface relationships vs authenticity yearning
953
- - Meaning pursuit vs confronting meaninglessness
954
-
955
- 4. **Social Message Internalization:**
956
- - Implication through situation and character, not direct claim
957
- - Connection between individual pain and social structure
958
- - Intersection of micro daily life and macro problems
959
-
960
- 5. **Narrative Momentum:**
961
- - Start as inevitable result of previous part
962
- - Add new conflict layers
963
- - Create tension toward next part
964
-
965
- **Literary Taboos:**
966
- - Clichéd expressions or trite metaphors
967
- - Direct emotion explanation
968
- - Moral judgment or preaching
969
- - Artificial resolution or comfort
970
-
971
- Make Part {part_number} a profound literary achievement."""
972
- }
973
-
974
- return lang_prompts.get(language, lang_prompts["Korean"])
975
-
976
- def create_part_critic_prompt(self, part_number: int, part_content: str,
977
- master_plan: str, accumulated_content: str,
978
- story_bible: StoryBible, language: str) -> str:
979
- """Part-by-part immediate critique - Enhanced version"""
980
-
981
- lang_prompts = {
982
- "Korean": f"""파트 {part_number}의 문학적 성취도를 엄격히 평가하세요.
983
-
984
- **마스터플랜 파트 {part_number} 요구사항:**
985
- {self._extract_part_plan(master_plan, part_number)}
986
-
987
- **작성된 내용:**
988
- {part_content}
989
-
990
- **스토리 바이블 체크:**
991
- - 캐릭터: {', '.join(story_bible.characters.keys())}
992
- - 설정: {', '.join(story_bible.settings.keys())}
993
-
994
- **평가 기준:**
995
-
996
- 1. **문학적 수사 (30%)**
997
- - 은유와 상징의 독창성
998
- - 언어의 시적 밀도
999
- - 이미지의 선명도와 깊이
1000
- - 문장의 리듬과 음악성
1001
-
1002
- 2. **철학적 깊이 (25%)**
1003
- - 실존적 질문의 제기
1004
- - 현대인의 조건 탐구
1005
- - 보편성과 특수성의 균형
1006
- - 사유의 독창성
1007
-
1008
- 3. **사회적 통찰 (20%)**
1009
- - 시대정신의 포착
1010
- - 구조와 개인의 관계
1011
- - 비판적 시각의 예리함
1012
- - 대안적 상상력
1013
-
1014
- 4. **서사적 완성도 (25%)**
1015
- - 인과관계의 필연성
1016
- - 긴장감의 유지
1017
- - 인물의 입체성
1018
- - 구조적 통일성
1019
-
1020
- **구체적 지적사항:**
1021
- - 진부한 표현: [예시와 대안]
1022
- - 철학적 천착 부족: [보완 방향]
1023
- - 사회적 메시지 불명확: [강화 방안]
1024
- - 서사적 허점: [수정 필요]
1025
-
1026
- **필수 개선 요구:**
1027
- 문학적 수준을 노벨상 급으로 끌어올리기 위한 구체적 수정안을 제시하세요.""",
1028
-
1029
- "English": f"""Strictly evaluate literary achievement of Part {part_number}.
1030
-
1031
- **Master Plan Part {part_number} Requirements:**
1032
- {self._extract_part_plan(master_plan, part_number)}
1033
-
1034
- **Written Content:**
1035
- {part_content}
1036
-
1037
- **Story Bible Check:**
1038
- - Characters: {', '.join(story_bible.characters.keys()) if story_bible.characters else 'None yet'}
1039
- - Settings: {', '.join(story_bible.settings.keys()) if story_bible.settings else 'None yet'}
1040
-
1041
- **Evaluation Criteria:**
1042
-
1043
- 1. **Literary Rhetoric (30%)**
1044
- - Originality of metaphor and symbol
1045
- - Poetic density of language
1046
- - Clarity and depth of imagery
1047
- - Rhythm and musicality of sentences
1048
-
1049
- 2. **Philosophical Depth (25%)**
1050
- - Raising existential questions
1051
- - Exploring modern human condition
1052
- - Balance of universality and specificity
1053
- - Originality of thought
1054
-
1055
- 3. **Social Insight (20%)**
1056
- - Capturing zeitgeist
1057
- - Relationship between structure and individual
1058
- - Sharpness of critical perspective
1059
- - Alternative imagination
1060
-
1061
- 4. **Narrative Completion (25%)**
1062
- - Inevitability of causality
1063
- - Maintaining tension
1064
- - Character dimensionality
1065
- - Structural unity
1066
-
1067
- **Specific Points:**
1068
- - Clichéd expressions: [examples and alternatives]
1069
- - Insufficient philosophical exploration: [enhancement direction]
1070
- - Unclear social message: [strengthening methods]
1071
- - Narrative gaps: [needed revisions]
1072
-
1073
- **Required Improvements:**
1074
- Provide specific revisions to elevate literary level to Nobel Prize standard."""
1075
- }
1076
-
1077
- return lang_prompts.get(language, lang_prompts["Korean"])
1078
-
1079
- def create_writer_revision_prompt(self, part_number: int, original_content: str,
1080
- critic_feedback: str, language: str) -> str:
1081
- """Writer revision prompt"""
1082
-
1083
- lang_prompts = {
1084
- "Korean": f"""파트 {part_number}를 비평에 따라 수정하세요.
1085
-
1086
- **원본:**
1087
- {original_content}
1088
-
1089
- **비평 피드백:**
1090
- {critic_feedback}
1091
-
1092
- **수정 지침:**
1093
- 1. 모든 '필수 수정' 사항을 반영
1094
- 2. 가능한 '권장 개선' 사항도 포함
1095
- 3. 원본의 강점은 유지
1096
- 4. 분량 {MIN_WORDS_PER_PART}단어 이상 유지
1097
- 5. 작가로서의 일관된 목소리 유지
1098
- 6. 문학적 수준을 한 단계 높이기
1099
-
1100
- 수정본만 제시하세요. 설명은 불필요합니다.""",
1101
-
1102
- "English": f"""Revise Part {part_number} according to critique.
1103
-
1104
- **Original:**
1105
- {original_content}
1106
-
1107
- **Critique Feedback:**
1108
- {critic_feedback}
1109
-
1110
- **Revision Guidelines:**
1111
- 1. Reflect all 'Required fixes'
1112
- 2. Include 'Recommended improvements' where possible
1113
- 3. Maintain original strengths
1114
- 4. Keep length {MIN_WORDS_PER_PART}+ words
1115
- 5. Maintain consistent authorial voice
1116
- 6. Elevate literary level
1117
-
1118
- Present only the revision. No explanation needed."""
1119
- }
1120
-
1121
- return lang_prompts.get(language, lang_prompts["Korean"])
1122
-
1123
- def create_final_critic_prompt(self, complete_novel: str, word_count: int,
1124
- story_bible: StoryBible, language: str) -> str:
1125
- """Final comprehensive evaluation"""
1126
-
1127
- lang_prompts = {
1128
- "Korean": f"""완성된 소설을 종합 평가하세요.
1129
-
1130
- **작품 정보:**
1131
- - 총 분량: {word_count}단어
1132
- - 목표: 8,000단어
1133
-
1134
- **평가 기준:**
1135
-
1136
- 1. **서사적 통합성 (30점)**
1137
- - 10개 파트가 하나의 이야기로 통합되었는가?
1138
- - 인과관계가 명확하고 필연적인가?
1139
- - 반복이나 순환 없이 진행되는가?
1140
-
1141
- 2. **캐릭터 아크 (25점)**
1142
- - 주인공의 변화가 설득력 있는가?
1143
- - 변화가 점진적이고 자연스러운가?
1144
- - 최종 상태가 초기와 명확히 다른가?
1145
-
1146
- 3. **문학적 성취 (25점)**
1147
- - 주제가 깊이 있게 탐구되었는가?
1148
- - 상징이 효과적으로 활용되었는가?
1149
- - 문체가 일관되고 아름다운가?
1150
- - 현대적 철학과 사회적 메시지가 녹아있는가?
1151
-
1152
- 4. **기술적 완성도 (20점)**
1153
- - 목표 분량을 달성했는가?
1154
- - 각 파트가 균형 있게 전개되었는가?
1155
- - 문법과 표현이 정확한가?
1156
-
1157
- **총점: /100점**
1158
-
1159
- 구체적인 강점과 약점을 제시하세요.""",
1160
-
1161
- "English": f"""Comprehensively evaluate the completed novel.
1162
-
1163
- **Work Info:**
1164
- - Total length: {word_count} words
1165
- - Target: 8,000 words
1166
-
1167
- **Evaluation Criteria:**
1168
-
1169
- 1. **Narrative Integration (30 points)**
1170
- - Are 10 parts integrated into one story?
1171
- - Clear and inevitable causality?
1172
- - Progress without repetition or cycles?
1173
-
1174
- 2. **Character Arc (25 points)**
1175
- - Convincing protagonist transformation?
1176
- - Gradual and natural changes?
1177
- - Final state clearly different from initial?
1178
-
1179
- 3. **Literary Achievement (25 points)**
1180
- - Theme explored with depth?
1181
- - Symbols used effectively?
1182
- - Consistent and beautiful style?
1183
- - Contemporary philosophy and social message integrated?
1184
-
1185
- 4. **Technical Completion (20 points)**
1186
- - Target length achieved?
1187
- - Each part balanced in development?
1188
- - Grammar and expression accurate?
1189
-
1190
- **Total Score: /100 points**
1191
-
1192
- Present specific strengths and weaknesses."""
1193
- }
1194
-
1195
- return lang_prompts.get(language, lang_prompts["Korean"])
1196
-
1197
- def _extract_part_plan(self, master_plan: str, part_number: int) -> str:
1198
- """Extract specific part plan from master plan"""
1199
- lines = master_plan.split('\n')
1200
- part_section = []
1201
- capturing = False
1202
-
1203
- for line in lines:
1204
- if f"Part {part_number}:" in line or f"파트 {part_number}:" in line:
1205
- capturing = True
1206
- elif capturing and (f"Part {part_number+1}:" in line or f"파트 {part_number+1}:" in line):
1207
- break
1208
- elif capturing:
1209
- part_section.append(line)
1210
-
1211
- return '\n'.join(part_section) if part_section else "Cannot find the part plan."
1212
-
1213
- # --- LLM call functions ---
1214
- def call_llm_sync(self, messages: List[Dict[str, str]], role: str, language: str) -> str:
1215
- full_content = ""
1216
- for chunk in self.call_llm_streaming(messages, role, language):
1217
- full_content += chunk
1218
- if full_content.startswith("❌"):
1219
- raise Exception(f"LLM Call Failed: {full_content}")
1220
- return full_content
1221
-
1222
- def call_llm_streaming(self, messages: List[Dict[str, str]], role: str,
1223
- language: str) -> Generator[str, None, None]:
1224
- try:
1225
- system_prompts = self.get_system_prompts(language)
1226
- full_messages = [{"role": "system", "content": system_prompts.get(role, "")}, *messages]
1227
-
1228
- max_tokens = 15000 if role == "writer" else 10000
1229
-
1230
- payload = {
1231
- "model": self.model_id,
1232
- "messages": full_messages,
1233
- "max_tokens": max_tokens,
1234
- "temperature": 0.8,
1235
- "top_p": 0.95,
1236
- "presence_penalty": 0.5,
1237
- "frequency_penalty": 0.3,
1238
- "stream": True
1239
- }
1240
-
1241
- response = requests.post(
1242
- self.api_url,
1243
- headers=self.create_headers(),
1244
- json=payload,
1245
- stream=True,
1246
- timeout=180
1247
- )
1248
-
1249
- if response.status_code != 200:
1250
- yield f"❌ API Error (Status Code: {response.status_code})"
1251
- return
1252
-
1253
- buffer = ""
1254
- # 스트리밍 개선: iter_content 사용
1255
- for chunk in response.iter_content(chunk_size=None, decode_unicode=True):
1256
- if not chunk:
1257
- continue
1258
-
1259
- buffer += chunk
1260
- lines = buffer.split('\n')
1261
- buffer = lines[-1] # 마지막 불완전한 라인 보관
1262
-
1263
- for line in lines[:-1]:
1264
- if not line.strip():
1265
- continue
1266
-
1267
- if not line.startswith("data: "):
1268
- continue
1269
-
1270
- data_str = line[6:]
1271
- if data_str == "[DONE]":
1272
- break
1273
-
1274
- try:
1275
- data = json.loads(data_str)
1276
- choices = data.get("choices", [])
1277
- if choices and choices[0].get("delta", {}).get("content"):
1278
- content = choices[0]["delta"]["content"]
1279
- yield content
1280
- time.sleep(0.005) # 더 부드러운 스트리밍
1281
- except Exception as e:
1282
- logger.error(f"Chunk processing error: {str(e)}")
1283
- continue
1284
-
1285
- except Exception as e:
1286
- logger.error(f"Streaming error: {type(e).__name__}: {str(e)}")
1287
- yield f"❌ Error occurred: {str(e)}"
1288
-
1289
- def get_system_prompts(self, language: str) -> Dict[str, str]:
1290
- """Role-specific system prompts - Enhanced version"""
1291
-
1292
- base_prompts = {
1293
- "Korean": {
1294
- "director": """당신은 현대 세계문학의 정점을 지향하는 작품을 설계합니다.
1295
- 깊은 철학적 통찰과 날카로운 사회 비판을 결합하세요.
1296
- 인간 조건의 복잡성을 10개의 유기적 파트로 구현하세요.
1297
- 독자의 영혼을 뒤흔들 강렬한 첫문장부터 시작하세요.""",
1298
-
1299
- "critic_director": """서사 구조의 논리성과 실현 가능성을 검증하는 전문가입니다.
1300
- 인과관계의 허점을 찾아내세요.
1301
- 캐릭터 발전의 신빙성을 평가하세요.
1302
- 철학적 깊이와 문학적 가치를 판단하세요.
1303
- 8,000단어 분량의 적절성을 판단하세요.""",
1304
-
1305
- "writer": """당신은 언어의 연금술사입니다.
1306
- 일상어를 시로, 구체를 추상으로, 개인을 보편으로 변환하세요.
1307
- 현대인의 영혼의 어둠과 빛을 동시에 포착하세요.
1308
- 독자가 자신을 재발견하게 만드는 거울이 되세요.""",
1309
-
1310
- "critic_final": """당신은 작품의 문학적 잠재력을 극대화하는 조력자입니다.
1311
- 평범함을 비범함으로 이끄는 날카로운 통찰을 제공하세요.
1312
- 작가의 무의식에 잠든 보석을 발굴하세요.
1313
- 타협 없는 기준으로 최고를 요구하세요."""
1314
- },
1315
- "English": {
1316
- "director": """You design works aiming for the pinnacle of contemporary world literature.
1317
- Combine deep philosophical insights with sharp social criticism.
1318
- Implement the complexity of the human condition in 10 organic parts.
1319
- Start with an intense opening sentence that shakes the reader's soul.""",
1320
-
1321
- "critic_director": """You are an expert verifying narrative logic and feasibility.
1322
- Find gaps in causality.
1323
- Evaluate credibility of character development.
1324
- Judge philosophical depth and literary value.
1325
- Judge appropriateness of 8,000-word length.""",
1326
-
1327
- "writer": """You are an alchemist of language.
1328
- Transform everyday language into poetry, concrete into abstract, individual into universal.
1329
- Capture both darkness and light of the modern soul.
1330
- Become a mirror where readers rediscover themselves.""",
1331
-
1332
- "critic_final": """You are a collaborator maximizing the work's literary potential.
1333
- Provide sharp insights leading ordinariness to extraordinariness.
1334
- Excavate gems sleeping in the writer's unconscious.
1335
- Demand the best with uncompromising standards."""
1336
- }
1337
- }
1338
-
1339
- prompts = base_prompts.get(language, base_prompts["Korean"]).copy()
1340
-
1341
- # Add part-specific critic prompts
1342
- for i in range(1, 11):
1343
- prompts[f"critic_part{i}"] = f"""You are Part {i} dedicated critic.
1344
- Review causality with previous parts as top priority.
1345
- Verify character consistency and development.
1346
- Evaluate alignment with master plan.
1347
- Assess literary level and philosophical depth.
1348
- Provide specific and actionable revision instructions."""
1349
-
1350
- return prompts
1351
-
1352
- # --- Main process ---
1353
- def process_novel_stream(self, query: str, language: str,
1354
- session_id: Optional[str] = None) -> Generator[Tuple[str, List[Dict[str, Any]], str], None, None]:
1355
- """Single writer novel generation process - 세션 복원 개선"""
1356
- try:
1357
- resume_from_stage = 0
1358
- if session_id:
1359
- self.current_session_id = session_id
1360
- session = NovelDatabase.get_session(session_id)
1361
- if session:
1362
- query = session['user_query']
1363
- language = session['language']
1364
- resume_from_stage = session['current_stage'] + 1
1365
-
1366
- # 세션 복원 시 tracker 로드
1367
- saved_tracker = NovelDatabase.load_narrative_tracker(session_id)
1368
- if saved_tracker:
1369
- self.narrative_tracker = saved_tracker
1370
- logger.info(f"Restored narrative tracker for session {session_id[:8]}...")
1371
-
1372
- # 개방된 문장 복원
1373
- if session.get('opening_sentence'):
1374
- self.narrative_tracker.story_bible.opening_sentence = session['opening_sentence']
1375
- else:
1376
- self.current_session_id = NovelDatabase.create_session(query, language)
1377
- logger.info(f"Created new session: {self.current_session_id[:8]}...")
1378
-
1379
- stages = []
1380
- if resume_from_stage > 0:
1381
- stages = [{
1382
- "name": s['stage_name'],
1383
- "status": s['status'],
1384
- "content": s.get('content', ''),
1385
- "word_count": s.get('word_count', 0),
1386
- "momentum": s.get('narrative_momentum', 0.0)
1387
- } for s in NovelDatabase.get_stages(self.current_session_id)]
1388
-
1389
- total_words = NovelDatabase.get_total_words(self.current_session_id)
1390
-
1391
- for stage_idx in range(resume_from_stage, len(UNIFIED_STAGES)):
1392
- role, stage_name = UNIFIED_STAGES[stage_idx]
1393
- if stage_idx >= len(stages):
1394
- stages.append({
1395
- "name": stage_name,
1396
- "status": "active",
1397
- "content": "",
1398
- "word_count": 0,
1399
- "momentum": 0.0
1400
- })
1401
- else:
1402
- stages[stage_idx]["status"] = "active"
1403
-
1404
- yield f"🔄 Processing... (Current {total_words:,} words)", stages, self.current_session_id
1405
-
1406
- prompt = self.get_stage_prompt(stage_idx, role, query, language, stages)
1407
- stage_content = ""
1408
-
1409
- for chunk in self.call_llm_streaming([{"role": "user", "content": prompt}], role, language):
1410
- stage_content += chunk
1411
- stages[stage_idx]["content"] = stage_content
1412
- stages[stage_idx]["word_count"] = len(stage_content.split())
1413
- yield f"🔄 {stage_name} writing... ({total_words + stages[stage_idx]['word_count']:,} words)", stages, self.current_session_id
1414
-
1415
- # Content processing and tracking
1416
- if role == "writer":
1417
- # Calculate part number
1418
- part_num = self._get_part_number(stage_idx)
1419
- if part_num:
1420
- # Revision인 경우 accumulated_content 업데이트
1421
- if "Revision" in stage_name:
1422
- # 기존 내용 교체
1423
- if len(self.narrative_tracker.accumulated_content) >= part_num:
1424
- self.narrative_tracker.accumulated_content[part_num-1] = stage_content
1425
- else:
1426
- self.narrative_tracker.accumulated_content.append(stage_content)
1427
- else:
1428
- # 초안인 경우에만 추가
1429
- if "Revision" not in stage_name:
1430
- self.narrative_tracker.accumulated_content.append(stage_content)
1431
-
1432
- self.narrative_tracker.word_count_by_part[part_num] = len(stage_content.split())
1433
-
1434
- # Calculate narrative momentum
1435
- momentum = self.narrative_tracker.calculate_narrative_momentum(part_num, stage_content)
1436
- stages[stage_idx]["momentum"] = momentum
1437
-
1438
- # Update story bible
1439
- self._update_story_bible_from_content(stage_content, part_num)
1440
-
1441
- stages[stage_idx]["status"] = "complete"
1442
- NovelDatabase.save_stage(
1443
- self.current_session_id, stage_idx, stage_name, role,
1444
- stage_content, "complete", stages[stage_idx].get("momentum", 0.0)
1445
- )
1446
-
1447
- # 매 스테이지마다 tracker 저장 (세션 복원 개선)
1448
- NovelDatabase.save_narrative_tracker(self.current_session_id, self.narrative_tracker)
1449
- total_words = NovelDatabase.get_total_words(self.current_session_id)
1450
- yield f"✅ {stage_name} completed (Total {total_words:,} words)", stages, self.current_session_id
1451
-
1452
- # Final processing
1453
- final_novel = NovelDatabase.get_writer_content(self.current_session_id)
1454
- final_word_count = len(final_novel.split())
1455
- final_report = self.generate_literary_report(final_novel, final_word_count, language)
1456
-
1457
- NovelDatabase.update_final_novel(self.current_session_id, final_novel, final_report)
1458
- yield f"✅ Novel completed! Total {final_word_count:,} words", stages, self.current_session_id
1459
-
1460
- except Exception as e:
1461
- logger.error(f"Novel generation process error: {e}", exc_info=True)
1462
- yield f"❌ Error occurred: {e}", stages if 'stages' in locals() else [], self.current_session_id
1463
-
1464
- def get_stage_prompt(self, stage_idx: int, role: str, query: str,
1465
- language: str, stages: List[Dict]) -> str:
1466
- """Generate stage-specific prompt"""
1467
- if stage_idx == 0: # Director initial planning
1468
- return self.create_director_initial_prompt(query, language)
1469
-
1470
- if stage_idx == 1: # Director plan review
1471
- return self.create_critic_director_prompt(stages[0]["content"], query, language)
1472
-
1473
- if stage_idx == 2: # Director final master plan
1474
- return self.create_director_final_prompt(stages[0]["content"], stages[1]["content"], query, language)
1475
-
1476
- master_plan = stages[2]["content"]
1477
-
1478
- # Writer part writing
1479
- if role == "writer" and "Revision" not in stages[stage_idx]["name"]:
1480
- part_num = self._get_part_number(stage_idx)
1481
- accumulated = '\n\n'.join(self.narrative_tracker.accumulated_content)
1482
- return self.create_writer_prompt(part_num, master_plan, accumulated,
1483
- self.narrative_tracker.story_bible, language)
1484
-
1485
- # Part-specific critique
1486
- if role.startswith("critic_part"):
1487
- part_num = int(role.replace("critic_part", ""))
1488
- # Find writer content for this part
1489
- writer_content = stages[stage_idx-1]["content"]
1490
- accumulated = '\n\n'.join(self.narrative_tracker.accumulated_content[:-1])
1491
- return self.create_part_critic_prompt(part_num, writer_content, master_plan,
1492
- accumulated, self.narrative_tracker.story_bible, language)
1493
-
1494
- # Writer revision
1495
- if role == "writer" and "Revision" in stages[stage_idx]["name"]:
1496
- part_num = self._get_part_number(stage_idx)
1497
- original_content = stages[stage_idx-2]["content"] # Original
1498
- critic_feedback = stages[stage_idx-1]["content"] # Critique
1499
- return self.create_writer_revision_prompt(part_num, original_content,
1500
- critic_feedback, language)
1501
-
1502
- # Final critique
1503
- if role == "critic_final":
1504
- complete_novel = NovelDatabase.get_writer_content(self.current_session_id)
1505
- word_count = len(complete_novel.split())
1506
- return self.create_final_critic_prompt(complete_novel, word_count,
1507
- self.narrative_tracker.story_bible, language)
1508
-
1509
- return ""
1510
-
1511
- def create_director_final_prompt(self, initial_plan: str, critic_feedback: str,
1512
- user_query: str, language: str) -> str:
1513
- """Director final master plan"""
1514
- return f"""Reflect the critique and complete the final master plan.
1515
-
1516
- **Original Theme:** {user_query}
1517
-
1518
- **Initial Plan:**
1519
- {initial_plan}
1520
-
1521
- **Critique Feedback:**
1522
- {critic_feedback}
1523
-
1524
- **Final Master Plan Requirements:**
1525
- 1. Reflect all critique points
1526
- 2. Specific content and causality for 10 parts
1527
- 3. Clear transformation stages of protagonist
1528
- 4. Meaning evolution process of central symbol
1529
- 5. Feasibility of 800 words per part
1530
- 6. Implementation of philosophical depth and social message
1531
-
1532
- Present concrete and executable final plan."""
1533
-
1534
- def _get_part_number(self, stage_idx: int) -> Optional[int]:
1535
- """Extract part number from stage index"""
1536
- stage_name = UNIFIED_STAGES[stage_idx][1]
1537
- match = re.search(r'Part (\d+)', stage_name)
1538
- if match:
1539
- return int(match.group(1))
1540
- return None
1541
-
1542
- def _update_story_bible_from_content(self, content: str, part_num: int):
1543
- """Auto-update story bible from content"""
1544
- # Simple keyword-based extraction (more sophisticated NLP needed in reality)
1545
- lines = content.split('\n')
1546
-
1547
- # Extract character names (words starting with capital letters)
1548
- for line in lines:
1549
- words = line.split()
1550
- for word in words:
1551
- if word and word[0].isupper() and len(word) > 1:
1552
- if word not in self.narrative_tracker.story_bible.characters:
1553
- self.narrative_tracker.story_bible.characters[word] = {
1554
- "first_appearance": part_num,
1555
- "traits": []
1556
- }
1557
-
1558
- def generate_literary_report(self, complete_novel: str, word_count: int, language: str) -> str:
1559
- """Generate final literary evaluation report"""
1560
- prompt = self.create_final_critic_prompt(complete_novel, word_count,
1561
- self.narrative_tracker.story_bible, language)
1562
- try:
1563
- report = self.call_llm_sync([{"role": "user", "content": prompt}],
1564
- "critic_final", language)
1565
- return report
1566
- except Exception as e:
1567
- logger.error(f"Final report generation failed: {e}")
1568
- return "Error occurred during report generation"
1569
-
1570
-
1571
-
1572
- # --- Utility functions ---
1573
- def process_query(query: str, language: str, session_id: Optional[str] = None) -> Generator[Tuple[str, str, str, str], None, None]:
1574
- """Main query processing function"""
1575
- if not query.strip():
1576
- yield "", "", "❌ Please enter a theme.", session_id
1577
- return
1578
-
1579
- system = UnifiedLiterarySystem()
1580
- stages_markdown = ""
1581
- novel_content = ""
1582
-
1583
- for status, stages, current_session_id in system.process_novel_stream(query, language, session_id):
1584
- stages_markdown = format_stages_display(stages)
1585
-
1586
- # Get final novel content
1587
- if stages and all(s.get("status") == "complete" for s in stages[-10:]):
1588
- novel_content = NovelDatabase.get_writer_content(current_session_id)
1589
- novel_content = format_novel_display(novel_content)
1590
-
1591
- yield stages_markdown, novel_content, status or "🔄 Processing...", current_session_id
1592
-
1593
- def get_active_sessions(language: str) -> List[str]:
1594
- """Get active session list"""
1595
- sessions = NovelDatabase.get_active_sessions()
1596
- return [f"{s['session_id'][:8]}... - {s['user_query'][:50]}... ({s['created_at']}) [{s['total_words']:,} words]"
1597
- for s in sessions]
1598
-
1599
- def auto_recover_session(language: str) -> Tuple[Optional[str], str]:
1600
- """Auto-recover recent session"""
1601
- sessions = NovelDatabase.get_active_sessions()
1602
- if sessions:
1603
- latest_session = sessions[0]
1604
- return latest_session['session_id'], f"Session {latest_session['session_id'][:8]}... recovered"
1605
- return None, "No session to recover."
1606
-
1607
- def resume_session(session_id: str, language: str) -> Generator[Tuple[str, str, str, str], None, None]:
1608
- """Resume session"""
1609
- if not session_id:
1610
- yield "", "", "❌ No session ID.", session_id
1611
- return
1612
-
1613
- # 전체 ID가 아닌 경우 처리
1614
- if "..." in session_id:
1615
- session_id = session_id.split("...")[0]
1616
- # 전체 세션 찾기
1617
- sessions = NovelDatabase.get_active_sessions()
1618
- for s in sessions:
1619
- if s['session_id'].startswith(session_id):
1620
- session_id = s['session_id']
1621
- break
1622
-
1623
- session = NovelDatabase.get_session(session_id)
1624
- if not session:
1625
- yield "", "", "❌ Session not found.", None
1626
- return
1627
-
1628
- yield from process_query(session['user_query'], session['language'], session_id)
1629
-
1630
- def download_novel(novel_text: str, format_type: str, language: str, session_id: str) -> Optional[str]:
1631
- """Generate novel download file"""
1632
- if not novel_text or not session_id:
1633
- return None
1634
-
1635
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
1636
- filename = f"novel_{session_id[:8]}_{timestamp}"
1637
-
1638
- try:
1639
- if format_type == "DOCX" and DOCX_AVAILABLE:
1640
- return export_to_docx(novel_text, filename, language, session_id)
1641
- else:
1642
- return export_to_txt(novel_text, filename)
1643
- except Exception as e:
1644
- logger.error(f"File generation failed: {e}")
1645
- return None
1646
-
1647
- def format_stages_display(stages: List[Dict]) -> str:
1648
- """Stage progress display - 개선된 버전"""
1649
- markdown = "## 🎬 Progress Status\n\n"
1650
-
1651
- # Calculate total word count (DB에서 직접 가져오기)
1652
- if stages and len(stages) > 0:
1653
- # 현재 세션 ID 찾기 (역방향 검색)
1654
- session_id = None
1655
- for stage in reversed(stages):
1656
- if stage.get('content'):
1657
- # DB에서 세션 ID 가져오기는 복잡하므로, 실제 writer content만 계산
1658
- break
1659
-
1660
- # 실제 작성된 단어 수 계산 (리비전 우선)
1661
- total_words = 0
1662
- for s in stages:
1663
- if s.get('name', '').startswith('✍️ Writer:') and 'Revision' in s.get('name', ''):
1664
- total_words += s.get('word_count', 0)
1665
-
1666
- # 리비전이 없는 경우 초안 계산
1667
- if total_words == 0:
1668
- for s in stages:
1669
- if s.get('name', '').startswith('✍️ Writer:') and 'Revision' not in s.get('name', ''):
1670
- total_words += s.get('word_count', 0)
1671
-
1672
- markdown += f"**Total Word Count: {total_words:,} / {TARGET_WORDS:,}**\n\n"
1673
-
1674
- # Progress summary
1675
- completed_parts = sum(1 for s in stages
1676
- if 'Revision' in s.get('name', '') and s.get('status') == 'complete')
1677
- markdown += f"**Completed Parts: {completed_parts} / 10**\n\n"
1678
-
1679
- # Average narrative momentum
1680
- momentum_scores = [s.get('momentum', 0) for s in stages if s.get('momentum', 0) > 0]
1681
- if momentum_scores:
1682
- avg_momentum = sum(momentum_scores) / len(momentum_scores)
1683
- markdown += f"**Average Narrative Momentum: {avg_momentum:.1f} / 10**\n\n"
1684
-
1685
- markdown += "---\n\n"
1686
-
1687
- # Display each stage
1688
- current_part = 0
1689
- for i, stage in enumerate(stages):
1690
- status_icon = "✅" if stage['status'] == 'complete' else "🔄" if stage['status'] == 'active' else "⏳"
1691
-
1692
- # Add part divider
1693
- if 'Part' in stage.get('name', '') and 'Critic' not in stage.get('name', ''):
1694
- part_match = re.search(r'Part (\d+)', stage['name'])
1695
- if part_match:
1696
- new_part = int(part_match.group(1))
1697
- if new_part != current_part:
1698
- current_part = new_part
1699
- markdown += f"\n### 📚 Part {current_part}\n\n"
1700
-
1701
- markdown += f"{status_icon} **{stage['name']}**"
1702
-
1703
- if stage.get('word_count', 0) > 0:
1704
- markdown += f" ({stage['word_count']:,} words)"
1705
-
1706
- if stage.get('momentum', 0) > 0:
1707
- markdown += f" [Momentum: {stage['momentum']:.1f}/10]"
1708
-
1709
- markdown += "\n"
1710
-
1711
- if stage['content'] and stage['status'] == 'complete':
1712
- # Adjust preview length by role
1713
- preview_length = 300 if 'writer' in stage.get('name', '').lower() else 200
1714
- preview = stage['content'][:preview_length] + "..." if len(stage['content']) > preview_length else stage['content']
1715
- markdown += f"> {preview}\n\n"
1716
- elif stage['status'] == 'active':
1717
- markdown += "> *Writing...*\n\n"
1718
-
1719
- return markdown
1720
-
1721
- def format_novel_display(novel_text: str) -> str:
1722
- """Display novel content - Enhanced part separation"""
1723
- if not novel_text:
1724
- return "No completed content yet."
1725
-
1726
- formatted = "# 📖 Completed Novel\n\n"
1727
-
1728
- # Display word count
1729
- word_count = len(novel_text.split())
1730
- formatted += f"**Total Length: {word_count:,} words (Target: {TARGET_WORDS:,} words)**\n\n"
1731
-
1732
- # Achievement rate
1733
- achievement = (word_count / TARGET_WORDS) * 100
1734
- formatted += f"**Achievement Rate: {achievement:.1f}%**\n\n"
1735
- formatted += "---\n\n"
1736
-
1737
- # Display each part separately
1738
- parts = novel_text.split('\n\n')
1739
-
1740
- for i, part in enumerate(parts):
1741
- if part.strip():
1742
- # Add part title
1743
- if i < len(NARRATIVE_PHASES):
1744
- formatted += f"## {NARRATIVE_PHASES[i]}\n\n"
1745
-
1746
- formatted += f"{part}\n\n"
1747
-
1748
- # Part divider
1749
- if i < len(parts) - 1:
1750
- formatted += "---\n\n"
1751
-
1752
- return formatted
1753
-
1754
- def export_to_docx(content: str, filename: str, language: str, session_id: str) -> str:
1755
- """Export to DOCX file - Korean standard book format"""
1756
- doc = Document()
1757
-
1758
- # Korean standard book format (152mm x 225mm)
1759
- section = doc.sections[0]
1760
- section.page_height = Mm(225) # 225mm
1761
- section.page_width = Mm(152) # 152mm
1762
- section.top_margin = Mm(20) # Top margin 20mm
1763
- section.bottom_margin = Mm(20) # Bottom margin 20mm
1764
- section.left_margin = Mm(20) # Left margin 20mm
1765
- section.right_margin = Mm(20) # Right margin 20mm
1766
-
1767
- # Generate title from session info
1768
- session = NovelDatabase.get_session(session_id)
1769
-
1770
- # Title generation function
1771
- def generate_title(user_query: str, content_preview: str) -> str:
1772
- """Generate title based on theme and content"""
1773
- # Simple rule-based title generation (could use LLM)
1774
- if len(user_query) < 20:
1775
- return user_query
1776
- else:
1777
- # Extract key keywords from theme
1778
- keywords = user_query.split()[:5]
1779
- return " ".join(keywords)
1780
-
1781
- # Title page
1782
- title = generate_title(session["user_query"], content[:500]) if session else "Untitled"
1783
-
1784
- # Title style settings
1785
- title_para = doc.add_paragraph()
1786
- title_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
1787
- title_para.paragraph_format.space_before = Pt(100)
1788
-
1789
- title_run = title_para.add_run(title)
1790
- if language == "Korean":
1791
- title_run.font.name = 'Batang'
1792
- title_run._element.rPr.rFonts.set(qn('w:eastAsia'), 'Batang')
1793
- else:
1794
- title_run.font.name = 'Times New Roman'
1795
- title_run.font.size = Pt(20)
1796
- title_run.bold = True
1797
-
1798
- # Page break
1799
- doc.add_page_break()
1800
-
1801
- # Body style settings
1802
- style = doc.styles['Normal']
1803
- if language == "Korean":
1804
- style.font.name = 'Batang'
1805
- style._element.rPr.rFonts.set(qn('w:eastAsia'), 'Batang')
1806
- else:
1807
- style.font.name = 'Times New Roman'
1808
- style.font.size = Pt(10.5) # Standard size for novels
1809
- style.paragraph_format.line_spacing = 1.8 # 180% line spacing
1810
- style.paragraph_format.space_after = Pt(0)
1811
- style.paragraph_format.first_line_indent = Mm(10) # 10mm indentation
1812
-
1813
- # Clean content - Extract pure text only
1814
- def clean_content(text: str) -> str:
1815
- """Remove unnecessary markdown, part numbers, etc."""
1816
- # Remove part titles/numbers patterns
1817
- patterns_to_remove = [
1818
- r'^#{1,6}\s+.*', # Markdown headers
1819
- r'^\*\*.*\*\*', # 굵은 글씨 **text**
1820
- r'^Part\s*\d+.*', # "Part 1 …" 형식
1821
- r'^\d+\.\s+.*:.*', # "1. 제목: …" 형식
1822
- r'^---+', # 구분선
1823
- r'^\s*\[.*\]\s*', # 대괄호 라벨
1824
- ]
1825
-
1826
- lines = text.split('\n')
1827
- cleaned_lines = []
1828
-
1829
- for line in lines:
1830
- # Keep empty lines
1831
- if not line.strip():
1832
- cleaned_lines.append('')
1833
- continue
1834
-
1835
- # Remove unnecessary lines through pattern matching
1836
- skip_line = False
1837
- for pattern in patterns_to_remove:
1838
- if re.match(pattern, line.strip(), re.MULTILINE):
1839
- skip_line = True
1840
- break
1841
-
1842
- if not skip_line:
1843
- # Remove markdown emphasis
1844
- cleaned_line = line
1845
- cleaned_line = re.sub(r'\*\*(.*?)\*\*', r'\1', cleaned_line) # **text** -> text
1846
- cleaned_line = re.sub(r'\*(.*?)\*', r'\1', cleaned_line) # *text* -> text
1847
- cleaned_line = re.sub(r'`(.*?)`', r'\1', cleaned_line) # `text` -> text
1848
- cleaned_lines.append(cleaned_line.strip())
1849
-
1850
- # Remove consecutive empty lines (keep only 1)
1851
- final_lines = []
1852
- prev_empty = False
1853
- for line in cleaned_lines:
1854
- if not line:
1855
- if not prev_empty:
1856
- final_lines.append('')
1857
- prev_empty = True
1858
- else:
1859
- final_lines.append(line)
1860
- prev_empty = False
1861
-
1862
- return '\n'.join(final_lines)
1863
-
1864
- # Clean content
1865
- cleaned_content = clean_content(content)
1866
-
1867
- # Add body text
1868
- paragraphs = cleaned_content.split('\n')
1869
- for para_text in paragraphs:
1870
- if para_text.strip():
1871
- para = doc.add_paragraph(para_text.strip())
1872
- # Reconfirm style (apply font)
1873
- for run in para.runs:
1874
- if language == "Korean":
1875
- run.font.name = 'Batang'
1876
- run._element.rPr.rFonts.set(qn('w:eastAsia'), 'Batang')
1877
- else:
1878
- run.font.name = 'Times New Roman'
1879
- else:
1880
- # Empty line for paragraph separation
1881
- doc.add_paragraph()
1882
-
1883
- # Save file
1884
- filepath = f"{filename}.docx"
1885
- doc.save(filepath)
1886
- return filepath
1887
-
1888
- def export_to_txt(content: str, filename: str) -> str:
1889
- """Export to TXT file"""
1890
- filepath = f"{filename}.txt"
1891
- with open(filepath, 'w', encoding='utf-8') as f:
1892
- # Header
1893
- f.write("=" * 80 + "\n")
1894
- f.write(f"Generated on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
1895
- f.write(f"Total word count: {len(content.split()):,} words\n")
1896
- f.write("=" * 80 + "\n\n")
1897
-
1898
- # Body
1899
- f.write(content)
1900
-
1901
- # Footer
1902
- f.write("\n\n" + "=" * 80 + "\n")
1903
- f.write("AI Literary Creation System v2.0\n")
1904
- f.write("=" * 80 + "\n")
1905
-
1906
- return filepath
1907
-
1908
- # CSS styles
1909
- custom_css = """
1910
- .gradio-container {
1911
- background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
1912
- min-height: 100vh;
1913
- }
1914
-
1915
- .main-header {
1916
- background-color: rgba(255, 255, 255, 0.05);
1917
- backdrop-filter: blur(20px);
1918
- padding: 40px;
1919
- border-radius: 20px;
1920
- margin-bottom: 30px;
1921
- text-align: center;
1922
- color: white;
1923
- border: 2px solid rgba(255, 255, 255, 0.1);
1924
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
1925
- }
1926
-
1927
- .header-title {
1928
- font-size: 2.8em;
1929
- margin-bottom: 15px;
1930
- font-weight: 700;
1931
- }
1932
-
1933
- .header-description {
1934
- font-size: 0.85em;
1935
- color: #d0d0d0;
1936
- line-height: 1.4;
1937
- margin-top: 20px;
1938
- text-align: left;
1939
- max-width: 900px;
1940
- margin-left: auto;
1941
- margin-right: auto;
1942
- }
1943
-
1944
- .badges-container {
1945
- display: flex;
1946
- justify-content: center;
1947
- gap: 10px;
1948
- margin-top: 20px;
1949
- margin-bottom: 20px;
1950
- }
1951
-
1952
- .progress-note {
1953
- background: linear-gradient(135deg, rgba(255, 107, 107, 0.1), rgba(255, 230, 109, 0.1));
1954
- border-left: 4px solid #ff6b6b;
1955
- padding: 20px;
1956
- margin: 25px auto;
1957
- border-radius: 10px;
1958
- color: #fff;
1959
- max-width: 800px;
1960
- font-weight: 500;
1961
- }
1962
-
1963
- .warning-note {
1964
- background: rgba(255, 193, 7, 0.1);
1965
- border-left: 4px solid #ffc107;
1966
- padding: 15px;
1967
- margin: 20px auto;
1968
- border-radius: 8px;
1969
- color: #ffd700;
1970
- max-width: 800px;
1971
- font-size: 0.9em;
1972
- }
1973
-
1974
- .input-section {
1975
- background-color: rgba(255, 255, 255, 0.08);
1976
- backdrop-filter: blur(15px);
1977
- padding: 25px;
1978
- border-radius: 15px;
1979
- margin-bottom: 25px;
1980
- border: 1px solid rgba(255, 255, 255, 0.1);
1981
- box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
1982
- }
1983
-
1984
- .session-section {
1985
- background-color: rgba(255, 255, 255, 0.06);
1986
- backdrop-filter: blur(10px);
1987
- padding: 20px;
1988
- border-radius: 12px;
1989
- margin-top: 25px;
1990
- color: white;
1991
- border: 1px solid rgba(255, 255, 255, 0.08);
1992
- }
1993
-
1994
- #stages-display {
1995
- background-color: rgba(255, 255, 255, 0.97);
1996
- padding: 25px;
1997
- border-radius: 15px;
1998
- max-height: 650px;
1999
- overflow-y: auto;
2000
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
2001
- color: #2c3e50;
2002
- }
2003
-
2004
- #novel-output {
2005
- background-color: rgba(255, 255, 255, 0.97);
2006
- padding: 35px;
2007
- border-radius: 15px;
2008
- max-height: 750px;
2009
- overflow-y: auto;
2010
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
2011
- color: #2c3e50;
2012
- line-height: 1.8;
2013
- }
2014
-
2015
- .download-section {
2016
- background-color: rgba(255, 255, 255, 0.92);
2017
- padding: 20px;
2018
- border-radius: 12px;
2019
- margin-top: 25px;
2020
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
2021
- }
2022
-
2023
- /* Progress indicator improvements */
2024
- .progress-bar {
2025
- background-color: #e0e0e0;
2026
- height: 25px;
2027
- border-radius: 12px;
2028
- overflow: hidden;
2029
- margin: 15px 0;
2030
- box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1);
2031
- }
2032
-
2033
- .progress-fill {
2034
- background: linear-gradient(90deg, #4CAF50, #8BC34A);
2035
- height: 100%;
2036
- transition: width 0.5s ease;
2037
- box-shadow: 0 2px 8px rgba(76, 175, 80, 0.3);
2038
- }
2039
-
2040
- /* Scrollbar styles */
2041
- ::-webkit-scrollbar {
2042
- width: 10px;
2043
- }
2044
-
2045
- ::-webkit-scrollbar-track {
2046
- background: rgba(0, 0, 0, 0.1);
2047
- border-radius: 5px;
2048
- }
2049
-
2050
- ::-webkit-scrollbar-thumb {
2051
- background: rgba(0, 0, 0, 0.3);
2052
- border-radius: 5px;
2053
- }
2054
-
2055
- ::-webkit-scrollbar-thumb:hover {
2056
- background: rgba(0, 0, 0, 0.5);
2057
- }
2058
-
2059
- /* Button hover effects */
2060
- .gr-button:hover {
2061
- transform: translateY(-2px);
2062
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
2063
- transition: all 0.3s ease;
2064
- }
2065
- """
2066
-
2067
- # Create Gradio interface
2068
- def create_interface():
2069
- with gr.Blocks(css=custom_css, title="AGI NOVEL Generator") as interface:
2070
- gr.HTML("""
2071
- <div class="main-header">
2072
- <h1 class="header-title">📚 AGI NOVEL Generator</h1>
2073
-
2074
- <div class="badges-container">
2075
- <a href="https://huggingface.co/OpenFreeAI" target="_blank">
2076
- <img src="https://img.shields.io/static/v1?label=Community&message=OpenFree_AI&color=%23800080&labelColor=%23000080&logo=HUGGINGFACE&logoColor=%23ffa500&style=for-the-badge" alt="badge">
2077
- </a>
2078
- <a href="https://discord.gg/openfreeai" target="_blank">
2079
- <img src="https://img.shields.io/static/v1?label=Discord&message=Openfree%20AI&color=%230000ff&labelColor=%23800080&logo=discord&logoColor=white&style=for-the-badge" alt="badge">
2080
- </a>
2081
- <a href="https://huggingface.co/spaces/openfree/Best-AI" target="_blank">
2082
- <img src="https://img.shields.io/static/v1?label=OpenFree&message=BEST%20AI%20Services&color=%230000ff&labelColor=%23000080&logo=huggingface&logoColor=%23ffa500&style=for-the-badge" alt="badge">
2083
- </a>
2084
- </div>
2085
-
2086
- <p class="header-description">
2087
- Artificial General Intelligence (AGI) denotes an artificial system possessing human-level, general-purpose intelligence and is now commonly framed as AI that can outperform humans in most economically and intellectually valuable tasks. Demonstrating such breadth requires evaluating not only calculation, logical reasoning, and perception but also the distinctly human faculties of creativity and language. Among the creative tests, the most demanding is the production of a full-length novel running 100k–200k words. An extended narrative forces an AGI candidate to exhibit (1) sustained long-term memory and context tracking (2) intricate causal and plot planning (3) nuanced cultural and emotional expression (4) autonomous self-censorship and ethical filtering to avoid harmful or biased content and (5) verifiable originality beyond simple recombination of training data.
2088
- </p>
2089
-
2090
- <div class="warning-note">
2091
- ⏱️ <strong>Note:</strong> Creating a complete novel takes approximately 20 minutes. If your web session disconnects, you can restore your work using the "Session Recovery" feature.
2092
- </div>
2093
-
2094
- <div class="progress-note">
2095
- 🎯 <strong>Core Innovation:</strong> Not fragmented texts from multiple writers,
2096
- but a genuine full-length novel written consistently by a single author from beginning to end.
2097
- </div>
2098
- </div>
2099
- """)
2100
-
2101
- # State management
2102
- current_session_id = gr.State(None)
2103
-
2104
- with gr.Row():
2105
- with gr.Column(scale=1):
2106
- with gr.Group(elem_classes=["input-section"]):
2107
- query_input = gr.Textbox(
2108
- label="Novel Theme",
2109
- placeholder="""Enter your novella theme.
2110
- Examples: Character transformation, relationship evolution, social conflict and personal choice...""",
2111
- lines=5
2112
- )
2113
-
2114
- language_select = gr.Radio(
2115
- choices=["English", "Korean"],
2116
- value="English",
2117
- label="Language"
2118
- )
2119
-
2120
- with gr.Row():
2121
- submit_btn = gr.Button("🚀 Start Writing", variant="primary", scale=2)
2122
- clear_btn = gr.Button("🗑️ Clear", scale=1)
2123
-
2124
- status_text = gr.Textbox(
2125
- label="Progress Status",
2126
- interactive=False,
2127
- value="🔄 Ready"
2128
- )
2129
-
2130
- # Session management
2131
- with gr.Group(elem_classes=["session-section"]):
2132
- gr.Markdown("### 💾 Active Works")
2133
- session_dropdown = gr.Dropdown(
2134
- label="Saved Sessions",
2135
- choices=[],
2136
- interactive=True
2137
- )
2138
- with gr.Row():
2139
- refresh_btn = gr.Button("🔄 Refresh", scale=1)
2140
- resume_btn = gr.Button("▶️ Resume", variant="secondary", scale=1)
2141
- auto_recover_btn = gr.Button("♻️ Recover Recent Work", scale=1)
2142
-
2143
- with gr.Column(scale=2):
2144
- with gr.Tab("📝 Writing Process"):
2145
- stages_display = gr.Markdown(
2146
- value="Writing process will be displayed in real-time...",
2147
- elem_id="stages-display"
2148
- )
2149
-
2150
- with gr.Tab("📖 Completed Work"):
2151
- novel_output = gr.Markdown(
2152
- value="Completed novel will be displayed here...",
2153
- elem_id="novel-output"
2154
- )
2155
-
2156
- with gr.Group(elem_classes=["download-section"]):
2157
- gr.Markdown("### 📥 Download Work")
2158
- with gr.Row():
2159
- format_select = gr.Radio(
2160
- choices=["DOCX", "TXT"],
2161
- value="DOCX" if DOCX_AVAILABLE else "TXT",
2162
- label="File Format"
2163
- )
2164
- download_btn = gr.Button("⬇️ Download", variant="secondary")
2165
-
2166
- download_file = gr.File(
2167
- label="Download File",
2168
- visible=False
2169
- )
2170
-
2171
- # Hidden state
2172
- novel_text_state = gr.State("")
2173
-
2174
- # Examples
2175
- with gr.Row():
2176
- gr.Examples(
2177
- examples=[
2178
- ["A daughter discovering her mother's hidden past through old letters"],
2179
- ["An architect losing sight who learns to design through touch and sound"],
2180
- ["A translator replaced by AI rediscovering the essence of language through classical literature transcription"],
2181
- ["A middle-aged man who lost his job finding new meaning in rural life"],
2182
- ["A doctor with war trauma healing through Doctors Without Borders"],
2183
- ["Community solidarity to save a neighborhood bookstore from redevelopment"],
2184
- ["A year with a professor losing memory and his last student"]
2185
- ],
2186
- inputs=query_input,
2187
- label="💡 Theme Examples"
2188
- )
2189
-
2190
- # Event handlers
2191
- def refresh_sessions():
2192
- try:
2193
- sessions = get_active_sessions("English")
2194
- return gr.update(choices=sessions)
2195
- except Exception as e:
2196
- logger.error(f"Session refresh error: {str(e)}")
2197
- return gr.update(choices=[])
2198
-
2199
- def handle_auto_recover(language):
2200
- session_id, message = auto_recover_session(language)
2201
- return session_id, message
2202
-
2203
- # Event connections
2204
- submit_btn.click(
2205
- fn=process_query,
2206
- inputs=[query_input, language_select, current_session_id],
2207
- outputs=[stages_display, novel_output, status_text, current_session_id]
2208
- )
2209
-
2210
- novel_output.change(
2211
- fn=lambda x: x,
2212
- inputs=[novel_output],
2213
- outputs=[novel_text_state]
2214
- )
2215
-
2216
- resume_btn.click(
2217
- fn=lambda x: x.split("...")[0] if x and "..." in x else x,
2218
- inputs=[session_dropdown],
2219
- outputs=[current_session_id]
2220
- ).then(
2221
- fn=resume_session,
2222
- inputs=[current_session_id, language_select],
2223
- outputs=[stages_display, novel_output, status_text, current_session_id]
2224
- )
2225
-
2226
- auto_recover_btn.click(
2227
- fn=handle_auto_recover,
2228
- inputs=[language_select],
2229
- outputs=[current_session_id, status_text]
2230
- ).then(
2231
- fn=resume_session,
2232
- inputs=[current_session_id, language_select],
2233
- outputs=[stages_display, novel_output, status_text, current_session_id]
2234
- )
2235
-
2236
- refresh_btn.click(
2237
- fn=refresh_sessions,
2238
- outputs=[session_dropdown]
2239
- )
2240
-
2241
- clear_btn.click(
2242
- fn=lambda: ("", "", "🔄 Ready", "", None),
2243
- outputs=[stages_display, novel_output, status_text, novel_text_state, current_session_id]
2244
- )
2245
-
2246
- def handle_download(format_type, language, session_id, novel_text):
2247
- if not session_id or not novel_text:
2248
- return gr.update(visible=False)
2249
-
2250
- file_path = download_novel(novel_text, format_type, language, session_id)
2251
- if file_path:
2252
- return gr.update(value=file_path, visible=True)
2253
- else:
2254
- return gr.update(visible=False)
2255
-
2256
- download_btn.click(
2257
- fn=handle_download,
2258
- inputs=[format_select, language_select, current_session_id, novel_text_state],
2259
- outputs=[download_file]
2260
- )
2261
-
2262
- # Load sessions on start
2263
- interface.load(
2264
- fn=refresh_sessions,
2265
- outputs=[session_dropdown]
2266
- )
2267
-
2268
- return interface
2269
-
2270
- # Main execution
2271
- if __name__ == "__main__":
2272
- logger.info("AGI NOVEL Generator v2.0 Starting...")
2273
- logger.info("=" * 60)
2274
-
2275
- # Environment check (보안 개선)
2276
- logger.info(f"API Endpoint: {API_URL}")
2277
- logger.info(f"Target Length: {TARGET_WORDS:,} words")
2278
- logger.info(f"Minimum Words per Part: {MIN_WORDS_PER_PART:,} words")
2279
- logger.info("System Features: Single writer + Immediate part-by-part critique")
2280
-
2281
- if BRAVE_SEARCH_API_KEY:
2282
- logger.info("Web search enabled.")
2283
- else:
2284
- logger.warning("Web search disabled.")
2285
-
2286
- if DOCX_AVAILABLE:
2287
- logger.info("DOCX export enabled.")
2288
- else:
2289
- logger.warning("DOCX export disabled.")
2290
-
2291
- logger.info("=" * 60)
2292
-
2293
- # Initialize database
2294
- logger.info("Initializing database...")
2295
- NovelDatabase.init_db()
2296
- logger.info("Database initialization complete.")
2297
-
2298
- # Create and launch interface
2299
- interface = create_interface()
2300
-
2301
- interface.launch(
2302
- server_name="0.0.0.0",
2303
- server_port=7860,
2304
- share=False,
2305
- debug=True
2306
- )