openfree commited on
Commit
cf2c020
·
verified ·
1 Parent(s): ac07338

Update app-backup.py

Browse files
Files changed (1) hide show
  1. app-backup.py +837 -502
app-backup.py CHANGED
@@ -38,7 +38,11 @@ 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_v4.db"
 
 
 
 
42
 
43
  # --- 환경 변수 검증 ---
44
  if not FRIENDLI_TOKEN:
@@ -51,18 +55,32 @@ if not BRAVE_SEARCH_API_KEY:
51
  # --- 전역 변수 ---
52
  db_lock = threading.Lock()
53
 
54
- # 문학적 단계 구성 (내면 서술과 사회적 통찰 중심)
55
- LITERARY_STAGES = [
56
- ("director", "🎬 감독자: 사회적 맥락과 인물 심리 기획"),
57
- ("critic", "📝 비평가: 사회 비판적 깊이와 상징성 검토"),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  ("director", "🎬 감독자: 수정된 마스터플랜"),
59
  ] + [
60
- (f"writer{i}", f"✍️ 작가 {i}: 초안")
61
  for i in range(1, 11)
62
  ] + [
63
- ("critic", "📝 비평가: 중간 검토 (내적 일관성과 주제 심화)"),
64
  ] + [
65
- (f"writer{i}", f"✍️ 작가 {i}: 수정본")
66
  for i in range(1, 11)
67
  ] + [
68
  ("critic", f"📝 비평가: 최종 검토 및 문학적 평가"),
@@ -71,74 +89,147 @@ LITERARY_STAGES = [
71
 
72
  # --- 데이터 클래스 ---
73
  @dataclass
74
- class CharacterPsychology:
75
- """인물의 심리적 상태와 내면"""
76
  name: str
77
- age: int
78
- social_class: str # 계급적 위치
79
- occupation: str
80
- inner_conflict: str # 내적 갈등
81
- worldview: str # 세계관
82
- desires: List[str] # 욕망들
83
- fears: List[str] # 두려움들
84
- coping_mechanisms: List[str] # 방어기제
85
- relationships: Dict[str, str] # 타인과의 관계
86
-
87
  @dataclass
88
- class SymbolicElement:
89
- """상징적 요소"""
90
- symbol: str
91
- meaning: str
92
- appearances: List[int] # 등장하는 장들
93
- evolution: str # 상징의 의미 변화
 
 
94
 
95
  @dataclass
96
- class SocialContext:
97
- """사회적 맥락"""
98
- economic_system: str
99
- class_structure: str
100
- power_dynamics: str
101
- social_issues: List[str]
102
- cultural_atmosphere: str
103
 
104
 
105
  # --- 핵심 로직 클래스 ---
106
- class LiteraryConsistencyTracker:
107
- """문학적 일관성 추적 시스템"""
108
  def __init__(self):
109
- self.characters: Dict[str, CharacterPsychology] = {}
110
- self.symbols: Dict[str, SymbolicElement] = {}
111
- self.social_context: Optional[SocialContext] = None
112
- self.themes: List[str] = []
113
- self.narrative_voice: str = "" # 서술 시점과 문체
114
- self.tone: str = "" # 전체적인 톤
115
 
116
- def register_character(self, character: CharacterPsychology):
117
- """인물 등록"""
118
- self.characters[character.name] = character
119
- logger.info(f"Character registered: {character.name}, Class: {character.social_class}")
120
 
121
- def register_symbol(self, symbol: SymbolicElement):
122
- """상징 등록"""
123
- self.symbols[symbol.symbol] = symbol
124
- logger.info(f"Symbol registered: {symbol.symbol} = {symbol.meaning}")
 
 
 
 
 
 
 
 
 
 
 
 
125
 
126
- def check_thematic_consistency(self, content: str, chapter: int) -> List[str]:
127
- """주제적 일관성 검사"""
 
 
 
 
 
128
  issues = []
129
 
130
- # 사회비판적 요소가 유지되는지
131
- if self.social_context and not any(issue.lower() in content.lower()
132
- for issue in self.social_context.social_issues):
133
- issues.append("사회적 맥락이 약화되었습니다. 계급, 불평등 등의 주제를 유지하세요.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
 
135
- # 내면 서술이 충분한지
136
- introspective_keywords = ['생각했다', '느꼈다', '기억', '의식', '마음',
137
- 'thought', 'felt', 'remembered', 'consciousness']
138
- if not any(keyword in content for keyword in introspective_keywords):
139
- issues.append("내면 서술이 부족합니다. 인물의 심리를 더 깊이 탐구하세요.")
 
 
 
 
 
 
 
 
 
 
 
 
140
 
141
- return issues
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
 
143
 
144
  class NovelDatabase:
@@ -160,8 +251,8 @@ class NovelDatabase:
160
  current_stage INTEGER DEFAULT 0,
161
  final_novel TEXT,
162
  literary_report TEXT,
163
- social_context TEXT,
164
- narrative_style TEXT
165
  )
166
  ''')
167
 
@@ -175,7 +266,7 @@ class NovelDatabase:
175
  content TEXT,
176
  word_count INTEGER DEFAULT 0,
177
  status TEXT DEFAULT 'pending',
178
- literary_score REAL DEFAULT 0.0,
179
  created_at TEXT DEFAULT (datetime('now')),
180
  updated_at TEXT DEFAULT (datetime('now')),
181
  FOREIGN KEY (session_id) REFERENCES sessions(session_id),
@@ -184,27 +275,13 @@ class NovelDatabase:
184
  ''')
185
 
186
  cursor.execute('''
187
- CREATE TABLE IF NOT EXISTS characters (
188
- id INTEGER PRIMARY KEY AUTOINCREMENT,
189
- session_id TEXT NOT NULL,
190
- name TEXT NOT NULL,
191
- age INTEGER,
192
- social_class TEXT,
193
- occupation TEXT,
194
- inner_conflict TEXT,
195
- worldview TEXT,
196
- created_at TEXT DEFAULT (datetime('now')),
197
- FOREIGN KEY (session_id) REFERENCES sessions(session_id),
198
- UNIQUE(session_id, name)
199
- )
200
- ''')
201
-
202
- cursor.execute('''
203
- CREATE TABLE IF NOT EXISTS symbols (
204
  id INTEGER PRIMARY KEY AUTOINCREMENT,
205
  session_id TEXT NOT NULL,
206
- symbol TEXT NOT NULL,
207
- meaning TEXT,
 
 
208
  created_at TEXT DEFAULT (datetime('now')),
209
  FOREIGN KEY (session_id) REFERENCES sessions(session_id)
210
  )
@@ -238,38 +315,111 @@ class NovelDatabase:
238
  @staticmethod
239
  def save_stage(session_id: str, stage_number: int, stage_name: str,
240
  role: str, content: str, status: str = 'complete',
241
- literary_score: float = 0.0):
242
  word_count = len(content.split()) if content else 0
243
  with NovelDatabase.get_db() as conn:
244
  cursor = conn.cursor()
245
  cursor.execute('''
246
- INSERT INTO stages (session_id, stage_number, stage_name, role, content, word_count, status, literary_score)
247
  VALUES (?, ?, ?, ?, ?, ?, ?, ?)
248
  ON CONFLICT(session_id, stage_number)
249
- DO UPDATE SET content=?, word_count=?, status=?, stage_name=?, literary_score=?, updated_at=datetime('now')
250
- ''', (session_id, stage_number, stage_name, role, content, word_count, status, literary_score,
251
- content, word_count, status, stage_name, literary_score))
252
- cursor.execute(
253
- "UPDATE sessions SET updated_at = datetime('now'), current_stage = ? WHERE session_id = ?",
254
- (stage_number, session_id)
255
- )
 
 
 
 
 
 
 
 
 
 
256
  conn.commit()
257
 
258
  @staticmethod
259
  def get_writer_content(session_id: str) -> str:
260
- """작가 콘텐츠 가져오기"""
261
  with NovelDatabase.get_db() as conn:
262
  all_content = []
263
  for writer_num in range(1, 11):
264
- row = conn.cursor().execute(
265
- "SELECT content FROM stages WHERE session_id = ? AND role = ? AND stage_name LIKE '%수정본%' ORDER BY stage_number DESC LIMIT 1",
266
- (session_id, f'writer{writer_num}')
267
- ).fetchone()
 
 
 
 
 
 
 
 
 
 
 
 
 
268
  if row and row['content']:
269
- content = row['content'].strip()
270
- all_content.append(content)
271
  return '\n\n'.join(all_content)
272
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
273
  @staticmethod
274
  def get_session(session_id: str) -> Optional[Dict]:
275
  with NovelDatabase.get_db() as conn:
@@ -295,7 +445,7 @@ class NovelDatabase:
295
  def get_active_sessions() -> List[Dict]:
296
  with NovelDatabase.get_db() as conn:
297
  rows = conn.cursor().execute(
298
- "SELECT session_id, user_query, language, created_at, current_stage FROM sessions WHERE status = 'active' ORDER BY updated_at DESC LIMIT 10"
299
  ).fetchall()
300
  return [dict(row) for row in rows]
301
 
@@ -347,13 +497,13 @@ class WebSearchIntegration:
347
  return "\n".join(extracted)
348
 
349
 
350
- class LiteraryNovelSystem:
351
- """문학적 소설 생성 시스템"""
352
  def __init__(self):
353
  self.token = FRIENDLI_TOKEN
354
  self.api_url = API_URL
355
  self.model_id = MODEL_ID
356
- self.consistency_tracker = LiteraryConsistencyTracker()
357
  self.web_search = WebSearchIntegration()
358
  self.current_session_id = None
359
  NovelDatabase.init_db()
@@ -361,161 +511,205 @@ class LiteraryNovelSystem:
361
  def create_headers(self):
362
  return {"Authorization": f"Bearer {self.token}", "Content-Type": "application/json"}
363
 
364
- # --- 프롬프트 생성 함수들 (문학적 깊이 중심) ---
365
  def create_director_initial_prompt(self, user_query: str, language: str) -> str:
366
- """감독자 초기 기획 프롬프트 (사회비판적 리얼리즘)"""
367
  search_results_str = ""
368
  if self.web_search.enabled:
369
- # 사회적 맥락 검색
370
- queries = [f"{user_query} 사회 문제", f"{user_query} 계급 갈등", f"{user_query} social inequality"]
371
  for q in queries[:1]:
372
  results = self.web_search.search(q, count=2, language=language)
373
  if results:
374
  search_results_str += self.web_search.extract_relevant_info(results) + "\n"
375
 
376
  lang_prompts = {
377
- "Korean": f"""당신은 한국 현대 문학의 전통을 잇는 소설 기획자입니다.
378
- 한강, 김애란, 천명관, 정세랑 현대 한국 작가들의 문학적 성취를 참고하여,
379
- 사회비판적 리얼리즘과 내면 심리 탐구가 결합된 30페이지 중편소설을 기획하세요.
380
 
381
- **사용자 주제:** {user_query}
382
 
383
  **참고 자료:**
384
  {search_results_str if search_results_str else "N/A"}
385
 
386
- **기획 요구사항:**
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
387
 
388
- 1. **주제와 사회적 맥락**
389
- - 핵심 주제: 현대 한국 사회의 구조적 문제 (계급, 빈곤, 소외, 젠더, 세대 등)
390
- - 비판적 시각: 개인의 문제를 사회 구조와 연결
391
- - 현실성: 2020년대 한국의 구체적 현실 반영
392
 
393
- 2. **서술 방식과 문체**
394
- - 시점: 1인칭 또는 제한적 3인칭 (내면 접근 가능)
395
- - 문체: 담담하지만 날카로운 관찰, 일상어와 문학적 표현의 균형
396
- - 내면 서술: 의식의 흐름, 회상, 자기 성찰 적극 활용
 
397
 
398
- 3. **인물 설정** (2-4명의 핵심 인물)
399
- | 이름 | 나이 | 계급적 위치 | 직업 | 내적 갈등 | 욕망 | 두려움 |
400
- - 각 인물은 특정 계급과 사회적 위치를 대표
401
- - 겉모습과 내면의 괴리 표현
402
- - 구조적 억압 속에서의 개인적 선택
403
 
404
- 4. **상징과 은유**
405
- - 핵심 상징: (예: 개구리=소외된 자들의 목소리, 연못=계급 경계)
406
- - 반복되는 이미지나 모티프
407
- - 일상적 사물에 담긴 사회적 의미
408
 
409
- 5. **플롯 구조**
410
- - 극적 사건보다 일상 미묘한 변화 중심
411
- - 인물의 인식 변화가 곧 서사의 진행
412
- - 열린 결말: 해결보다는 문제 제기와 성찰
413
 
414
- **절대 금지사항:**
415
- - 선악 구분이 명확한 평면적 인물
416
- - 급작스러운 사건이나 극적 반전
417
- - 교훈적이거나 계몽적인 메시지
418
- - 안이한 희망이나 화해
419
 
420
- 내면과 사회를 동시에 포착하는 섬세한 기획을 작성하세요.""",
421
-
422
- "English": f"""You are a literary director planning a 30-page novella in the tradition of contemporary social realism.
423
- Drawing from authors like George Saunders, Zadie Smith, and Sally Rooney, create a work that combines psychological depth with social critique.
424
 
425
- **User Theme:** {user_query}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
426
 
427
- **Reference Material:**
428
- {search_results_str if search_results_str else "N/A"}
 
 
 
 
 
 
 
429
 
430
- **Planning Requirements:**
431
-
432
- 1. **Theme and Social Context**
433
- - Core theme: Structural problems in contemporary society (class, inequality, alienation)
434
- - Critical perspective: Connect individual struggles to social systems
435
- - Realism: Reflect specific contemporary realities
436
-
437
- 2. **Narrative Style**
438
- - POV: First person or limited third person (with access to interiority)
439
- - Style: Understated yet sharp observation, balance of vernacular and literary
440
- - Interior narration: Stream of consciousness, memory, self-reflection
441
-
442
- 3. **Character Design** (2-4 main characters)
443
- | Name | Age | Class Position | Occupation | Inner Conflict | Desires | Fears |
444
- - Each character represents specific social position
445
- - Gap between appearance and interior life
446
- - Individual choices within structural constraints
447
-
448
- 4. **Symbols and Metaphors**
449
- - Key symbols with social meaning
450
- - Recurring images or motifs
451
- - Everyday objects as social commentary
452
-
453
- 5. **Plot Structure**
454
- - Focus on subtle changes over dramatic events
455
- - Character perception shifts drive narrative
456
- - Open ending: Questions over resolutions
457
-
458
- **Absolutely Avoid:**
459
- - Clear-cut heroes and villains
460
- - Sudden dramatic events or twists
461
- - Didactic or preachy messages
462
- - Easy hope or reconciliation
463
-
464
- Create a nuanced plan that captures both interior life and social reality."""
465
  }
466
 
467
  return lang_prompts.get(language, lang_prompts["Korean"])
468
 
469
  def create_critic_director_prompt(self, director_plan: str, user_query: str, language: str) -> str:
470
- """비평가의 감독자 기획 검토"""
471
  lang_prompts = {
472
- "Korean": f"""당신은 문학 비평가입니다. 다음 관점에서 기획을 검토하세요:
 
473
 
474
  **원 주제:** {user_query}
475
 
476
  **감독자 기획:**
477
  {director_plan}
478
 
479
- **검토 기준:**
 
 
 
 
 
 
 
 
 
 
480
 
481
- 1. **사회비판적 깊이**
482
- - 개인과 구조의 연결이 설득력 있는가?
483
- - 현실의 복잡성을 단순화하지 않았는가?
484
- - 진부한 사회 비판에 그치지 않았는가?
485
 
486
- 2. **문학적 완성도**
487
- - 내면 서술과 외적 현실의 균형
488
- - 상징과 은유의 적절성
489
- - 인물의 입체성과 신빙성
490
 
491
- 3. **현대성과 보편성**
492
- - 2020년대 한국의 특수성 반영
493
- - 동시에 보편적 인간 조건 탐구
494
 
495
  구체적 개선 방향을 제시하세요.""",
496
-
497
- "English": f"""You are a literary critic. Review the plan from these perspectives:
 
498
 
499
  **Original Theme:** {user_query}
500
 
501
  **Director's Plan:**
502
  {director_plan}
503
 
504
- **Review Criteria:**
505
 
506
- 1. **Social Critical Depth**
507
- - Is the connection between individual and structure convincing?
508
- - Does it avoid oversimplifying complex realities?
509
- - Does it go beyond clichéd social criticism?
510
 
511
- 2. **Literary Merit**
512
- - Balance of interiority and external reality
513
- - Effectiveness of symbols and metaphors
514
- - Character complexity and credibility
515
 
516
- 3. **Contemporary Relevance**
517
- - Reflects specific contemporary context
518
- - While exploring universal human conditions
 
 
 
 
 
 
 
 
 
519
 
520
  Provide specific improvements."""
521
  }
@@ -523,174 +717,258 @@ Provide specific improvements."""
523
  return lang_prompts.get(language, lang_prompts["Korean"])
524
 
525
  def create_writer_prompt(self, writer_number: int, director_plan: str,
526
- previous_content: str, user_query: str,
527
- language: str) -> str:
528
- """작가 프롬프트 (내면 서술 중심)"""
 
 
 
529
 
530
  lang_prompts = {
531
- "Korean": f"""당신은 작가 {writer_number}번입니다. 한국 현대 문학의 전통에 따라 작성하세요.
 
532
 
533
- **마스터플랜:**
534
  {director_plan}
535
 
536
- **이전 내용:**
537
- {previous_content[-2000:] if previous_content else "시작"}
 
 
 
 
 
 
538
 
539
  **작성 지침:**
540
 
541
- 1. **분량**: 1,300-1,500 단어
 
 
542
 
543
- 2. **서술 방식**
544
- - 인물의 내면 의식을 깊이 탐구
545
- - 관찰과 성찰의 교차
546
- - 현재와 과거의 자연스러운 오버랩
547
 
548
- 3. **문체**
549
- - 담담하면서도 날카로운 시선
550
- - 일상어 속에 스며든 시적 표현
551
- - 짧은 문장과 문장의 리듬감
552
 
553
- 4. **내용 전개**
554
- - 사건보다 작은 순간의 의미
555
- - 대화는 최소화, 있어도 함축적
556
- - 인물의 인식 변화가 플롯
557
 
558
- 5. **사회적 맥락**
559
- - 개인의 일상에 스며든 구조적 억압
560
- - 직접적 비판보다 간접적 드러냄
561
- - 독자가 스스로 깨닫게 하는 서술
562
 
563
- **반드시 포함할 요소:**
564
- - 인물의 내적 독백이나 의식의 흐름
565
- - 구체적인 감각적 디테일
566
- - 사회적 맥락을 암시하는 일상의 순간
567
- - 상징이나 은유의 자연스러운 활용
568
 
569
- 깊이 있는 내면 탐구와 섬세한 사회 관찰을 보여주세요.""",
570
-
571
- "English": f"""You are Writer #{writer_number}. Write in the contemporary literary tradition.
572
 
573
- **Masterplan:**
 
 
 
574
  {director_plan}
575
 
576
- **Previous Content:**
577
- {previous_content[-2000:] if previous_content else "Beginning"}
 
 
 
 
 
 
578
 
579
  **Writing Guidelines:**
580
 
581
- 1. **Length**: 1,300-1,500 words
 
 
582
 
583
- 2. **Narrative Approach**
584
- - Deep exploration of character consciousness
585
- - Intersection of observation and reflection
586
- - Natural overlap of present and past
587
 
588
- 3. **Style**
589
- - Understated yet sharp perspective
590
- - Poetic expression within everyday language
591
- - Rhythm of short and long sentences
592
 
593
- 4. **Development**
594
- - Meaning in small moments over big events
595
- - Minimal, implicit dialogue
596
- - Character perception shifts as plot
597
 
598
- 5. **Social Context**
599
- - Structural oppression in daily life
600
- - Indirect rather than direct critique
601
- - Let readers discover meaning
602
 
603
- **Must Include:**
604
- - Interior monologue or stream of consciousness
605
- - Concrete sensory details
606
- - Everyday moments revealing social context
607
- - Natural use of symbols and metaphors
608
 
609
- Show deep interior exploration and subtle social observation."""
610
  }
611
 
612
  return lang_prompts.get(language, lang_prompts["Korean"])
613
 
614
- def create_critic_consistency_prompt(self, all_content: str, user_query: str,
615
- language: str) -> str:
616
- """비평가 중간 검토"""
617
- return f"""문학 비평가로서 현재까지의 작품을 검토하세요.
 
 
 
 
 
 
 
618
 
619
  **원 주제:** {user_query}
620
 
 
 
 
 
 
621
  **작품 내용 (최근 부분):**
622
- {all_content[-3000:]}
 
 
 
 
 
 
 
 
 
 
 
 
623
 
624
- **검토 항목:**
 
 
 
625
 
626
- 1. **내적 일관성**
627
- - 인물의 의식과 행동의 일치
628
- - 서술 시점의 일관성
629
- - 문체와 톤의 통일성
 
 
 
 
 
 
630
 
631
- 2. **주제 심화**
632
- - 초기 설정한 사회적 문제의 지속적 탐구
633
- - 상징과 은유의 발전
634
- - 깊이 있는 성찰의 누적
635
 
636
- 3. **문학적 완성도**
637
- - 진부함이나 상투성 회피
638
- - 독창적 표현과 관찰
639
- - 여운과 함축성
640
 
641
- 작가에게 구체적 개선 방향을 제시하세요."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
642
 
643
  def create_writer_revision_prompt(self, writer_number: int, initial_content: str,
644
  critic_feedback: str, language: str) -> str:
645
  """작가 수정 프롬프트"""
 
 
646
  return f"""작가 {writer_number}번, 비평을 반영하여 수정하세요.
647
 
648
  **초안:**
649
  {initial_content}
650
 
651
- **비평:**
652
  {critic_feedback}
653
 
654
- **수정 방향:**
655
- 1. 내면 서술 강화
656
- 2. 사회적 맥락 심화
657
- 3. 문학적 표현 개선
658
- 4. 진부함 제거
659
 
 
660
  수정본만 제시하세요."""
661
 
662
- def create_critic_final_prompt(self, complete_novel: str, language: str) -> str:
663
- """최종 비평"""
664
- return f"""완성된 소설의 문학적 가치를 평가하세요.
665
 
666
- **작품 (일부):**
667
- {complete_novel[-3000:]}
 
668
 
669
- **평가 기준:**
 
670
 
671
- 1. **주제 의식 (30점)**
672
- - 사회 비판의 예리함
673
- - 인간 조건에 대한 통찰
674
- - 현대성과 보편성의 조화
675
 
676
- 2. **인물과 심리 (25점)**
677
- - 내면 묘사의 깊이
678
- - 인물의 신빙성
679
- - 복잡성과 모순의 포착
 
680
 
681
- 3. **문체와 기법 (25점)**
682
- - 문장의 완성도
 
 
683
  - 상징과 은유의 효과
684
- - 독창성과 참신함
685
 
686
- 4. **구조와 완결성 (20점)**
687
- - 전체 구성의 균형
688
- - 여운과 개방성
689
- - 독자에게 던지는 질문
 
 
 
 
 
690
 
691
  **총점: /100점**
692
 
693
- 한국 현대 문학의 맥락에서 작품의 의의를 논하세요."""
694
 
695
  # --- LLM 호출 함수들 ---
696
  def call_llm_sync(self, messages: List[Dict[str, str]], role: str, language: str) -> str:
@@ -706,13 +984,16 @@ Show deep interior exploration and subtle social observation."""
706
  system_prompts = self.get_system_prompts(language)
707
  full_messages = [{"role": "system", "content": system_prompts.get(role, "")}, *messages]
708
 
 
 
 
709
  payload = {
710
  "model": self.model_id,
711
  "messages": full_messages,
712
- "max_tokens": 10000,
713
- "temperature": 0.8, # 더 창의적인 출력
714
  "top_p": 0.95,
715
- "presence_penalty": 0.5, # 반복 방지
716
  "frequency_penalty": 0.3,
717
  "stream": True
718
  }
@@ -769,46 +1050,40 @@ Show deep interior exploration and subtle social observation."""
769
  """역할별 시스템 프롬프트"""
770
  base_prompts = {
771
  "Korean": {
772
- "director": """당신은 한강, 김애란, 천명관 등 한국 현대 작가들의 작품을 깊이 이해하는 문학 기획자입니다.
773
- 사회비판적 리얼리즘과 심리적 깊이를 결합한 작품을 기획하세요.
774
- 개인의 내면과 사회 구조의 상호작용을 섬세하게 포착하는 것이 핵심입니다.""",
775
 
776
- "critic": """당신은 현대 한국 문학을 깊이 이해하는 비평가입니다.
777
- 작품이 진부한 사회 비판이나 표면적 심리 묘사에 그치지 않고,
778
- 진정한 문학적 가치를 지니는지 엄격하게 평가하세요.""",
779
 
780
- "writer_base": """당신은 현대 한국 문학의 전통을 잇는 작가입니다.
781
- 내면 의식의 흐름과 날카로운 사회 관찰을 결합하여,
782
- 독자에게 깊은 여운을 남기는 문장을 쓰세요.
783
- '보여주기'보다 '의식하기'를, 사건보다 인식을 중시하세요."""
784
  },
785
  "English": {
786
- "director": """You are a literary planner deeply versed in contemporary social realist fiction.
787
- Plan works that combine social critique with psychological depth.
788
- The key is capturing the subtle interplay between individual consciousness and social structures.""",
789
 
790
- "critic": """You are a critic well-versed in contemporary literary fiction.
791
- Evaluate whether works go beyond superficial social commentary or psychology
792
- to achieve genuine literary value.""",
793
 
794
- "writer_base": """You are a writer in the contemporary literary tradition.
795
- Combine stream of consciousness with sharp social observation
796
- to create resonant prose. Prioritize consciousness over showing,
797
- perception over events."""
798
  }
799
  }
800
 
801
  prompts = base_prompts.get(language, base_prompts["Korean"]).copy()
802
 
803
  # 특수 작가 프롬프트
804
- if language == "Korean":
805
- prompts["writer1"] = prompts["writer_base"] + "\n특히 도입부에서 독자를 작품의 분위기로 서서히 끌어들이세요."
806
- prompts["writer5"] = prompts["writer_base"] + "\n중반부의 심리적 밀도를 높이고 갈등을 내면화하세요."
807
- prompts["writer10"] = prompts["writer_base"] + "\n열린 결말로 독자에게 질문을 던지세요."
808
-
809
- for i in range(2, 10):
810
- if f"writer{i}" not in prompts:
811
- prompts[f"writer{i}"] = prompts["writer_base"]
812
 
813
  return prompts
814
 
@@ -824,6 +1099,10 @@ perception over events."""
824
  query = session['user_query']
825
  language = session['language']
826
  resume_from_stage = session['current_stage'] + 1
 
 
 
 
827
  else:
828
  self.current_session_id = NovelDatabase.create_session(query, language)
829
  logger.info(f"Created new session: {self.current_session_id}")
@@ -834,22 +1113,27 @@ perception over events."""
834
  "name": s['stage_name'],
835
  "status": s['status'],
836
  "content": s.get('content', ''),
837
- "literary_score": s.get('literary_score', 0.0)
 
838
  } for s in NovelDatabase.get_stages(self.current_session_id)]
839
 
840
- for stage_idx in range(resume_from_stage, len(LITERARY_STAGES)):
841
- role, stage_name = LITERARY_STAGES[stage_idx]
 
 
 
842
  if stage_idx >= len(stages):
843
  stages.append({
844
  "name": stage_name,
845
  "status": "active",
846
  "content": "",
847
- "literary_score": 0.0
 
848
  })
849
  else:
850
  stages[stage_idx]["status"] = "active"
851
 
852
- yield "", stages, self.current_session_id
853
 
854
  prompt = self.get_stage_prompt(stage_idx, role, query, language, stages)
855
  stage_content = ""
@@ -857,25 +1141,38 @@ perception over events."""
857
  for chunk in self.call_llm_streaming([{"role": "user", "content": prompt}], role, language):
858
  stage_content += chunk
859
  stages[stage_idx]["content"] = stage_content
860
- yield "", stages, self.current_session_id
861
-
862
- # 문학적 점수 평가
863
- literary_score = self.evaluate_literary_quality(stage_content, role)
864
- stages[stage_idx]["literary_score"] = literary_score
 
 
 
 
 
 
865
 
866
  stages[stage_idx]["status"] = "complete"
867
  NovelDatabase.save_stage(
868
  self.current_session_id, stage_idx, stage_name, role,
869
- stage_content, "complete", literary_score
870
  )
871
- yield "", stages, self.current_session_id
 
 
 
 
 
 
872
 
873
  # 최종 소설 정리
874
  final_novel = NovelDatabase.get_writer_content(self.current_session_id)
875
- final_report = self.generate_literary_report(final_novel, language)
 
876
 
877
  NovelDatabase.update_final_novel(self.current_session_id, final_novel, final_report)
878
- yield f"✅ 소설 완성! 총 {len(final_novel.split())}단어", stages, self.current_session_id
879
 
880
  except Exception as e:
881
  logger.error(f"소설 생성 프로세스 오류: {e}", exc_info=True)
@@ -894,12 +1191,20 @@ perception over events."""
894
 
895
  if 3 <= stage_idx <= 12: # 작가 초안
896
  writer_num = stage_idx - 2
897
- previous_content = self.get_all_content(stages, stage_idx)
898
- return self.create_writer_prompt(writer_num, master_plan, previous_content, query, language)
 
 
 
 
 
 
899
 
900
  if stage_idx == 13: # 비평가 중간 검토
901
- all_content = self.get_all_content(stages, stage_idx)
902
- return self.create_critic_consistency_prompt(all_content, query, language)
 
 
903
 
904
  if 14 <= stage_idx <= 23: # 작가 수정
905
  writer_num = stage_idx - 13
@@ -908,14 +1213,15 @@ perception over events."""
908
  return self.create_writer_revision_prompt(writer_num, initial_content, feedback, language)
909
 
910
  if stage_idx == 24: # 최종 검토
911
- complete_novel = self.get_all_writer_content(stages)
912
- return self.create_critic_final_prompt(complete_novel, language)
 
913
 
914
  return ""
915
 
916
  def create_director_revision_prompt(self, initial_plan: str, critic_feedback: str, user_query: str, language: str) -> str:
917
  """감독자 수정 프롬프트"""
918
- return f"""비평을 반영하여 기획을 수정하세요.
919
 
920
  **원 주제:** {user_query}
921
 
@@ -925,60 +1231,82 @@ perception over events."""
925
  **비평:**
926
  {critic_feedback}
927
 
928
- **수정 방향:**
929
- 1. 사회비판적 깊이 강화
930
- 2. 인물의 내면 복잡성 증대
931
- 3. 상징과 은유 정교화
932
- 4. 진부함 제거
933
 
934
- 수정된 마스터플랜을 제시하세요."""
935
 
936
- def get_all_content(self, stages: List[Dict], current_stage: int) -> str:
937
- """현재까지의 모든 내용"""
938
- contents = []
939
- for i, s in enumerate(stages):
940
- if i < current_stage and s["content"] and "writer" in s.get("name", ""):
941
- contents.append(s["content"])
942
- return "\n\n".join(contents)
 
 
 
 
943
 
944
- def get_all_writer_content(self, stages: List[Dict]) -> str:
945
- """모든 작가 최종본"""
946
  contents = []
947
  for i, s in enumerate(stages):
948
- if 14 <= i <= 23 and s["content"]:
949
  contents.append(s["content"])
950
  return "\n\n".join(contents)
951
 
952
- def evaluate_literary_quality(self, content: str, role: str) -> float:
953
- """문학적 품질 평가"""
954
- if not content or not role.startswith("writer"):
955
- return 0.0
956
 
957
- score = 5.0 # 기본 점수
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
958
 
959
- # 내면 서술 평가
960
- introspective_patterns = ['생각했다', '느꼈다', '기억', '의식', '떠올렸다',
961
- '마음', '머릿속', '가슴', 'thought', 'felt', 'remembered']
962
- introspection_count = sum(1 for pattern in introspective_patterns if pattern in content)
963
- score += min(2.0, introspection_count * 0.2)
964
 
965
- # 감각적 디테일
966
- sensory_patterns = ['냄새', '소리', '빛', '그림자', '촉감', '맛', '온도', '색',
967
- 'smell', 'sound', 'light', 'shadow', 'touch', 'taste']
968
- sensory_count = sum(1 for pattern in sensory_patterns if pattern in content)
969
- score += min(1.5, sensory_count * 0.15)
970
 
971
- # 사회적 맥락
972
- social_patterns = ['임대', '계급', '빈곤', '격차', '차별', '소외', '불평등',
973
- 'rent', 'class', 'poverty', 'gap', 'discrimination']
974
- social_count = sum(1 for pattern in social_patterns if pattern in content)
975
- score += min(1.5, social_count * 0.3)
976
 
977
  return min(10.0, score)
978
 
979
- def generate_literary_report(self, complete_novel: str, language: str) -> str:
980
- """최종 문학적 평가 보고서"""
981
- prompt = self.create_critic_final_prompt(complete_novel, language)
982
  try:
983
  report = self.call_llm_sync([{"role": "user", "content": prompt}], "critic", language)
984
  return report
@@ -994,7 +1322,7 @@ def process_query(query: str, language: str, session_id: Optional[str] = None) -
994
  yield "", "", "❌ 주제를 입력해주세요.", session_id
995
  return
996
 
997
- system = LiteraryNovelSystem()
998
  stages_markdown = ""
999
  novel_content = ""
1000
 
@@ -1011,13 +1339,14 @@ def process_query(query: str, language: str, session_id: Optional[str] = None) -
1011
  def get_active_sessions(language: str) -> List[str]:
1012
  """활성 세션 목록"""
1013
  sessions = NovelDatabase.get_active_sessions()
1014
- return [f"{s['session_id'][:8]}... - {s['user_query'][:50]}... ({s['created_at']})"
1015
  for s in sessions]
1016
 
1017
  def auto_recover_session(language: str) -> Tuple[Optional[str], str]:
1018
  """최근 세션 자동 복구"""
1019
- latest_session = NovelDatabase.get_latest_active_session()
1020
- if latest_session:
 
1021
  return latest_session['session_id'], f"세션 {latest_session['session_id'][:8]}... 복��됨"
1022
  return None, "복구할 세션이 없습니다."
1023
 
@@ -1057,17 +1386,27 @@ def download_novel(novel_text: str, format_type: str, language: str, session_id:
1057
  def format_stages_display(stages: List[Dict]) -> str:
1058
  """단계별 진행 상황 표시"""
1059
  markdown = "## 🎬 진행 상황\n\n"
 
 
 
 
 
1060
  for i, stage in enumerate(stages):
1061
  status_icon = "✅" if stage['status'] == 'complete' else "🔄" if stage['status'] == 'active' else "⏳"
1062
  markdown += f"{status_icon} **{stage['name']}**"
1063
 
1064
- if stage.get('literary_score', 0) > 0:
1065
- markdown += f" (문학성: {stage['literary_score']:.1f}/10)"
 
 
 
1066
 
1067
  markdown += "\n"
 
1068
  if stage['content']:
1069
  preview = stage['content'][:200] + "..." if len(stage['content']) > 200 else stage['content']
1070
  markdown += f"> {preview}\n\n"
 
1071
  return markdown
1072
 
1073
  def format_novel_display(novel_text: str) -> str:
@@ -1076,24 +1415,32 @@ def format_novel_display(novel_text: str) -> str:
1076
  return "아직 완성된 내용이 없습니다."
1077
 
1078
  formatted = "# 📖 완성된 소설\n\n"
1079
- formatted += novel_text
 
 
 
 
 
 
 
 
 
 
1080
 
1081
  return formatted
1082
 
1083
  def export_to_docx(content: str, filename: str, language: str, session_id: str) -> str:
1084
- """DOCX 파일로 내보내기 (문학 소설 형식)"""
1085
  doc = Document()
1086
 
1087
- # 신국판 크기 (152mm x 225mm)
1088
  section = doc.sections[0]
1089
- section.page_height = Inches(8.86) # 225mm
1090
- section.page_width = Inches(5.98) # 152mm
1091
-
1092
- # 여백 설정
1093
- section.top_margin = Inches(0.79) # 20mm
1094
- section.bottom_margin = Inches(0.79)
1095
- section.left_margin = Inches(0.79)
1096
- section.right_margin = Inches(0.79)
1097
 
1098
  # 세션 정보
1099
  session = NovelDatabase.get_session(session_id)
@@ -1102,26 +1449,27 @@ def export_to_docx(content: str, filename: str, language: str, session_id: str)
1102
  title_para = doc.add_paragraph()
1103
  title_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
1104
 
1105
- # 빈 줄 추가
1106
- for _ in range(8):
1107
- doc.add_paragraph()
1108
-
1109
  if session:
1110
  title_run = title_para.add_run(session["user_query"])
1111
- title_run.font.size = Pt(16)
1112
- title_run.font.name = '바탕'
 
 
 
 
 
 
 
1113
 
1114
  # 페이지 나누기
1115
  doc.add_page_break()
1116
 
1117
  # 본문 스타일 설정
1118
  style = doc.styles['Normal']
1119
- style.font.name = '바탕'
1120
- style.font.size = Pt(10.5)
1121
- style.paragraph_format.line_spacing = 1.8
1122
- style.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
1123
- style.paragraph_format.first_line_indent = Inches(0.35)
1124
- style.paragraph_format.space_after = Pt(3)
1125
 
1126
  # 본문 추가
1127
  paragraphs = content.split('\n\n')
@@ -1142,52 +1490,50 @@ def export_to_txt(content: str, filename: str) -> str:
1142
  return filepath
1143
 
1144
 
1145
- # CSS 스타일 (문학적 분위기)
1146
  custom_css = """
1147
  .gradio-container {
1148
- background: linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 50%, #1a1a1a 100%);
1149
  min-height: 100vh;
1150
  }
1151
 
1152
  .main-header {
1153
- background-color: rgba(255, 255, 255, 0.03);
1154
  backdrop-filter: blur(10px);
1155
  padding: 30px;
1156
  border-radius: 12px;
1157
  margin-bottom: 30px;
1158
  text-align: center;
1159
  color: white;
1160
- border: 1px solid rgba(255, 255, 255, 0.1);
1161
  }
1162
 
1163
- .literary-note {
1164
- background-color: rgba(255, 255, 255, 0.05);
1165
- border-left: 3px solid #888;
1166
  padding: 15px;
1167
  margin: 20px 0;
1168
  border-radius: 8px;
1169
- color: #ccc;
1170
- font-style: italic;
1171
- font-family: 'Georgia', serif;
1172
  }
1173
 
1174
  .input-section {
1175
- background-color: rgba(255, 255, 255, 0.05);
1176
  backdrop-filter: blur(10px);
1177
  padding: 20px;
1178
  border-radius: 12px;
1179
  margin-bottom: 20px;
1180
- border: 1px solid rgba(255, 255, 255, 0.1);
1181
  }
1182
 
1183
  .session-section {
1184
- background-color: rgba(255, 255, 255, 0.05);
1185
  backdrop-filter: blur(10px);
1186
  padding: 15px;
1187
  border-radius: 8px;
1188
  margin-top: 20px;
1189
  color: white;
1190
- border: 1px solid rgba(255, 255, 255, 0.1);
1191
  }
1192
 
1193
  #stages-display {
@@ -1200,15 +1546,12 @@ custom_css = """
1200
  }
1201
 
1202
  #novel-output {
1203
- background-color: rgba(255, 255, 255, 0.98);
1204
- padding: 40px;
1205
  border-radius: 12px;
1206
  max-height: 700px;
1207
  overflow-y: auto;
1208
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
1209
- font-family: '바탕', 'Batang', 'Georgia', serif;
1210
- line-height: 2;
1211
- color: #333;
1212
  }
1213
 
1214
  .download-section {
@@ -1219,50 +1562,41 @@ custom_css = """
1219
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
1220
  }
1221
 
1222
- /* 문학적 스타일 */
1223
- #novel-output p {
1224
- text-indent: 2em;
1225
- margin-bottom: 1em;
1226
- text-align: justify;
 
 
1227
  }
1228
 
1229
- #novel-output h1 {
1230
- color: #1a1a1a;
1231
- font-weight: normal;
1232
- text-align: center;
1233
- margin: 2em 0;
1234
- font-size: 1.8em;
1235
- }
1236
-
1237
- /* 인용문이나 내적 독백 스타일 */
1238
- #novel-output blockquote {
1239
- margin: 1.5em 2em;
1240
- font-style: italic;
1241
- color: #555;
1242
- border-left: 3px solid #ccc;
1243
- padding-left: 1em;
1244
  }
1245
  """
1246
 
1247
 
1248
  # Gradio 인터페이스 생성
1249
  def create_interface():
1250
- with gr.Blocks(css=custom_css, title="AI 문학 소설 생성 시스템") as interface:
1251
  gr.HTML("""
1252
  <div class="main-header">
1253
- <h1 style="font-size: 2.5em; margin-bottom: 10px; font-weight: normal;">
1254
- 📖 AI 문학 소설 생성 시스템
1255
  </h1>
1256
- <h3 style="color: #ccc; margin-bottom: 20px; font-weight: normal;">
1257
- 현대 한국 문학의 전통을 잇는 소설 창작
1258
  </h3>
1259
- <p style="font-size: 1.1em; color: #ddd; max-width: 800px; margin: 0 auto;">
1260
- 사회비판적 리얼리즘과 심리적 깊이를 결합한 30페이지 중편소설을 생성합니다.
1261
  <br>
1262
- 개인의 내면과 사회 구조의 상호작용을 섬세하게 포착합니다.
1263
  </p>
1264
- <div class="literary-note">
1265
- "가장 개인적인 것이 가장 보편적인 것이다"
1266
  </div>
1267
  </div>
1268
  """)
@@ -1275,7 +1609,7 @@ def create_interface():
1275
  with gr.Group(elem_classes=["input-section"]):
1276
  query_input = gr.Textbox(
1277
  label="소설 주제 / Novel Theme",
1278
- placeholder="현대 사회의 문제나 인간의 조건에 대한 주제를 입력하세요...\nEnter themes about contemporary social issues or human condition...",
1279
  lines=4
1280
  )
1281
 
@@ -1297,7 +1631,7 @@ def create_interface():
1297
 
1298
  # 세션 관리
1299
  with gr.Group(elem_classes=["session-section"]):
1300
- gr.Markdown("### 💾 이전 세션 재개")
1301
  session_dropdown = gr.Dropdown(
1302
  label="세션 선택",
1303
  choices=[],
@@ -1306,10 +1640,10 @@ def create_interface():
1306
  with gr.Row():
1307
  refresh_btn = gr.Button("🔄 목록 새로고침", scale=1)
1308
  resume_btn = gr.Button("▶️ 선택 재개", variant="secondary", scale=1)
1309
- auto_recover_btn = gr.Button("♻️ 자동 복구", scale=1)
1310
 
1311
  with gr.Column(scale=2):
1312
- with gr.Tab("📝 창작 과정"):
1313
  stages_display = gr.Markdown(
1314
  value="창작 과정이 여기에 표시됩니다...",
1315
  elem_id="stages-display"
@@ -1339,21 +1673,20 @@ def create_interface():
1339
  # 숨겨진 상태
1340
  novel_text_state = gr.State("")
1341
 
1342
- # 예제 (문학적 주제)
1343
  with gr.Row():
1344
  gr.Examples(
1345
  examples=[
1346
- ["도시 빈민의 주거권과 계급 갈등"],
1347
- ["비정규직 청년의 불안과 소외"],
1348
- ["돌봄 노동자의 보이지 않는 "],
1349
- ["젠트리피케이션과 도시 난민"],
1350
- ["The invisible labor of care workers"],
1351
- ["Digital divide and social isolation"],
1352
- ["세대 단절과 노인 빈곤"],
1353
- ["환경 재난 시대의 불평등"]
1354
  ],
1355
  inputs=query_input,
1356
- label="💡 문학적 주제 예시"
1357
  )
1358
 
1359
  # 이벤트 핸들러
@@ -1367,7 +1700,7 @@ def create_interface():
1367
 
1368
  def handle_auto_recover(language):
1369
  session_id, message = auto_recover_session(language)
1370
- return session_id
1371
 
1372
  # 이벤트 연결
1373
  submit_btn.click(
@@ -1383,7 +1716,7 @@ def create_interface():
1383
  )
1384
 
1385
  resume_btn.click(
1386
- fn=lambda x: x,
1387
  inputs=[session_dropdown],
1388
  outputs=[current_session_id]
1389
  ).then(
@@ -1395,7 +1728,7 @@ def create_interface():
1395
  auto_recover_btn.click(
1396
  fn=handle_auto_recover,
1397
  inputs=[language_select],
1398
- outputs=[current_session_id]
1399
  ).then(
1400
  fn=resume_session,
1401
  inputs=[current_session_id, language_select],
@@ -1413,7 +1746,7 @@ def create_interface():
1413
  )
1414
 
1415
  def handle_download(format_type, language, session_id, novel_text):
1416
- if not session_id:
1417
  return gr.update(visible=False)
1418
 
1419
  file_path = download_novel(novel_text, format_type, language, session_id)
@@ -1439,11 +1772,13 @@ def create_interface():
1439
 
1440
  # 메인 실행
1441
  if __name__ == "__main__":
1442
- logger.info("AI 문학 소설 생성 시스템 시작...")
1443
  logger.info("=" * 60)
1444
 
1445
  # 환경 확인
1446
  logger.info(f"API 엔드포인트: {API_URL}")
 
 
1447
 
1448
  if BRAVE_SEARCH_API_KEY:
1449
  logger.info("웹 검색이 활성화되었습니다.")
@@ -1470,4 +1805,4 @@ if __name__ == "__main__":
1470
  server_port=7860,
1471
  share=False,
1472
  debug=True
1473
- )
 
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_v5.db"
42
+
43
+ # 목표 분량 설정
44
+ TARGET_WORDS = 8000 # 안전 마진을 위해 8000단어
45
+ MIN_WORDS_PER_WRITER = 800 # 각 작가 최소 분량
46
 
47
  # --- 환경 변수 검증 ---
48
  if not FRIENDLI_TOKEN:
 
55
  # --- 전역 변수 ---
56
  db_lock = threading.Lock()
57
 
58
+ # 서사 진행 단계 정의
59
+ NARRATIVE_PHASES = [
60
+ "도입: 일상과 균열",
61
+ "발전 1: 불안의 고조",
62
+ "발전 2: 외부 충격",
63
+ "발전 3: 내적 갈등 심화",
64
+ "절정 1: 위기의 정점",
65
+ "절정 2: 선택의 순간",
66
+ "하강 1: 결과와 여파",
67
+ "하강 2: 새로운 인식",
68
+ "결말 1: 변화된 일상",
69
+ "결말 2: 열린 질문"
70
+ ]
71
+
72
+ # 단계별 구성
73
+ PROGRESSIVE_STAGES = [
74
+ ("director", "🎬 감독자: 통합된 서사 구조 기획"),
75
+ ("critic", "📝 비평가: 서사 진행성과 깊이 검토"),
76
  ("director", "🎬 감독자: 수정된 마스터플랜"),
77
  ] + [
78
+ (f"writer{i}", f"✍️ 작가 {i}: 초안 - {NARRATIVE_PHASES[i-1]}")
79
  for i in range(1, 11)
80
  ] + [
81
+ ("critic", "📝 비평가: 중간 검토 (서사 누적성과 변화)"),
82
  ] + [
83
+ (f"writer{i}", f"✍️ 작가 {i}: 수정본 - {NARRATIVE_PHASES[i-1]}")
84
  for i in range(1, 11)
85
  ] + [
86
  ("critic", f"📝 비평가: 최종 검토 및 문학적 평가"),
 
89
 
90
  # --- 데이터 클래스 ---
91
  @dataclass
92
+ class CharacterArc:
93
+ """인물의 변화 궤적 추적"""
94
  name: str
95
+ initial_state: Dict[str, Any] # 초기 상태
96
+ phase_states: Dict[int, Dict[str, Any]] = field(default_factory=dict) # 단계별 상태
97
+ transformations: List[str] = field(default_factory=list) # 주요 변화들
98
+ relationships_evolution: Dict[str, List[str]] = field(default_factory=dict) # 관계 변화
99
+
 
 
 
 
 
100
  @dataclass
101
+ class PlotThread:
102
+ """플롯 라인 추적"""
103
+ thread_id: str
104
+ description: str
105
+ introduction_phase: int
106
+ development_phases: List[int]
107
+ resolution_phase: Optional[int]
108
+ status: str = "active" # active, resolved, suspended
109
 
110
  @dataclass
111
+ class SymbolicEvolution:
112
+ """상징의 의미 변화 추적"""
113
+ symbol: str
114
+ initial_meaning: str
115
+ phase_meanings: Dict[int, str] = field(default_factory=dict)
116
+ transformation_complete: bool = False
 
117
 
118
 
119
  # --- 핵심 로직 클래스 ---
120
+ class ProgressiveNarrativeTracker:
121
+ """서사 진행과 누적을 추적하는 시스템"""
122
  def __init__(self):
123
+ self.character_arcs: Dict[str, CharacterArc] = {}
124
+ self.plot_threads: Dict[str, PlotThread] = {}
125
+ self.symbolic_evolutions: Dict[str, SymbolicEvolution] = {}
126
+ self.phase_summaries: Dict[int, str] = {}
127
+ self.accumulated_events: List[Dict[str, Any]] = []
128
+ self.thematic_deepening: List[str] = []
129
 
130
+ def register_character_arc(self, name: str, initial_state: Dict[str, Any]):
131
+ """캐릭터 아크 등록"""
132
+ self.character_arcs[name] = CharacterArc(name=name, initial_state=initial_state)
133
+ logger.info(f"Character arc registered: {name}")
134
 
135
+ def update_character_state(self, name: str, phase: int, new_state: Dict[str, Any], transformation: str):
136
+ """캐릭터 상태 업데이트 및 변화 기록"""
137
+ if name in self.character_arcs:
138
+ arc = self.character_arcs[name]
139
+ arc.phase_states[phase] = new_state
140
+ arc.transformations.append(f"Phase {phase}: {transformation}")
141
+ logger.info(f"Character {name} transformed in phase {phase}: {transformation}")
142
+
143
+ def add_plot_thread(self, thread_id: str, description: str, intro_phase: int):
144
+ """새로운 플롯 라인 추가"""
145
+ self.plot_threads[thread_id] = PlotThread(
146
+ thread_id=thread_id,
147
+ description=description,
148
+ introduction_phase=intro_phase,
149
+ development_phases=[]
150
+ )
151
 
152
+ def develop_plot_thread(self, thread_id: str, phase: int):
153
+ """플롯 라인 발전"""
154
+ if thread_id in self.plot_threads:
155
+ self.plot_threads[thread_id].development_phases.append(phase)
156
+
157
+ def check_narrative_progression(self, current_phase: int) -> Tuple[bool, List[str]]:
158
+ """서사가 실제로 진행되고 있는지 확인"""
159
  issues = []
160
 
161
+ # 1. 캐릭터 변화 확인
162
+ static_characters = []
163
+ for name, arc in self.character_arcs.items():
164
+ if len(arc.transformations) < current_phase // 3: # 최소 3단계마다 변화 필요
165
+ static_characters.append(name)
166
+
167
+ if static_characters:
168
+ issues.append(f"다음 인물들의 변화가 부족합니다: {', '.join(static_characters)}")
169
+
170
+ # 2. 플롯 진행 확인
171
+ unresolved_threads = []
172
+ for thread_id, thread in self.plot_threads.items():
173
+ if thread.status == "active" and len(thread.development_phases) < 2:
174
+ unresolved_threads.append(thread.description)
175
+
176
+ if unresolved_threads:
177
+ issues.append(f"진전되지 않은 플롯: {', '.join(unresolved_threads)}")
178
+
179
+ # 3. 상징 발전 확인
180
+ static_symbols = []
181
+ for symbol, evolution in self.symbolic_evolutions.items():
182
+ if len(evolution.phase_meanings) < current_phase // 4:
183
+ static_symbols.append(symbol)
184
+
185
+ if static_symbols:
186
+ issues.append(f"의미가 발전하지 않은 상징: {', '.join(static_symbols)}")
187
 
188
+ return len(issues) == 0, issues
189
+
190
+ def generate_phase_requirements(self, phase: int) -> str:
191
+ """각 단계별 필수 요구사항 생성"""
192
+ requirements = []
193
+
194
+ # 이전 단계 요약
195
+ if phase > 1 and (phase-1) in self.phase_summaries:
196
+ requirements.append(f"이전 단계 핵심: {self.phase_summaries[phase-1]}")
197
+
198
+ # 단계별 특수 요구사항
199
+ phase_name = NARRATIVE_PHASES[phase-1] if phase <= 10 else "수정"
200
+
201
+ if "도입" in phase_name:
202
+ requirements.append("- 일상의 균열을 보여주되, 큰 사건이 아닌 미묘한 변화로 시작")
203
+ requirements.append("- 주요 인물들의 초기 상태와 관계 설정")
204
+ requirements.append("- 핵심 상징 도입 (자연스럽게)")
205
 
206
+ elif "발전" in phase_name:
207
+ requirements.append("- 이전 단계의 균열/갈등이 구체화되고 심화")
208
+ requirements.append("- 새로운 사건이나 인식이 추가되어 복잡성 증가")
209
+ requirements.append("- 인물 간 관계의 미묘한 변화")
210
+
211
+ elif "절정" in phase_name:
212
+ requirements.append("- 축적된 갈등이 임계점에 도달")
213
+ requirements.append("- 인물의 내적 선택이나 인식의 전환점")
214
+ requirements.append("- 상징의 의미가 전복되거나 심화")
215
+
216
+ elif "하강" in phase_name:
217
+ requirements.append("- 절정의 여파와 그로 인한 변화")
218
+ requirements.append("- 새로운 균형점을 찾아가는 과정")
219
+ requirements.append("- 인물들의 변화된 관계와 인식")
220
+
221
+ elif "결말" in phase_name:
222
+ requirements.append("- 변화된 일상의 모습")
223
+ requirements.append("- 해결되지 않은 질문들")
224
+ requirements.append("- 여운과 성찰의 여지")
225
+
226
+ # 반복 방지 요구사항
227
+ requirements.append("\n⚠️ 절대 금지사항:")
228
+ requirements.append("- 이전 단계와 동일한 사건이나 갈등 반복")
229
+ requirements.append("- 인물이 같은 생각이나 감정에 머무르기")
230
+ requirements.append("- 플롯이 제자리걸음하기")
231
+
232
+ return "\n".join(requirements)
233
 
234
 
235
  class NovelDatabase:
 
251
  current_stage INTEGER DEFAULT 0,
252
  final_novel TEXT,
253
  literary_report TEXT,
254
+ total_words INTEGER DEFAULT 0,
255
+ narrative_tracker TEXT
256
  )
257
  ''')
258
 
 
266
  content TEXT,
267
  word_count INTEGER DEFAULT 0,
268
  status TEXT DEFAULT 'pending',
269
+ progression_score REAL DEFAULT 0.0,
270
  created_at TEXT DEFAULT (datetime('now')),
271
  updated_at TEXT DEFAULT (datetime('now')),
272
  FOREIGN KEY (session_id) REFERENCES sessions(session_id),
 
275
  ''')
276
 
277
  cursor.execute('''
278
+ CREATE TABLE IF NOT EXISTS plot_threads (
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
279
  id INTEGER PRIMARY KEY AUTOINCREMENT,
280
  session_id TEXT NOT NULL,
281
+ thread_id TEXT NOT NULL,
282
+ description TEXT,
283
+ introduction_phase INTEGER,
284
+ status TEXT DEFAULT 'active',
285
  created_at TEXT DEFAULT (datetime('now')),
286
  FOREIGN KEY (session_id) REFERENCES sessions(session_id)
287
  )
 
315
  @staticmethod
316
  def save_stage(session_id: str, stage_number: int, stage_name: str,
317
  role: str, content: str, status: str = 'complete',
318
+ progression_score: float = 0.0):
319
  word_count = len(content.split()) if content else 0
320
  with NovelDatabase.get_db() as conn:
321
  cursor = conn.cursor()
322
  cursor.execute('''
323
+ INSERT INTO stages (session_id, stage_number, stage_name, role, content, word_count, status, progression_score)
324
  VALUES (?, ?, ?, ?, ?, ?, ?, ?)
325
  ON CONFLICT(session_id, stage_number)
326
+ DO UPDATE SET content=?, word_count=?, status=?, stage_name=?, progression_score=?, updated_at=datetime('now')
327
+ ''', (session_id, stage_number, stage_name, role, content, word_count, status, progression_score,
328
+ content, word_count, status, stage_name, progression_score))
329
+
330
+ # 단어 업데이트
331
+ cursor.execute('''
332
+ UPDATE sessions
333
+ SET total_words = (
334
+ SELECT SUM(word_count)
335
+ FROM stages
336
+ WHERE session_id = ? AND role LIKE 'writer%' AND content IS NOT NULL
337
+ ),
338
+ updated_at = datetime('now'),
339
+ current_stage = ?
340
+ WHERE session_id = ?
341
+ ''', (session_id, stage_number, session_id))
342
+
343
  conn.commit()
344
 
345
  @staticmethod
346
  def get_writer_content(session_id: str) -> str:
347
+ """작가 콘텐츠 가져오기 (수정본 우선)"""
348
  with NovelDatabase.get_db() as conn:
349
  all_content = []
350
  for writer_num in range(1, 11):
351
+ # 수정본이 있으면 수정본을, 없으면 초안을
352
+ row = conn.cursor().execute('''
353
+ SELECT content FROM stages
354
+ WHERE session_id = ? AND role = ?
355
+ AND stage_name LIKE '%수정본%'
356
+ ORDER BY stage_number DESC LIMIT 1
357
+ ''', (session_id, f'writer{writer_num}')).fetchone()
358
+
359
+ if not row or not row['content']:
360
+ # 수정본이 없으면 초안 사용
361
+ row = conn.cursor().execute('''
362
+ SELECT content FROM stages
363
+ WHERE session_id = ? AND role = ?
364
+ AND stage_name LIKE '%초안%'
365
+ ORDER BY stage_number DESC LIMIT 1
366
+ ''', (session_id, f'writer{writer_num}')).fetchone()
367
+
368
  if row and row['content']:
369
+ all_content.append(row['content'].strip())
370
+
371
  return '\n\n'.join(all_content)
372
 
373
+ @staticmethod
374
+ def get_total_words(session_id: str) -> int:
375
+ """총 단어 수 가져오기"""
376
+ with NovelDatabase.get_db() as conn:
377
+ row = conn.cursor().execute(
378
+ 'SELECT total_words FROM sessions WHERE session_id = ?',
379
+ (session_id,)
380
+ ).fetchone()
381
+ return row['total_words'] if row and row['total_words'] else 0
382
+
383
+ @staticmethod
384
+ def save_narrative_tracker(session_id: str, tracker: ProgressiveNarrativeTracker):
385
+ """서사 추적기 저장"""
386
+ with NovelDatabase.get_db() as conn:
387
+ tracker_data = json.dumps({
388
+ 'character_arcs': {k: asdict(v) for k, v in tracker.character_arcs.items()},
389
+ 'plot_threads': {k: asdict(v) for k, v in tracker.plot_threads.items()},
390
+ 'phase_summaries': tracker.phase_summaries,
391
+ 'thematic_deepening': tracker.thematic_deepening
392
+ })
393
+ conn.cursor().execute(
394
+ 'UPDATE sessions SET narrative_tracker = ? WHERE session_id = ?',
395
+ (tracker_data, session_id)
396
+ )
397
+ conn.commit()
398
+
399
+ @staticmethod
400
+ def load_narrative_tracker(session_id: str) -> Optional[ProgressiveNarrativeTracker]:
401
+ """서사 추적기 로드"""
402
+ with NovelDatabase.get_db() as conn:
403
+ row = conn.cursor().execute(
404
+ 'SELECT narrative_tracker FROM sessions WHERE session_id = ?',
405
+ (session_id,)
406
+ ).fetchone()
407
+
408
+ if row and row['narrative_tracker']:
409
+ data = json.loads(row['narrative_tracker'])
410
+ tracker = ProgressiveNarrativeTracker()
411
+
412
+ # 데이터 복원
413
+ for name, arc_data in data.get('character_arcs', {}).items():
414
+ tracker.character_arcs[name] = CharacterArc(**arc_data)
415
+ for thread_id, thread_data in data.get('plot_threads', {}).items():
416
+ tracker.plot_threads[thread_id] = PlotThread(**thread_data)
417
+ tracker.phase_summaries = data.get('phase_summaries', {})
418
+ tracker.thematic_deepening = data.get('thematic_deepening', [])
419
+
420
+ return tracker
421
+ return None
422
+
423
  @staticmethod
424
  def get_session(session_id: str) -> Optional[Dict]:
425
  with NovelDatabase.get_db() as conn:
 
445
  def get_active_sessions() -> List[Dict]:
446
  with NovelDatabase.get_db() as conn:
447
  rows = conn.cursor().execute(
448
+ "SELECT session_id, user_query, language, created_at, current_stage, total_words FROM sessions WHERE status = 'active' ORDER BY updated_at DESC LIMIT 10"
449
  ).fetchall()
450
  return [dict(row) for row in rows]
451
 
 
497
  return "\n".join(extracted)
498
 
499
 
500
+ class ProgressiveLiterarySystem:
501
+ """진행형 문학 소설 생성 시스템"""
502
  def __init__(self):
503
  self.token = FRIENDLI_TOKEN
504
  self.api_url = API_URL
505
  self.model_id = MODEL_ID
506
+ self.narrative_tracker = ProgressiveNarrativeTracker()
507
  self.web_search = WebSearchIntegration()
508
  self.current_session_id = None
509
  NovelDatabase.init_db()
 
511
  def create_headers(self):
512
  return {"Authorization": f"Bearer {self.token}", "Content-Type": "application/json"}
513
 
514
+ # --- 프롬프트 생성 함수들 ---
515
  def create_director_initial_prompt(self, user_query: str, language: str) -> str:
516
+ """감독자 초기 기획 - 통합된 서사 구조"""
517
  search_results_str = ""
518
  if self.web_search.enabled:
519
+ queries = [f"{user_query} 사회 문제", f"{user_query} 현대 한국"]
 
520
  for q in queries[:1]:
521
  results = self.web_search.search(q, count=2, language=language)
522
  if results:
523
  search_results_str += self.web_search.extract_relevant_info(results) + "\n"
524
 
525
  lang_prompts = {
526
+ "Korean": f"""당신은 현대 한국 문학의 거장입니다.
527
+ 단편이 아닌 중편 소설(8,000단어 이상)을 위한 통합된 서사 구조를 기획하세요.
 
528
 
529
+ **주제:** {user_query}
530
 
531
  **참고 자료:**
532
  {search_results_str if search_results_str else "N/A"}
533
 
534
+ **필수 요구사항:**
535
+
536
+ 1. **통합된 서사 구조 (가장 중요)**
537
+ - 10개 단계가 유기적으로 연결된 단일 서사
538
+ - 각 단계는 이전 단계의 결과로 자연스럽게 이어짐
539
+ - 반복이 아닌 축적과 발전
540
+
541
+ 단계별 서사 진행:
542
+ 1) 도입: 일상과 균열 - 평범한 일상 속 첫 균열
543
+ 2) 발전 1: 불안의 고조 - 균열이 확대되며 불안 증폭
544
+ 3) 발전 2: 외부 충격 - 예상치 못한 외부 사건
545
+ 4) 발전 3: 내적 갈등 심화 - 가치관의 충돌
546
+ 5) 절정 1: 위기의 정점 - 모든 갈등이 극대화
547
+ 6) 절정 2: 선택의 순간 - 결정적 선택
548
+ 7) 하강 1: 결과와 여파 - 선택의 직접적 결과
549
+ 8) 하강 2: 새로운 인식 - 변화된 세계관
550
+ 9) 결말 1: 변화된 일상 - 새로운 균형
551
+ 10) 결말 2: 열린 질문 - 독자에게 던지는 질문
552
+
553
+ 2. **인물의 변화 궤적**
554
+ - 주인공: 초기 상태 → 중간 변화 → 최종 상태 (명확한 arc)
555
+ - 주요 인물들도 각자의 변화 경험
556
+ - 관계의 역동적 변화
557
+
558
+ 3. **주요 플롯 라인** (2-3개)
559
+ - 메인 플롯: 전체를 관통하는 핵심 갈등
560
+ - 서브 플롯: 메인과 연결되며 주제를 심화
561
+
562
+ 4. **상징의 진화**
563
+ - 핵심 상징 1-2개 설정
564
+ - 단계별로 의미가 변화/심화/전복
565
 
566
+ 5. **사회적 맥락**
567
+ - 개인의 문제가 사회 구조와 연결
568
+ - 구체적인 한국 사회의 현실 반영
 
569
 
570
+ **절대 금지사항:**
571
+ - 동일한 사건이나 상황의 반복
572
+ - 인물이 같은 감정/생각에 머무르기
573
+ - 플롯의 리셋이나 순환 구조
574
+ - 각 단계가 독립된 에피소드로 존재
575
 
576
+ **분량 계획:**
577
+ - 8,000단어 이상
578
+ - 각 단계 평균 800단어
579
+ - 균형 잡힌 서사 전개
 
580
 
581
+ 하나의 강력한 서사가 시작부터 끝까지 관통하는 작품을 기획하세요.""",
 
 
 
582
 
583
+ "English": f"""You are a master of contemporary literary fiction.
584
+ Plan an integrated narrative structure for a novella (8,000+ words), not a collection of short stories.
 
 
585
 
586
+ **Theme:** {user_query}
 
 
 
 
587
 
588
+ **Reference:**
589
+ {search_results_str if search_results_str else "N/A"}
 
 
590
 
591
+ **Essential Requirements:**
592
+
593
+ 1. **Integrated Narrative Structure (Most Important)**
594
+ - Single narrative with 10 organically connected phases
595
+ - Each phase naturally follows from previous results
596
+ - Accumulation and development, not repetition
597
+
598
+ Phase Progression:
599
+ 1) Introduction: Daily life and first crack
600
+ 2) Development 1: Rising anxiety
601
+ 3) Development 2: External shock
602
+ 4) Development 3: Deepening internal conflict
603
+ 5) Climax 1: Peak crisis
604
+ 6) Climax 2: Moment of choice
605
+ 7) Falling Action 1: Direct consequences
606
+ 8) Falling Action 2: New awareness
607
+ 9) Resolution 1: Changed daily life
608
+ 10) Resolution 2: Open questions
609
+
610
+ 2. **Character Transformation Arcs**
611
+ - Protagonist: Clear progression from initial → middle → final state
612
+ - Supporting characters also experience change
613
+ - Dynamic relationship evolution
614
+
615
+ 3. **Plot Threads** (2-3)
616
+ - Main plot: Core conflict throughout
617
+ - Subplots: Connected and deepening themes
618
+
619
+ 4. **Symbolic Evolution**
620
+ - 1-2 core symbols
621
+ - Meaning transforms across phases
622
 
623
+ 5. **Social Context**
624
+ - Individual problems connected to social structures
625
+ - Specific contemporary realities
626
+
627
+ **Absolutely Forbidden:**
628
+ - Repetition of same events/situations
629
+ - Characters stuck in same emotions
630
+ - Plot resets or circular structure
631
+ - Independent episodes
632
 
633
+ **Length Planning:**
634
+ - Total 8,000+ words
635
+ - ~800 words per phase
636
+ - Balanced progression
637
+
638
+ Create a work with one powerful narrative from beginning to end."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
639
  }
640
 
641
  return lang_prompts.get(language, lang_prompts["Korean"])
642
 
643
  def create_critic_director_prompt(self, director_plan: str, user_query: str, language: str) -> str:
644
+ """비평가의 감독자 기획 검토 - 서사 통합성 중심"""
645
  lang_prompts = {
646
+ "Korean": f"""당신은 서사 구조 전문 비평가입니다.
647
+ 이 기획이 진정한 '장편 소설'인지 엄격히 검토하세요.
648
 
649
  **원 주제:** {user_query}
650
 
651
  **감독자 기획:**
652
  {director_plan}
653
 
654
+ **핵심 검토 사항:**
655
+
656
+ 1. **서사의 통합성과 진행성**
657
+ - 10개 단계가 하나의 이야기로 연결되는가?
658
+ - 각 단계가 이전 단계의 필연적 결과인가?
659
+ - 동일한 상황의 반복은 없는가?
660
+
661
+ 2. **인물 변화의 궤적**
662
+ - 주인공이 명확한 변화의 arc를 가지는가?
663
+ - 변화가 구체적이고 신빙성 있는가?
664
+ - 관계의 발전이 계획되어 있는가?
665
 
666
+ 3. **플롯의 축적성**
667
+ - 갈등이 점진적으로 심화되는가?
668
+ - 새로운 요소가 추가되며 복잡성이 증가하는가?
669
+ - 해결이 자연스럽고 필연적인가?
670
 
671
+ 4. **분량과 밀도**
672
+ - 8,000단어를 채울 충분한 내용인가?
673
+ - 단계가 800단어의 밀도를 가질 수 있는가?
 
674
 
675
+ **판정:**
676
+ - 통과: 진정한 장편 서사 구조
677
+ - 재작성: 반복적/순환적 구조
678
 
679
  구체적 개선 방향을 제시하세요.""",
680
+
681
+ "English": f"""You are a narrative structure critic.
682
+ Strictly review whether this plan is a true 'novel' rather than repeated episodes.
683
 
684
  **Original Theme:** {user_query}
685
 
686
  **Director's Plan:**
687
  {director_plan}
688
 
689
+ **Key Review Points:**
690
 
691
+ 1. **Narrative Integration and Progression**
692
+ - Do 10 phases connect as one story?
693
+ - Does each phase necessarily follow from previous?
694
+ - No repetition of same situations?
695
 
696
+ 2. **Character Transformation Arcs**
697
+ - Clear protagonist transformation arc?
698
+ - Concrete and credible changes?
699
+ - Planned relationship development?
700
 
701
+ 3. **Plot Accumulation**
702
+ - Progressive conflict deepening?
703
+ - Added complexity through new elements?
704
+ - Natural and inevitable resolution?
705
+
706
+ 4. **Length and Density**
707
+ - Sufficient content for 8,000 words?
708
+ - Can each phase sustain 800 words?
709
+
710
+ **Verdict:**
711
+ - Pass: True novel structure
712
+ - Rewrite: Repetitive/circular structure
713
 
714
  Provide specific improvements."""
715
  }
 
717
  return lang_prompts.get(language, lang_prompts["Korean"])
718
 
719
  def create_writer_prompt(self, writer_number: int, director_plan: str,
720
+ previous_content: str, phase_requirements: str,
721
+ narrative_summary: str, language: str) -> str:
722
+ """작가 프롬프트 - 서사 진행 강제"""
723
+
724
+ phase_name = NARRATIVE_PHASES[writer_number-1]
725
+ target_words = MIN_WORDS_PER_WRITER
726
 
727
  lang_prompts = {
728
+ "Korean": f"""당신은 작가 {writer_number}번입니다.
729
+ **현재 단계: {phase_name}**
730
 
731
+ **전체 서사 구조:**
732
  {director_plan}
733
 
734
+ **지금까지의 이야기 요약:**
735
+ {narrative_summary}
736
+
737
+ **이전 내용 (직전 부분):**
738
+ {previous_content[-1500:] if previous_content else "시작"}
739
+
740
+ **이번 단계 필수 요구사항:**
741
+ {phase_requirements}
742
 
743
  **작성 지침:**
744
 
745
+ 1. **분량**: {target_words}-900 단어 (필수)
746
+ - 내면 묘사와 구체적 디테일로 분량 확보
747
+ - 장면을 충분히 전개하고 깊이 있게 묘사
748
 
749
+ 2. **서사 진행 (가장 중요)**
750
+ - 이전 단계에서 일어난 일의 직접적 결과로 시작
751
+ - 새로운 사건/인식/변화를 추가하여 이야기 전진
752
+ - 다음 단계로 자연스럽게 연결될 고리 마련
753
 
754
+ 3. **인물의 변화**
755
+ - 단계에서 인물이 겪는 구체적 변화 묘사
756
+ - 내면의 미묘한 변화도 포착
757
+ - 관계의 역학 변화 반영
758
 
759
+ 4. **문체와 기법**
760
+ - 한국 현대 문학의 섬세한 심리 묘사
761
+ - 일상 사회적 맥락 녹여내기
762
+ - 감각적 디테일과 내면 의식의 균형
763
 
764
+ 5. **연속성 유지**
765
+ - 인물의 목소리와 말투 일관성
766
+ - 공간과 시간의 연속성
767
+ - 상징과 모티프의 발전
768
 
769
+ **절대 금지:**
770
+ - 이전과 동일한 상황 반복
771
+ - 서사의 정체나 후퇴
772
+ - 분량 미달 (최소 {target_words}단어)
 
773
 
774
+ 이전의 흐름을 이어받아 새로운 국면으로 발전시키세요.""",
 
 
775
 
776
+ "English": f"""You are Writer #{writer_number}.
777
+ **Current Phase: {phase_name}**
778
+
779
+ **Overall Narrative Structure:**
780
  {director_plan}
781
 
782
+ **Story So Far:**
783
+ {narrative_summary}
784
+
785
+ **Previous Content (immediately before):**
786
+ {previous_content[-1500:] if previous_content else "Beginning"}
787
+
788
+ **Phase Requirements:**
789
+ {phase_requirements}
790
 
791
  **Writing Guidelines:**
792
 
793
+ 1. **Length**: {target_words}-900 words (mandatory)
794
+ - Use interior description and concrete details
795
+ - Fully develop scenes with depth
796
 
797
+ 2. **Narrative Progression (Most Important)**
798
+ - Start as direct result of previous phase
799
+ - Add new events/awareness/changes to advance story
800
+ - Create natural connection to next phase
801
 
802
+ 3. **Character Change**
803
+ - Concrete changes in this phase
804
+ - Capture subtle interior shifts
805
+ - Reflect relationship dynamics
806
 
807
+ 4. **Style and Technique**
808
+ - Delicate psychological portrayal
809
+ - Social context in daily life
810
+ - Balance sensory details with consciousness
811
 
812
+ 5. **Continuity**
813
+ - Consistent character voices
814
+ - Spatial/temporal continuity
815
+ - Symbol/motif development
816
 
817
+ **Absolutely Forbidden:**
818
+ - Repeating previous situations
819
+ - Narrative stagnation/regression
820
+ - Under word count (minimum {target_words})
 
821
 
822
+ Continue the flow and develop into new phase."""
823
  }
824
 
825
  return lang_prompts.get(language, lang_prompts["Korean"])
826
 
827
+ def create_critic_consistency_prompt(self, all_content: str,
828
+ narrative_tracker: ProgressiveNarrativeTracker,
829
+ user_query: str, language: str) -> str:
830
+ """비평가 중간 검토 - 서사 누적성 확인"""
831
+
832
+ # 서사 진행 체크
833
+ phase_count = len(narrative_tracker.phase_summaries)
834
+ progression_ok, issues = narrative_tracker.check_narrative_progression(phase_count)
835
+
836
+ lang_prompts = {
837
+ "Korean": f"""서사 진행 전문 비평가로서 작품을 검토하세요.
838
 
839
  **원 주제:** {user_query}
840
 
841
+ **현재까지 진행된 서사 단계:** {phase_count}/10
842
+
843
+ **발견된 진행 문제:**
844
+ {chr(10).join(issues) if issues else "없음"}
845
+
846
  **작품 내용 (최근 부분):**
847
+ {all_content[-4000:]}
848
+
849
+ **집중 검토 사항:**
850
+
851
+ 1. **서사의 축적과 진행**
852
+ - 이야기가 실제로 전진하고 있는가?
853
+ - 각 단계가 이전의 결과로 연결되는가?
854
+ - 동일한 갈등이나 상황이 반복되지 않는가?
855
+
856
+ 2. **인물의 변화 궤적**
857
+ - 주인공이 초기와 비교해 어떻게 변했는가?
858
+ - 변화가 설득력 있고 점진적인가?
859
+ - 관계가 역동적으로 발전하는가?
860
 
861
+ 3. **주제의 심화**
862
+ - 초기 주제가 어떻게 발전했는가?
863
+ - 새로운 층위가 추가되었는가?
864
+ - 복잡성이 증가했는가?
865
 
866
+ 4. **분량과 밀도**
867
+ - 현재까지 단어 수 확인
868
+ - 목표(8,000단어)에 도달 가능한가?
869
+
870
+ **수정 지시:**
871
+ 각 작가에게 구체적인 진행 방향 제시.""",
872
+
873
+ "English": f"""As a narrative progression critic, review the work.
874
+
875
+ **Original Theme:** {user_query}
876
 
877
+ **Narrative Phases Completed:** {phase_count}/10
 
 
 
878
 
879
+ **Detected Progression Issues:**
880
+ {chr(10).join(issues) if issues else "None"}
 
 
881
 
882
+ **Work Content (recent):**
883
+ {all_content[-4000:]}
884
+
885
+ **Focus Review Areas:**
886
+
887
+ 1. **Narrative Accumulation and Progress**
888
+ - Is story actually moving forward?
889
+ - Does each phase connect as result of previous?
890
+ - No repetition of same conflicts/situations?
891
+
892
+ 2. **Character Transformation Arcs**
893
+ - How has protagonist changed from beginning?
894
+ - Are changes credible and gradual?
895
+ - Dynamic relationship development?
896
+
897
+ 3. **Thematic Deepening**
898
+ - How has initial theme developed?
899
+ - New layers added?
900
+ - Increased complexity?
901
+
902
+ 4. **Length and Density**
903
+ - Current total word count
904
+ - Can reach 8,000 word target?
905
+
906
+ **Revision Instructions:**
907
+ Specific progression directions for each writer."""
908
+ }
909
+
910
+ return lang_prompts.get(language, lang_prompts["Korean"])
911
 
912
  def create_writer_revision_prompt(self, writer_number: int, initial_content: str,
913
  critic_feedback: str, language: str) -> str:
914
  """작가 수정 프롬프트"""
915
+ target_words = MIN_WORDS_PER_WRITER
916
+
917
  return f"""작가 {writer_number}번, 비평을 반영하여 수정하세요.
918
 
919
  **초안:**
920
  {initial_content}
921
 
922
+ **비평 피드백:**
923
  {critic_feedback}
924
 
925
+ **수정 핵심:**
926
+ 1. 서사 진행성 강화 - 반복 제거, 새로운 전개 추가
927
+ 2. 인물 변화 구체화 - 이전과 달라진 모습 명확히
928
+ 3. 분량 확보 - 최소 {target_words}단어 유지
929
+ 4. 내면 묘사와 사회적 맥락 심화
930
 
931
+ 전면 재작성이 필요하면 과감히 수정하세요.
932
  수정본만 제시하세요."""
933
 
934
+ def create_critic_final_prompt(self, complete_novel: str, word_count: int, language: str) -> str:
935
+ """최종 비평 - AGI 평가 기준"""
936
+ return f"""완성된 소설을 AGI 튜링테스트 기준으로 평가하세요.
937
 
938
+ **작품 정보:**
939
+ - 총 분량: {word_count}단어
940
+ - 목표 분량: 8,000단어 이상
941
 
942
+ **작품 (마지막 부분):**
943
+ {complete_novel[-3000:]}
944
 
945
+ **평가 기준 (AGI 튜링테스트):**
 
 
 
946
 
947
+ 1. **장편소설로서의 완성도 (40점)**
948
+ - 통합된 서사 구조 (반복 없음)
949
+ - 인물의 명확한 변화 arc
950
+ - 플롯의 축적과 해결
951
+ - 8,000단어 이상 분량
952
 
953
+ 2. **문학적 성취 (30점)**
954
+ - 주제 의식의 깊이
955
+ - 인물 심리의 설득력
956
+ - 문체의 일관성과 아름다움
957
  - 상징과 은유의 효과
 
958
 
959
+ 3. **사회적 통찰 (20점)**
960
+ - 현대 사회 문제 포착
961
+ - 개인과 구조의 연결
962
+ - 보편성과 특수성 균형
963
+
964
+ 4. **독창성과 인간성 (10점)**
965
+ - AI가 아닌 인간 작가의 느낌
966
+ - 독창적 표현과 통찰
967
+ - 감정적 진정성
968
 
969
  **총점: /100점**
970
 
971
+ 특히 '반복 구조' 문제가 있었는지 엄격히 평가하세요."""
972
 
973
  # --- LLM 호출 함수들 ---
974
  def call_llm_sync(self, messages: List[Dict[str, str]], role: str, language: str) -> str:
 
984
  system_prompts = self.get_system_prompts(language)
985
  full_messages = [{"role": "system", "content": system_prompts.get(role, "")}, *messages]
986
 
987
+ # 작가 역할일 때는 더 많은 토큰 허용
988
+ max_tokens = 15000 if role.startswith("writer") else 10000
989
+
990
  payload = {
991
  "model": self.model_id,
992
  "messages": full_messages,
993
+ "max_tokens": max_tokens,
994
+ "temperature": 0.8,
995
  "top_p": 0.95,
996
+ "presence_penalty": 0.5,
997
  "frequency_penalty": 0.3,
998
  "stream": True
999
  }
 
1050
  """역할별 시스템 프롬프트"""
1051
  base_prompts = {
1052
  "Korean": {
1053
+ "director": """당신은 한국 현대 문학의 거장입니다.
1054
+ 반복이 아닌 진행, 순환이 아닌 발전을 통해 하나의 강력한 서사를 구축하세요.
1055
+ 개인의 문제를 사회 구조와 연결하며, 인물의 진정한 변화를 그려내세요.""",
1056
 
1057
+ "critic": """당신은 엄격한 문학 비평가입니다.
1058
+ 특히 '반복 구조'와 '서사 정체'를 철저히 감시하세요.
1059
+ 작품이 진정한 장편소설인지, 아니면 반복되는 단편의 집합인지 구별하세요.""",
1060
 
1061
+ "writer_base": """당신은 현대 한국 문학 작가입니다.
1062
+ 이전 단계의 결과를 받아 새로운 국면으로 발전시키세요.
1063
+ 최소 800단어를 작성하며, 내면과 사회를 동시에 포착하세요.
1064
+ 절대 이전과 같은 상황을 반복하지 마세요."""
1065
  },
1066
  "English": {
1067
+ "director": """You are a master of contemporary literary fiction.
1068
+ Build one powerful narrative through progression not repetition, development not cycles.
1069
+ Connect individual problems to social structures while depicting genuine character transformation.""",
1070
 
1071
+ "critic": """You are a strict literary critic.
1072
+ Vigilantly monitor for 'repetitive structure' and 'narrative stagnation'.
1073
+ Distinguish whether this is a true novel or a collection of repeated episodes.""",
1074
 
1075
+ "writer_base": """You are a contemporary literary writer.
1076
+ Take results from previous phase and develop into new territory.
1077
+ Write minimum 800 words, capturing both interior and society.
1078
+ Never repeat previous situations."""
1079
  }
1080
  }
1081
 
1082
  prompts = base_prompts.get(language, base_prompts["Korean"]).copy()
1083
 
1084
  # 특수 작가 프롬프트
1085
+ for i in range(1, 11):
1086
+ prompts[f"writer{i}"] = prompts["writer_base"]
 
 
 
 
 
 
1087
 
1088
  return prompts
1089
 
 
1099
  query = session['user_query']
1100
  language = session['language']
1101
  resume_from_stage = session['current_stage'] + 1
1102
+ # 서사 추적기 복원
1103
+ saved_tracker = NovelDatabase.load_narrative_tracker(session_id)
1104
+ if saved_tracker:
1105
+ self.narrative_tracker = saved_tracker
1106
  else:
1107
  self.current_session_id = NovelDatabase.create_session(query, language)
1108
  logger.info(f"Created new session: {self.current_session_id}")
 
1113
  "name": s['stage_name'],
1114
  "status": s['status'],
1115
  "content": s.get('content', ''),
1116
+ "word_count": s.get('word_count', 0),
1117
+ "progression_score": s.get('progression_score', 0.0)
1118
  } for s in NovelDatabase.get_stages(self.current_session_id)]
1119
 
1120
+ # 단어 추적
1121
+ total_words = NovelDatabase.get_total_words(self.current_session_id)
1122
+
1123
+ for stage_idx in range(resume_from_stage, len(PROGRESSIVE_STAGES)):
1124
+ role, stage_name = PROGRESSIVE_STAGES[stage_idx]
1125
  if stage_idx >= len(stages):
1126
  stages.append({
1127
  "name": stage_name,
1128
  "status": "active",
1129
  "content": "",
1130
+ "word_count": 0,
1131
+ "progression_score": 0.0
1132
  })
1133
  else:
1134
  stages[stage_idx]["status"] = "active"
1135
 
1136
+ yield f"🔄 진행 중... (현재 {total_words:,}단어)", stages, self.current_session_id
1137
 
1138
  prompt = self.get_stage_prompt(stage_idx, role, query, language, stages)
1139
  stage_content = ""
 
1141
  for chunk in self.call_llm_streaming([{"role": "user", "content": prompt}], role, language):
1142
  stage_content += chunk
1143
  stages[stage_idx]["content"] = stage_content
1144
+ stages[stage_idx]["word_count"] = len(stage_content.split())
1145
+ yield f"🔄 {stage_name} 작성 중... ({total_words + stages[stage_idx]['word_count']:,}단어)", stages, self.current_session_id
1146
+
1147
+ # 진행도 평가
1148
+ if role.startswith("writer"):
1149
+ writer_num = int(re.search(r'\d+', role).group())
1150
+ progression_score = self.evaluate_progression(stage_content, writer_num)
1151
+ stages[stage_idx]["progression_score"] = progression_score
1152
+
1153
+ # 서사 추적기 업데이트
1154
+ self.update_narrative_tracker(stage_content, writer_num)
1155
 
1156
  stages[stage_idx]["status"] = "complete"
1157
  NovelDatabase.save_stage(
1158
  self.current_session_id, stage_idx, stage_name, role,
1159
+ stage_content, "complete", stages[stage_idx].get("progression_score", 0.0)
1160
  )
1161
+
1162
+ # 서사 추적기 저장
1163
+ NovelDatabase.save_narrative_tracker(self.current_session_id, self.narrative_tracker)
1164
+
1165
+ # 총 단어 수 업데이트
1166
+ total_words = NovelDatabase.get_total_words(self.current_session_id)
1167
+ yield f"✅ {stage_name} 완료 (총 {total_words:,}단어)", stages, self.current_session_id
1168
 
1169
  # 최종 소설 정리
1170
  final_novel = NovelDatabase.get_writer_content(self.current_session_id)
1171
+ final_word_count = len(final_novel.split())
1172
+ final_report = self.generate_literary_report(final_novel, final_word_count, language)
1173
 
1174
  NovelDatabase.update_final_novel(self.current_session_id, final_novel, final_report)
1175
+ yield f"✅ 소설 완성! 총 {final_word_count:,}단어 (목표: {TARGET_WORDS:,}단어)", stages, self.current_session_id
1176
 
1177
  except Exception as e:
1178
  logger.error(f"소설 생성 프로세스 오류: {e}", exc_info=True)
 
1191
 
1192
  if 3 <= stage_idx <= 12: # 작가 초안
1193
  writer_num = stage_idx - 2
1194
+ previous_content = self.get_previous_writer_content(stages, writer_num)
1195
+ phase_requirements = self.narrative_tracker.generate_phase_requirements(writer_num)
1196
+ narrative_summary = self.generate_narrative_summary(stages, writer_num)
1197
+
1198
+ return self.create_writer_prompt(
1199
+ writer_num, master_plan, previous_content,
1200
+ phase_requirements, narrative_summary, language
1201
+ )
1202
 
1203
  if stage_idx == 13: # 비평가 중간 검토
1204
+ all_content = self.get_all_writer_content(stages, 12)
1205
+ return self.create_critic_consistency_prompt(
1206
+ all_content, self.narrative_tracker, query, language
1207
+ )
1208
 
1209
  if 14 <= stage_idx <= 23: # 작가 수정
1210
  writer_num = stage_idx - 13
 
1213
  return self.create_writer_revision_prompt(writer_num, initial_content, feedback, language)
1214
 
1215
  if stage_idx == 24: # 최종 검토
1216
+ complete_novel = self.get_all_writer_content(stages, 23)
1217
+ word_count = len(complete_novel.split())
1218
+ return self.create_critic_final_prompt(complete_novel, word_count, language)
1219
 
1220
  return ""
1221
 
1222
  def create_director_revision_prompt(self, initial_plan: str, critic_feedback: str, user_query: str, language: str) -> str:
1223
  """감독자 수정 프롬프트"""
1224
+ return f"""비평을 반영하여 통합된 서사 구조를 완성하세요.
1225
 
1226
  **원 주제:** {user_query}
1227
 
 
1231
  **비평:**
1232
  {critic_feedback}
1233
 
1234
+ **핵심 수정 사항:**
1235
+ 1. 반복 구조 완전 제거
1236
+ 2. 10단계가 하나의 이야기로 연결
1237
+ 3. 인물의 명확한 변화 궤적
1238
+ 4. 8,000단어 분량 계획
1239
 
1240
+ 단계가 이전의 필연적 결과가 되도록 수정하세요."""
1241
 
1242
+ def get_previous_writer_content(self, stages: List[Dict], current_writer: int) -> str:
1243
+ """이전 작가의 내용 가져오기"""
1244
+ if current_writer == 1:
1245
+ return ""
1246
+
1247
+ # 바로 이전 작가의 내용
1248
+ prev_idx = current_writer + 1 # stages 인덱스는 writer_num + 2
1249
+ if prev_idx < len(stages) and stages[prev_idx]["content"]:
1250
+ return stages[prev_idx]["content"]
1251
+
1252
+ return ""
1253
 
1254
+ def get_all_writer_content(self, stages: List[Dict], up_to_stage: int) -> str:
1255
+ """특정 단계까지의 모든 작가 내용"""
1256
  contents = []
1257
  for i, s in enumerate(stages):
1258
+ if i <= up_to_stage and "writer" in s.get("name", "") and s["content"]:
1259
  contents.append(s["content"])
1260
  return "\n\n".join(contents)
1261
 
1262
+ def generate_narrative_summary(self, stages: List[Dict], up_to_writer: int) -> str:
1263
+ """현재까지의 서사 요약"""
1264
+ if up_to_writer == 1:
1265
+ return "첫 시작입니다."
1266
 
1267
+ summary_parts = []
1268
+ for i in range(1, up_to_writer):
1269
+ if i in self.narrative_tracker.phase_summaries:
1270
+ summary_parts.append(f"[{NARRATIVE_PHASES[i-1]}]: {self.narrative_tracker.phase_summaries[i]}")
1271
+
1272
+ return "\n".join(summary_parts) if summary_parts else "이전 내용을 이어받아 진행하세요."
1273
+
1274
+ def update_narrative_tracker(self, content: str, writer_num: int):
1275
+ """서사 추적기 업데이트"""
1276
+ # 간단한 요약 생성 (실제로는 더 정교한 분석 필요)
1277
+ lines = content.split('\n')
1278
+ key_events = [line.strip() for line in lines if len(line.strip()) > 50][:3]
1279
+
1280
+ if key_events:
1281
+ summary = " ".join(key_events[:2])[:200] + "..."
1282
+ self.narrative_tracker.phase_summaries[writer_num] = summary
1283
+
1284
+ def evaluate_progression(self, content: str, phase: int) -> float:
1285
+ """서사 진행도 평가"""
1286
+ score = 5.0
1287
 
1288
+ # 분량 체크
1289
+ word_count = len(content.split())
1290
+ if word_count >= MIN_WORDS_PER_WRITER:
1291
+ score += 2.0
 
1292
 
1293
+ # 새로운 요소 체크
1294
+ if phase > 1:
1295
+ prev_summary = self.narrative_tracker.phase_summaries.get(phase-1, "")
1296
+ if prev_summary and len(set(content.split()) - set(prev_summary.split())) > 100:
1297
+ score += 1.5
1298
 
1299
+ # 변화 언급 체크
1300
+ change_keywords = ['변했', '달라졌', '새로운', '이제는', ' 이상',
1301
+ 'changed', 'different', 'new', 'now', 'no longer']
1302
+ if any(keyword in content for keyword in change_keywords):
1303
+ score += 1.5
1304
 
1305
  return min(10.0, score)
1306
 
1307
+ def generate_literary_report(self, complete_novel: str, word_count: int, language: str) -> str:
1308
+ """최종 문학적 평가"""
1309
+ prompt = self.create_critic_final_prompt(complete_novel, word_count, language)
1310
  try:
1311
  report = self.call_llm_sync([{"role": "user", "content": prompt}], "critic", language)
1312
  return report
 
1322
  yield "", "", "❌ 주제를 입력해주세요.", session_id
1323
  return
1324
 
1325
+ system = ProgressiveLiterarySystem()
1326
  stages_markdown = ""
1327
  novel_content = ""
1328
 
 
1339
  def get_active_sessions(language: str) -> List[str]:
1340
  """활성 세션 목록"""
1341
  sessions = NovelDatabase.get_active_sessions()
1342
+ return [f"{s['session_id'][:8]}... - {s['user_query'][:50]}... ({s['created_at']}) [{s['total_words']:,}단어]"
1343
  for s in sessions]
1344
 
1345
  def auto_recover_session(language: str) -> Tuple[Optional[str], str]:
1346
  """최근 세션 자동 복구"""
1347
+ sessions = NovelDatabase.get_active_sessions()
1348
+ if sessions:
1349
+ latest_session = sessions[0]
1350
  return latest_session['session_id'], f"세션 {latest_session['session_id'][:8]}... 복��됨"
1351
  return None, "복구할 세션이 없습니다."
1352
 
 
1386
  def format_stages_display(stages: List[Dict]) -> str:
1387
  """단계별 진행 상황 표시"""
1388
  markdown = "## 🎬 진행 상황\n\n"
1389
+
1390
+ # 총 단어 수 계산
1391
+ total_words = sum(s.get('word_count', 0) for s in stages if 'writer' in s.get('name', ''))
1392
+ markdown += f"**총 단어 수: {total_words:,} / {TARGET_WORDS:,}**\n\n"
1393
+
1394
  for i, stage in enumerate(stages):
1395
  status_icon = "✅" if stage['status'] == 'complete' else "🔄" if stage['status'] == 'active' else "⏳"
1396
  markdown += f"{status_icon} **{stage['name']}**"
1397
 
1398
+ if stage.get('word_count', 0) > 0:
1399
+ markdown += f" ({stage['word_count']:,}단어)"
1400
+
1401
+ if stage.get('progression_score', 0) > 0:
1402
+ markdown += f" [진행도: {stage['progression_score']:.1f}/10]"
1403
 
1404
  markdown += "\n"
1405
+
1406
  if stage['content']:
1407
  preview = stage['content'][:200] + "..." if len(stage['content']) > 200 else stage['content']
1408
  markdown += f"> {preview}\n\n"
1409
+
1410
  return markdown
1411
 
1412
  def format_novel_display(novel_text: str) -> str:
 
1415
  return "아직 완성된 내용이 없습니다."
1416
 
1417
  formatted = "# 📖 완성된 소설\n\n"
1418
+
1419
+ # 단어 수 표시
1420
+ word_count = len(novel_text.split())
1421
+ formatted += f"**총 분량: {word_count:,}단어 (목표: {TARGET_WORDS:,}단어)**\n\n"
1422
+ formatted += "---\n\n"
1423
+
1424
+ # 각 단계를 구분하여 표시
1425
+ sections = novel_text.split('\n\n')
1426
+ for i, section in enumerate(sections):
1427
+ if section.strip():
1428
+ formatted += f"{section}\n\n"
1429
 
1430
  return formatted
1431
 
1432
  def export_to_docx(content: str, filename: str, language: str, session_id: str) -> str:
1433
+ """DOCX 파일로 내보내기"""
1434
  doc = Document()
1435
 
1436
+ # 페이지 설정
1437
  section = doc.sections[0]
1438
+ section.page_height = Inches(11)
1439
+ section.page_width = Inches(8.5)
1440
+ section.top_margin = Inches(1)
1441
+ section.bottom_margin = Inches(1)
1442
+ section.left_margin = Inches(1.25)
1443
+ section.right_margin = Inches(1.25)
 
 
1444
 
1445
  # 세션 정보
1446
  session = NovelDatabase.get_session(session_id)
 
1449
  title_para = doc.add_paragraph()
1450
  title_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
1451
 
 
 
 
 
1452
  if session:
1453
  title_run = title_para.add_run(session["user_query"])
1454
+ title_run.font.size = Pt(24)
1455
+ title_run.bold = True
1456
+
1457
+ # 메타 정보
1458
+ doc.add_paragraph()
1459
+ meta_para = doc.add_paragraph()
1460
+ meta_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
1461
+ meta_para.add_run(f"생성일: {datetime.now().strftime('%Y년 %m월 %d일')}\n")
1462
+ meta_para.add_run(f"총 단어 수: {len(content.split()):,}단어")
1463
 
1464
  # 페이지 나누기
1465
  doc.add_page_break()
1466
 
1467
  # 본문 스타일 설정
1468
  style = doc.styles['Normal']
1469
+ style.font.name = 'Calibri'
1470
+ style.font.size = Pt(11)
1471
+ style.paragraph_format.line_spacing = 1.5
1472
+ style.paragraph_format.space_after = Pt(6)
 
 
1473
 
1474
  # 본문 추가
1475
  paragraphs = content.split('\n\n')
 
1490
  return filepath
1491
 
1492
 
1493
+ # CSS 스타일
1494
  custom_css = """
1495
  .gradio-container {
1496
+ background: linear-gradient(135deg, #1e3c72 0%, #2a5298 50%, #1e3c72 100%);
1497
  min-height: 100vh;
1498
  }
1499
 
1500
  .main-header {
1501
+ background-color: rgba(255, 255, 255, 0.1);
1502
  backdrop-filter: blur(10px);
1503
  padding: 30px;
1504
  border-radius: 12px;
1505
  margin-bottom: 30px;
1506
  text-align: center;
1507
  color: white;
1508
+ border: 1px solid rgba(255, 255, 255, 0.2);
1509
  }
1510
 
1511
+ .progress-note {
1512
+ background-color: rgba(255, 223, 0, 0.1);
1513
+ border-left: 3px solid #ffd700;
1514
  padding: 15px;
1515
  margin: 20px 0;
1516
  border-radius: 8px;
1517
+ color: #fff;
 
 
1518
  }
1519
 
1520
  .input-section {
1521
+ background-color: rgba(255, 255, 255, 0.1);
1522
  backdrop-filter: blur(10px);
1523
  padding: 20px;
1524
  border-radius: 12px;
1525
  margin-bottom: 20px;
1526
+ border: 1px solid rgba(255, 255, 255, 0.2);
1527
  }
1528
 
1529
  .session-section {
1530
+ background-color: rgba(255, 255, 255, 0.1);
1531
  backdrop-filter: blur(10px);
1532
  padding: 15px;
1533
  border-radius: 8px;
1534
  margin-top: 20px;
1535
  color: white;
1536
+ border: 1px solid rgba(255, 255, 255, 0.2);
1537
  }
1538
 
1539
  #stages-display {
 
1546
  }
1547
 
1548
  #novel-output {
1549
+ background-color: rgba(255, 255, 255, 0.95);
1550
+ padding: 30px;
1551
  border-radius: 12px;
1552
  max-height: 700px;
1553
  overflow-y: auto;
1554
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
 
 
 
1555
  }
1556
 
1557
  .download-section {
 
1562
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
1563
  }
1564
 
1565
+ /* 진행 표시기 스타일 */
1566
+ .progress-bar {
1567
+ background-color: #e0e0e0;
1568
+ height: 20px;
1569
+ border-radius: 10px;
1570
+ overflow: hidden;
1571
+ margin: 10px 0;
1572
  }
1573
 
1574
+ .progress-fill {
1575
+ background-color: #4CAF50;
1576
+ height: 100%;
1577
+ transition: width 0.3s ease;
 
 
 
 
 
 
 
 
 
 
 
1578
  }
1579
  """
1580
 
1581
 
1582
  # Gradio 인터페이스 생성
1583
  def create_interface():
1584
+ with gr.Blocks(css=custom_css, title="AI 진행형 장편소설 생성 시스템") as interface:
1585
  gr.HTML("""
1586
  <div class="main-header">
1587
+ <h1 style="font-size: 2.5em; margin-bottom: 10px;">
1588
+ 📚 AI 진행형 장편소설 생성 시스템
1589
  </h1>
1590
+ <h3 style="color: #ddd; margin-bottom: 20px;">
1591
+ 8,000단어 이상의 통합된 서사 구조를 가진 중편소설 창작
1592
  </h3>
1593
+ <p style="font-size: 1.1em; color: #eee; max-width: 800px; margin: 0 auto;">
1594
+ 10개의 유기적으로 연결된 단계를 통해 하나의 완전한 이야기를 만들어냅니다.
1595
  <br>
1596
+ 단계는 이전 단계의 필연적 결과로 이어지며, 인물의 변화와 성장을 추적합니다.
1597
  </p>
1598
+ <div class="progress-note">
1599
+ 반복이 아닌 축적, 순환이 아닌 진행을 통한 진정한 장편 서사
1600
  </div>
1601
  </div>
1602
  """)
 
1609
  with gr.Group(elem_classes=["input-section"]):
1610
  query_input = gr.Textbox(
1611
  label="소설 주제 / Novel Theme",
1612
+ placeholder="중편소설의 주제를 입력하세요. 인물의 변화와 성장이 중심이 되는 이야기...\nEnter the theme for your novella. Focus on character transformation and growth...",
1613
  lines=4
1614
  )
1615
 
 
1631
 
1632
  # 세션 관리
1633
  with gr.Group(elem_classes=["session-section"]):
1634
+ gr.Markdown("### 💾 진행 중인 세션")
1635
  session_dropdown = gr.Dropdown(
1636
  label="세션 선택",
1637
  choices=[],
 
1640
  with gr.Row():
1641
  refresh_btn = gr.Button("🔄 목록 새로고침", scale=1)
1642
  resume_btn = gr.Button("▶️ 선택 재개", variant="secondary", scale=1)
1643
+ auto_recover_btn = gr.Button("♻️ 최근 세션 복구", scale=1)
1644
 
1645
  with gr.Column(scale=2):
1646
+ with gr.Tab("📝 창작 진행"):
1647
  stages_display = gr.Markdown(
1648
  value="창작 과정이 여기에 표시됩니다...",
1649
  elem_id="stages-display"
 
1673
  # 숨겨진 상태
1674
  novel_text_state = gr.State("")
1675
 
1676
+ # 예제
1677
  with gr.Row():
1678
  gr.Examples(
1679
  examples=[
1680
+ ["실직한 중년 남성이 새로운 삶의 의미를 찾아가는 여정"],
1681
+ ["도시에서 시골로 이주한 청년의 적응과 성장 이야기"],
1682
+ [" 세대가 함께 사는 가족의 갈등과 화해"],
1683
+ ["A middle-aged woman's journey to rediscover herself after divorce"],
1684
+ ["The transformation of a cynical journalist through unexpected encounters"],
1685
+ ["작은 서점을 운영하는 노부부의 마지막 1년"],
1686
+ ["AI 시대에 일자리를 잃은 번역가의 새로운 도전"]
 
1687
  ],
1688
  inputs=query_input,
1689
+ label="💡 주제 예시"
1690
  )
1691
 
1692
  # 이벤트 핸들러
 
1700
 
1701
  def handle_auto_recover(language):
1702
  session_id, message = auto_recover_session(language)
1703
+ return session_id, message
1704
 
1705
  # 이벤트 연결
1706
  submit_btn.click(
 
1716
  )
1717
 
1718
  resume_btn.click(
1719
+ fn=lambda x: x.split("...")[0] if x and "..." in x else x,
1720
  inputs=[session_dropdown],
1721
  outputs=[current_session_id]
1722
  ).then(
 
1728
  auto_recover_btn.click(
1729
  fn=handle_auto_recover,
1730
  inputs=[language_select],
1731
+ outputs=[current_session_id, status_text]
1732
  ).then(
1733
  fn=resume_session,
1734
  inputs=[current_session_id, language_select],
 
1746
  )
1747
 
1748
  def handle_download(format_type, language, session_id, novel_text):
1749
+ if not session_id or not novel_text:
1750
  return gr.update(visible=False)
1751
 
1752
  file_path = download_novel(novel_text, format_type, language, session_id)
 
1772
 
1773
  # 메인 실행
1774
  if __name__ == "__main__":
1775
+ logger.info("AI 진행형 장편소설 생성 시스템 시작...")
1776
  logger.info("=" * 60)
1777
 
1778
  # 환경 확인
1779
  logger.info(f"API 엔드포인트: {API_URL}")
1780
+ logger.info(f"목표 분량: {TARGET_WORDS:,}단어")
1781
+ logger.info(f"작가당 최소 분량: {MIN_WORDS_PER_WRITER:,}단어")
1782
 
1783
  if BRAVE_SEARCH_API_KEY:
1784
  logger.info("웹 검색이 활성화되었습니다.")
 
1805
  server_port=7860,
1806
  share=False,
1807
  debug=True
1808
+ )