openfree's picture
Update app.py
7719eef verified
raw
history blame
115 kB
import gradio as gr
import os
import json
import requests
from datetime import datetime
import time
from typing import List, Dict, Any, Generator, Tuple, Optional, Set
import logging
import re
import tempfile
from pathlib import Path
import sqlite3
import hashlib
import threading
from contextlib import contextmanager
from dataclasses import dataclass, field, asdict
from collections import defaultdict
# --- λ‘œκΉ… μ„€μ • ---
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
# --- Document export imports ---
try:
from docx import Document
from docx.shared import Inches, Pt, RGBColor
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.enum.style import WD_STYLE_TYPE
from docx.oxml.ns import qn
from docx.oxml import OxmlElement
DOCX_AVAILABLE = True
except ImportError:
DOCX_AVAILABLE = False
logger.warning("python-docx not installed. DOCX export will be disabled.")
# --- ν™˜κ²½ λ³€μˆ˜ 및 μƒμˆ˜ ---
FRIENDLI_TOKEN = os.getenv("FRIENDLI_TOKEN", "")
BRAVE_SEARCH_API_KEY = os.getenv("BRAVE_SEARCH_API_KEY", "")
API_URL = "https://api.friendli.ai/dedicated/v1/chat/completions"
MODEL_ID = "dep89a2fld32mcm"
DB_PATH = "novel_sessions_v7.db"
# λͺ©ν‘œ λΆ„λŸ‰ μ„€μ • (κΈ°μ‘΄ κ°’μœΌλ‘œ 볡원)
TARGET_WORDS = 8000 # μ•ˆμ „ λ§ˆμ§„μ„ μœ„ν•΄ 8000단어
MIN_WORDS_PER_WRITER = 800 # 각 μž‘κ°€ μ΅œμ†Œ λΆ„λŸ‰ 800단어
# --- ν™˜κ²½ λ³€μˆ˜ 검증 ---
if not FRIENDLI_TOKEN:
logger.error("FRIENDLI_TOKEN not set. Application will not work properly.")
FRIENDLI_TOKEN = "dummy_token_for_testing"
if not BRAVE_SEARCH_API_KEY:
logger.warning("BRAVE_SEARCH_API_KEY not set. Web search features will be disabled.")
# --- μ „μ—­ λ³€μˆ˜ ---
db_lock = threading.Lock()
# μ„œμ‚¬ μ§„ν–‰ 단계 μ •μ˜
NARRATIVE_PHASES = [
"λ„μž…: 일상과 κ· μ—΄",
"λ°œμ „ 1: λΆˆμ•ˆμ˜ κ³ μ‘°",
"λ°œμ „ 2: μ™ΈλΆ€ 좩격",
"λ°œμ „ 3: 내적 κ°ˆλ“± 심화",
"μ ˆμ • 1: μœ„κΈ°μ˜ 정점",
"μ ˆμ • 2: μ„ νƒμ˜ μˆœκ°„",
"ν•˜κ°• 1: 결과와 μ—¬νŒŒ",
"ν•˜κ°• 2: μƒˆλ‘œμš΄ 인식",
"결말 1: λ³€ν™”λœ 일상",
"결말 2: μ—΄λ¦° 질문"
]
# νŽΈμ§‘μž 단계 μ‚¬μš© μ—¬λΆ€ (False둜 μ„€μ •ν•˜λ©΄ νŽΈμ§‘μž 단계 κ±΄λ„ˆλ›°κΈ°)
USE_EDITOR_STAGE = False # νŽΈμ§‘μžκ°€ κ³Όλ„ν•˜κ²Œ μ‚­μ œν•˜λŠ” 문제둜 λΉ„ν™œμ„±ν™”
# 단계별 ꡬ성 - νŽΈμ§‘μž 단계 쑰건뢀 포함
PROGRESSIVE_STAGES = [
("director", "🎬 κ°λ…μž: ν†΅ν•©λœ μ„œμ‚¬ ꡬ쑰 기획"),
("critic", "πŸ“ 비평가: μ„œμ‚¬ μ§„ν–‰μ„±κ³Ό 깊이 κ²€ν† "),
("director", "🎬 κ°λ…μž: μˆ˜μ •λœ λ§ˆμŠ€ν„°ν”Œλžœ"),
] + [
(f"writer{i}", f"✍️ μž‘κ°€ {i}: μ΄ˆμ•ˆ - {NARRATIVE_PHASES[i-1]}")
for i in range(1, 11)
] + [
("critic", "πŸ“ 비평가: 쀑간 κ²€ν†  (μ„œμ‚¬ λˆ„μ μ„±κ³Ό λ³€ν™”)"),
] + [
(f"writer{i}", f"✍️ μž‘κ°€ {i}: μˆ˜μ •λ³Έ - {NARRATIVE_PHASES[i-1]}")
for i in range(1, 11)
]
# νŽΈμ§‘μž 단계 쑰건뢀 μΆ”κ°€
if USE_EDITOR_STAGE:
PROGRESSIVE_STAGES.extend([
("editor", "βœ‚οΈ νŽΈμ§‘μž: 반볡 제거 및 μ„œμ‚¬ μž¬κ΅¬μ„±"),
("critic", f"πŸ“ 비평가: μ΅œμ’… κ²€ν†  및 문학적 평가"),
])
else:
PROGRESSIVE_STAGES.append(
("critic", f"πŸ“ 비평가: μ΅œμ’… κ²€ν†  및 문학적 평가"),
)
# μ ˆλŒ€ κΈˆμ§€μ‚¬ν•­ - μ–Έμ–΄ 였λ₯˜ λ°©μ§€ (κ°„μ†Œν™”)
STRICT_RULES = """
κΈˆμ§€μ‚¬ν•­:
1. 일본어, 쀑ꡭ어 λ“± μ™Έκ΅­μ–΄ 문자 μ‚¬μš© κΈˆμ§€
2. 이전 단계 μ‚¬κ±΄μ˜ λ‹¨μˆœ 반볡 κΈˆμ§€
3. 캐릭터 이름 λ³€κ²½ κΈˆμ§€
"""
# --- 데이터 클래슀 ---
@dataclass
class CharacterArc:
"""인물의 λ³€ν™” ꢀ적 좔적"""
name: str
initial_state: Dict[str, Any] # 초기 μƒνƒœ
phase_states: Dict[int, Dict[str, Any]] = field(default_factory=dict) # 단계별 μƒνƒœ
transformations: List[str] = field(default_factory=list) # μ£Όμš” λ³€ν™”λ“€
relationships_evolution: Dict[str, List[str]] = field(default_factory=dict) # 관계 λ³€ν™”
@dataclass
class PlotThread:
"""ν”Œλ‘― 라인 좔적"""
thread_id: str
description: str
introduction_phase: int
development_phases: List[int]
resolution_phase: Optional[int]
status: str = "active" # active, resolved, suspended
@dataclass
class SymbolicEvolution:
"""μƒμ§•μ˜ 의미 λ³€ν™” 좔적"""
symbol: str
initial_meaning: str
phase_meanings: Dict[int, str] = field(default_factory=dict)
transformation_complete: bool = False
@dataclass
class CharacterConsistency:
"""캐릭터 일관성 관리"""
primary_names: Dict[str, str] = field(default_factory=dict) # role -> canonical name
aliases: Dict[str, List[str]] = field(default_factory=dict) # canonical -> aliases
name_history: List[Tuple[int, str, str]] = field(default_factory=list) # (phase, role, used_name)
def validate_name(self, phase: int, role: str, name: str) -> bool:
"""이름 일관성 검증"""
if role in self.primary_names:
canonical = self.primary_names[role]
if name != canonical and name not in self.aliases.get(canonical, []):
return False
return True
def register_name(self, phase: int, role: str, name: str):
"""이름 등둝"""
if role not in self.primary_names:
self.primary_names[role] = name
self.name_history.append((phase, role, name))
# --- 핡심 둜직 클래슀 ---
class LanguageFilter:
"""μ–Έμ–΄ ν˜Όμž… 및 특수기호 였λ₯˜ λ°©μ§€"""
def __init__(self):
self.forbidden_patterns = [
r'[ぁ-γ‚“]+', # νžˆλΌκ°€λ‚˜
r'[γ‚‘-γƒΆ]+', # κ°€νƒ€μΉ΄λ‚˜
r'[\u4e00-\u9fff]+', # ν•œμž
r'\$\s*[.,]', # 특수기호 였λ₯˜
r'[λŠ”μ€μ΄κ°€μ„λ₯Όμ—μ˜μ™€κ³Όλ„λ‘œλΆ€ν„°κΉŒμ§€λ§Œλ“ ν•œλ‹€]\s*\$' # 쑰사 λ’€ 특수기호
]
def clean_text(self, text: str) -> str:
"""μƒμ„±λœ ν…μŠ€νŠΈ μ •μ œ"""
import re
cleaned = text
# 일본어 문자 제거
for pattern in self.forbidden_patterns:
cleaned = re.sub(pattern, '', cleaned)
# 연속 곡백 정리
cleaned = re.sub(r'\s+', ' ', cleaned)
# λ¬Έμž₯ 끝 정리
cleaned = re.sub(r'([.!?])\s*\$', r'\1', cleaned)
return cleaned.strip()
class ContentDeduplicator:
"""쀑볡 μ½˜ν…μΈ  감지 및 제거"""
def __init__(self):
self.seen_paragraphs = set()
self.seen_key_phrases = set()
self.similarity_threshold = 0.85
def check_similarity(self, text1: str, text2: str) -> float:
"""두 ν…μŠ€νŠΈμ˜ μœ μ‚¬λ„ μΈ‘μ •"""
# κ°„λ‹¨ν•œ Jaccard μœ μ‚¬λ„ κ΅¬ν˜„
words1 = set(text1.lower().split())
words2 = set(text2.lower().split())
intersection = words1.intersection(words2)
union = words1.union(words2)
return len(intersection) / len(union) if union else 0
def extract_key_phrases(self, text: str) -> List[str]:
"""핡심 문ꡬ μΆ”μΆœ"""
# 20자 μ΄μƒμ˜ λ¬Έμž₯듀을 핡심 문ꡬ둜 κ°„μ£Ό
sentences = [s.strip() for s in re.split(r'[.!?]', text) if len(s.strip()) > 20]
return sentences[:5] # μƒμœ„ 5개만
def is_duplicate(self, paragraph: str) -> bool:
"""쀑볡 문단 감지"""
# 핡심 문ꡬ 체크
key_phrases = self.extract_key_phrases(paragraph)
for phrase in key_phrases:
if phrase in self.seen_key_phrases:
return True
# 전체 문단 μœ μ‚¬λ„ 체크
for seen in self.seen_paragraphs:
if self.check_similarity(paragraph, seen) > self.similarity_threshold:
return True
# 쀑볡이 μ•„λ‹ˆλ©΄ μ €μž₯
self.seen_paragraphs.add(paragraph)
self.seen_key_phrases.update(key_phrases)
return False
def get_used_elements(self) -> List[str]:
"""μ‚¬μš©λœ 핡심 μš”μ†Œ λ°˜ν™˜"""
return list(self.seen_key_phrases)[:10] # 졜근 10개
def count_repetitions(self, content: str) -> int:
"""ν…μŠ€νŠΈ λ‚΄μ˜ 반볡 횟수 계산"""
paragraphs = content.split('\n\n')
repetitions = 0
for i, para1 in enumerate(paragraphs):
for para2 in paragraphs[i+1:]:
if self.check_similarity(para1, para2) > 0.7:
repetitions += 1
return repetitions
class RealTimeConsistencyChecker:
"""μ‹€μ‹œκ°„ μ„œμ‚¬ 일관성 검증"""
def __init__(self):
self.plot_graph = {} # 인과관계 κ·Έλž˜ν”„
self.character_states = {} # 캐릭터 μƒνƒœ 좔적
self.event_timeline = [] # μ‹œκ°„μˆœ 이벀트
self.resolved_conflicts = set() # ν•΄κ²°λœ κ°ˆλ“±
def validate_new_content(self, phase: int, content: str,
previous_contents: List[str]) -> Tuple[bool, List[str]]:
"""μƒˆ μ½˜ν…μΈ μ˜ 일관성 검증"""
issues = []
# 1. μ‹œκ°„ μ—­ν–‰ 검사
time_markers = self.extract_time_markers(content)
if self.check_time_contradiction(time_markers):
issues.append("μ‹œκ°„ μˆœμ„œ λͺ¨μˆœ 발견")
# 2. 캐릭터 μƒνƒœ λͺ¨μˆœ 검사
character_actions = self.extract_character_actions(content)
for char, action in character_actions.items():
if not self.is_action_possible(char, action, phase):
issues.append(f"{char}의 행동이 이전 μƒνƒœμ™€ λͺ¨μˆœλ¨")
# 3. 이미 ν•΄κ²°λœ κ°ˆλ“±μ˜ μž¬λ“±μž₯ 검사
conflicts = self.extract_conflicts(content)
for conflict in conflicts:
if conflict in self.resolved_conflicts:
issues.append(f"이미 ν•΄κ²°λœ κ°ˆλ“± '{conflict}'이 λ‹€μ‹œ λ“±μž₯")
return len(issues) == 0, issues
def extract_time_markers(self, content: str) -> List[str]:
"""μ‹œκ°„ ν‘œμ§€ μΆ”μΆœ"""
markers = re.findall(r'(μ•„μΉ¨|점심|저녁|λ°€|μƒˆλ²½|μ˜€μ „|μ˜€ν›„|λ‹€μŒλ‚ |λ©°μΉ  ν›„|일주일 ν›„)', content)
return markers
def extract_character_actions(self, content: str) -> Dict[str, str]:
"""캐릭터 행동 μΆ”μΆœ"""
actions = {}
# κ°„λ‹¨ν•œ νŒ¨ν„΄ λ§€μΉ­ (μ‹€μ œλ‘œλŠ” 더 μ •κ΅ν•œ NLP ν•„μš”)
patterns = re.findall(r'(\w+)(?:이|κ°€|은|λŠ”)\s+(\w+(?:ν–ˆλ‹€|ν•œλ‹€|ν•˜λ‹€))', content)
for char, action in patterns:
actions[char] = action
return actions
def extract_conflicts(self, content: str) -> List[str]:
"""κ°ˆλ“± μš”μ†Œ μΆ”μΆœ"""
conflict_keywords = ['κ°ˆλ“±', 'λŒ€λ¦½', '좩돌', '문제', 'μœ„κΈ°']
conflicts = []
for keyword in conflict_keywords:
if keyword in content:
# μ£Όλ³€ λ¬Έλ§₯ μΆ”μΆœ
idx = content.index(keyword)
context = content[max(0, idx-20):min(len(content), idx+20)]
conflicts.append(context)
return conflicts
def check_time_contradiction(self, markers: List[str]) -> bool:
"""μ‹œκ°„ λͺ¨μˆœ 검사"""
# κ°„λ‹¨ν•œ μ‹œκ°„ μˆœμ„œ 체크
time_order = ['μƒˆλ²½', 'μ•„μΉ¨', 'μ˜€μ „', '점심', 'μ˜€ν›„', '저녁', 'λ°€']
prev_idx = -1
for marker in markers:
if marker in time_order:
curr_idx = time_order.index(marker)
if curr_idx < prev_idx:
return True
prev_idx = curr_idx
return False
def is_action_possible(self, character: str, action: str, phase: int) -> bool:
"""캐릭터 행동 κ°€λŠ₯μ„± 검사"""
# 캐릭터 μƒνƒœ 기반 검증
if character not in self.character_states:
self.character_states[character] = {"phase": phase, "status": "active"}
return True
char_state = self.character_states[character]
# 예: 죽은 캐릭터가 λ‹€μ‹œ λ“±μž₯ν•˜λŠ” 경우 λ“±
if char_state.get("status") == "dead" and "ν–ˆλ‹€" in action:
return False
return True
class ProgressionMonitor:
"""μ‹€μ‹œκ°„ μ„œμ‚¬ μ§„ν–‰ λͺ¨λ‹ˆν„°λ§"""
def __init__(self):
self.phase_keywords = {}
self.locations = set()
self.characters = set()
def count_new_characters(self, content: str, phase: int) -> int:
"""μƒˆλ‘œμš΄ 인물 λ“±μž₯ 횟수"""
# κ°„λ‹¨ν•œ 고유λͺ…사 μΆ”μΆœ (λŒ€λ¬Έμžλ‘œ μ‹œμž‘ν•˜λŠ” 단어)
potential_names = re.findall(r'\b[A-Zκ°€-힣][a-zκ°€-힣]+\b', content)
new_chars = set(potential_names) - self.characters
self.characters.update(new_chars)
return len(new_chars)
def count_new_locations(self, content: str, phase: int) -> int:
"""μƒˆλ‘œμš΄ μž₯μ†Œ λ“±μž₯ 횟수"""
# μž₯μ†Œ κ΄€λ ¨ ν‚€μ›Œλ“œ
location_markers = ['μ—μ„œ', '으둜', '에', '의', 'at', 'in', 'to']
new_locs = 0
for marker in location_markers:
matches = re.findall(rf'(\S+)\s*{marker}', content)
for match in matches:
if match not in self.locations and len(match) > 2:
self.locations.add(match)
new_locs += 1
return new_locs
def calculate_content_difference(self, current_phase: int, content: str, previous_content: str) -> float:
"""이전 λ‹¨κ³„μ™€μ˜ λ‚΄μš© 차이 λΉ„μœ¨"""
if not previous_content:
return 1.0
dedup = ContentDeduplicator()
return 1.0 - dedup.check_similarity(content, previous_content)
def count_repetitions(self, content: str) -> int:
"""반볡 횟수 계산"""
paragraphs = content.split('\n\n')
repetitions = 0
for i, para1 in enumerate(paragraphs):
for para2 in paragraphs[i+1:]:
similarity = ContentDeduplicator().check_similarity(para1, para2)
if similarity > 0.7:
repetitions += 1
return repetitions
def calculate_progression_score(self, current_phase: int, content: str, previous_content: str = "") -> Dict[str, float]:
"""진행도 점수 계산"""
scores = {
"new_elements": 0.0, # μƒˆλ‘œμš΄ μš”μ†Œ
"character_growth": 0.0, # 인물 μ„±μž₯
"plot_advancement": 0.0, # ν”Œλ‘― μ§„μ „
"no_repetition": 0.0 # 반볡 μ—†μŒ
}
# μƒˆλ‘œμš΄ μš”μ†Œ 체크
new_characters = self.count_new_characters(content, current_phase)
new_locations = self.count_new_locations(content, current_phase)
scores["new_elements"] = min(10, (new_characters * 3 + new_locations * 2))
# μ„±μž₯ κ΄€λ ¨ ν‚€μ›Œλ“œ
growth_keywords = ["κΉ¨λ‹¬μ•˜λ‹€", "μ΄μ œλŠ”", "λ‹¬λΌμ‘Œλ‹€", "μƒˆλ‘­κ²Œ", "λΉ„λ‘œμ†Œ", "λ³€ν–ˆλ‹€", "더 이상"]
growth_count = sum(1 for k in growth_keywords if k in content)
scores["character_growth"] = min(10, growth_count * 2)
# ν”Œλ‘― μ§„μ „ (이전 λ‹¨κ³„μ™€μ˜ 차이)
if current_phase > 1 and previous_content:
diff_ratio = self.calculate_content_difference(current_phase, content, previous_content)
scores["plot_advancement"] = min(10, diff_ratio * 10)
else:
scores["plot_advancement"] = 8.0 # 첫 λ‹¨κ³„λŠ” κΈ°λ³Έ 점수
# 반볡 체크 (μ—­μ‚°)
repetition_count = self.count_repetitions(content)
scores["no_repetition"] = max(0, 10 - repetition_count * 2)
return scores
class ProgressiveNarrativeTracker:
"""μ„œμ‚¬ μ§„ν–‰κ³Ό λˆ„μ μ„ μΆ”μ ν•˜λŠ” μ‹œμŠ€ν…œ"""
def __init__(self):
self.character_arcs: Dict[str, CharacterArc] = {}
self.plot_threads: Dict[str, PlotThread] = {}
self.symbolic_evolutions: Dict[str, SymbolicEvolution] = {}
self.phase_summaries: Dict[int, str] = {}
self.accumulated_events: List[Dict[str, Any]] = []
self.thematic_deepening: List[str] = []
self.philosophical_insights: List[str] = [] # 철학적 톡찰 좔적
self.literary_devices: Dict[int, List[str]] = {} # 문학적 기법 μ‚¬μš© 좔적
self.character_consistency = CharacterConsistency() # 캐릭터 일관성 μΆ”κ°€
self.content_deduplicator = ContentDeduplicator() # 쀑볡 감지기 μΆ”κ°€
self.progression_monitor = ProgressionMonitor() # 진행도 λͺ¨λ‹ˆν„° μΆ”κ°€
self.used_expressions: Set[str] = set() # μ‚¬μš©λœ ν‘œν˜„ 좔적
self.consistency_checker = RealTimeConsistencyChecker() # μ‹€μ‹œκ°„ 일관성 체컀 μΆ”κ°€
def register_character_arc(self, name: str, initial_state: Dict[str, Any]):
"""캐릭터 아크 등둝"""
self.character_arcs[name] = CharacterArc(name=name, initial_state=initial_state)
self.character_consistency.register_name(0, "protagonist", name)
logger.info(f"Character arc registered: {name}")
def update_character_state(self, name: str, phase: int, new_state: Dict[str, Any], transformation: str):
"""캐릭터 μƒνƒœ μ—…λ°μ΄νŠΈ 및 λ³€ν™” 기둝"""
if name in self.character_arcs:
arc = self.character_arcs[name]
arc.phase_states[phase] = new_state
arc.transformations.append(f"Phase {phase}: {transformation}")
logger.info(f"Character {name} transformed in phase {phase}: {transformation}")
def add_plot_thread(self, thread_id: str, description: str, intro_phase: int):
"""μƒˆλ‘œμš΄ ν”Œλ‘― 라인 μΆ”κ°€"""
self.plot_threads[thread_id] = PlotThread(
thread_id=thread_id,
description=description,
introduction_phase=intro_phase,
development_phases=[]
)
def develop_plot_thread(self, thread_id: str, phase: int):
"""ν”Œλ‘― 라인 λ°œμ „"""
if thread_id in self.plot_threads:
self.plot_threads[thread_id].development_phases.append(phase)
def check_narrative_progression(self, current_phase: int) -> Tuple[bool, List[str]]:
"""μ„œμ‚¬κ°€ μ‹€μ œλ‘œ μ§„ν–‰λ˜κ³  μžˆλŠ”μ§€ 확인"""
issues = []
# 1. 캐릭터 λ³€ν™” 확인
static_characters = []
for name, arc in self.character_arcs.items():
if len(arc.transformations) < current_phase // 3: # μ΅œμ†Œ 3λ‹¨κ³„λ§ˆλ‹€ λ³€ν™” ν•„μš”
static_characters.append(name)
if static_characters:
issues.append(f"λ‹€μŒ μΈλ¬Όλ“€μ˜ λ³€ν™”κ°€ λΆ€μ‘±ν•©λ‹ˆλ‹€: {', '.join(static_characters)}")
# 2. ν”Œλ‘― μ§„ν–‰ 확인
unresolved_threads = []
for thread_id, thread in self.plot_threads.items():
if thread.status == "active" and len(thread.development_phases) < 2:
unresolved_threads.append(thread.description)
if unresolved_threads:
issues.append(f"μ§„μ „λ˜μ§€ μ•Šμ€ ν”Œλ‘―: {', '.join(unresolved_threads)}")
# 3. 상징 λ°œμ „ 확인
static_symbols = []
for symbol, evolution in self.symbolic_evolutions.items():
if len(evolution.phase_meanings) < current_phase // 4:
static_symbols.append(symbol)
if static_symbols:
issues.append(f"μ˜λ―Έκ°€ λ°œμ „ν•˜μ§€ μ•Šμ€ 상징: {', '.join(static_symbols)}")
# 4. 철학적 깊이 확인
if len(self.philosophical_insights) < current_phase // 2:
issues.append("철학적 μ„±μ°°κ³Ό 인간에 λŒ€ν•œ 톡찰이 λΆ€μ‘±ν•©λ‹ˆλ‹€")
# 5. 문학적 기법 λ‹€μ–‘μ„±
unique_devices = set()
for devices in self.literary_devices.values():
unique_devices.update(devices)
if len(unique_devices) < 5:
issues.append("문학적 기법이 λ‹¨μ‘°λ‘­μŠ΅λ‹ˆλ‹€. 더 λ‹€μ–‘ν•œ ν‘œν˜„ 기법이 ν•„μš”ν•©λ‹ˆλ‹€")
# 6. 캐릭터 이름 일관성
name_issues = []
for phase, role, name in self.character_consistency.name_history:
if not self.character_consistency.validate_name(phase, role, name):
name_issues.append(f"Phase {phase}: {role} 이름 뢈일치 ({name})")
if name_issues:
issues.extend(name_issues)
# 7. 인과관계 체크 (μΆ”κ°€)
if current_phase > 3:
# μ‚¬κ±΄μ˜ 연쇄성 확인
if len(self.accumulated_events) < current_phase - 1:
issues.append("사건 κ°„ 인과관계가 뢈λͺ…ν™•ν•©λ‹ˆλ‹€. 각 사건이 λ‹€μŒ μ‚¬κ±΄μ˜ 원인이 λ˜μ–΄μ•Ό ν•©λ‹ˆλ‹€")
return len(issues) == 0, issues
def generate_phase_requirements(self, phase: int) -> str:
"""각 단계별 ν•„μˆ˜ μš”κ΅¬μ‚¬ν•­ 생성 (κ°„μ†Œν™”)"""
requirements = []
# 이전 단계 μš”μ•½
if phase > 1 and (phase-1) in self.phase_summaries:
requirements.append(f"이전 단계 핡심: {self.phase_summaries[phase-1][:200]}...")
# μ‚¬μš©λœ ν‘œν˜„ λͺ©λ‘ (5개만)
if self.used_expressions:
requirements.append("\n❌ λ‹€μŒ ν‘œν˜„μ€ 이미 μ‚¬μš©λ¨:")
for expr in list(self.used_expressions)[-5:]: # 졜근 5개만
requirements.append(f"- {expr[:50]}...")
# 단계별 특수 μš”κ΅¬μ‚¬ν•­
phase_name = NARRATIVE_PHASES[phase-1] if phase <= 10 else "μˆ˜μ •"
requirements.append(f"\nβœ… {phase_name} ν•„μˆ˜ 포함:")
requirements.append(f"- μ΅œμ†Œ {MIN_WORDS_PER_WRITER}단어 μž‘μ„±")
requirements.append("- ꡬ체적인 μž₯λ©΄ λ¬˜μ‚¬μ™€ λŒ€ν™”")
requirements.append("- 인물의 λ‚΄λ©΄ 탐ꡬ")
requirements.append("- 이전 λ‹¨κ³„μ˜ 결과둜 μ‹œμž‘")
return "\n".join(requirements)
def extract_used_elements(self, content: str):
"""μ‚¬μš©λœ 핡심 ν‘œν˜„ μΆ”μΆœ 및 μ €μž₯"""
# 20자 μ΄μƒμ˜ νŠΉμ§•μ μΈ λ¬Έμž₯λ“€ μΆ”μΆœ
sentences = re.findall(r'[^.!?]+[.!?]', content)
for sent in sentences:
if len(sent) > 20 and len(sent) < 100:
self.used_expressions.add(sent.strip())
class NovelDatabase:
"""λ°μ΄ν„°λ² μ΄μŠ€ 관리"""
@staticmethod
def init_db():
with sqlite3.connect(DB_PATH) as conn:
conn.execute("PRAGMA journal_mode=WAL")
cursor = conn.cursor()
# κΈ°μ‘΄ ν…Œμ΄λΈ”λ“€
cursor.execute('''
CREATE TABLE IF NOT EXISTS sessions (
session_id TEXT PRIMARY KEY,
user_query TEXT NOT NULL,
language TEXT NOT NULL,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now')),
status TEXT DEFAULT 'active',
current_stage INTEGER DEFAULT 0,
final_novel TEXT,
literary_report TEXT,
total_words INTEGER DEFAULT 0,
narrative_tracker TEXT
)
''')
cursor.execute('''
CREATE TABLE IF NOT EXISTS stages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL,
stage_number INTEGER NOT NULL,
stage_name TEXT NOT NULL,
role TEXT NOT NULL,
content TEXT,
word_count INTEGER DEFAULT 0,
status TEXT DEFAULT 'pending',
progression_score REAL DEFAULT 0.0,
repetition_score REAL DEFAULT 0.0,
consistency_check TEXT,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (session_id) REFERENCES sessions(session_id),
UNIQUE(session_id, stage_number)
)
''')
cursor.execute('''
CREATE TABLE IF NOT EXISTS plot_threads (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL,
thread_id TEXT NOT NULL,
description TEXT,
introduction_phase INTEGER,
status TEXT DEFAULT 'active',
created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (session_id) REFERENCES sessions(session_id)
)
''')
# μƒˆλ‘œμš΄ ν…Œμ΄λΈ”: 쀑볡 감지 기둝
cursor.execute('''
CREATE TABLE IF NOT EXISTS duplicate_detection (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL,
phase INTEGER NOT NULL,
duplicate_content TEXT,
original_phase INTEGER,
similarity_score REAL,
created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (session_id) REFERENCES sessions(session_id)
)
''')
# μƒˆλ‘œμš΄ ν…Œμ΄λΈ”: ν’ˆμ§ˆ 평가 기둝
cursor.execute('''
CREATE TABLE IF NOT EXISTS quality_evaluations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL,
evaluation_type TEXT NOT NULL,
score REAL,
details TEXT,
created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (session_id) REFERENCES sessions(session_id)
)
''')
conn.commit()
# κΈ°μ‘΄ λ©”μ„œλ“œλ“€ μœ μ§€
@staticmethod
@contextmanager
def get_db():
with db_lock:
conn = sqlite3.connect(DB_PATH, timeout=30.0)
conn.row_factory = sqlite3.Row
try:
yield conn
finally:
conn.close()
@staticmethod
def create_session(user_query: str, language: str) -> str:
session_id = hashlib.md5(f"{user_query}{datetime.now()}".encode()).hexdigest()
with NovelDatabase.get_db() as conn:
conn.cursor().execute(
'INSERT INTO sessions (session_id, user_query, language) VALUES (?, ?, ?)',
(session_id, user_query, language)
)
conn.commit()
return session_id
@staticmethod
def save_stage(session_id: str, stage_number: int, stage_name: str,
role: str, content: str, status: str = 'complete',
progression_score: float = 0.0, repetition_score: float = 0.0,
consistency_check: str = ""):
word_count = len(content.split()) if content else 0
with NovelDatabase.get_db() as conn:
cursor = conn.cursor()
cursor.execute('''
INSERT INTO stages (session_id, stage_number, stage_name, role, content, word_count, status, progression_score, repetition_score, consistency_check)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(session_id, stage_number)
DO UPDATE SET content=?, word_count=?, status=?, stage_name=?, progression_score=?, repetition_score=?, consistency_check=?, updated_at=datetime('now')
''', (session_id, stage_number, stage_name, role, content, word_count, status, progression_score, repetition_score, consistency_check,
content, word_count, status, stage_name, progression_score, repetition_score, consistency_check))
# 총 단어 수 μ—…λ°μ΄νŠΈ
cursor.execute('''
UPDATE sessions
SET total_words = (
SELECT SUM(word_count)
FROM stages
WHERE session_id = ? AND role LIKE 'writer%' AND content IS NOT NULL
),
updated_at = datetime('now'),
current_stage = ?
WHERE session_id = ?
''', (session_id, stage_number, session_id))
conn.commit()
@staticmethod
def get_writer_content(session_id: str) -> str:
"""μž‘κ°€ μ½˜ν…μΈ  κ°€μ Έμ˜€κΈ° (μˆ˜μ •λ³Έ μš°μ„ )"""
with NovelDatabase.get_db() as conn:
all_content = []
for writer_num in range(1, 11):
# μˆ˜μ •λ³Έμ΄ 있으면 μˆ˜μ •λ³Έμ„, μ—†μœΌλ©΄ μ΄ˆμ•ˆμ„
row = conn.cursor().execute('''
SELECT content FROM stages
WHERE session_id = ? AND role = ?
AND stage_name LIKE '%μˆ˜μ •λ³Έ%'
ORDER BY stage_number DESC LIMIT 1
''', (session_id, f'writer{writer_num}')).fetchone()
if not row or not row['content']:
# μˆ˜μ •λ³Έμ΄ μ—†μœΌλ©΄ μ΄ˆμ•ˆ μ‚¬μš©
row = conn.cursor().execute('''
SELECT content FROM stages
WHERE session_id = ? AND role = ?
AND stage_name LIKE '%μ΄ˆμ•ˆ%'
ORDER BY stage_number DESC LIMIT 1
''', (session_id, f'writer{writer_num}')).fetchone()
if row and row['content']:
all_content.append(row['content'].strip())
return '\n\n'.join(all_content)
@staticmethod
def get_total_words(session_id: str) -> int:
"""총 단어 수 κ°€μ Έμ˜€κΈ°"""
with NovelDatabase.get_db() as conn:
row = conn.cursor().execute(
'SELECT total_words FROM sessions WHERE session_id = ?',
(session_id,)
).fetchone()
return row['total_words'] if row and row['total_words'] else 0
@staticmethod
def save_narrative_tracker(session_id: str, tracker: ProgressiveNarrativeTracker):
"""μ„œμ‚¬ 좔적기 μ €μž₯"""
with NovelDatabase.get_db() as conn:
tracker_data = json.dumps({
'character_arcs': {k: asdict(v) for k, v in tracker.character_arcs.items()},
'plot_threads': {k: asdict(v) for k, v in tracker.plot_threads.items()},
'phase_summaries': tracker.phase_summaries,
'thematic_deepening': tracker.thematic_deepening,
'philosophical_insights': tracker.philosophical_insights,
'literary_devices': tracker.literary_devices,
'character_consistency': asdict(tracker.character_consistency),
'used_expressions': list(tracker.used_expressions)
})
conn.cursor().execute(
'UPDATE sessions SET narrative_tracker = ? WHERE session_id = ?',
(tracker_data, session_id)
)
conn.commit()
@staticmethod
def load_narrative_tracker(session_id: str) -> Optional[ProgressiveNarrativeTracker]:
"""μ„œμ‚¬ 좔적기 λ‘œλ“œ"""
with NovelDatabase.get_db() as conn:
row = conn.cursor().execute(
'SELECT narrative_tracker FROM sessions WHERE session_id = ?',
(session_id,)
).fetchone()
if row and row['narrative_tracker']:
data = json.loads(row['narrative_tracker'])
tracker = ProgressiveNarrativeTracker()
# 데이터 볡원
for name, arc_data in data.get('character_arcs', {}).items():
tracker.character_arcs[name] = CharacterArc(**arc_data)
for thread_id, thread_data in data.get('plot_threads', {}).items():
tracker.plot_threads[thread_id] = PlotThread(**thread_data)
tracker.phase_summaries = data.get('phase_summaries', {})
tracker.thematic_deepening = data.get('thematic_deepening', [])
tracker.philosophical_insights = data.get('philosophical_insights', [])
tracker.literary_devices = data.get('literary_devices', {})
# 캐릭터 일관성 볡원
if 'character_consistency' in data:
tracker.character_consistency = CharacterConsistency(**data['character_consistency'])
# μ‚¬μš©λœ ν‘œν˜„ 볡원
if 'used_expressions' in data:
tracker.used_expressions = set(data['used_expressions'])
return tracker
return None
@staticmethod
def save_duplicate_detection(session_id: str, phase: int, duplicate_content: str,
original_phase: int, similarity_score: float):
"""쀑볡 감지 기둝 μ €μž₯"""
with NovelDatabase.get_db() as conn:
conn.cursor().execute('''
INSERT INTO duplicate_detection
(session_id, phase, duplicate_content, original_phase, similarity_score)
VALUES (?, ?, ?, ?, ?)
''', (session_id, phase, duplicate_content, original_phase, similarity_score))
conn.commit()
@staticmethod
def save_quality_evaluation(session_id: str, evaluation_type: str, score: float, details: str):
"""ν’ˆμ§ˆ 평가 μ €μž₯"""
with NovelDatabase.get_db() as conn:
conn.cursor().execute('''
INSERT INTO quality_evaluations
(session_id, evaluation_type, score, details)
VALUES (?, ?, ?, ?)
''', (session_id, evaluation_type, score, details))
conn.commit()
@staticmethod
def get_session(session_id: str) -> Optional[Dict]:
with NovelDatabase.get_db() as conn:
row = conn.cursor().execute('SELECT * FROM sessions WHERE session_id = ?', (session_id,)).fetchone()
return dict(row) if row else None
@staticmethod
def get_stages(session_id: str) -> List[Dict]:
with NovelDatabase.get_db() as conn:
rows = conn.cursor().execute('SELECT * FROM stages WHERE session_id = ? ORDER BY stage_number', (session_id,)).fetchall()
return [dict(row) for row in rows]
@staticmethod
def update_final_novel(session_id: str, final_novel: str, literary_report: str = ""):
with NovelDatabase.get_db() as conn:
conn.cursor().execute(
"UPDATE sessions SET final_novel = ?, status = 'complete', updated_at = datetime('now'), literary_report = ? WHERE session_id = ?",
(final_novel, literary_report, session_id)
)
conn.commit()
@staticmethod
def get_active_sessions() -> List[Dict]:
with NovelDatabase.get_db() as conn:
rows = conn.cursor().execute(
"""SELECT session_id, user_query, language, created_at, current_stage,
COALESCE(total_words, 0) as total_words
FROM sessions
WHERE status = 'active'
ORDER BY updated_at DESC
LIMIT 10"""
).fetchall()
return [dict(row) for row in rows]
class WebSearchIntegration:
"""μ›Ή 검색 κΈ°λŠ₯"""
def __init__(self):
self.brave_api_key = BRAVE_SEARCH_API_KEY
self.search_url = "https://api.search.brave.com/res/v1/web/search"
self.enabled = bool(self.brave_api_key)
def search(self, query: str, count: int = 3, language: str = "en") -> List[Dict]:
if not self.enabled:
return []
headers = {
"Accept": "application/json",
"X-Subscription-Token": self.brave_api_key
}
params = {
"q": query,
"count": count,
"search_lang": "ko" if language == "Korean" else "en",
"text_decorations": False,
"safesearch": "moderate"
}
try:
response = requests.get(self.search_url, headers=headers, params=params, timeout=10)
response.raise_for_status()
results = response.json().get("web", {}).get("results", [])
return results
except requests.exceptions.RequestException as e:
logger.error(f"μ›Ή 검색 API 였λ₯˜: {e}")
return []
def extract_relevant_info(self, results: List[Dict], max_chars: int = 1500) -> str:
if not results:
return ""
extracted = []
total_chars = 0
for i, result in enumerate(results[:3], 1):
title = result.get("title", "")
description = result.get("description", "")
info = f"[{i}] {title}: {description}"
if total_chars + len(info) < max_chars:
extracted.append(info)
total_chars += len(info)
else:
break
return "\n".join(extracted)
class ProgressiveLiterarySystem:
"""μ§„ν–‰ν˜• λ¬Έν•™ μ†Œμ„€ 생성 μ‹œμŠ€ν…œ"""
def __init__(self):
self.token = FRIENDLI_TOKEN
self.api_url = API_URL
self.model_id = MODEL_ID
self.narrative_tracker = ProgressiveNarrativeTracker()
self.web_search = WebSearchIntegration()
self.language_filter = LanguageFilter() # μ–Έμ–΄ ν•„ν„° μΆ”κ°€
self.current_session_id = None
NovelDatabase.init_db()
def augment_prompt_if_needed(self, user_query: str, language: str) -> str:
"""ν”„λ‘¬ν”„νŠΈ 증강 - νŠΉλ³„ν•œ μ§€μ‹œκ°€ 없을 λ•Œ μ˜ˆμ‹œ ν˜•μ‹μœΌλ‘œ 증강"""
# νŠΉλ³„ν•œ μ§€μ‹œ ν‚€μ›Œλ“œ 체크
special_keywords = ['인과관계', '캐릭터 일관성', 'μ„€μ • 일관성', '문체 일관성',
'causality', 'character consistency', 'setting consistency', 'style consistency']
has_special_instruction = any(keyword in user_query.lower() for keyword in special_keywords)
if has_special_instruction:
return user_query
# νŠΉλ³„ν•œ μ§€μ‹œκ°€ μ—†μœΌλ©΄ μ˜ˆμ‹œ ν˜•μ‹μœΌλ‘œ 증강
example_augmentation = {
"Korean": f"""인과관계 μΉ˜λ°€ν•¨: {user_query}. λͺ¨λ“  사건은 필연적 μΈκ³Όκ΄€κ³„λ‘œ μ—°κ²°λ˜λ©°, μ™ΈλΆ€ 좩격이 내적 κ°ˆλ“±μ„ μ‹¬ν™”μ‹œν‚€κ³ , 각 선택이 λ‹€μŒ ꡭ면을 κ²°μ •ν•œλ‹€. μš°μ—°μ€ μ—†κ³  λͺ¨λ“  μž₯면이 결말을 ν–₯ν•œ ν•„μˆ˜ μš”μ†Œκ°€ λœλ‹€.
캐릭터 일관성: 주인곡은 μ²˜μŒλΆ€ν„° λκΉŒμ§€ 핡심 κ°€μΉ˜κ΄€μ„ μœ μ§€ν•˜λ˜, 사건을 톡해 κ·Έ κ°€μΉ˜κ΄€μ˜ μƒˆλ‘œμš΄ 면을 λ°œκ²¬ν•œλ‹€. 각 인물은 κ³ μœ ν•œ λ§νˆ¬μ™€ 행동 νŒ¨ν„΄μ„ κ°€μ§€λ©°, κ°ˆλ“± μƒν™©μ—μ„œλ„ μΊλ¦­ν„°μ˜ λ³Έμ§ˆμ—μ„œ λ²—μ–΄λ‚˜μ§€ μ•ŠλŠ”λ‹€.
μ„€μ • 일관성: 핡심 상징물은 이야기 전체λ₯Ό κ΄€ν†΅ν•˜λ©° μ μ§„μ μœΌλ‘œ μ˜λ―Έκ°€ ν™•μž₯λœλ‹€. 곡간과 μ‹œκ°„ 섀정은 λ‹¨μˆœν•œ 배경이 μ•„λ‹ˆλΌ μ„œμ‚¬μ˜ ν•„μˆ˜ μš”μ†Œλ‘œ κΈ°λŠ₯ν•œλ‹€.
문체 일관성: μ„ νƒν•œ μ„œμˆ  μ‹œμ κ³Ό 문체 톀을 λκΉŒμ§€ μœ μ§€ν•˜λ˜, μ„œμ‚¬μ˜ 흐름에 따라 리듬과 ν˜Έν‘μ„ μ‘°μ ˆν•œλ‹€. μž₯λ₯΄μ  νŠΉμ„±μ„ μ‚΄λ¦° 문체둜 λ…μžλ₯Ό λͺ°μž…μ‹œν‚¨λ‹€.""",
"English": f"""Causal Tightness: {user_query}. All events connect through necessary causality, external shocks deepen internal conflicts, each choice determines the next phase. No coincidences - every scene is essential to the conclusion.
Character Consistency: Protagonist maintains core values throughout while discovering new facets through events. Each character has unique speech patterns and behaviors, staying true to essence even in conflict.
Setting Consistency: Core symbols pervade the entire story with gradually expanding meanings. Space and time settings function as narrative necessities, not mere backdrops.
Style Consistency: Maintain chosen POV and tone throughout while adjusting rhythm and pacing to narrative flow. Genre-appropriate style immerses readers."""
}
return example_augmentation.get(language, example_augmentation["Korean"])
def create_headers(self):
return {"Authorization": f"Bearer {self.token}", "Content-Type": "application/json"}
# --- ν”„λ‘¬ν”„νŠΈ 생성 ν•¨μˆ˜λ“€ ---
def create_director_initial_prompt(self, user_query: str, language: str) -> str:
"""κ°λ…μž 초기 기획 - ν†΅ν•©λœ μ„œμ‚¬ ꡬ쑰"""
# ν”„λ‘¬ν”„νŠΈ 증강 - νŠΉλ³„ν•œ μ§€μ‹œκ°€ 없을 λ•Œ μ˜ˆμ‹œ ν˜•μ‹μœΌλ‘œ 증강
augmented_query = self.augment_prompt_if_needed(user_query, language)
search_results_str = ""
if self.web_search.enabled:
# 철학적 ν‚€μ›Œλ“œ μΆ”κ°€ (쿼리 길이 μ œν•œ)
short_query = user_query[:50] if len(user_query) > 50 else user_query
queries = [
f"{short_query} 철학적 의미", # 철학적 관점
f"인간 쑴재 의미 {short_query}", # 싀쑴적 주제
f"{short_query} λ¬Έν•™ μž‘ν’ˆ",
]
for q in queries[:2]: # 2κ°œκΉŒμ§€λ§Œ 검색
try:
results = self.web_search.search(q, count=2, language=language)
if results:
search_results_str += self.web_search.extract_relevant_info(results)[:500] + "\n"
except Exception as e:
logger.warning(f"검색 쿼리 μ‹€νŒ¨: {q[:50]}... - {str(e)}")
continue
lang_prompts = {
"Korean": f"""당신은 노벨문학상 μˆ˜μƒμž‘κ°€ μˆ˜μ€€μ˜ ν•œκ΅­ λ¬Έν•™ κ±°μž₯μž…λ‹ˆλ‹€.
μ€‘νŽΈ μ†Œμ„€(8,000단어)을 μœ„ν•œ ν†΅ν•©λœ μ„œμ‚¬ ꡬ쑰λ₯Ό κΈ°νšν•˜μ„Έμš”.
**주제:** {augmented_query}
**참고 자료:**
{search_results_str[:500] if search_results_str else "N/A"}
**ν•„μˆ˜ μš”κ΅¬μ‚¬ν•­:**
1. **인과관계가 μΉ˜λ°€ν•œ 톡합 μ„œμ‚¬**
- 10개 단계가 ν•„μ—°μ μœΌλ‘œ μ—°κ²°λœ 단일 μ„œμ‚¬
- 각 λ‹¨κ³„λŠ” 이전 λ‹¨κ³„μ˜ 직접적 결과둜 이어짐
- μ™ΈλΆ€ 좩격과 내적 κ°ˆλ“±μ˜ 인과관계 λͺ…ν™•νžˆ
- 주인곡과 μ‘°μ—°λ“€μ˜ 이름/μ„€μ • μ²˜μŒλΆ€ν„° κ³ μ •
단계별 μ§„ν–‰:
1) λ„μž…: 일상과 κ· μ—΄
2) λ°œμ „ 1: λΆˆμ•ˆμ˜ κ³ μ‘°
3) λ°œμ „ 2: μ™ΈλΆ€ 좩격
4) λ°œμ „ 3: 내적 κ°ˆλ“± 심화
5) μ ˆμ • 1: μœ„κΈ°μ˜ 정점
6) μ ˆμ • 2: μ„ νƒμ˜ μˆœκ°„
7) ν•˜κ°• 1: 결과와 μ—¬νŒŒ
8) ν•˜κ°• 2: μƒˆλ‘œμš΄ 인식
9) 결말 1: λ³€ν™”λœ 일상
10) 결말 2: μ—΄λ¦° 질문
2. **캐릭터 일관성과 μž…μ²΄μ„±**
- 각 인물의 핡심 κ°€μΉ˜κ΄€/성격 λͺ…μ‹œ
- 주인곡: 초기 β†’ 쀑간 β†’ μ΅œμ’… μƒνƒœμ˜ λ³€ν™” ꢀ적
- μ‘°μ—°λ“€: κ³ μœ ν•œ λ§νˆ¬μ™€ 행동 νŒ¨ν„΄ μ„€μ •
- κ°ˆλ“±μ΄ 인물의 λ³Έμ§ˆμ„ λ“œλŸ¬λ‚΄λ„λ‘ 섀계
3. **μ„€μ •κ³Ό μƒμ§•μ˜ μΌκ΄€λœ ν™œμš©**
- 핡심 상징물(1-2개)의 의미 λ³€ν™” 좔적
- 곡간/μ‹œκ°„ μ„€μ •μ˜ μ„œμ‚¬μ  κΈ°λŠ₯ λͺ…μ‹œ
- λ°˜λ³΅λ˜λŠ” λͺ¨ν‹°ν”„μ˜ 점진적 의미 ν™•μž₯
4. **문체 일관성 κ³„νš**
- μ„œμˆ  μ‹œμ κ³Ό 문체 톀 κ²°μ •
- μž₯λ₯΄μ  νŠΉμ„±μ„ μ‚΄λ¦° 문체 μ „λž΅
- 리듬과 호흑의 λ³€μ£Ό κ³„νš
5. **λΆ„λŸ‰ κ³„νš**
- 총 8,000단어
- 각 단계 800단어
μΉ˜λ°€ν•˜κ³  유기적인 κ³„νšμ„ μ œμ‹œν•˜μ„Έμš”.""",
"English": f"""You are a Nobel Prize-winning literary master.
Plan an integrated narrative structure for a novella (8,000 words).
**Theme:** {augmented_query}
**Reference:**
{search_results_str[:500] if search_results_str else "N/A"}
**Requirements:**
1. **Causally Tight Integrated Structure**
- 10 phases connected by necessity
- Each phase as direct result of previous
- Clear external conflicts and internal struggles
- Fixed protagonist/supporting cast names
2. **Character Consistency & Depth**
- Core values/personality for each character
- Protagonist's transformation trajectory
- Unique speech patterns and behaviors
- Conflicts reveal character essence
3. **Consistent Settings & Symbols**
- 1-2 core symbols with evolving meanings
- Narrative function of space/time settings
- Progressive expansion of recurring motifs
4. **Stylistic Consistency Plan**
- Narrative POV and tone decisions
- Genre-appropriate style strategy
- Rhythm and pacing variations
5. **Length Plan**
- Total 8,000 words
- 800 words per phase
Present precise, organic plan."""
}
return lang_prompts.get(language, lang_prompts["Korean"])
def create_writer_prompt_enhanced(self, writer_number: int, director_plan: str,
previous_content: str, phase_requirements: str,
narrative_summary: str, language: str,
used_elements: List[str]) -> str:
"""κ°•ν™”λœ μž‘κ°€ ν”„λ‘¬ν”„νŠΈ - λΆ„λŸ‰ 문제 ν•΄κ²°"""
phase_name = NARRATIVE_PHASES[writer_number-1]
target_words = MIN_WORDS_PER_WRITER
# ν˜„μž¬κΉŒμ§€ 총 단어 수
total_words = NovelDatabase.get_total_words(self.current_session_id) if self.current_session_id else 0
remaining_words = TARGET_WORDS - total_words
lang_prompts = {
"Korean": f"""당신은 μž‘κ°€ {writer_number}λ²ˆμž…λ‹ˆλ‹€.
**ν˜„μž¬ 단계: {phase_name}**
**전체 κ³„νš (μš”μ•½):**
{director_plan[:800]}
**이전 λ‚΄μš©:**
{previous_content[-800:] if previous_content else "μ‹œμž‘"}
**ν˜„μž¬ μ§„ν–‰ 상황:**
- ν˜„μž¬κΉŒμ§€ 총 단어: {total_words}
- λͺ©ν‘œκΉŒμ§€ 남은 단어: {remaining_words}
- 이번 단계 μ΅œμ†Œ 단어: {target_words}
**μž‘μ„± μ§€μΉ¨:**
1. **ν•„μˆ˜ λΆ„λŸ‰: {target_words}-1000 단어**
2. **μ„œμ‚¬μ˜ ν•„μ—°μ„± 확보:**
- 이전 λ‹¨κ³„μ˜ 직접적 결과둜 μ‹œμž‘
- μƒˆλ‘œμš΄ 사건은 κΈ°μ‘΄ κ°ˆλ“±μ˜ 논리적 κ·€κ²°
- 인물의 선택이 λ‹€μŒ ꡭ면을 κ²°μ •
- μš°μ—°μ΄ μ•„λ‹Œ ν•„μ—°μœΌλ‘œ μ „κ°œ
3. **캐릭터 일관성 μœ μ§€:**
- μ„€μ •λœ 인물λͺ…κ³Ό 관계 μ—„μˆ˜
- 각 인물의 고유 말투 μœ μ§€
- μ„±κ²©μ˜ 일관성 μ†μ—μ„œ λ³€ν™” ν‘œν˜„
- 행동이 μΊλ¦­ν„°μ˜ λ³Έμ§ˆμ—μ„œ 좜발
4. **ꡬ체적 μž₯λ©΄ ꡬ성:**
- 감각적 λ¬˜μ‚¬ (μ‹œκ°, 청각, 촉각 λ“±)
- μƒμƒν•œ λŒ€ν™” (인물별 μ–΄νˆ¬ 차별화)
- λ‚΄λ©΄ μ‹¬λ¦¬μ˜ ꡬ체적 ν‘œν˜„
- 곡간과 μ‹œκ°„μ˜ λͺ…ν™•ν•œ μ„€μ •
5. **κΈˆμ§€μ‚¬ν•­:**
- 이전 상황 λ‹¨μˆœ 반볡
- 캐릭터 이름/μ„€μ • λ³€κ²½
- κ°‘μž‘μŠ€λŸ¬μš΄ μ„€μ • λ³€ν™”
- λΆ„λŸ‰ 미달
**μ€‘μš”: λ°˜λ“œμ‹œ {target_words}단어 이상, 인과관계가 λͺ…ν™•ν•œ μ„œμ‚¬λ₯Ό μž‘μ„±ν•˜μ„Έμš”!**
이제 μ‹œμž‘ν•˜μ„Έμš”:""",
"English": f"""You are Writer #{writer_number}.
**Current Phase: {phase_name}**
**Overall Plan (Summary):**
{director_plan[:800]}
**Previous Content:**
{previous_content[-800:] if previous_content else "Beginning"}
**Progress Status:**
- Total words so far: {total_words}
- Words remaining to target: {remaining_words}
- Minimum words this phase: {target_words}
**Writing Guidelines:**
1. **Required Length: {target_words}-1000 words**
2. **Narrative Necessity:**
- Start from direct results of previous
- New events as logical consequences
- Character choices determine next phase
- Necessity, not coincidence
3. **Character Consistency:**
- Maintain established names/relationships
- Keep unique speech patterns
- Express change within consistent personality
- Actions stem from character essence
4. **Concrete Scene Construction:**
- Sensory descriptions (visual, auditory, tactile)
- Vivid dialogue (differentiated speech)
- Specific psychological expression
- Clear spatial/temporal settings
5. **Forbidden:**
- Simple repetition
- Character name/setting changes
- Sudden setting shifts
- Under word count
**IMPORTANT: Must write {target_words}+ words with clear causality!**
Begin now:"""
}
return lang_prompts.get(language, lang_prompts["Korean"])
def create_critic_consistency_prompt_enhanced(self, all_content: str,
narrative_tracker: ProgressiveNarrativeTracker,
user_query: str, language: str) -> str:
"""κ°•ν™”λœ 비평가 쀑간 κ²€ν† """
# μ„œμ‚¬ μ§„ν–‰ 체크
phase_count = len(narrative_tracker.phase_summaries)
progression_ok, issues = narrative_tracker.check_narrative_progression(phase_count)
# 쀑볡 감지
duplicates = []
paragraphs = all_content.split('\n\n')
for i, para1 in enumerate(paragraphs[:10]): # 졜근 10개만
for j, para2 in enumerate(paragraphs[i+1:i+11]):
if narrative_tracker.content_deduplicator.check_similarity(para1, para2) > 0.7:
duplicates.append(f"문단 {i+1}κ³Ό 문단 {i+j+2} μœ μ‚¬")
# 인과관계 체크 (μΆ”κ°€)
causality_issues = []
if phase_count > 3:
# 각 단계가 이전 λ‹¨κ³„μ˜ 결과인지 확인
if "κ·ΈλŸ¬λ‚˜ κ°‘μžκΈ°" in all_content or "μš°μ—°νžˆ" in all_content or "λœ»λ°–μ—" in all_content:
causality_issues.append("μš°μ—°μ  사건 λ°œμƒ - 필연적 μ „κ°œ ν•„μš”")
lang_prompts = {
"Korean": f"""μ„œμ‚¬ 진행을 κ²€ν† ν•˜μ„Έμš”.
**원 주제:** {user_query}
**ν˜„μž¬ μ§„ν–‰:** {phase_count}/10 단계
**발견된 문제:**
{chr(10).join(issues[:5]) if issues else "μ—†μŒ"}
**쀑볡 발견:**
{chr(10).join(duplicates[:3]) if duplicates else "μ—†μŒ"}
**인과관계 문제:**
{chr(10).join(causality_issues) if causality_issues else "μ—†μŒ"}
**κ²€ν†  ν•­λͺ©:**
1. μ„œμ‚¬κ°€ ν•„μ—°μ μœΌλ‘œ μ§„ν–‰λ˜λŠ”κ°€?
2. 인물이 μΌκ΄€λ˜κ²Œ λ³€ν™”ν•˜λŠ”κ°€?
3. λ°˜λ³΅μ΄λ‚˜ μžκΈ°λ³΅μ œκ°€ μžˆλŠ”κ°€?
4. 각 단계가 μ΄μ „μ˜ 결과인가?
5. λΆ„λŸ‰μ΄ μΆ©λΆ„ν•œκ°€?
**νŒμ •:** 톡과/μž¬μž‘μ„± ν•„μš”
μΉ˜λ°€ν•œ 인과관계와 캐릭터 일관성을 μ€‘μ‹¬μœΌλ‘œ ν‰κ°€ν•˜μ„Έμš”.""",
"English": f"""Review narrative progression.
**Theme:** {user_query}
**Progress:** {phase_count}/10 phases
**Issues Found:**
{chr(10).join(issues[:5]) if issues else "None"}
**Duplications:**
{chr(10).join(duplicates[:3]) if duplicates else "None"}
**Causality Issues:**
{chr(10).join(causality_issues) if causality_issues else "None"}
**Review Items:**
1. Is narrative progressing necessarily?
2. Are characters changing consistently?
3. Any repetitions or self-copying?
4. Does each phase result from previous?
5. Sufficient length?
**Verdict:** Pass/Rewrite needed
Evaluate focusing on tight causality and character consistency."""
}
return lang_prompts.get(language, lang_prompts["Korean"])
def create_writer_revision_prompt(self, writer_number: int, initial_content: str,
critic_feedback: str, language: str) -> str:
"""μž‘κ°€ μˆ˜μ • ν”„λ‘¬ν”„νŠΈ (κ°„μ†Œν™”)"""
target_words = MIN_WORDS_PER_WRITER
lang_prompts = {
"Korean": f"""μž‘κ°€ {writer_number}번, μˆ˜μ •ν•˜μ„Έμš”.
**비평 μš”μ :**
{critic_feedback[:500]}
**μˆ˜μ • λ°©ν–₯:**
1. 반볡 제거, μƒˆλ‘œμš΄ μ „κ°œ μΆ”κ°€
2. μ΅œμ†Œ {target_words}단어 μœ μ§€
3. 인물 λ³€ν™” ꡬ체화
4. λŒ€ν™”μ™€ λ¬˜μ‚¬ μΆ”κ°€
μ „λ©΄ μž¬μž‘μ„±μ΄ ν•„μš”ν•˜λ©΄ 과감히 μˆ˜μ •ν•˜μ„Έμš”.
μˆ˜μ •λ³Έλ§Œ μ œμ‹œν•˜μ„Έμš”.""",
"English": f"""Writer #{writer_number}, revise.
**Critique Points:**
{critic_feedback[:500]}
**Revision Direction:**
1. Remove repetition, add new development
2. Maintain {target_words} words minimum
3. Specify character changes
4. Add dialogue and description
Boldly rewrite if needed.
Present only revised version."""
}
return lang_prompts.get(language, lang_prompts["Korean"])
def create_editor_prompt(self, complete_novel: str, issues: List[str], language: str) -> str:
"""νŽΈμ§‘μž ν”„λ‘¬ν”„νŠΈ - 보쑴 쀑심 νŽΈμ§‘"""
current_word_count = len(complete_novel.split())
min_words = int(current_word_count * 0.95) # 95% 이상 μœ μ§€
lang_prompts = {
"Korean": f"""당신은 보쑴을 μ€‘μ‹œν•˜λŠ” νŽΈμ§‘μžμž…λ‹ˆλ‹€.
**원고 λΆ„λŸ‰: {current_word_count}단어**
**νŽΈμ§‘ ν›„ μ΅œμ†Œ λΆ„λŸ‰: {min_words}단어 (ν•„μˆ˜!)**
**νŽΈμ§‘ κ·œμΉ™:**
1. **원고 보쑴이 μ΅œμš°μ„ **
- μ›κ³ μ˜ 95% 이상을 λ°˜λ“œμ‹œ μœ μ§€
- μ‚­μ œλ³΄λ‹€λŠ” μˆ˜μ •μ„ μš°μ„ 
- 전체 문단 μ‚­μ œλŠ” μ ˆλŒ€ κΈˆμ§€
2. **ν—ˆμš©λ˜λŠ” νŽΈμ§‘:**
- μ™„μ „νžˆ λ™μΌν•œ λ¬Έμž₯이 μ—°μ†μœΌλ‘œ λ‚˜μ˜¬ λ•Œλ§Œ ν•˜λ‚˜ 제거
- 문법 였λ₯˜λ‚˜ μ˜€νƒ€ μˆ˜μ •
- 연결이 μ–΄μƒ‰ν•œ 뢀뢄에 접속사 μΆ”κ°€ (1-2단어)
3. **μ ˆλŒ€ κΈˆμ§€μ‚¬ν•­:**
- 문단 ν†΅μ§Έλ‘œ μ‚­μ œ ❌
- λ‚΄μš© μš”μ•½μ΄λ‚˜ μΆ•μ•½ ❌
- μž‘κ°€μ˜ 문체 λ³€κ²½ ❌
- 쀄거리 μž¬κ΅¬μ„± ❌
4. **νŽΈμ§‘ 방법:**
- 쀑볡 λ¬Έμž₯: 두 번째 κ²ƒλ§Œ 제거
- μ–΄μƒ‰ν•œ μ—°κ²°: μ ‘μ†μ‚¬λ‘œ μ—°κ²°
- μ˜€νƒ€: μ΅œμ†Œν•œμ˜ μˆ˜μ •
**μ€‘μš”: 거의 λͺ¨λ“  λ‚΄μš©μ„ κ·ΈλŒ€λ‘œ μœ μ§€ν•˜λ©΄μ„œ μ•„μ£Ό μž‘μ€ 문제만 μˆ˜μ •ν•˜μ„Έμš”.**
원고 전체λ₯Ό λ‹€μ‹œ μž‘μ„±ν•˜μ§€ 말고, 원본을 λ³΅μ‚¬ν•œ ν›„ μ΅œμ†Œν•œμ˜ μˆ˜μ •λ§Œ κ°€ν•˜μ„Έμš”.
νŽΈμ§‘λœ 전체 원고λ₯Ό μ œμ‹œν•˜μ„Έμš”. ({min_words}단어 이상 ν•„μˆ˜!)""",
"English": f"""You are a preservation-focused editor.
**Manuscript length: {current_word_count} words**
**Minimum after editing: {min_words} words (REQUIRED!)**
**Editing Rules:**
1. **Preservation is Priority**
- Must keep 95%+ of manuscript
- Prefer correction over deletion
- Never delete whole paragraphs
2. **Allowed Edits:**
- Remove only when exact same sentence appears consecutively
- Fix grammar errors or typos
- Add conjunctions for awkward connections (1-2 words)
3. **Absolutely Forbidden:**
- Deleting whole paragraphs ❌
- Summarizing or abbreviating ❌
- Changing author's style ❌
- Restructuring plot ❌
4. **Editing Method:**
- Duplicates: Remove only second occurrence
- Awkward connections: Connect with conjunctions
- Typos: Minimal fixes
**IMPORTANT: Keep almost everything while fixing only tiny issues.**
Don't rewrite the manuscript, copy the original and apply minimal edits.
Present the full edited manuscript. ({min_words}+ words required!)"""
}
return lang_prompts.get(language, lang_prompts["Korean"])
def create_critic_final_prompt(self, content: str, query: str, language: str) -> str:
"""μ΅œμ’… 비평"""
word_count = len(content.split())
# μ„œμ‚¬ ꡬ쑰 뢄석 (μΆ”κ°€)
has_single_plot = self.check_single_narrative(content)
character_consistency = self.check_character_consistency_in_final(content)
lang_prompts = {
"Korean": f"""μ™„μ„±λœ μ†Œμ„€μ„ ν‰κ°€ν•˜μ„Έμš”.
**주제:** {query}
**λΆ„λŸ‰:** {word_count}단어 (λͺ©ν‘œ: 8,000)
**평가 κΈ°μ€€:**
1. ν†΅ν•©λœ μ„œμ‚¬ ꡬ쑰 (30점)
- λ‹¨μΌν•œ ν”Œλ‘― 쑴재 μ—¬λΆ€
- μΈκ³Όκ΄€κ³„μ˜ μΉ˜λ°€ν•¨
- 각 λ‹¨κ³„μ˜ ν•„μ—°μ„±
2. 캐릭터 일관성 (25점)
- 이름과 μ„€μ •μ˜ 톡일성
- 성격과 ν–‰λ™μ˜ 일관성
- λ³€ν™”μ˜ 섀득λ ₯
3. 문학적 완성도 (25점)
- 문체의 일관성
- 상징과 λͺ¨ν‹°ν”„ ν™œμš©
- 문학적 κΈ°λ²•μ˜ λ‹€μ–‘μ„±
4. 주제 μ˜μ‹κ³Ό 톡찰 (20점)
- 철학적 깊이
- 인간에 λŒ€ν•œ 톡찰
- 독창적 해석
**감점 μš”μΈ:**
- ν”Œλ‘― 자기볡제 (-10점)
- 캐릭터 μ„€μ • 좩돌 (-10점)
- λ‹¨μˆœ 반볡 (-5점)
- λΆ„λŸ‰ 미달 (-5점)
**μ’…ν•© 평가:**
μž‘ν’ˆμ΄ ν•˜λ‚˜μ˜ μ™„κ²°λœ μž₯νŽΈμ†Œμ„€λ‘œ κΈ°λŠ₯ν•˜λŠ”κ°€?
총점: /100점
μž‘ν’ˆμ˜ 문학적 κ°€μΉ˜μ™€ ꡬ쑰적 완성도λ₯Ό μ’…ν•©μ μœΌλ‘œ ν‰κ°€ν•˜μ„Έμš”.""",
"English": f"""Evaluate the completed novel.
**Theme:** {query}
**Length:** {word_count} words (target: 8,000)
**Criteria:**
1. Integrated Narrative Structure (30 pts)
- Single plot existence
- Causal tightness
- Necessity of each phase
2. Character Consistency (25 pts)
- Name/setting unity
- Personality/behavior consistency
- Convincing transformation
3. Literary Quality (25 pts)
- Stylistic consistency
- Symbol/motif usage
- Literary technique variety
4. Thematic Insight (20 pts)
- Philosophical depth
- Human insight
- Original interpretation
**Deductions:**
- Plot self-replication (-10 pts)
- Character setting conflicts (-10 pts)
- Simple repetition (-5 pts)
- Under length (-5 pts)
**Overall Assessment:**
Does the work function as a complete novel?
Total: /100 points
Comprehensively evaluate literary value and structural completeness."""
}
return lang_prompts.get(language, lang_prompts["Korean"])
# --- LLM 호좜 ν•¨μˆ˜λ“€ ---
def call_llm_sync(self, messages: List[Dict[str, str]], role: str, language: str) -> str:
full_content = ""
for chunk in self.call_llm_streaming(messages, role, language):
full_content += chunk
if full_content.startswith("❌"):
raise Exception(f"LLM Call Failed: {full_content}")
# μ–Έμ–΄ ν•„ν„° 적용
filtered_content = self.language_filter.clean_text(full_content)
return filtered_content
def call_llm_streaming(self, messages: List[Dict[str, str]], role: str, language: str) -> Generator[str, None, None]:
try:
system_prompts = self.get_system_prompts(language)
full_messages = [{"role": "system", "content": system_prompts.get(role, "")}, *messages]
# μž‘κ°€μ™€ νŽΈμ§‘μž 역할일 λ•ŒλŠ” 더 λ§Žμ€ 토큰 ν—ˆμš©
if role.startswith("writer"):
max_tokens = 25000
elif role == "editor":
# νŽΈμ§‘μžλŠ” 원본 길이 + μ—¬μœ λΆ„
max_tokens = 30000 # νŽΈμ§‘μžμ—κ²Œ 더 λ§Žμ€ 토큰 ν• λ‹Ή
else:
max_tokens = 10000
payload = {
"model": self.model_id,
"messages": full_messages,
"max_tokens": max_tokens,
"temperature": 0.8,
"top_p": 0.95,
"presence_penalty": 0.5,
"frequency_penalty": 0.3,
"stream": True
}
response = requests.post(
self.api_url,
headers=self.create_headers(),
json=payload,
stream=True,
timeout=180
)
if response.status_code != 200:
yield f"❌ API 였λ₯˜ (μƒνƒœ μ½”λ“œ: {response.status_code})"
return
buffer = ""
for line in response.iter_lines():
if not line:
continue
try:
line_str = line.decode('utf-8').strip()
if not line_str.startswith("data: "):
continue
data_str = line_str[6:]
if data_str == "[DONE]":
break
data = json.loads(data_str)
choices = data.get("choices", [])
if choices and choices[0].get("delta", {}).get("content"):
content = choices[0]["delta"]["content"]
buffer += content
if len(buffer) >= 50 or '\n' in buffer:
# μ–Έμ–΄ ν•„ν„° 적용
clean_buffer = self.language_filter.clean_text(buffer)
yield clean_buffer
buffer = ""
time.sleep(0.01)
except Exception as e:
logger.error(f"청크 처리 였λ₯˜: {str(e)}")
continue
if buffer:
clean_buffer = self.language_filter.clean_text(buffer)
yield clean_buffer
except Exception as e:
logger.error(f"슀트리밍 였λ₯˜: {type(e).__name__}: {str(e)}")
yield f"❌ 였λ₯˜ λ°œμƒ: {str(e)}"
def get_system_prompts(self, language: str) -> Dict[str, str]:
"""역할별 μ‹œμŠ€ν…œ ν”„λ‘¬ν”„νŠΈ (κ°„μ†Œν™”)"""
base_prompts = {
"Korean": {
"director": """당신은 노벨문학상 μˆ˜μƒ μž‘κ°€μž…λ‹ˆλ‹€.
μΉ˜λ°€ν•œ μΈκ³Όκ΄€κ³„μ˜ 단일 μ„œμ‚¬λ₯Ό μ„€κ³„ν•˜μ„Έμš”.
캐릭터 이름과 섀정을 μ²˜μŒλΆ€ν„° λͺ…ν™•νžˆ κ³ μ •ν•˜μ„Έμš”.
ν”Œλ‘―μ˜ 자기볡제 없이 필연적 μ „κ°œλ₯Ό κ΅¬μΆ•ν•˜μ„Έμš”.""",
"critic": """당신은 λ¬Έν•™ λΉ„ν‰κ°€μž…λ‹ˆλ‹€.
μ„œμ‚¬μ˜ 인과관계와 캐릭터 일관성을 μ—„κ²©νžˆ κ²€ν† ν•˜μ„Έμš”.
ν”Œλ‘― μžκΈ°λ³΅μ œμ™€ μ„€μ • μΆ©λŒμ„ μ°Ύμ•„λ‚΄μ„Έμš”.
μž‘ν’ˆμ΄ ν•˜λ‚˜μ˜ μ™„κ²°λœ μ†Œμ„€μΈμ§€ ν‰κ°€ν•˜μ„Έμš”.""",
"writer_base": f"""당신은 ν•œκ΅­ λ¬Έν•™ μž‘κ°€μž…λ‹ˆλ‹€.
λ°˜λ“œμ‹œ {MIN_WORDS_PER_WRITER}단어 이상 μž‘μ„±ν•˜μ„Έμš”.
이전 λ‹¨κ³„μ˜ 필연적 결과둜 μ‹œμž‘ν•˜μ„Έμš”.
캐릭터 이름과 섀정을 μ ˆλŒ€ λ³€κ²½ν•˜μ§€ λ§ˆμ„Έμš”.
ꡬ체적 μž₯λ©΄κ³Ό μƒμƒν•œ λŒ€ν™”λ‘œ μ„œμ‚¬λ₯Ό μ „κ°œν•˜μ„Έμš”.""",
"editor": """당신은 원고 보쑴을 μ΅œμš°μ„ μœΌλ‘œ ν•˜λŠ” νŽΈμ§‘μžμž…λ‹ˆλ‹€.
μ›κ³ μ˜ 95% 이상을 λ°˜λ“œμ‹œ μœ μ§€ν•˜μ„Έμš”.
전체 문단을 μ‚­μ œν•˜μ§€ λ§ˆμ„Έμš”.
원본을 λ³΅μ‚¬ν•œ ν›„ μ΅œμ†Œν•œμ˜ μˆ˜μ •λ§Œ ν•˜μ„Έμš”.
νŽΈμ§‘ 후에도 거의 λͺ¨λ“  λ‚΄μš©μ΄ λ‚¨μ•„μžˆμ–΄μ•Ό ν•©λ‹ˆλ‹€."""
},
"English": {
"director": """You are a Nobel Prize-winning author.
Design a single narrative with tight causality.
Fix character names and settings clearly from start.
Build necessary development without plot self-replication.""",
"critic": """You are a literary critic.
Strictly review narrative causality and character consistency.
Find plot self-replication and setting conflicts.
Evaluate if work is one complete novel.""",
"writer_base": f"""You are a literary writer.
Must write at least {MIN_WORDS_PER_WRITER} words.
Start as necessary result of previous phase.
Never change character names or settings.
Develop narrative with concrete scenes and vivid dialogue.""",
"editor": """You are a preservation-focused editor.
Must maintain 95%+ of manuscript.
Never delete whole paragraphs.
Copy original and apply minimal edits only.
Almost all content must remain after editing."""
}
}
prompts = base_prompts.get(language, base_prompts["Korean"]).copy()
# 특수 μž‘κ°€ ν”„λ‘¬ν”„νŠΈ
for i in range(1, 11):
prompts[f"writer{i}"] = prompts["writer_base"]
return prompts
# --- 메인 ν”„λ‘œμ„ΈμŠ€ ---
def process_novel_stream(self, query: str, language: str, session_id: Optional[str] = None) -> Generator[Tuple[str, List[Dict[str, Any]], str], None, None]:
"""μ†Œμ„€ 생성 ν”„λ‘œμ„ΈμŠ€"""
try:
resume_from_stage = 0
if session_id:
self.current_session_id = session_id
session = NovelDatabase.get_session(session_id)
if session:
query = session['user_query']
language = session['language']
resume_from_stage = session['current_stage'] + 1
# μ„œμ‚¬ 좔적기 볡원
saved_tracker = NovelDatabase.load_narrative_tracker(session_id)
if saved_tracker:
self.narrative_tracker = saved_tracker
else:
self.current_session_id = NovelDatabase.create_session(query, language)
logger.info(f"Created new session: {self.current_session_id}")
stages = []
if resume_from_stage > 0:
stages = [{
"name": s['stage_name'],
"status": s['status'],
"content": s.get('content', ''),
"word_count": s.get('word_count', 0),
"progression_score": s.get('progression_score', 0.0),
"repetition_score": s.get('repetition_score', 0.0),
"consistency_check": s.get('consistency_check', '')
} for s in NovelDatabase.get_stages(self.current_session_id)]
# 총 단어 수 좔적
total_words = NovelDatabase.get_total_words(self.current_session_id)
for stage_idx in range(resume_from_stage, len(PROGRESSIVE_STAGES)):
role, stage_name = PROGRESSIVE_STAGES[stage_idx]
if stage_idx >= len(stages):
stages.append({
"name": stage_name,
"status": "active",
"content": "",
"word_count": 0,
"progression_score": 0.0,
"repetition_score": 0.0,
"consistency_check": ""
})
else:
stages[stage_idx]["status"] = "active"
yield f"πŸ”„ μ§„ν–‰ 쀑... (ν˜„μž¬ {total_words:,}단어)", stages, self.current_session_id
prompt = self.get_stage_prompt(stage_idx, role, query, language, stages)
stage_content = ""
for chunk in self.call_llm_streaming([{"role": "user", "content": prompt}], role, language):
stage_content += chunk
stages[stage_idx]["content"] = stage_content
stages[stage_idx]["word_count"] = len(stage_content.split())
yield f"πŸ”„ {stage_name} μž‘μ„± 쀑... ({total_words + stages[stage_idx]['word_count']:,}단어)", stages, self.current_session_id
# μž‘κ°€ 역할일 λ•Œ 단어 수 체크 및 μž¬μƒμ„±
if role.startswith("writer"):
word_count = len(stage_content.split())
writer_num = int(re.search(r'\d+', role).group())
# 단어 μˆ˜κ°€ λΆ€μ‘±ν•˜λ©΄ μΆ”κ°€ 생성 μš”μ²­
if word_count < MIN_WORDS_PER_WRITER * 0.9: # 90% 미만이면
retry_prompt = f"""
ν˜„μž¬ {word_count}λ‹¨μ–΄λ§Œ μž‘μ„±λ˜μ—ˆμŠ΅λ‹ˆλ‹€.
μ΅œμ†Œ {MIN_WORDS_PER_WRITER}단어가 ν•„μš”ν•©λ‹ˆλ‹€.
λ‹€μŒμ„ μΆ”κ°€ν•˜μ—¬ {MIN_WORDS_PER_WRITER - word_count}단어 이상 더 μž‘μ„±ν•˜μ„Έμš”:
- 더 μžμ„Έν•œ 인물 λ¬˜μ‚¬
- μΆ”κ°€ λŒ€ν™” μž₯λ©΄
- λ°°κ²½κ³Ό λΆ„μœ„κΈ° λ¬˜μ‚¬
- 인물의 κ³Όκ±° νšŒμƒ
- 내적 독백 ν™•λŒ€
κΈ°μ‘΄ λ‚΄μš©μ— μžμ—°μŠ€λŸ½κ²Œ μ΄μ–΄μ„œ μž‘μ„±ν•˜μ„Έμš”:
"""
additional_content = self.call_llm_sync(
[{"role": "user", "content": retry_prompt}],
role,
language
)
stage_content += "\n\n" + additional_content
stages[stage_idx]["content"] = stage_content
stages[stage_idx]["word_count"] = len(stage_content.split())
# 진행도 평가
previous_content = self.get_previous_writer_content(stages, writer_num)
# 진행도 점수 계산
progression_scores = self.narrative_tracker.progression_monitor.calculate_progression_score(
writer_num, stage_content, previous_content
)
progression_score = sum(progression_scores.values()) / len(progression_scores)
stages[stage_idx]["progression_score"] = progression_score
# λ°˜λ³΅λ„ 점수 계산
repetition_score = 10.0 - self.narrative_tracker.progression_monitor.count_repetitions(stage_content)
stages[stage_idx]["repetition_score"] = max(0, repetition_score)
# 일관성 체크
consistency_ok, consistency_issues = self.narrative_tracker.consistency_checker.validate_new_content(
writer_num, stage_content, [s["content"] for s in stages[:stage_idx] if s["content"]]
)
stages[stage_idx]["consistency_check"] = "톡과" if consistency_ok else "; ".join(consistency_issues)
# μ„œμ‚¬ 좔적기 μ—…λ°μ΄νŠΈ
self.update_narrative_tracker(stage_content, writer_num)
self.narrative_tracker.extract_used_elements(stage_content)
# νŽΈμ§‘ 단계 νŠΉλ³„ 처리
if role == "editor" and stage_content:
# νŽΈμ§‘ μ „ν›„ 단어 수 비ꡐ
original_novel = ""
for i in range(1, 11):
for s in stages:
if f"writer{i}" in s.get("name", "") and "μˆ˜μ •λ³Έ" in s.get("name", "") and s["content"]:
original_novel += s["content"] + "\n\n"
original_words = len(original_novel.split())
edited_words = len(stage_content.split())
logger.info(f"νŽΈμ§‘ κ²°κ³Ό: {original_words}단어 β†’ {edited_words}단어")
# νŽΈμ§‘μœΌλ‘œ 20% 이상 μ‚­μ œλ˜μ—ˆλ‹€λ©΄ 원본 μ‚¬μš©
if edited_words < original_words * 0.8:
logger.warning(f"νŽΈμ§‘μžκ°€ κ³Όλ„ν•˜κ²Œ μ‚­μ œν•¨ ({100 - (edited_words/original_words*100):.1f}% μ‚­μ œ). 원본 μœ μ§€.")
stage_content = original_novel
stages[stage_idx]["content"] = stage_content
stages[stage_idx]["word_count"] = original_words
stages[stage_idx]["note"] = "νŽΈμ§‘ κ³Όλ„λ‘œ 원본 μœ μ§€"
stages[stage_idx]["status"] = "complete"
NovelDatabase.save_stage(
self.current_session_id, stage_idx, stage_name, role,
stage_content, "complete",
stages[stage_idx].get("progression_score", 0.0),
stages[stage_idx].get("repetition_score", 0.0),
stages[stage_idx].get("consistency_check", "")
)
# μ„œμ‚¬ 좔적기 μ €μž₯
NovelDatabase.save_narrative_tracker(self.current_session_id, self.narrative_tracker)
# 총 단어 수 μ—…λ°μ΄νŠΈ
total_words = NovelDatabase.get_total_words(self.current_session_id)
# 단어 수 κ²½κ³ 
if role.startswith("writer") and stages[stage_idx]["word_count"] < MIN_WORDS_PER_WRITER:
logger.warning(f"단어 수 λΆ€μ‘±: {stage_name} - {stages[stage_idx]['word_count']}단어")
yield f"βœ… {stage_name} μ™„λ£Œ (총 {total_words:,}단어)", stages, self.current_session_id
# μ΅œμ’… μ†Œμ„€ 정리
final_novel = NovelDatabase.get_writer_content(self.current_session_id)
# νŽΈμ§‘μžκ°€ μ²˜λ¦¬ν•œ λ‚΄μš©μ΄ 있으면 그것을 μ‚¬μš©
edited_content = self.get_edited_content(stages)
if edited_content:
final_novel = edited_content
final_word_count = len(final_novel.split())
final_report = self.generate_literary_report(final_novel, query, language)
# ν’ˆμ§ˆ 평가 μ €μž₯
self.save_quality_evaluation(final_report)
NovelDatabase.update_final_novel(self.current_session_id, final_novel, final_report)
# μ΅œμ’… 단어 수 확인
if final_word_count < TARGET_WORDS:
yield f"⚠️ μ†Œμ„€ μ™„μ„±! 총 {final_word_count:,}단어 (λͺ©ν‘œ 미달: {TARGET_WORDS - final_word_count:,}단어 λΆ€μ‘±)", stages, self.current_session_id
else:
yield f"βœ… μ†Œμ„€ μ™„μ„±! 총 {final_word_count:,}단어 (λͺ©ν‘œ: {TARGET_WORDS:,}단어 달성!)", stages, self.current_session_id
except Exception as e:
logger.error(f"μ†Œμ„€ 생성 ν”„λ‘œμ„ΈμŠ€ 였λ₯˜: {e}", exc_info=True)
yield f"❌ 였λ₯˜ λ°œμƒ: {e}", stages if 'stages' in locals() else [], self.current_session_id
def get_stage_prompt(self, stage_idx, role, query, language, stages):
"""단계별 ν”„λ‘¬ν”„νŠΈ 생성"""
if stage_idx == 0:
# 첫 번째 단계 - κ°λ…μž 초기 기획
return self.create_director_initial_prompt(query, language)
elif stage_idx == 1:
# 두 번째 단계 - 비평가가 κ°λ…μž 기획 κ²€ν† 
if role == "critic":
return self.create_critic_consistency_prompt_enhanced(
stages[0]["content"],
self.narrative_tracker,
query,
language
)
elif stage_idx == 2:
# μ„Έ 번째 단계 - κ°λ…μž μˆ˜μ •
if role == "director":
return self.create_director_revision_prompt(
stages[0]["content"], # 초기 기획
stages[1]["content"], # 비평
query,
language
)
else:
# μž‘κ°€ 단계듀
if role.startswith("writer"):
writer_num = int(re.search(r'\d+', role).group())
# 이전 μž‘κ°€λ“€μ˜ λ‚΄μš© κ°€μ Έμ˜€κΈ°
previous_content = ""
if writer_num > 1:
# writer_num - 1κΉŒμ§€μ˜ λͺ¨λ“  μž‘κ°€ λ‚΄μš©
for i in range(1, writer_num):
for stage in stages:
if f"writer{i}" in stage.get("name", "") and stage["content"]:
previous_content += stage["content"] + "\n\n"
# κ°λ…μž ν”Œλžœ κ°€μ Έμ˜€κΈ°
director_plan = ""
for stage in stages[:3]: # 처음 3개 λ‹¨κ³„μ—μ„œ κ°λ…μž ν”Œλžœ μ°ΎκΈ°
if "κ°λ…μž" in stage.get("name", "") and "μˆ˜μ •" in stage.get("name", "") and stage["content"]:
director_plan = stage["content"]
break
if not director_plan: # μˆ˜μ •λœ ν”Œλžœμ΄ μ—†μœΌλ©΄ 초기 ν”Œλžœ μ‚¬μš©
director_plan = stages[0]["content"] if stages and stages[0]["content"] else ""
# μ„œμ‚¬ μš”μ•½ 생성
narrative_summary = self.generate_narrative_summary(stages, writer_num)
# 단계별 μš”κ΅¬μ‚¬ν•­ 생성
phase_requirements = self.narrative_tracker.generate_phase_requirements(writer_num)
# μ‚¬μš©λœ μš”μ†Œλ“€
used_elements = list(self.narrative_tracker.used_expressions)
# μ΄ˆμ•ˆμΈμ§€ μˆ˜μ •λ³ΈμΈμ§€ 확인
stage_name = stages[stage_idx]["name"] if stage_idx < len(stages) else ""
if "μ΄ˆμ•ˆ" in stage_name:
return self.create_writer_prompt_enhanced(
writer_num,
director_plan,
previous_content,
phase_requirements,
narrative_summary,
language,
used_elements
)
else: # μˆ˜μ •λ³Έ
# ν•΄λ‹Ή μž‘κ°€μ˜ μ΄ˆμ•ˆ μ°ΎκΈ°
initial_content = ""
critic_feedback = ""
# μ΄ˆμ•ˆ μ°ΎκΈ°
for i, stage in enumerate(stages):
if f"writer{writer_num}" in stage.get("name", "") and "μ΄ˆμ•ˆ" in stage.get("name", ""):
initial_content = stage["content"]
break
# 쀑간 비평 μ°ΎκΈ°
for i, stage in enumerate(stages):
if "critic" in stage.get("name", "") and "쀑간 κ²€ν† " in stage.get("name", ""):
critic_feedback = stage["content"]
break
return self.create_writer_revision_prompt(
writer_num,
initial_content,
critic_feedback,
language
)
elif role == "critic":
# 쀑간 비평가 κ²€ν† 
if "쀑간 κ²€ν† " in stages[stage_idx]["name"]:
# λͺ¨λ“  μž‘κ°€ μ½˜ν…μΈ  μˆ˜μ§‘
all_content = ""
for stage in stages:
if "writer" in stage.get("name", "") and "μ΄ˆμ•ˆ" in stage.get("name", "") and stage["content"]:
all_content += stage["content"] + "\n\n"
return self.create_critic_consistency_prompt_enhanced(
all_content,
self.narrative_tracker,
query,
language
)
# μ΅œμ’… 비평
else:
# νŽΈμ§‘λœ λ‚΄μš©μ΄ 있으면 그것을, μ—†μœΌλ©΄ λͺ¨λ“  μž‘κ°€ μˆ˜μ •λ³Έ
final_content = ""
for stage in stages:
if "νŽΈμ§‘μž" in stage.get("name", "") and stage["content"]:
final_content = stage["content"]
break
if not final_content:
# νŽΈμ§‘λœ λ‚΄μš©μ΄ μ—†μœΌλ©΄ λͺ¨λ“  μž‘κ°€ μˆ˜μ •λ³Έ μˆ˜μ§‘
for i in range(1, 11):
for stage in stages:
if f"writer{i}" in stage.get("name", "") and "μˆ˜μ •λ³Έ" in stage.get("name", "") and stage["content"]:
final_content += stage["content"] + "\n\n"
return self.create_critic_final_prompt(final_content, query, language)
elif role == "editor":
# νŽΈμ§‘μž - λͺ¨λ“  μž‘κ°€ μˆ˜μ •λ³Έ μˆ˜μ§‘
complete_novel = ""
writer_contents = []
for i in range(1, 11):
for stage in stages:
if f"writer{i}" in stage.get("name", "") and "μˆ˜μ •λ³Έ" in stage.get("name", "") and stage["content"]:
writer_contents.append(stage["content"])
complete_novel += stage["content"] + "\n\n"
# 문제점 감지 - 맀우 μ‹¬κ°ν•œ 문제만
issues = []
# μ™„μ „νžˆ λ™μΌν•œ 문단 체크
paragraphs = complete_novel.split('\n\n')
seen_paragraphs = set()
exact_duplicates = 0
for para in paragraphs:
if para.strip() in seen_paragraphs and len(para.strip()) > 100:
exact_duplicates += 1
seen_paragraphs.add(para.strip())
if exact_duplicates > 5:
issues.append(f"{exact_duplicates}개의 μ™„μ „νžˆ λ™μΌν•œ 문단 발견")
# λ¬Έμ œκ°€ 거의 μ—†μœΌλ©΄ νŽΈμ§‘ μ΅œμ†Œν™”
if len(issues) == 0:
issues = ["원고 μƒνƒœκ°€ μ–‘ν˜Έν•©λ‹ˆλ‹€. νŽΈμ§‘μ„ μ΅œμ†Œν™”ν•˜μ„Έμš”."]
return self.create_editor_prompt(complete_novel, issues, language)
# κΈ°λ³Έκ°’ (μ—λŸ¬ λ°©μ§€)
return f"μ—­ν•  {role}에 λŒ€ν•œ ν”„λ‘¬ν”„νŠΈκ°€ μ •μ˜λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€."
def create_director_revision_prompt(self, initial_plan: str, critic_feedback: str, user_query: str, language: str) -> str:
"""κ°λ…μž μˆ˜μ • ν”„λ‘¬ν”„νŠΈ"""
lang_prompts = {
"Korean": f"""비평을 λ°˜μ˜ν•˜μ—¬ μˆ˜μ •ν•˜μ„Έμš”.
**원 주제:** {user_query}
**비평 μš”μ :**
{critic_feedback[:500]}
**μˆ˜μ • 핡심:**
1. 10단계가 μΈκ³Όκ΄€κ³„λ‘œ κΈ΄λ°€νžˆ μ—°κ²°
2. 캐릭터 이름과 μ„€μ • μ²˜μŒλΆ€ν„° κ³ μ •
3. 각 단계가 μ΄μ „μ˜ 필연적 κ²°κ³Ό
4. ν”Œλ‘―μ˜ 자기볡제 λ°©μ§€
5. 8,000단어 달성 μ „λž΅
**ꡬ쑰 κ°•ν™”:**
- μ™ΈλΆ€ 사건이 내적 λ³€ν™”λ₯Ό μ΄‰λ°œ
- μ„ νƒμ˜ κ²°κ³Όκ°€ λ‹€μŒ κ΅­λ©΄ κ²°μ •
- μš°μ—°μ΄ μ•„λ‹Œ 필연적 μ „κ°œ
μΉ˜λ°€ν•œ μΈκ³Όκ΄€κ³„μ˜ μˆ˜μ • κ³„νšμ„ μ œμ‹œν•˜μ„Έμš”.""",
"English": f"""Revise based on critique.
**Original Theme:** {user_query}
**Critique Points:**
{critic_feedback[:500]}
**Revision Focus:**
1. 10 phases tightly connected by causality
2. Character names/settings fixed from start
3. Each phase as necessary result of previous
4. Prevent plot self-replication
5. Strategy to achieve 8,000 words
**Structure Enhancement:**
- External events trigger internal changes
- Choices determine next phase
- Necessary, not coincidental development
Present revision plan with tight causality."""
}
return lang_prompts.get(language, lang_prompts["Korean"])
def get_previous_writer_content(self, stages: List[Dict], current_writer: int) -> str:
"""이전 μž‘κ°€μ˜ λ‚΄μš© κ°€μ Έμ˜€κΈ°"""
if current_writer == 1:
return ""
# 이전 μž‘κ°€λ“€μ˜ λ‚΄μš© μˆ˜μ§‘
previous_content = []
for i in range(1, current_writer):
for stage in stages:
if f"writer{i}" in stage.get("name", "") and stage["content"]:
previous_content.append(stage["content"])
break
return "\n\n".join(previous_content)
def get_all_writer_content(self, stages: List[Dict], up_to_stage: int) -> str:
"""νŠΉμ • λ‹¨κ³„κΉŒμ§€μ˜ λͺ¨λ“  μž‘κ°€ λ‚΄μš©"""
contents = []
for i, s in enumerate(stages):
if i <= up_to_stage and "writer" in s.get("name", "") and s["content"]:
contents.append(s["content"])
return "\n\n".join(contents)
def get_edited_content(self, stages: List[Dict]) -> str:
"""νŽΈμ§‘λœ λ‚΄μš© κ°€μ Έμ˜€κΈ°"""
for s in stages:
if "νŽΈμ§‘μž" in s.get("name", "") and s["content"]:
return s["content"]
return ""
def generate_narrative_summary(self, stages: List[Dict], up_to_writer: int) -> str:
"""ν˜„μž¬κΉŒμ§€μ˜ μ„œμ‚¬ μš”μ•½"""
if up_to_writer == 1:
return "첫 μ‹œμž‘μž…λ‹ˆλ‹€."
summary_parts = []
for i in range(1, up_to_writer):
if i in self.narrative_tracker.phase_summaries:
summary_parts.append(f"[{NARRATIVE_PHASES[i-1]}]: {self.narrative_tracker.phase_summaries[i][:100]}...")
return "\n".join(summary_parts) if summary_parts else "이전 λ‚΄μš©μ„ 이어받아 μ§„ν–‰ν•˜μ„Έμš”."
def update_narrative_tracker(self, content: str, writer_num: int):
"""μ„œμ‚¬ 좔적기 μ—…λ°μ΄νŠΈ"""
# κ°„λ‹¨ν•œ μš”μ•½ 생성 (μ‹€μ œλ‘œλŠ” 더 μ •κ΅ν•œ 뢄석 ν•„μš”)
lines = content.split('\n')
key_events = [line.strip() for line in lines if len(line.strip()) > 50][:3]
if key_events:
summary = " ".join(key_events[:2])[:200] + "..."
self.narrative_tracker.phase_summaries[writer_num] = summary
# 철학적 톡찰 μΆ”μΆœ (κ°„λ‹¨ν•œ ν‚€μ›Œλ“œ 기반)
philosophical_keywords = ['쑴재', '의미', 'μ‚Ά', '죽음', '인간', '고톡', '희망', 'μ‚¬λž‘',
'existence', 'meaning', 'life', 'death', 'human', 'suffering', 'hope', 'love']
for keyword in philosophical_keywords:
if keyword in content:
self.narrative_tracker.philosophical_insights.append(f"Phase {writer_num}: {keyword} 탐ꡬ")
break
# 문학적 기법 감지
literary_devices = []
if '처럼' in content or 'like' in content or 'as if' in content:
literary_devices.append('λΉ„μœ ')
if '...' in content or 'β€”' in content:
literary_devices.append('μ˜μ‹μ˜ 흐름')
if content.count('"') > 4:
literary_devices.append('λŒ€ν™”')
if literary_devices:
self.narrative_tracker.literary_devices[writer_num] = literary_devices
def detect_issues(self, content: str) -> List[str]:
"""문제점 감지 - μ‹¬κ°ν•œ 문제만"""
issues = []
# μ™„μ „νžˆ λ™μΌν•œ λ¬Έλ‹¨λ§Œ 감지
paragraphs = content.split('\n\n')
exact_duplicates = 0
for i, para1 in enumerate(paragraphs):
for j, para2 in enumerate(paragraphs[i+1:]):
if para1.strip() == para2.strip() and len(para1.strip()) > 50:
exact_duplicates += 1
if exact_duplicates > 0:
issues.append(f"{exact_duplicates}개의 μ™„μ „νžˆ λ™μΌν•œ 문단 발견")
# 5회 이상 λ°˜λ³΅λ˜λŠ” ν‘œν˜„λ§Œ 감지
repetitive_phrases = ["μŠ΅κΈ°κ°€ μ°¬ μ•„μΉ¨", "λ‚˜λΌλ―Έ μ–΄ν”Œ", "43λ§Œμ›", "κ°œκ΅¬λ¦¬μ•Œμ„ λ°”λΌλ³΄μ•˜λ‹€"]
for phrase in repetitive_phrases:
count = content.count(phrase)
if count > 5: # μž„κ³„κ°’μ„ 3μ—μ„œ 5둜 상ν–₯
issues.append(f"'{phrase}' ν‘œν˜„μ΄ {count}회 κ³Όλ„ν•˜κ²Œ 반볡됨")
# μ‹¬κ°ν•œ 캐릭터 이름 뢈일치만
name_variations = ["λ‚˜λΌλ―Έ", "μ•ˆμ •", "λ‚˜"]
found_names = [name for name in name_variations if content.count(name) > 10]
if len(found_names) > 2:
issues.append(f"주인곡 이름 μ‹¬κ°ν•œ 뢈일치: {', '.join(found_names)}")
# μ–Έμ–΄ 였λ₯˜ 감지
if re.search(r'[ぁ-γ‚“γ‚‘-γƒΆδΈ€-ιΎ―]', content):
issues.append("일본어/쀑ꡭ어 문자 발견")
return issues[:3] # κ°€μž₯ μ‹¬κ°ν•œ 3개만 λ°˜ν™˜
def evaluate_progression(self, content: str, phase: int) -> float:
"""μ„œμ‚¬ 진행도 평가"""
score = 5.0
# λΆ„λŸ‰ 체크
word_count = len(content.split())
if word_count >= MIN_WORDS_PER_WRITER:
score += 2.0
# μƒˆλ‘œμš΄ μš”μ†Œ 체크
if phase > 1:
prev_summary = self.narrative_tracker.phase_summaries.get(phase-1, "")
if prev_summary and len(set(content.split()) - set(prev_summary.split())) > 100:
score += 1.5
# λ³€ν™” μ–ΈκΈ‰ 체크
change_keywords = ['λ³€ν–ˆ', 'λ‹¬λΌμ‘Œ', 'μƒˆλ‘œμš΄', 'μ΄μ œλŠ”', '더 이상',
'changed', 'different', 'new', 'now', 'no longer']
if any(keyword in content for keyword in change_keywords):
score += 1.5
# 철학적 깊이 체크
philosophical_keywords = ['쑴재', '의미', 'μ‚Άμ˜', 'μΈκ°„μ˜', 'μ™œ', 'existence', 'meaning', 'life', 'human', 'why']
if any(keyword in content for keyword in philosophical_keywords):
score += 0.5
# 문학적 기법 체크
if not any(phrase in content for phrase in ['λŠκΌˆλ‹€', 'μ˜€λ‹€', 'felt', 'was']):
score += 0.5 # 보여주기 기법 μ‚¬μš©
return min(10.0, score)
def generate_literary_report(self, complete_novel: str, query: str, language: str) -> str:
"""μ΅œμ’… 문학적 평가"""
prompt = self.create_critic_final_prompt(complete_novel, query, language)
try:
report = self.call_llm_sync([{"role": "user", "content": prompt}], "critic", language)
return report
except Exception as e:
logger.error(f"μ΅œμ’… λ³΄κ³ μ„œ 생성 μ‹€νŒ¨: {e}")
return "λ³΄κ³ μ„œ 생성 쀑 였λ₯˜ λ°œμƒ"
def check_single_narrative(self, content: str) -> bool:
"""단일 μ„œμ‚¬ ꡬ쑰 확인"""
# 주인곡 μ΄λ¦„μ˜ 일관성 체크
names = re.findall(r'[κ°€-힣]{2,4}(?=이|κ°€|은|λŠ”|을|λ₯Ό)', content)
name_counts = {}
for name in names:
name_counts[name] = name_counts.get(name, 0) + 1
# κ°€μž₯ 많이 λ“±μž₯ν•˜λŠ” 이름이 μ „μ²΄μ˜ 50% 이상인지
if name_counts:
max_count = max(name_counts.values())
total_count = sum(name_counts.values())
return max_count / total_count > 0.5
return False
def check_character_consistency_in_final(self, content: str) -> float:
"""μ΅œμ’… μž‘ν’ˆμ˜ 캐릭터 일관성 점수"""
# 캐릭터 이름과 직업/μ„€μ •μ˜ 일관성 체크
consistency_score = 1.0
# 같은 이름이 λ‹€λ₯Έ μ„€μ •μœΌλ‘œ λ“±μž₯ν•˜λŠ”μ§€ 체크
name_professions = {}
patterns = [
r'([κ°€-힣]{2,4})(?:은|λŠ”|이|κ°€)\s+(\w+(?:μž‘κ°€|기자|ꡐ사|μ˜μ‚¬|λ³€ν˜Έμ‚¬))',
r'(\w+(?:μž‘κ°€|기자|ꡐ사|μ˜μ‚¬|λ³€ν˜Έμ‚¬))\s+([κ°€-힣]{2,4})'
]
for pattern in patterns:
matches = re.findall(pattern, content)
for match in matches:
name = match[0] if 'κ°€' <= match[0][0] <= '힣' else match[1]
profession = match[1] if 'κ°€' <= match[0][0] <= '힣' else match[0]
if name in name_professions and name_professions[name] != profession:
consistency_score -= 0.2 # λΆˆμΌμΉ˜λ§ˆλ‹€ 감점
else:
name_professions[name] = profession
return max(0, consistency_score)
def save_quality_evaluation(self, report: str):
"""ν’ˆμ§ˆ 평가 μ €μž₯"""
try:
# 점수 μΆ”μΆœ (κ°„λ‹¨ν•œ νŒ¨ν„΄ λ§€μΉ­)
score_match = re.search(r'총점:\s*(\d+(?:\.\d+)?)/100', report)
score = float(score_match.group(1)) if score_match else 0.0
NovelDatabase.save_quality_evaluation(
self.current_session_id,
"final_evaluation",
score,
report
)
except Exception as e:
logger.error(f"ν’ˆμ§ˆ 평가 μ €μž₯ μ‹€νŒ¨: {e}")
# --- μœ ν‹Έλ¦¬ν‹° ν•¨μˆ˜λ“€ ---
def process_query(query: str, language: str, session_id: Optional[str] = None) -> Generator[Tuple[str, str, str, str], None, None]:
"""메인 쿼리 처리 ν•¨μˆ˜"""
if not query.strip():
yield "", "", "❌ 주제λ₯Ό μž…λ ₯ν•΄μ£Όμ„Έμš”.", session_id
return
system = ProgressiveLiterarySystem()
stages_markdown = ""
novel_content = ""
for status, stages, current_session_id in system.process_novel_stream(query, language, session_id):
stages_markdown = format_stages_display(stages)
# μ΅œμ’… μ†Œμ„€ λ‚΄μš© κ°€μ Έμ˜€κΈ°
if stages and all(s.get("status") == "complete" for s in stages[-10:]):
novel_content = NovelDatabase.get_writer_content(current_session_id)
# νŽΈμ§‘λœ λ‚΄μš©μ΄ 있으면 그것을 μ‚¬μš©
edited = system.get_edited_content(stages)
if edited:
novel_content = edited
novel_content = format_novel_display(novel_content)
yield stages_markdown, novel_content, status or "πŸ”„ 처리 쀑...", current_session_id
def get_active_sessions(language: str) -> List[str]:
"""ν™œμ„± μ„Έμ…˜ λͺ©λ‘"""
sessions = NovelDatabase.get_active_sessions()
result = []
for s in sessions:
# None κ°’ 체크 μΆ”κ°€
session_id = s.get('session_id', '')
user_query = s.get('user_query', '')
created_at = s.get('created_at', '')
total_words = s.get('total_words', 0) or 0 # None일 경우 0으둜 처리
if session_id and user_query: # ν•„μˆ˜ 값이 μžˆλŠ” 경우만 μΆ”κ°€
result.append(
f"{session_id[:8]}... - {user_query[:50]}... ({created_at}) [{total_words:,}단어]"
)
return result
def auto_recover_session(language: str) -> Tuple[Optional[str], str]:
"""졜근 μ„Έμ…˜ μžλ™ 볡ꡬ"""
sessions = NovelDatabase.get_active_sessions()
if sessions:
latest_session = sessions[0]
return latest_session['session_id'], f"μ„Έμ…˜ {latest_session['session_id'][:8]}... 볡ꡬ됨"
return None, "볡ꡬ할 μ„Έμ…˜μ΄ μ—†μŠ΅λ‹ˆλ‹€."
def resume_session(session_id: str, language: str) -> Generator[Tuple[str, str, str, str], None, None]:
"""μ„Έμ…˜ 재개"""
if not session_id:
yield "", "", "❌ μ„Έμ…˜ IDκ°€ μ—†μŠ΅λ‹ˆλ‹€.", session_id
return
if "..." in session_id:
session_id = session_id.split("...")[0]
session = NovelDatabase.get_session(session_id)
if not session:
yield "", "", "❌ μ„Έμ…˜μ„ 찾을 수 μ—†μŠ΅λ‹ˆλ‹€.", None
return
yield from process_query(session['user_query'], session['language'], session_id)
def download_novel(novel_text: str, format_type: str, language: str, session_id: str) -> Optional[str]:
"""μ†Œμ„€ λ‹€μš΄λ‘œλ“œ 파일 생성"""
if not novel_text or not session_id:
return None
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"novel_{session_id[:8]}_{timestamp}"
try:
if format_type == "DOCX" and DOCX_AVAILABLE:
return export_to_docx(novel_text, filename, language, session_id)
else:
return export_to_txt(novel_text, filename)
except Exception as e:
logger.error(f"파일 생성 μ‹€νŒ¨: {e}")
return None
def format_stages_display(stages: List[Dict]) -> str:
"""단계별 μ§„ν–‰ 상황 ν‘œμ‹œ"""
markdown = "## 🎬 μ§„ν–‰ 상황\n\n"
# 총 단어 수 계산
total_words = sum(s.get('word_count', 0) for s in stages if 'writer' in s.get('name', ''))
progress_percent = (total_words / TARGET_WORDS) * 100 if TARGET_WORDS > 0 else 0
markdown += f"**총 단어 수: {total_words:,} / {TARGET_WORDS:,}**\n"
markdown += f"**μ§„ν–‰λ₯ : {progress_percent:.1f}%**\n"
markdown += f"{'β–ˆ' * int(progress_percent // 5)}{'β–‘' * (20 - int(progress_percent // 5))}\n\n"
for i, stage in enumerate(stages):
status_icon = "βœ…" if stage['status'] == 'complete' else "πŸ”„" if stage['status'] == 'active' else "⏳"
markdown += f"{status_icon} **{stage['name']}**"
if stage.get('word_count', 0) > 0:
markdown += f" ({stage['word_count']:,}단어)"
# μž‘κ°€ λ‹¨κ³„μ—μ„œ 단어 수 λΆ€μ‘± κ²½κ³ 
if 'writer' in stage.get('name', '') and stage['word_count'] < MIN_WORDS_PER_WRITER:
markdown += f" ⚠️ **λΆ„λŸ‰ λΆ€μ‘±!**"
# 진행도와 λ°˜λ³΅λ„ 점수 ν‘œμ‹œ
if stage.get('progression_score', 0) > 0:
markdown += f" [진행도: {stage['progression_score']:.1f}/10]"
if stage.get('repetition_score', 0) > 0:
markdown += f" [λ°˜λ³΅λ„: {stage['repetition_score']:.1f}/10]"
# 일관성 체크 ν‘œμ‹œ
if stage.get('consistency_check'):
if stage['consistency_check'] == "톡과":
markdown += " βœ”οΈ"
else:
markdown += f" ⚠️ {stage['consistency_check']}"
markdown += "\n"
if stage['content']:
preview = stage['content'][:200] + "..." if len(stage['content']) > 200 else stage['content']
markdown += f"> {preview}\n\n"
return markdown
def format_novel_display(novel_text: str) -> str:
"""μ†Œμ„€ λ‚΄μš© ν‘œμ‹œ"""
if not novel_text:
return "아직 μ™„μ„±λœ λ‚΄μš©μ΄ μ—†μŠ΅λ‹ˆλ‹€."
formatted = "# πŸ“– μ™„μ„±λœ μ†Œμ„€\n\n"
# 단어 수 ν‘œμ‹œ
word_count = len(novel_text.split())
formatted += f"**총 λΆ„λŸ‰: {word_count:,}단어 (λͺ©ν‘œ: {TARGET_WORDS:,}단어)**\n\n"
if word_count < TARGET_WORDS:
shortage = TARGET_WORDS - word_count
formatted += f"⚠️ **주의: λͺ©ν‘œ λΆ„λŸ‰μ— {shortage:,}단어 λΆ€μ‘±ν•©λ‹ˆλ‹€.**\n\n"
else:
formatted += f"βœ… **λͺ©ν‘œ λΆ„λŸ‰ 달성!**\n\n"
formatted += "---\n\n"
# 각 단계λ₯Ό κ΅¬λΆ„ν•˜μ—¬ ν‘œμ‹œ
sections = novel_text.split('\n\n')
for i, section in enumerate(sections):
if section.strip():
formatted += f"{section}\n\n"
return formatted
def export_to_docx(content: str, filename: str, language: str, session_id: str) -> str:
"""DOCX 파일둜 내보내기"""
doc = Document()
# νŽ˜μ΄μ§€ μ„€μ •
section = doc.sections[0]
section.page_height = Inches(11)
section.page_width = Inches(8.5)
section.top_margin = Inches(1)
section.bottom_margin = Inches(1)
section.left_margin = Inches(1.25)
section.right_margin = Inches(1.25)
# μ„Έμ…˜ 정보
session = NovelDatabase.get_session(session_id)
# 제λͺ© νŽ˜μ΄μ§€
title_para = doc.add_paragraph()
title_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
if session:
title_run = title_para.add_run(session["user_query"])
title_run.font.size = Pt(24)
title_run.bold = True
# 메타 정보
doc.add_paragraph()
meta_para = doc.add_paragraph()
meta_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
meta_para.add_run(f"생성일: {datetime.now().strftime('%Yλ…„ %mμ›” %d일')}\n")
meta_para.add_run(f"총 단어 수: {len(content.split()):,}단어")
# νŽ˜μ΄μ§€ λ‚˜λˆ„κΈ°
doc.add_page_break()
# λ³Έλ¬Έ μŠ€νƒ€μΌ μ„€μ •
style = doc.styles['Normal']
style.font.name = 'Calibri'
style.font.size = Pt(11)
style.paragraph_format.line_spacing = 1.5
style.paragraph_format.space_after = Pt(6)
# λ³Έλ¬Έ μΆ”κ°€
paragraphs = content.split('\n\n')
for para_text in paragraphs:
if para_text.strip():
para = doc.add_paragraph(para_text.strip())
# 파일 μ €μž₯
filepath = f"{filename}.docx"
doc.save(filepath)
return filepath
def export_to_txt(content: str, filename: str) -> str:
"""TXT 파일둜 내보내기"""
filepath = f"{filename}.txt"
with open(filepath, 'w', encoding='utf-8') as f:
f.write(content)
return filepath
# CSS μŠ€νƒ€μΌ
custom_css = """
.gradio-container {
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 50%, #1e3c72 100%);
min-height: 100vh;
}
.main-header {
background-color: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
padding: 30px;
border-radius: 12px;
margin-bottom: 30px;
text-align: center;
color: white;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.progress-note {
background-color: rgba(255, 223, 0, 0.1);
border-left: 3px solid #ffd700;
padding: 15px;
margin: 20px 0;
border-radius: 8px;
color: #fff;
}
.improvement-note {
background-color: rgba(0, 255, 127, 0.1);
border-left: 3px solid #00ff7f;
padding: 15px;
margin: 20px 0;
border-radius: 8px;
color: #fff;
}
.warning-note {
background-color: rgba(255, 69, 0, 0.1);
border-left: 3px solid #ff4500;
padding: 15px;
margin: 20px 0;
border-radius: 8px;
color: #fff;
}
.input-section {
background-color: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
padding: 20px;
border-radius: 12px;
margin-bottom: 20px;
border: 1px solid rgba(255, 255, 255, 0.2);
}
.session-section {
background-color: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
padding: 15px;
border-radius: 8px;
margin-top: 20px;
color: white;
border: 1px solid rgba(255, 255, 255, 0.2);
}
#stages-display {
background-color: rgba(255, 255, 255, 0.95);
padding: 20px;
border-radius: 12px;
max-height: 600px;
overflow-y: auto;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
#novel-output {
background-color: rgba(255, 255, 255, 0.95);
padding: 30px;
border-radius: 12px;
max-height: 700px;
overflow-y: auto;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.download-section {
background-color: rgba(255, 255, 255, 0.9);
padding: 15px;
border-radius: 8px;
margin-top: 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
/* μ§„ν–‰ ν‘œμ‹œκΈ° μŠ€νƒ€μΌ */
.progress-bar {
background-color: #e0e0e0;
height: 20px;
border-radius: 10px;
overflow: hidden;
margin: 10px 0;
}
.progress-fill {
background-color: #4CAF50;
height: 100%;
transition: width 0.3s ease;
}
/* 점수 ν‘œμ‹œ μŠ€νƒ€μΌ */
.score-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 12px;
font-size: 0.9em;
font-weight: bold;
margin-left: 5px;
}
.score-high {
background-color: #4CAF50;
color: white;
}
.score-medium {
background-color: #FF9800;
color: white;
}
.score-low {
background-color: #F44336;
color: white;
}
/* 일관성 체크 ν‘œμ‹œ */
.consistency-pass {
color: #4CAF50;
font-weight: bold;
}
.consistency-fail {
color: #F44336;
font-weight: bold;
}
/* λ³΄κ³ μ„œ ν‘œμ‹œ μŠ€νƒ€μΌ */
#report-display {
background-color: rgba(255, 255, 255, 0.95);
padding: 20px;
border-radius: 12px;
max-height: 600px;
overflow-y: auto;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
/* λΆ„λŸ‰ κ²½κ³  μŠ€νƒ€μΌ */
.word-count-warning {
background-color: #FFF3CD;
border-left: 4px solid #FFC107;
padding: 10px 15px;
margin: 10px 0;
border-radius: 4px;
}
"""
# Gradio μΈν„°νŽ˜μ΄μŠ€ 생성
def create_interface():
with gr.Blocks(css=custom_css, title="AI μ§„ν–‰ν˜• μž₯νŽΈμ†Œμ„€ 생성 μ‹œμŠ€ν…œ v3.1") as interface:
gr.HTML("""
<div class="main-header">
<h1 style="font-size: 2.5em; margin-bottom: 10px;">
πŸ“š AI μ§„ν–‰ν˜• μž₯νŽΈμ†Œμ„€ 생성 μ‹œμŠ€ν…œ v3.1
</h1>
<h3 style="color: #ddd; margin-bottom: 20px;">
λͺ©ν‘œ λΆ„λŸ‰ 달성을 μœ„ν•œ μ΅œμ ν™” 버전
</h3>
<p style="font-size: 1.1em; color: #eee; max-width: 800px; margin: 0 auto;">
10개의 유기적으둜 μ—°κ²°λœ 단계λ₯Ό 톡해 ν•˜λ‚˜μ˜ μ™„μ „ν•œ 이야기λ₯Ό λ§Œλ“€μ–΄λƒ…λ‹ˆλ‹€.
<br>
각 λ‹¨κ³„λŠ” 이전 λ‹¨κ³„μ˜ 필연적 결과둜 이어지며, 인물의 변화와 μ„±μž₯을 μΆ”μ ν•©λ‹ˆλ‹€.
</p>
<div class="progress-note">
⚑ 반볡이 μ•„λ‹Œ 좕적, μˆœν™˜μ΄ μ•„λ‹Œ 진행을 ν†΅ν•œ μ§„μ •ν•œ μž₯편 μ„œμ‚¬
</div>
<div class="improvement-note">
πŸ†• v3.1 핡심 κ°œμ„ μ‚¬ν•­:
<ul style="text-align: left; margin: 10px auto; max-width: 600px;">
<li>λͺ©ν‘œ 단어 수 8,000λ‹¨μ–΄λ‘œ μ‘°μ •</li>
<li>ν”„λ‘¬ν”„νŠΈ κ°„μ†Œν™”λ‘œ 생성 곡간 확보</li>
<li>단어 수 λΆ€μ‘± μ‹œ μžλ™ μž¬μƒμ„±</li>
<li>μ‹€μ‹œκ°„ μ§„ν–‰λ₯  ν‘œμ‹œ</li>
<li>λΆ„λŸ‰ 미달 κ²½κ³  μ‹œμŠ€ν…œ</li>
<li>ν”„λ‘¬ν”„νŠΈ μžλ™ 증강 κΈ°λŠ₯</li>
<li>인과관계와 캐릭터 일관성 κ°•ν™”</li>
</ul>
</div>
<div class="warning-note">
⚠️ λΆ„λŸ‰ λͺ©ν‘œ: 각 μž‘κ°€λ‹Ή μ΅œμ†Œ 800단어, 전체 8,000단어 이상
</div>
</div>
""")
# μƒνƒœ 관리
current_session_id = gr.State(None)
with gr.Row():
with gr.Column(scale=1):
with gr.Group(elem_classes=["input-section"]):
query_input = gr.Textbox(
label="μ†Œμ„€ 주제 / Novel Theme",
placeholder="μ€‘νŽΈμ†Œμ„€μ˜ 주제λ₯Ό μž…λ ₯ν•˜μ„Έμš”. 인물의 변화와 μ„±μž₯이 쀑심이 λ˜λŠ” 이야기...\nEnter the theme for your novella. Focus on character transformation and growth...",
lines=4
)
language_select = gr.Radio(
choices=["Korean", "English"],
value="Korean",
label="μ–Έμ–΄ / Language"
)
with gr.Row():
submit_btn = gr.Button("πŸš€ μ†Œμ„€ 생성 μ‹œμž‘", variant="primary", scale=2)
clear_btn = gr.Button("πŸ—‘οΈ μ΄ˆκΈ°ν™”", scale=1)
status_text = gr.Textbox(
label="μƒνƒœ",
interactive=False,
value="πŸ”„ μ€€λΉ„ μ™„λ£Œ"
)
# μ„Έμ…˜ 관리
with gr.Group(elem_classes=["session-section"]):
gr.Markdown("### πŸ’Ύ μ§„ν–‰ 쀑인 μ„Έμ…˜")
session_dropdown = gr.Dropdown(
label="μ„Έμ…˜ 선택",
choices=[],
interactive=True
)
with gr.Row():
refresh_btn = gr.Button("πŸ”„ λͺ©λ‘ μƒˆλ‘œκ³ μΉ¨", scale=1)
resume_btn = gr.Button("▢️ 선택 재개", variant="secondary", scale=1)
auto_recover_btn = gr.Button("♻️ 졜근 μ„Έμ…˜ 볡ꡬ", scale=1)
with gr.Column(scale=2):
with gr.Tab("πŸ“ μ°½μž‘ μ§„ν–‰"):
stages_display = gr.Markdown(
value="μ°½μž‘ 과정이 여기에 ν‘œμ‹œλ©λ‹ˆλ‹€...",
elem_id="stages-display"
)
with gr.Tab("πŸ“– μ™„μ„±λœ μ†Œμ„€"):
novel_output = gr.Markdown(
value="μ™„μ„±λœ μ†Œμ„€μ΄ 여기에 ν‘œμ‹œλ©λ‹ˆλ‹€...",
elem_id="novel-output"
)
with gr.Group(elem_classes=["download-section"]):
gr.Markdown("### πŸ“₯ μ†Œμ„€ λ‹€μš΄λ‘œλ“œ")
with gr.Row():
format_select = gr.Radio(
choices=["DOCX", "TXT"],
value="DOCX" if DOCX_AVAILABLE else "TXT",
label="ν˜•μ‹"
)
download_btn = gr.Button("⬇️ λ‹€μš΄λ‘œλ“œ", variant="secondary")
download_file = gr.File(
label="λ‹€μš΄λ‘œλ“œλœ 파일",
visible=False
)
with gr.Tab("πŸ“Š 평가 λ³΄κ³ μ„œ"):
report_display = gr.Markdown(
value="평가 λ³΄κ³ μ„œκ°€ 여기에 ν‘œμ‹œλ©λ‹ˆλ‹€...",
elem_id="report-display"
)
# μˆ¨κ²¨μ§„ μƒνƒœ
novel_text_state = gr.State("")
# 예제
with gr.Row():
gr.Examples(
examples=[
["κΈ°μ΄ˆμƒν™œμˆ˜κΈ‰μžκ°€ 된 μ²­λ…„μ˜ 생쑴과 μ‘΄μ—„μ„± μ°ΎκΈ°"],
["μ‹€μ§ν•œ 쀑년 남성이 μƒˆλ‘œμš΄ μ‚Άμ˜ 의미λ₯Ό μ°Ύμ•„κ°€λŠ” μ—¬μ •"],
["λ„μ‹œμ—μ„œ μ‹œκ³¨λ‘œ μ΄μ£Όν•œ μ²­λ…„μ˜ 적응과 μ„±μž₯ 이야기"],
["μ„Έ μ„ΈλŒ€κ°€ ν•¨κ»˜ μ‚¬λŠ” κ°€μ‘±μ˜ κ°ˆλ“±κ³Ό ν™”ν•΄"],
["A middle-aged woman's journey to rediscover herself after divorce"],
["The transformation of a cynical journalist through unexpected encounters"],
["μž‘μ€ μ„œμ μ„ μš΄μ˜ν•˜λŠ” λ…ΈλΆ€λΆ€μ˜ λ§ˆμ§€λ§‰ 1λ…„"],
["AI μ‹œλŒ€μ— 일자리λ₯Ό μžƒμ€ λ²ˆμ—­κ°€μ˜ μƒˆλ‘œμš΄ 도전"],
["재개발둜 μ‚¬λΌμ Έκ°€λŠ” λ™λ„€μ—μ„œμ˜ λ§ˆμ§€λ§‰ κ³„μ ˆ"]
],
inputs=query_input,
label="πŸ’‘ 주제 μ˜ˆμ‹œ"
)
# 이벀트 ν•Έλ“€λŸ¬
def refresh_sessions():
try:
sessions = get_active_sessions("Korean")
return gr.update(choices=sessions)
except Exception as e:
logger.error(f"Error refreshing sessions: {str(e)}")
logger.error(f"Full error: {e}", exc_info=True) # 전체 μŠ€νƒ 트레이슀 λ‘œκΉ…
return gr.update(choices=[])
def handle_auto_recover(language):
session_id, message = auto_recover_session(language)
return session_id, message
def update_displays(stages_md, novel_md, status, session_id):
"""λͺ¨λ“  λ””μŠ€ν”Œλ ˆμ΄ μ—…λ°μ΄νŠΈ"""
# 평가 λ³΄κ³ μ„œ κ°€μ Έμ˜€κΈ°
report = ""
if session_id:
session = NovelDatabase.get_session(session_id)
if session and session.get('literary_report'):
report = session['literary_report']
return stages_md, novel_md, status, session_id, report
# 이벀트 μ—°κ²°
submit_btn.click(
fn=process_query,
inputs=[query_input, language_select, current_session_id],
outputs=[stages_display, novel_output, status_text, current_session_id]
).then(
fn=lambda s, n, st, sid: (s, n, st, sid, NovelDatabase.get_session(sid).get('literary_report', '') if sid and NovelDatabase.get_session(sid) else ''),
inputs=[stages_display, novel_output, status_text, current_session_id],
outputs=[stages_display, novel_output, status_text, current_session_id, report_display]
)
novel_output.change(
fn=lambda x: x,
inputs=[novel_output],
outputs=[novel_text_state]
)
resume_btn.click(
fn=lambda x: x.split("...")[0] if x and "..." in x else x,
inputs=[session_dropdown],
outputs=[current_session_id]
).then(
fn=resume_session,
inputs=[current_session_id, language_select],
outputs=[stages_display, novel_output, status_text, current_session_id]
).then(
fn=lambda s, n, st, sid: (s, n, st, sid, NovelDatabase.get_session(sid).get('literary_report', '') if sid and NovelDatabase.get_session(sid) else ''),
inputs=[stages_display, novel_output, status_text, current_session_id],
outputs=[stages_display, novel_output, status_text, current_session_id, report_display]
)
auto_recover_btn.click(
fn=handle_auto_recover,
inputs=[language_select],
outputs=[current_session_id, status_text]
).then(
fn=resume_session,
inputs=[current_session_id, language_select],
outputs=[stages_display, novel_output, status_text, current_session_id]
).then(
fn=lambda s, n, st, sid: (s, n, st, sid, NovelDatabase.get_session(sid).get('literary_report', '') if sid and NovelDatabase.get_session(sid) else ''),
inputs=[stages_display, novel_output, status_text, current_session_id],
outputs=[stages_display, novel_output, status_text, current_session_id, report_display]
)
refresh_btn.click(
fn=refresh_sessions,
outputs=[session_dropdown]
)
clear_btn.click(
fn=lambda: ("", "", "πŸ”„ μ€€λΉ„ μ™„λ£Œ", "", None, ""),
outputs=[stages_display, novel_output, status_text, novel_text_state, current_session_id, report_display]
)
def handle_download(format_type, language, session_id, novel_text):
if not session_id or not novel_text:
return gr.update(visible=False)
file_path = download_novel(novel_text, format_type, language, session_id)
if file_path:
return gr.update(value=file_path, visible=True)
else:
return gr.update(visible=False)
download_btn.click(
fn=handle_download,
inputs=[format_select, language_select, current_session_id, novel_text_state],
outputs=[download_file]
)
# μ‹œμž‘ μ‹œ μ„Έμ…˜ λ‘œλ“œ
interface.load(
fn=refresh_sessions,
outputs=[session_dropdown]
)
return interface
# 메인 μ‹€ν–‰
if __name__ == "__main__":
logger.info("AI μ§„ν–‰ν˜• μž₯νŽΈμ†Œμ„€ 생성 μ‹œμŠ€ν…œ v3.1 μ‹œμž‘...")
logger.info("=" * 60)
# ν™˜κ²½ 확인
logger.info(f"API μ—”λ“œν¬μΈνŠΈ: {API_URL}")
logger.info(f"λͺ©ν‘œ λΆ„λŸ‰: {TARGET_WORDS:,}단어")
logger.info(f"μž‘κ°€λ‹Ή μ΅œμ†Œ λΆ„λŸ‰: {MIN_WORDS_PER_WRITER:,}단어")
logger.info("μ£Όμš” κ°œμ„ μ‚¬ν•­:")
logger.info("- λΆ„λŸ‰ λͺ©ν‘œ 8,000λ‹¨μ–΄λ‘œ μ‘°μ •")
logger.info("- ν”„λ‘¬ν”„νŠΈ κ°„μ†Œν™”")
logger.info("- 단어 수 λΆ€μ‘± μ‹œ μžλ™ μž¬μƒμ„±")
logger.info("- μ‹€μ‹œκ°„ μ§„ν–‰λ₯  ν‘œμ‹œ")
logger.info("- ν”„λ‘¬ν”„νŠΈ μžλ™ 증강 κΈ°λŠ₯")
logger.info("- 인과관계와 캐릭터 일관성 κ°•ν™”")
if BRAVE_SEARCH_API_KEY:
logger.info("μ›Ή 검색이 ν™œμ„±ν™”λ˜μ—ˆμŠ΅λ‹ˆλ‹€.")
else:
logger.warning("μ›Ή 검색이 λΉ„ν™œμ„±ν™”λ˜μ—ˆμŠ΅λ‹ˆλ‹€.")
if DOCX_AVAILABLE:
logger.info("DOCX 내보내기가 ν™œμ„±ν™”λ˜μ—ˆμŠ΅λ‹ˆλ‹€.")
else:
logger.warning("DOCX 내보내기가 λΉ„ν™œμ„±ν™”λ˜μ—ˆμŠ΅λ‹ˆλ‹€.")
logger.info("=" * 60)
# λ°μ΄ν„°λ² μ΄μŠ€ μ΄ˆκΈ°ν™”
logger.info("λ°μ΄ν„°λ² μ΄μŠ€ μ΄ˆκΈ°ν™” 쀑...")
NovelDatabase.init_db()
logger.info("λ°μ΄ν„°λ² μ΄μŠ€ μ΄ˆκΈ°ν™” μ™„λ£Œ.")
# μΈν„°νŽ˜μ΄μŠ€ 생성 및 μ‹€ν–‰
interface = create_interface()
interface.launch(
server_name="0.0.0.0",
server_port=7860,
share=False,
debug=True
)