diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -22,16 +22,16 @@ 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 + 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.") + DOCX_AVAILABLE = False + logger.warning("python-docx not installed. DOCX export will be disabled.") # --- 환경 변수 및 상수 --- FRIENDLI_TOKEN = os.getenv("FRIENDLI_TOKEN", "") @@ -46,757 +46,757 @@ MIN_WORDS_PER_WRITER = 800 # 각 작가 최소 분량 # --- 환경 변수 검증 --- if not FRIENDLI_TOKEN: - logger.error("FRIENDLI_TOKEN not set. Application will not work properly.") - FRIENDLI_TOKEN = "dummy_token_for_testing" + 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.") + 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: 열린 질문" + "도입: 일상과 균열", + "발전 1: 불안의 고조", + "발전 2: 외부 충격", + "발전 3: 내적 갈등 심화", + "절정 1: 위기의 정점", + "절정 2: 선택의 순간", + "하강 1: 결과와 여파", + "하강 2: 새로운 인식", + "결말 1: 변화된 일상", + "결말 2: 열린 질문" ] # 단계별 구성 - 편집자 단계 추가 PROGRESSIVE_STAGES = [ - ("director", "🎬 감독자: 통합된 서사 구조 기획"), - ("critic", "📝 비평가: 서사 진행성과 깊이 검토"), - ("director", "🎬 감독자: 수정된 마스터플랜"), + ("director", "🎬 감독자: 통합된 서사 구조 기획"), + ("critic", "📝 비평가: 서사 진행성과 깊이 검토"), + ("director", "🎬 감독자: 수정된 마스터플랜"), ] + [ - (f"writer{i}", f"✍️ 작가 {i}: 초안 - {NARRATIVE_PHASES[i-1]}") - for i in range(1, 11) + (f"writer{i}", f"✍️ 작가 {i}: 초안 - {NARRATIVE_PHASES[i-1]}") + for i in range(1, 11) ] + [ - ("critic", "📝 비평가: 중간 검토 (서사 누적성과 변화)"), + ("critic", "📝 비평가: 중간 검토 (서사 누적성과 변화)"), ] + [ - (f"writer{i}", f"✍️ 작가 {i}: 수정본 - {NARRATIVE_PHASES[i-1]}") - for i in range(1, 11) + (f"writer{i}", f"✍️ 작가 {i}: 수정본 - {NARRATIVE_PHASES[i-1]}") + for i in range(1, 11) ] + [ - ("editor", "✂️ 편집자: 반복 제거 및 서사 재구성"), - ("critic", f"📝 비평가: 최종 검토 및 문학적 평가"), + ("editor", "✂️ 편집자: 반복 제거 및 서사 재구성"), + ("critic", f"📝 비평가: 최종 검토 및 문학적 평가"), ] # --- 데이터 클래스 --- @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) # 관계 변화 + """인물의 변화 궤적 추적""" + 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 + """플롯 라인 추적""" + 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 + """상징의 의미 변화 추적""" + 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)) + """캐릭터 일관성 관리""" + 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 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 __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개 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 + """실시간 서사 진행 모니터링""" + 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() # 사용된 표현 추적 - - 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) - - return len(issues) == 0, issues + """서사 진행과 누적을 추적하는 시스템""" + 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() # 사용된 표현 추적 + + 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) + + 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]}") - - # 사용된 표현 목록 - if self.used_expressions: - requirements.append("\n❌ 다음 표현/상황은 이미 사용됨 (재사용 금지):") - for expr in list(self.used_expressions)[-10:]: # 최근 10개 - requirements.append(f"- {expr[:50]}...") - - # 단계별 특수 요구사항 - phase_name = NARRATIVE_PHASES[phase-1] if phase <= 10 else "수정" - - if "도입" in phase_name: - requirements.append("\n✅ 필수 포함:") - requirements.append("- 일상의 균열을 보여주되, 큰 사건이 아닌 미묘한 변화로 시작") - requirements.append("- 주요 인물들의 초기 상태와 관계 설정") - requirements.append("- 핵심 상징 도입 (자연스럽게)") - requirements.append("- 주인공 이름 명확히 설정") - - elif "발전" in phase_name: - requirements.append("\n✅ 필수 포함:") - requirements.append("- 이전 단계의 균열/갈등이 구체화되고 심화") - requirements.append("- 새로운 사건이나 인식이 추가되어 복잡성 증가") - requirements.append("- 인물 간 관계의 미묘한 변화") - requirements.append("- 새로운 공간이나 시간대 탐색") - - elif "절정" in phase_name: - requirements.append("\n✅ 필수 포함:") - requirements.append("- 축적된 갈등이 임계점에 도달") - requirements.append("- 인물의 내적 선택이나 인식의 전환점") - requirements.append("- 상징의 의미가 전복되거나 심화") - requirements.append("- 이전과는 다른 행동이나 결정") - - elif "하강" in phase_name: - requirements.append("\n✅ 필수 포함:") - requirements.append("- 절정의 여파와 그로 인한 변화") - requirements.append("- 새로운 균형점을 찾아가는 과정") - requirements.append("- 인물들의 변화된 관계와 인식") - requirements.append("- 회복이나 상실의 구체적 묘사") - - elif "결말" in phase_name: - requirements.append("\n✅ 필수 포함:") - requirements.append("- 변화된 일상의 모습") - requirements.append("- 해결되지 않은 질문들") - requirements.append("- 여운과 성찰의 여지") - requirements.append("- 처음과 대비되는 마지막") - - # 철학·인간애 강화 체크리스트 - requirements.append("\n📌 필수 포함 요소:") - requirements.append("- 존재의 의미나 삶의 본질에 대한 성찰이 담긴 1문단 이상") - requirements.append("- 타인의 고통에 대한 공감이나 연민을 보여주는 구체적 장면 1개 이상") - requirements.append("- '보여주기(showing)' 기법: 직접 설명 대신 감각적 묘사와 행동으로 표현") - requirements.append("- 이 단계만의 독특한 문학적 장치나 은유 1개 이상") - - # 반복 방지 요구사항 - requirements.append("\n⚠️ 절대 금지사항:") - requirements.append("- 이전 단계와 동일한 사건이나 갈등 반복") - requirements.append("- 인물이 같은 생각이나 감정에 머무르기") - requirements.append("- 플롯이 제자리걸음하기") - requirements.append("- '~을 느꼈다', '~였다'와 같은 직접적 설명") - requirements.append("- 이미 얻은 깨달음을 잊고 다시 시작하기") - - # 진행 체크리스트 - requirements.append("\n☑️ 진행 체크리스트:") - requirements.append("□ 이전 단계의 결과가 이번 단계의 원인이 되는가?") - 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()) + 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]}") + + # 사용된 표현 목록 + if self.used_expressions: + requirements.append("\n❌ 다음 표현/상황은 이미 사용됨 (재사용 금지):") + for expr in list(self.used_expressions)[-10:]: # 최근 10개 + requirements.append(f"- {expr[:50]}...") + + # 단계별 특수 요구사항 + phase_name = NARRATIVE_PHASES[phase-1] if phase <= 10 else "수정" + + if "도입" in phase_name: + requirements.append("\n✅ 필수 포함:") + requirements.append("- 일상의 균열을 보여주되, 큰 사건이 아닌 미묘한 변화로 시작") + requirements.append("- 주요 인물들의 초기 상태와 관계 설정") + requirements.append("- 핵심 상징 도입 (자연스럽게)") + requirements.append("- 주인공 이름 명확히 설정") + + elif "발전" in phase_name: + requirements.append("\n✅ 필수 포함:") + requirements.append("- 이전 단계의 균열/갈등이 구체화되고 심화") + requirements.append("- 새로운 사건이나 인식이 추가되어 복잡성 증가") + requirements.append("- 인물 간 관계의 미묘한 변화") + requirements.append("- 새로운 공간이나 시간대 탐색") + + elif "절정" in phase_name: + requirements.append("\n✅ 필수 포함:") + requirements.append("- 축적된 갈등이 임계점에 도달") + requirements.append("- 인물의 내적 선택이나 인식의 전환점") + requirements.append("- 상징의 의미가 전복되거나 심화") + requirements.append("- 이전과는 다른 행동이나 결정") + + elif "하강" in phase_name: + requirements.append("\n✅ 필수 포함:") + requirements.append("- 절정의 여파와 그로 인한 변화") + requirements.append("- 새로운 균형점을 찾아가는 과정") + requirements.append("- 인물들의 변화된 관계와 인식") + requirements.append("- 회복이나 상실의 구체적 묘사") + + elif "결말" in phase_name: + requirements.append("\n✅ 필수 포함:") + requirements.append("- 변화된 일상의 모습") + requirements.append("- 해결되지 않은 질문들") + requirements.append("- 여운과 성찰의 여지") + requirements.append("- 처음과 대비되는 마지막") + + # 철학·인간애 강화 체크리스트 + requirements.append("\n📌 필수 포함 요소:") + requirements.append("- 존재의 의미나 삶의 본질에 대한 성찰이 담긴 1문단 이상") + requirements.append("- 타인의 고통에 대한 공감이나 연민을 보여주는 구체적 장면 1개 이상") + requirements.append("- '보여주기(showing)' 기법: 직접 설명 대신 감각적 묘사와 행동으로 표현") + requirements.append("- 이 단계만의 독특한 문학적 장치나 은유 1개 이상") + + # 반복 방지 요구사항 + requirements.append("\n⚠️ 절대 금지사항:") + requirements.append("- 이전 단계와 동일한 사건이나 갈등 반복") + requirements.append("- 인물이 같은 생각이나 감정에 머무르기") + requirements.append("- 플롯이 제자리걸음하기") + requirements.append("- '~을 느꼈다', '~였다'와 같은 직접적 설명") + requirements.append("- 이미 얻은 깨달음을 잊고 다시 시작하기") + + # 진행 체크리스트 + requirements.append("\n☑️ 진행 체크리스트:") + requirements.append("□ 이전 단계의 결과가 이번 단계의 원인이 되는가?") + 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, - 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) - ) - ''') - - 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): - 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) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) - ON CONFLICT(session_id, stage_number) - DO UPDATE SET content=?, word_count=?, status=?, stage_name=?, progression_score=?, repetition_score=?, updated_at=datetime('now') - ''', (session_id, stage_number, stage_name, role, content, word_count, status, progression_score, repetition_score, - content, word_count, status, stage_name, progression_score, repetition_score)) - - # 총 단어 수 업데이트 - 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 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, + 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) + ) + ''') + + 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): + 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) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(session_id, stage_number) + DO UPDATE SET content=?, word_count=?, status=?, stage_name=?, progression_score=?, repetition_score=?, updated_at=datetime('now') + ''', (session_id, stage_number, stage_name, role, content, word_count, status, progression_score, repetition_score, + content, word_count, status, stage_name, progression_score, repetition_score)) + + # 총 단어 수 업데이트 + 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 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 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 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, total_words FROM sessions WHERE status = 'active' ORDER BY updated_at DESC LIMIT 10" - ).fetchall() - return [dict(row) for row in rows] + @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, 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) + """웹 검색 기능""" + 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.current_session_id = None - NovelDatabase.init_db() - - 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: - """감독자 초기 기획 - 통합된 서사 구조""" - 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} 문학 작품", - f"{short_query} 현대 사회" - ] - for q in queries[:3]: # 3개까지만 검색 - try: - results = self.web_search.search(q, count=2, language=language) - if results: - search_results_str += self.web_search.extract_relevant_info(results) + "\n" - except Exception as e: - logger.warning(f"검색 쿼리 실패: {q[:50]}... - {str(e)}") - continue - - lang_prompts = { - "Korean": f"""당신은 노벨문학상 수상작가 수준의 한국 문학 거장입니다. + """진행형 문학 소설 생성 시스템""" + 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.current_session_id = None + NovelDatabase.init_db() + + 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: + """감독자 초기 기획 - 통합된 서사 구조""" + 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} 문학 작품", + f"{short_query} 현대 사회" + ] + for q in queries[:3]: # 3개까지만 검색 + try: + results = self.web_search.search(q, count=2, language=language) + if results: + search_results_str += self.web_search.extract_relevant_info(results) + "\n" + except Exception as e: + logger.warning(f"검색 쿼리 실패: {q[:50]}... - {str(e)}") + continue + + lang_prompts = { + "Korean": f"""당신은 노벨문학상 수상작가 수준의 한국 문학 거장입니다. 단편이 아닌 중편 소설(8,000단어 이상)을 위한 통합된 서사 구조를 기획하세요. 절대 '수동태'를 사용하지 마세요. **주제:** {user_query} @@ -807,47 +807,47 @@ class ProgressiveLiterarySystem: **필수 요구사항:** 1. **통합된 서사 구조 (가장 중요)** - - 10개 단계가 유기적으로 연결된 단일 서사 - - 각 단계는 이전 단계의 결과로 자연스럽게 이어짐 - - 반복이 아닌 축적과 발전 - - 주인공 이름은 처음부터 명확히 설정 (예: 나라미) - - 단계별 서사 진행: - 1) 도입: 일상과 균열 - 평범한 일상 속 첫 균열 - 2) 발전 1: 불안의 고조 - 균열이 확대되며 불안 증폭 - 3) 발전 2: 외부 충격 - 예상치 못한 외부 사건 - 4) 발전 3: 내적 갈등 심화 - 가치관의 충돌 - 5) 절정 1: 위기의 정점 - 모든 갈등이 극대화 - 6) 절정 2: 선택의 순간 - 결정적 선택 - 7) 하강 1: 결과와 여파 - 선택의 직접적 결과 - 8) 하강 2: 새로운 인식 - 변화된 세계관 - 9) 결말 1: 변화된 일상 - 새로운 균형 - 10) 결말 2: 열린 질문 - 독자에게 던지는 질문 + - 10개 단계가 유기적으로 연결된 단일 서사 + - 각 단계는 이전 단계의 결과로 자연스럽게 이어짐 + - 반복이 아닌 축적과 발전 + - 주인공 이름은 처음부터 명확히 설정 (예: 나라미) + + 단계별 서사 진행: + 1) 도입: 일상과 균열 - 평범한 일상 속 첫 균열 + 2) 발전 1: 불안의 고조 - 균열이 확대되며 불안 증폭 + 3) 발전 2: 외부 충격 - 예상치 못한 외부 사건 + 4) 발전 3: 내적 갈등 심화 - 가치관의 충돌 + 5) 절정 1: 위기의 정점 - 모든 갈등이 극대화 + 6) 절정 2: 선택의 순간 - 결정적 선택 + 7) 하강 1: 결과와 여파 - 선택의 직접적 결과 + 8) 하강 2: 새로운 인식 - 변화된 세계관 + 9) 결말 1: 변화된 일상 - 새로운 균형 + 10) 결말 2: 열린 질문 - 독자에게 던지는 질문 2. **인물의 변화 궤적** - - 주인공: 초기 상태 → 중간 변화 → 최종 상태 (명확한 arc) - - 주요 인물들도 각자의 변화 경험 - - 관계의 역동적 변화 - - 각 단계에서 인물이 어떻게 변화하는지 구체적으로 명시 + - 주인공: 초기 상태 → 중간 변화 → 최종 상태 (명확한 arc) + - 주요 인물들도 각자의 변화 경험 + - 관계의 역동적 변화 + - 각 단계에서 인물이 어떻게 변화하는지 구체적으로 명시 3. **주요 플롯 라인** (2-3개) - - 메인 플롯: 전체를 관통하는 핵심 갈등 - - 서브 플롯: 메인과 연결되며 주제를 심화 - - 각 플롯이 어느 단계에서 시작/발전/해결되는지 명시 + - 메인 플롯: 전체를 관통하는 핵심 갈등 + - 서브 플롯: 메인과 연결되며 주제를 심화 + - 각 플롯이 어느 단계에서 시작/발전/해결되는지 명시 4. **상징의 진화** - - 핵심 상징 1-2개 설정 ('개구리알' 같은 강렬하고 다층적��� 상징) - - 단계별로 의미가 변화/심화/전복 + - 핵심 상징 1-2개 설정 ('개구리알' 같은 강렬하고 다층적인 상징) + - 단계별로 의미가 변화/심화/전복 5. **사회적 맥락** - - 개인의 문제가 사회 구조와 연결 - - 구체적인 한국 사회의 현실 반영 + - 개인의 문제가 사회 구조와 연결 + - 구체적인 한국 사회의 현실 반영 6. **철학적 깊이와 인간애** - - 보편적 인간 조건에 대한 성찰 - - 타인의 고통에 대한 공감과 연민 - - 실존적 질문과 그에 대한 탐구 - - "왜 살아야 하는가"에 대한 나름의 답 + - 보편적 인간 조건에 대한 성찰 + - 타인의 고통에 대한 공감과 연민 + - 실존적 질문과 그에 대한 탐구 + - "왜 살아야 하는가"에 대한 나름의 답 **절대 금지사항:** - 동일한 사건이나 상황의 반복 @@ -863,7 +863,7 @@ class ProgressiveLiterarySystem: 하나의 강력한 서사가 시작부터 끝까지 관통하는 작품을 기획하세요.""", - "English": f"""You are a Nobel Prize-winning master of contemporary literary fiction. + "English": f"""You are a Nobel Prize-winning master of contemporary literary fiction. Plan an integrated narrative structure for a novella (8,000+ words), not a collection of short stories. **Theme:** {user_query} @@ -874,47 +874,47 @@ Plan an integrated narrative structure for a novella (8,000+ words), not a colle **Essential Requirements:** 1. **Integrated Narrative Structure (Most Important)** - - Single narrative with 10 organically connected phases - - Each phase naturally follows from previous results - - Accumulation and development, not repetition - - Protagonist name clearly established from beginning - - Phase Progression: - 1) Introduction: Daily life and first crack - 2) Development 1: Rising anxiety - 3) Development 2: External shock - 4) Development 3: Deepening internal conflict - 5) Climax 1: Peak crisis - 6) Climax 2: Moment of choice - 7) Falling Action 1: Direct consequences - 8) Falling Action 2: New awareness - 9) Resolution 1: Changed daily life - 10) Resolution 2: Open questions + - Single narrative with 10 organically connected phases + - Each phase naturally follows from previous results + - Accumulation and development, not repetition + - Protagonist name clearly established from beginning + + Phase Progression: + 1) Introduction: Daily life and first crack + 2) Development 1: Rising anxiety + 3) Development 2: External shock + 4) Development 3: Deepening internal conflict + 5) Climax 1: Peak crisis + 6) Climax 2: Moment of choice + 7) Falling Action 1: Direct consequences + 8) Falling Action 2: New awareness + 9) Resolution 1: Changed daily life + 10) Resolution 2: Open questions 2. **Character Transformation Arcs** - - Protagonist: Clear progression from initial → middle → final state - - Supporting characters also experience change - - Dynamic relationship evolution - - Specify how characters change in each phase + - Protagonist: Clear progression from initial → middle → final state + - Supporting characters also experience change + - Dynamic relationship evolution + - Specify how characters change in each phase 3. **Plot Threads** (2-3) - - Main plot: Core conflict throughout - - Subplots: Connected and deepening themes - - Specify which phase each plot starts/develops/resolves + - Main plot: Core conflict throughout + - Subplots: Connected and deepening themes + - Specify which phase each plot starts/develops/resolves 4. **Symbolic Evolution** - - 1-2 core symbols (like 'frog eggs' - intense and multilayered) - - Meaning transforms across phases + - 1-2 core symbols (like 'frog eggs' - intense and multilayered) + - Meaning transforms across phases 5. **Social Context** - - Individual problems connected to social structures - - Specific contemporary realities + - Individual problems connected to social structures + - Specific contemporary realities 6. **Philosophical Depth and Humanity** - - Reflection on universal human condition - - Empathy and compassion for others' suffering - - Existential questions and exploration - - Personal answer to "why should we live?" + - Reflection on universal human condition + - Empathy and compassion for others' suffering + - Existential questions and exploration + - Personal answer to "why should we live?" **Absolutely Forbidden:** - Repetition of same events/situations @@ -929,14 +929,14 @@ Plan an integrated narrative structure for a novella (8,000+ words), not a colle - Balanced progression Create a work with one powerful narrative from beginning to end.""" - } - - return lang_prompts.get(language, lang_prompts["Korean"]) + } + + return lang_prompts.get(language, lang_prompts["Korean"]) - def create_critic_director_prompt(self, director_plan: str, user_query: str, language: str) -> str: - """비평가의 감독�� 기획 검토 - 서사 통합성 중심""" - lang_prompts = { - "Korean": f"""당신은 서사 구조 전문 비평가입니다. + def create_critic_director_prompt(self, director_plan: str, user_query: str, language: str) -> str: + """비평가의 감독자 기획 검토 - 서사 통합성 중심""" + lang_prompts = { + "Korean": f"""당신은 서사 구조 전문 비평가입니다. 이 기획이 진정한 '장편 소설'인지 엄격히 검토하세요. **원 주제:** {user_query} @@ -947,28 +947,28 @@ Create a work with one powerful narrative from beginning to end.""" **핵심 검토 사항:** 1. **서사의 통합성과 진행성** - - 10개 단계가 하나의 이야기로 연결되는가? - - 각 단계가 이전 단계의 필연적 결과인가? - - 동일한 상황의 반복은 없는가? + - 10개 단계가 하나의 이야기로 연결되는가? + - 각 단계가 이전 단계의 필연적 결과인가? + - 동일한 상황의 반복은 없는가? 2. **인물 변화의 궤적** - - 주인공이 명확한 변화의 arc를 가지는가? - - 변화가 구체적이고 신빙성 있는가? - - 관계의 발전이 계획되어 있는가? - - 주인공 이름이 일관되게 설정되어 있는가? + - 주인공이 명확한 변화의 arc를 가지는가? + - 변화가 구체적이고 신빙성 있는가? + - 관계의 발전이 계획되어 있는가? + - 주인공 이름이 일관되게 설정되어 있는가? 3. **플롯의 축적성** - - 갈등이 점진적으로 심화되는가? - - 새로운 요소가 추가되며 복잡성이 증가하는가? - - 해결이 자연스럽고 필연적인가? + - 갈등이 점진적으로 심화되는가? + - 새로운 요소가 추가되며 복잡성이 증가하는가? + - 해결이 자연스럽고 필연적인가? 4. **분량과 밀도** - - 8,000단어를 채울 충분한 내용인가? - - 각 단계가 800단어의 밀도를 가질 수 있는가? + - 8,000단어를 채울 충분한 내용인가? + - 각 단계가 800단어의 밀도를 가질 수 있는가? 5. **철학적 깊이** - - 인간 존재에 대한 통찰이 계획되어 있는가? - - 단순한 사건 나열이 아닌 의미의 탐구가 있는가? + - 인간 존재에 대한 통찰이 계획되어 있는가? + - 단순한 사건 나열이 아닌 의미의 탐구가 있는가? **판정:** - 통과: 진정한 장편 서사 구조 @@ -976,7 +976,7 @@ Create a work with one powerful narrative from beginning to end.""" 구체적 개선 방향을 제시하세요.""", - "English": f"""You are a narrative structure critic. + "English": f"""You are a narrative structure critic. Strictly review whether this plan is a true 'novel' rather than repeated episodes. **Original Theme:** {user_query} @@ -987,49 +987,49 @@ Strictly review whether this plan is a true 'novel' rather than repeated episode **Key Review Points:** 1. **Narrative Integration and Progression** - - Do 10 phases connect as one story? - - Does each phase necessarily follow from previous? - - No repetition of same situations? + - Do 10 phases connect as one story? + - Does each phase necessarily follow from previous? + - No repetition of same situations? 2. **Character Transformation Arcs** - - Clear protagonist transformation arc? - - Concrete and credible changes? - - Planned relationship development? - - Consistent protagonist naming? + - Clear protagonist transformation arc? + - Concrete and credible changes? + - Planned relationship development? + - Consistent protagonist naming? 3. **Plot Accumulation** - - Progressive conflict deepening? - - Added complexity through new elements? - - Natural and inevitable resolution? + - Progressive conflict deepening? + - Added complexity through new elements? + - Natural and inevitable resolution? 4. **Length and Density** - - Sufficient content for 8,000 words? - - Can each phase sustain 800 words? + - Sufficient content for 8,000 words? + - Can each phase sustain 800 words? 5. **Philosophical Depth** - - Insights into human existence planned? - - Exploration of meaning, not just events? + - Insights into human existence planned? + - Exploration of meaning, not just events? **Verdict:** - Pass: True novel structure - Rewrite: Repetitive/circular structure Provide specific improvements.""" - } - - return lang_prompts.get(language, lang_prompts["Korean"]) + } + + 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 - - lang_prompts = { - "Korean": f"""당신은 작가 {writer_number}번입니다. + 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 + + lang_prompts = { + "Korean": f"""당신은 작가 {writer_number}번입니다. **현재 단계: {phase_name}** **전체 서사 구조:** @@ -1050,46 +1050,46 @@ Provide specific improvements.""" **작성 지침:** 1. **분량**: {target_words}-900 단어 (필수) - - 내면 묘사와 구체적 디테일로 분량 확보 - - 장면을 충분히 전개하고 깊이 있게 묘사 + - 내면 묘사와 구체적 디테일로 분량 확보 + - 장면을 충분히 전개하고 깊이 있게 묘사 2. **서사 진행 (가장 중요)** - - 이전 단계에서 일어난 일의 직접적 결과로 시작 - - 새로운 사건/인식/변화를 추가하여 이야기 전진 - - 다음 단계로 자연스럽게 연결될 고리 마련 - - 주인공의 깨달음이 리셋되지 않고 축적됨 + - 이전 단계에서 일어난 일의 직접적 결과로 시작 + - 새로운 사건/인식/변화를 추가하여 이야기 전진 + - 다음 단계로 자연스럽게 연결될 고리 마련 + - 주인공의 깨달음이 리셋되지 않고 축적됨 3. **인물의 변화** - - 이 단계에서 인물이 겪는 구체적 변화 묘사 - - 내면의 미묘한 변화도 포착 - - 관계의 역학 변화 반영 - - 이전 단계보다 성장한 모습 보여주기 + - 이 단계에서 인물이 겪는 구체적 변화 묘사 + - 내면의 미묘한 변화도 포착 + - 관계의 역학 변화 반영 + - 이전 단계보다 성장한 모습 보여주기 4. **문체와 기법** - - 한국 현대 문학의 섬세한 심리 묘사 - - 일상 속 사회적 맥락 녹여내기 - - 감각적 디테일과 내면 의식의 균형 + - 한국 현대 문학의 섬세한 심리 묘사 + - 일상 속 사회적 맥락 녹여내기 + - 감각적 디테일과 내면 의식의 균형 5. **연속성 유지** - - 인물의 목소리와 말투 일관성 - - 공간과 시간의 연속성 - - 상징과 모티프의 발전 - - 주인공 이름 일관성 (반드시 확인) + - 인물의 목소리와 말투 일관성 + - 공간과 시간의 연속성 + - 상징과 모티프의 발전 + - 주인공 이름 일관성 (반드시 확인) 6. **문학적 기법 필수 사용** - - "보여주기(showing)" 기법: 직접 설명 대신 감각적 묘사 - - 은유와 상징: 구체적 사물을 통한 추상적 의미 전달 - - 대화를 통한 성격 드러내기 - - 내적 독백과 의식의 흐름 기법 - + - "보여주기(showing)" 기법: 직접 설명 대신 감각적 묘사 + - 은유와 상징: 구체적 사물을 통한 추상적 의미 전달 + - 대화를 통한 성격 드러내기 + - 내적 독백과 의식의 흐름 기법 + 7. **철학적 성찰 포함** - - 각 단계마다 인간 존재에 대한 새로운 통찰 1개 이상 - - 구체적 사건 속에서 보편적 진리 발견 + - 각 단계마다 인간 존재에 대한 새로운 통찰 1개 이상 + - 구체적 사건 속에서 보편적 진리 발견 8. **새로운 요소 필수** - - 새로운 인물, 장소, 또는 상황 중 최소 1개 - - 이전과 다른 시간대나 공간 - - 갈등의 새로운 측면 드러내기 + - 새로운 인물, 장소, 또는 상황 중 최소 1개 + - 이전과 다른 시간대나 공간 + - 갈등의 새로운 측면 드러내기 **✅ 진행 체크리스트:** □ 이전 단계의 결과가 명확히 드러나는가? @@ -1107,7 +1107,7 @@ Provide specific improvements.""" 이전의 흐름을 이어받아 새로운 국면으로 발전시키세요.""", - "English": f"""You are Writer #{writer_number}. + "English": f"""You are Writer #{writer_number}. **Current Phase: {phase_name}** **Overall Narrative Structure:** @@ -1128,46 +1128,46 @@ Provide specific improvements.""" **Writing Guidelines:** 1. **Length**: {target_words}-900 words (mandatory) - - Use interior description and concrete details - - Fully develop scenes with depth + - Use interior description and concrete details + - Fully develop scenes with depth 2. **Narrative Progression (Most Important)** - - Start as direct result of previous phase - - Add new events/awareness/changes to advance story - - Create natural connection to next phase - - Accumulated insights, not reset + - Start as direct result of previous phase + - Add new events/awareness/changes to advance story + - Create natural connection to next phase + - Accumulated insights, not reset 3. **Character Change** - - Concrete changes in this phase - - Capture subtle interior shifts - - Reflect relationship dynamics - - Show growth from previous phase + - Concrete changes in this phase + - Capture subtle interior shifts + - Reflect relationship dynamics + - Show growth from previous phase 4. **Style and Technique** - - Delicate psychological portrayal - - Social context in daily life - - Balance sensory details with consciousness + - Delicate psychological portrayal + - Social context in daily life + - Balance sensory details with consciousness 5. **Continuity** - - Consistent character voices - - Spatial/temporal continuity - - Symbol/motif development - - Consistent protagonist naming + - Consistent character voices + - Spatial/temporal continuity + - Symbol/motif development + - Consistent protagonist naming 6. **Literary Techniques Required** - - "Showing" not telling - - Metaphors and symbols - - Character through dialogue - - Stream of consciousness - + - "Showing" not telling + - Metaphors and symbols + - Character through dialogue + - Stream of consciousness + 7. **Philosophical Reflection** - - New insights about human existence - - Universal truths in specific events + - New insights about human existence + - Universal truths in specific events 8. **New Elements Required** - - At least 1 new character, location, or situation - - Different time/space from before - - New aspect of conflict + - At least 1 new character, location, or situation + - Different time/space from before + - New aspect of conflict **✅ Progress Checklist:** □ Clear results from previous phase? @@ -1184,29 +1184,29 @@ Provide specific improvements.""" - Forgetting gained insights Continue the flow and develop into new phase.""" - } - - return lang_prompts.get(language, lang_prompts["Korean"]) + } + + 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[:20]): # 최근 20개 문단 - for j, para2 in enumerate(paragraphs[i+1:i+21]): - if narrative_tracker.content_deduplicator.check_similarity(para1, para2) > 0.7: - duplicates.append(f"문단 {i+1}과 문단 {i+j+2} 유사") - - lang_prompts = { - "Korean": f"""서사 진행 전문 비평가로서 작품을 엄격히 검토하세요. + 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[:20]): # 최근 20개 문단 + for j, para2 in enumerate(paragraphs[i+1:i+21]): + if narrative_tracker.content_deduplicator.check_similarity(para1, para2) > 0.7: + duplicates.append(f"문단 {i+1}과 문단 {i+j+2} 유사") + + lang_prompts = { + "Korean": f"""서사 진행 전문 비평가로서 작품을 엄격히 검토하세요. **원 주제:** {user_query} @@ -1224,30 +1224,30 @@ Continue the flow and develop into new phase.""" **필수 검증 항목:** 1. **반복 검출 (최우선)** - - 동일/유사 문장이 2회 이상 나타나는가? - - 같은 상황이 변주만 달리해 반복되는가? - - 각 단계별로 실제로 새로운 내용이 추가되었는가? - - "습기가 찬 아침", "나라미 어플", "43만원" 등 반복 표현? + - 동일/유사 문장이 2회 이상 나타나는가? + - 같은 상황이 변주만 달리해 반복되는가? + - 각 단계별로 실제로 새로운 내용이 추가되었는가? + - "습기가 찬 아침", "나라미 어플", "43만원" 등 반복 표현? 2. **서사 진행도 측정** - - 1단계와 현재 단계의 상황이 명확히 다른가? - - 주인공의 심리/인식이 변화했는가? - - 갈등이 심화/전환/해결 방향으로 움직였는가? - - 주인공의 깨달음이 리셋되지 않고 축적되는가? + - 1단계와 현재 단계의 상황이 명확히 다른가? + - 주인공의 심리/인식이 변화했는가? + - 갈등이 심화/전환/해결 방향으로 움직였는가? + - 주인공의 깨달음이 리셋되지 않고 축적되는가? 3. **설정 일관성** - - 모든 캐릭터 이름이 일관되는가? (특히 주인공) - - 시공간 설정이 논리적인가? - - 설정이 중간에 바뀌지 않는가? + - 모든 캐릭터 이름이 일관되는가? (특히 주인공) + - 시공간 설정이 논리적인가? + - 설정이 중간에 바뀌지 않는가? 4. **분량과 밀도** - - 현재까지 총 단어 수 확인 - - 목표(8,000단어)에 도달 가능한가? + - 현재까지 총 단어 수 확인 + - 목표(8,000단어)에 도달 가능한가? 5. **문학적 완성도** - - '보여주기' 기법이 잘 사용되고 있는가? - - 철학적 통찰이 자연스럽게 녹아있는가? - - 은유와 상징이 효과적인가? + - '보여주기' 기법이 잘 사용되고 있는가? + - 철학적 통찰이 자연스럽게 녹아있는가? + - 은유와 상징이 효과적인가? **불합격 기준:** - 2개 이상 단계에서 유사 내용 발견 시 @@ -1259,7 +1259,8 @@ Continue the flow and develop into new phase.""" 각 작가에게 구체적인 진행 방향과 금지사항 제시. 발견된 반복은 모두 제거하도록 명시.""", - "English": f"""As a narrative progression critic, strictly review the work. + "English": f"""As a narrative progression critic, strictly review the work. + **Original Theme:** {user_query} @@ -1268,8 +1269,6 @@ Continue the flow and develop into new phase.""" **Detected Progression Issues:** {chr(10).join(issues) if issues else "None"} - - **Detected Duplications:** {chr(10).join(duplicates) if duplicates else "None"} @@ -1279,30 +1278,30 @@ Continue the flow and develop into new phase.""" **Mandatory Verification Items:** 1. **Duplication Detection (Top Priority)** - - Same/similar sentences appearing 2+ times? - - Same situations with only variations? - - Actually new content in each phase? - - Repeated expressions like specific phrases? + - Same/similar sentences appearing 2+ times? + - Same situations with only variations? + - Actually new content in each phase? + - Repeated expressions like specific phrases? 2. **Narrative Progression Measurement** - - Clear difference between phase 1 and current? - - Protagonist's psychology/perception changed? - - Conflict deepening/turning/resolving? - - Insights accumulating, not resetting? + - Clear difference between phase 1 and current? + - Protagonist's psychology/perception changed? + - Conflict deepening/turning/resolving? + - Insights accumulating, not resetting? 3. **Setting Consistency** - - All character names consistent? (especially protagonist) - - Logical space/time settings? - - Settings not changing mid-story? + - All character names consistent? (especially protagonist) + - Logical space/time settings? + - Settings not changing mid-story? 4. **Length and Density** - - Current total word count - - Can reach 8,000 word target? + - Current total word count + - Can reach 8,000 word target? 5. **Literary Completion** - - "Showing" technique well used? - - Philosophical insights naturally integrated? - - Effective metaphors and symbols? + - "Showing" technique well used? + - Philosophical insights naturally integrated? + - Effective metaphors and symbols? **Failure Criteria:** - Similar content in 2+ phases @@ -1313,16 +1312,16 @@ Continue the flow and develop into new phase.""" **Revision Instructions:** Specific progression directions and prohibitions for each writer. All detected repetitions must be removed.""" - } - - return lang_prompts.get(language, lang_prompts["Korean"]) + } + + 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 - - return f"""작가 {writer_number}번, 비평을 반영하여 수정하세요. + def create_writer_revision_prompt(self, writer_number: int, initial_content: str, + critic_feedback: str, language: str) -> str: + """작가 수정 프롬프트""" + target_words = MIN_WORDS_PER_WRITER + + return f"""작가 {writer_number}번, 비평을 반영하여 수정하세요. **초안:** {initial_content} @@ -1348,11 +1347,11 @@ All detected repetitions must be removed.""" 전면 재작성이 필요하면 과감히 수정하세요. 수정본만 제시하세요.""" - def create_editor_prompt(self, complete_novel: str, issues: List[str], language: str) -> str: - """편집자 프롬프트 - 반복 제거 전문""" - - lang_prompts = { - "Korean": f"""당신은 전문 편집자입니다. + def create_editor_prompt(self, complete_novel: str, issues: List[str], language: str) -> str: + """편집자 프롬프트 - 반복 제거 전문""" + + lang_prompts = { + "Korean": f"""당신은 전문 편집자입니다. 완성된 원고에서 반복을 제거하고 서사를 매끄럽게 연결하세요. **발견된 문제:** @@ -1361,28 +1360,28 @@ All detected repetitions must be removed.""" **편집 지침:** 1. **반복 제거 (최우선)** - - 동일하거나 유사한 문단은 가장 효과적인 것 하나만 남기고 삭제 - - "습기가 찬 아침", "나라미 어플 43만원" 등 반복 표현 중 하나만 유지 - - 비슷한 장면(연못 응시, 계란 던지기 등)은 가장 강렬한 것만 선택 + - 동일하거나 유사한 문단은 가장 효과적인 것 하나만 남기고 삭제 + - "습기가 찬 아침", "나라미 어플 43만원" 등 반복 표현 중 하나만 유지 + - 비슷한 장면(연못 응시, 계란 던지기 등)은 가장 강렬한 것만 선택 2. **서사 재구성** - - 남은 장면들을 인과관계에 따라 재배열 - - 시간 순서와 감정의 흐름이 자연스럽게 연결되도록 - - 필요시 짧은 전환 문단 추가 (2-3문장) + - 남은 장면들을 인과관계에 따라 재배열 + - 시간 순서와 감정의 흐름이 자연스럽게 연결되도록 + - 필요시 짧은 전환 문단 추가 (2-3문장) 3. **캐릭터 일관성** - - 주인공 이름을 '나라미'로 통일 - - 다른 인물들의 이름도 일관성 확인 - - 인물의 성격과 말투 일관성 유지 + - 주인공 이름을 '나라미'로 통일 + - 다른 인물들의 이름도 일관성 확인 + - 인물의 성격과 말투 일관성 유지 4. **깨달음의 누적** - - 주인공의 각 깨달음이 이전보다 발전된 형태로 표현되도록 - - 동일한 수준의 인식 반복 제거 - - 마지막으로 갈수록 더 깊은 통찰이 되도록 + - 주인공의 각 깨달음이 이전보다 발전된 형태로 표현되도록 + - 동일한 수준의 인식 반복 제거 + - 마지막으로 갈수록 더 깊은 통찰이 되도록 5. **분량 조정** - - 반복 제거 후에도 8,000단어 이상 유지 - - 필요시 남은 장면들을 약간 확장 (묘사 추가) + - 반복 제거 후에도 8,000단어 이상 유지 + - 필요시 남은 장면들을 약간 확장 (묘사 추가) **편집 규칙:** - 작가의 원문 스타일과 문체는 최대한 보존 @@ -1394,7 +1393,7 @@ All detected repetitions must be removed.""" 반복이 완전히 제거되고 자연스럽게 흐르는 최종 원고를 제시하세요. 편집 전후의 주요 변경사항도 간단히 요약하세요.""", - "English": f"""You are a professional editor. + "English": f"""You are a professional editor. Remove repetitions and smooth narrative connections in the completed manuscript. **Identified Issues:** @@ -1403,28 +1402,28 @@ Remove repetitions and smooth narrative connections in the completed manuscript. **Editing Guidelines:** 1. **Repetition Removal (Top Priority)** - - Keep only most effective version of similar paragraphs - - Retain only one instance of repeated expressions - - Select most powerful version of similar scenes + - Keep only most effective version of similar paragraphs + - Retain only one instance of repeated expressions + - Select most powerful version of similar scenes 2. **Narrative Reconstruction** - - Rearrange remaining scenes by causality - - Natural flow of time and emotion - - Add brief transitions if needed (2-3 sentences) + - Rearrange remaining scenes by causality + - Natural flow of time and emotion + - Add brief transitions if needed (2-3 sentences) 3. **Character Consistency** - - Unify protagonist name - - Check other character name consistency - - Maintain character personality/voice + - Unify protagonist name + - Check other character name consistency + - Maintain character personality/voice 4. **Insight Accumulation** - - Each insight more developed than previous - - Remove same-level recognition repetitions - - Deeper insights toward the end + - Each insight more developed than previous + - Remove same-level recognition repetitions + - Deeper insights toward the end 5. **Length Adjustment** - - Maintain 8,000+ words after cuts - - Slightly expand remaining scenes if needed + - Maintain 8,000+ words after cuts + - Slightly expand remaining scenes if needed **Editing Rules:** - Preserve original style and voice @@ -1435,15 +1434,15 @@ Remove repetitions and smooth narrative connections in the completed manuscript. **Output:** Present final manuscript with repetitions removed and natural flow. Briefly summarize major changes.""" - } - - return lang_prompts.get(language, lang_prompts["Korean"]) + } + + return lang_prompts.get(language, lang_prompts["Korean"]) - def create_critic_final_prompt(self, complete_novel: str, word_count: int, language: str) -> str: - """최종 비평 - AGI 평가 기준""" - - lang_prompts = { - "Korean": f"""완성된 소설을 AGI 튜링테스트 기준으로 평가하세요. + def create_critic_final_prompt(self, complete_novel: str, word_count: int, language: str) -> str: + """최종 비평 - AGI 평가 기준""" + + lang_prompts = { + "Korean": f"""완성된 소설을 AGI 튜링테스트 기준으로 평가하세요. **작품 정보:** - 총 분량: {word_count}단어 @@ -1455,30 +1454,30 @@ Briefly summarize major changes.""" **평가 기준 (AGI 튜링테스트):** 1. **장편소설로서의 완성도 (30점)** - - 통합된 서사 구조 (반복 없음) - - 인물의 명확한 변화 arc - - 플롯의 축적과 해결 - - 8,000단어 이상 분량 - - 설정 일관성 (특히 인물 이름) + - 통합된 서사 구조 (반복 없음) + - 인물의 명확한 변화 arc + - 플롯의 축적과 해결 + - 8,000단어 이상 분량 + - 설정 일관성 (특히 인물 이름) 2. **문학적 성취 (35점)** - - 주제 의식의 깊이 - - 인물 심리의 설득력 - - 문체의 일관성과 아름다움 - - 상징과 은유의 효과 - - '보여주기' 기법의 활용도 + - 주제 의식의 깊이 + - 인물 심리의 설득력 + - 문체의 일관성과 아름다움 + - 상징과 은유의 효과 + - '보여주기' 기법의 활용도 3. **사회적 통찰 (25점)** - - 현대 사회 문제 포착 - - 개인과 구조의 연결 - - 보편성과 특수성 균형 - - 인간 조건에 대한 통찰 + - 현대 사회 문제 포착 + - 개인과 구조의 연결 + - 보편성과 특수성 균형 + - 인간 조건에 대한 통찰 4. **독창성과 인간성 (10점)** - - AI가 아닌 인간 작가의 느낌 - - 독창적 표현과 통찰 - - 감정적 진정성 - - 철학적 깊이 + - AI가 아닌 인간 작가의 느낌 + - 독창적 표현과 통찰 + - 감정적 진정성 + - 철학적 깊이 **특별 감점 요인:** - 내용/문장 반복 (-5점 per 중대 반복) @@ -1492,7 +1491,7 @@ Briefly summarize major changes.""" '개구리알' 같은 강렬한 중심 상징이 있는지 확인하세요. 편집 후에도 남은 반복이 있는지 세밀히 검토하세요.""", - "English": f"""Evaluate the completed novel by AGI Turing Test standards. + "English": f"""Evaluate the completed novel by AGI Turing Test standards. **Work Information:** - Total length: {word_count} words @@ -1504,30 +1503,30 @@ Briefly summarize major changes.""" **Evaluation Criteria (AGI Turing Test):** 1. **Completion as Novel (30 points)** - - Integrated narrative structure (no repetition) - - Clear character transformation arcs - - Plot accumulation and resolution - - 8,000+ word length - - Setting consistency (especially names) + - Integrated narrative structure (no repetition) + - Clear character transformation arcs + - Plot accumulation and resolution + - 8,000+ word length + - Setting consistency (especially names) 2. **Literary Achievement (35 points)** - - Depth of thematic consciousness - - Persuasiveness of character psychology - - Consistency and beauty of style - - Effectiveness of symbols and metaphors - - Use of "showing" technique + - Depth of thematic consciousness + - Persuasiveness of character psychology + - Consistency and beauty of style + - Effectiveness of symbols and metaphors + - Use of "showing" technique 3. **Social Insight (25 points)** - - Capturing contemporary social issues - - Connection between individual and structure - - Balance of universality and specificity - - Insights into human condition + - Capturing contemporary social issues + - Connection between individual and structure + - Balance of universality and specificity + - Insights into human condition 4. **Originality and Humanity (10 points)** - - Feeling of human author, not AI - - Original expressions and insights - - Emotional authenticity - - Philosophical depth + - Feeling of human author, not AI + - Original expressions and insights + - Emotional authenticity + - Philosophical depth **Special Deductions:** - Content/sentence repetition (-5 points per major repetition) @@ -1540,106 +1539,106 @@ Briefly summarize major changes.""" Strictly evaluate whether there are 'repetitive structure' issues. Check for powerful central symbols like 'frog eggs'. Carefully review for any remaining repetitions after editing.""" - } - - return lang_prompts.get(language, lang_prompts["Korean"]) + } + + 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}") - return full_content + # --- 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}") + return full_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] - - # 작가와 편집자 역할일 때는 더 많은 토큰 허용 - max_tokens = 15000 if role.startswith("writer") or role == "editor" else 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: - yield buffer - buffer = "" - time.sleep(0.01) - - except Exception as e: - logger.error(f"청크 처리 오류: {str(e)}") - continue - - if buffer: - yield buffer - - except Exception as e: - logger.error(f"스트리밍 오류: {type(e).__name__}: {str(e)}") - yield f"❌ 오류 발생: {str(e)}" + 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] + + # 작가와 편집자 역할일 때는 더 많은 토큰 허용 + max_tokens = 15000 if role.startswith("writer") or role == "editor" else 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: + yield buffer + buffer = "" + time.sleep(0.01) + + except Exception as e: + logger.error(f"청크 처리 오류: {str(e)}") + continue + + if buffer: + yield 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": """당신은 노벨문학상 수상작가 수준의 한국 문학 거장입니다. + def get_system_prompts(self, language: str) -> Dict[str, str]: + """역할별 시스템 프롬프트""" + + base_prompts = { + "Korean": { + "director": """당신은 노벨문학상 수상작가 수준의 한국 문학 거장입니다. 인간 존재의 보편적 조건과 한국 사회의 특수성을 동시에 포착하세요. '개구리알' 같은 강렬하고 다층적인 중심 상징을 창조하세요. 철학적 깊이와 문학적 아름다움을 동시에 추구하세요. 반복이 아닌 진행, 순환이 아닌 발전을 통해 하나의 강력한 서사를 구축하세요. 주인공의 이름과 설정을 명확히 하고 일관성을 유지하세요.""", - - "critic": """당신은 엄격한 문학 비평가입니다. + + "critic": """당신은 엄격한 문학 비평가입니다. 특히 '반복 구조'와 '서사 정체'를 철저히 감시하세요. 작품이 진정한 장편소설인지, 아니면 반복되는 단편의 집합인지 구별하세요. 문학적 기법의 효과성과 철학적 깊이를 평가하세요. 캐릭터 이름과 설정의 일관성을 반드시 확인하세요. 동일한 문장이나 상황의 반복을 절대 용납하지 마세요.""", - - "writer_base": """당신은 현대 한국 문학 작가입니다. + + "writer_base": """당신은 현대 한국 문학 작가입니다. '보여주기' 기법을 사용하여 독자의 상상력을 자극하세요. 직접적 설명보다 감각적 묘사와 행동으로 감정을 전달하세요. 각 장면에서 인간 존재에 대한 새로운 통찰을 담으세요. @@ -1648,29 +1647,29 @@ Carefully review for any remaining repetitions after editing.""" 절대 이전과 같은 상황을 반복하지 마세요. 주인공의 이름과 설정을 일관되게 유지하세요. 이미 얻은 깨달음을 잊지 말고 발전시키세요.""", - - "editor": """당신은 경험이 풍부한 문학 편집자입니다. + + "editor": """당신은 경험이 풍부한 문학 편집자입니다. 반복을 철저히 제거하고 서사의 흐름을 매끄럽게 만드세요. 원작의 문체와 주제는 보존하면서 구조적 문제를 해결하세요. 캐릭터 이름과 설정의 일관성을 확보하세요. 깨달음이 누적되고 발전하도록 편집하세요.""" - }, - "English": { - "director": """You are a Nobel Prize-winning master of contemporary literary fiction. + }, + "English": { + "director": """You are a Nobel Prize-winning master of contemporary literary fiction. Capture both universal human condition and specific social realities. Create intense, multilayered central symbols like 'frog eggs'. Pursue both philosophical depth and literary beauty. Build one powerful narrative through progression not repetition, development not cycles. Establish protagonist's name and settings clearly with consistency.""", - - "critic": """You are a strict literary critic. + + "critic": """You are a strict literary critic. Vigilantly monitor for 'repetitive structure' and 'narrative stagnation'. Distinguish whether this is a true novel or a collection of repeated episodes. Evaluate effectiveness of literary techniques and philosophical depth. Always check character name and setting consistency. Never tolerate repetition of same sentences or situations.""", - - "writer_base": """You are a contemporary literary writer. + + "writer_base": """You are a contemporary literary writer. Use 'showing' technique to stimulate reader's imagination. Convey emotions through sensory description and action rather than explanation. Include new insights about human existence in each scene. @@ -1679,185 +1678,185 @@ Write minimum 800 words, capturing both interior and society. Never repeat previous situations. Maintain protagonist's name and settings consistently. Don't forget gained insights, develop them further.""", - - "editor": """You are an experienced literary editor. + + "editor": """You are an experienced literary editor. Thoroughly remove repetitions and smooth narrative flow. Preserve original style and themes while solving structural issues. Ensure character name and setting consistency. Edit so insights accumulate and develop.""" - } - } - - prompts = base_prompts.get(language, base_prompts["Korean"]).copy() - - # 특수 작가 프롬프트 - for i in range(1, 11): - prompts[f"writer{i}"] = prompts["writer_base"] - - return prompts + } + } + + 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) - } 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 - }) - 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"): - writer_num = int(re.search(r'\d+', role).group()) - 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) - - # 서사 추적기 업데이트 - self.update_narrative_tracker(stage_content, writer_num) - self.narrative_tracker.extract_used_elements(stage_content) - - 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) - ) - - # 서사 추적기 저장 - NovelDatabase.save_narrative_tracker(self.current_session_id, self.narrative_tracker) - - # 총 단어 수 업데이트 - total_words = NovelDatabase.get_total_words(self.current_session_id) - yield f"✅ {stage_name} 완료 (총 {total_words:,}단어)", stages, self.current_session_id + # --- 메인 프로세스 --- + 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) + } 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 + }) + 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"): + writer_num = int(re.search(r'\d+', role).group()) + 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) + + # 서사 추적기 업데이트 + self.update_narrative_tracker(stage_content, writer_num) + self.narrative_tracker.extract_used_elements(stage_content) + + 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) + ) + + # 서사 추적기 저장 + NovelDatabase.save_narrative_tracker(self.current_session_id, self.narrative_tracker) + + # 총 단어 수 업데이트 + total_words = NovelDatabase.get_total_words(self.current_session_id) + 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, final_word_count, language) - - NovelDatabase.update_final_novel(self.current_session_id, final_novel, final_report) - 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: int, role: str, query: str, language: str, stages: List[Dict]) -> str: - """단계별 프롬프트 생성""" - if stage_idx == 0: - return self.create_director_initial_prompt(query, language) - if stage_idx == 1: - return self.create_critic_director_prompt(stages[0]["content"], query, language) - if stage_idx == 2: - return self.create_director_revision_prompt(stages[0]["content"], stages[1]["content"], query, language) - - master_plan = stages[2]["content"] - - if 3 <= stage_idx <= 12: # 작가 초안 - writer_num = stage_idx - 2 - previous_content = self.get_previous_writer_content(stages, writer_num) - phase_requirements = self.narrative_tracker.generate_phase_requirements(writer_num) - narrative_summary = self.generate_narrative_summary(stages, writer_num) - used_elements = list(self.narrative_tracker.used_expressions) - - return self.create_writer_prompt_enhanced( - writer_num, master_plan, previous_content, - phase_requirements, narrative_summary, language, used_elements - ) - - if stage_idx == 13: # 비평가 중간 검토 - all_content = self.get_all_writer_content(stages, 12) - return self.create_critic_consistency_prompt_enhanced( - all_content, self.narrative_tracker, query, language - ) - - if 14 <= stage_idx <= 23: # 작가 수정 - writer_num = stage_idx - 13 - initial_content = stages[2 + writer_num]["content"] - feedback = stages[13]["content"] - return self.create_writer_revision_prompt(writer_num, initial_content, feedback, language) - - if stage_idx == 24: # 편집자 - complete_novel = self.get_all_writer_content(stages, 23) - issues = self.detect_issues(complete_novel) - return self.create_editor_prompt(complete_novel, issues, language) - - if stage_idx == 25: # 최종 검토 - edited_novel = stages[24]["content"] if stages[24]["content"] else self.get_all_writer_content(stages, 23) - word_count = len(edited_novel.split()) - return self.create_critic_final_prompt(edited_novel, word_count, language) - - return "" + # 최종 소설 정리 + 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, final_word_count, language) + + NovelDatabase.update_final_novel(self.current_session_id, final_novel, final_report) + 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: int, role: str, query: str, language: str, stages: List[Dict]) -> str: + """단계별 프롬프트 생성""" + if stage_idx == 0: + return self.create_director_initial_prompt(query, language) + if stage_idx == 1: + return self.create_critic_director_prompt(stages[0]["content"], query, language) + if stage_idx == 2: + return self.create_director_revision_prompt(stages[0]["content"], stages[1]["content"], query, language) + + master_plan = stages[2]["content"] + + if 3 <= stage_idx <= 12: # 작가 초안 + writer_num = stage_idx - 2 + previous_content = self.get_previous_writer_content(stages, writer_num) + phase_requirements = self.narrative_tracker.generate_phase_requirements(writer_num) + narrative_summary = self.generate_narrative_summary(stages, writer_num) + used_elements = list(self.narrative_tracker.used_expressions) + + return self.create_writer_prompt_enhanced( + writer_num, master_plan, previous_content, + phase_requirements, narrative_summary, language, used_elements + ) + + if stage_idx == 13: # 비평가 중간 검토 + all_content = self.get_all_writer_content(stages, 12) + return self.create_critic_consistency_prompt_enhanced( + all_content, self.narrative_tracker, query, language + ) + + if 14 <= stage_idx <= 23: # 작가 수정 + writer_num = stage_idx - 13 + initial_content = stages[2 + writer_num]["content"] + feedback = stages[13]["content"] + return self.create_writer_revision_prompt(writer_num, initial_content, feedback, language) + + if stage_idx == 24: # 편집자 + complete_novel = self.get_all_writer_content(stages, 23) + issues = self.detect_issues(complete_novel) + return self.create_editor_prompt(complete_novel, issues, language) + + if stage_idx == 25: # 최종 검토 + edited_novel = stages[24]["content"] if stages[24]["content"] else self.get_all_writer_content(stages, 23) + word_count = len(edited_novel.split()) + return self.create_critic_final_prompt(edited_novel, word_count, language) + + return "" - def create_director_revision_prompt(self, initial_plan: str, critic_feedback: str, user_query: str, language: str) -> str: - """감독자 수정 프롬프트""" - return f"""비평을 반영하여 통합된 서사 구조를 완성하세요. + def create_director_revision_prompt(self, initial_plan: str, critic_feedback: str, user_query: str, language: str) -> str: + """감독자 수정 프롬프트""" + return f"""비평을 반영하여 통합된 서사 구조를 완성하세요. **원 주제:** {user_query} @@ -1879,676 +1878,676 @@ Edit so insights accumulate and develop.""" 각 단계가 이전의 필연적 결과가 되도록 수정하세요.""" - def get_previous_writer_content(self, stages: List[Dict], current_writer: int) -> str: - """이전 작가의 내용 가져오기""" - if current_writer == 1: - return "" - - # 바로 이전 작가의 내용 - prev_idx = current_writer + 1 # stages 인덱스는 writer_num + 2 - if prev_idx < len(stages) and stages[prev_idx]["content"]: - return stages[prev_idx]["content"] - - return "" - - 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]}") - - return "\n".join(summary_parts) if summary_parts else "이전 내용을 이어받아 진행하세요." + def get_previous_writer_content(self, stages: List[Dict], current_writer: int) -> str: + """이전 작가의 내용 가져오기""" + if current_writer == 1: + return "" + + # 바로 이전 작가의 내용 + prev_idx = current_writer + 1 # stages 인덱스는 writer_num + 2 + if prev_idx < len(stages) and stages[prev_idx]["content"]: + return stages[prev_idx]["content"] + + return "" + + 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]}") + + 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 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 = [] - - # 반복 감지 - duplicates = self.narrative_tracker.content_deduplicator.count_repetitions(content) - if duplicates > 0: - issues.append(f"{duplicates}개의 반복된 문단 발견") - - # 특정 반복 표현 감지 - repetitive_phrases = ["습기가 찬 아침", "나라미 어플", "43만원", "개구리알을 바라보았다"] - for phrase in repetitive_phrases: - count = content.count(phrase) - if count > 2: - issues.append(f"'{phrase}' 표현이 {count}회 반복됨") - - # 캐릭터 이름 일관성 - name_variations = ["나라미", "안정", "나"] - found_names = [name for name in name_variations if name in content] - if len(found_names) > 1: - issues.append(f"주인공 이름 불일치: {', '.join(found_names)}") - - return issues + def detect_issues(self, content: str) -> List[str]: + """문제점 감지""" + issues = [] + + # 반복 감지 + duplicates = self.narrative_tracker.content_deduplicator.count_repetitions(content) + if duplicates > 0: + issues.append(f"{duplicates}개의 반복된 문단 발견") + + # 특정 반복 표현 감지 + repetitive_phrases = ["습기가 찬 아침", "나라미 어플", "43만원", "개구리알을 바라보았다"] + for phrase in repetitive_phrases: + count = content.count(phrase) + if count > 2: + issues.append(f"'{phrase}' 표현이 {count}회 반복됨") + + # 캐릭터 이름 일관성 + name_variations = ["나라미", "안정", "나"] + found_names = [name for name in name_variations if name in content] + if len(found_names) > 1: + issues.append(f"주인공 이름 불일치: {', '.join(found_names)}") + + return issues - 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 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, word_count: int, language: str) -> str: - """최종 문학적 평가""" - prompt = self.create_critic_final_prompt(complete_novel, word_count, 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 generate_literary_report(self, complete_novel: str, word_count: int, language: str) -> str: + """최종 문학적 평가""" + prompt = self.create_critic_final_prompt(complete_novel, word_count, 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 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 + """메인 쿼리 처리 함수""" + 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() - return [f"{s['session_id'][:8]}... - {s['user_query'][:50]}... ({s['created_at']}) [{s['total_words']:,}단어]" - for s in sessions] + """활성 세션 목록""" + sessions = NovelDatabase.get_active_sessions() + return [f"{s['session_id'][:8]}... - {s['user_query'][:50]}... ({s['created_at']}) [{s['total_words']:,}단어]" + for s in sessions] 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, "복구할 세션이 없습니다." + """최근 세션 자동 복구""" + 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) + """세션 재개""" + 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 + """소설 다운로드 파일 생성""" + 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', '')) - markdown += f"**총 단어 수: {total_words:,} / {TARGET_WORDS:,}**\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 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]" - - markdown += "\n" - - if stage['content']: - preview = stage['content'][:200] + "..." if len(stage['content']) > 200 else stage['content'] - markdown += f"> {preview}\n\n" - - return markdown + """단계별 진행 상황 표시""" + markdown = "## 🎬 진행 상황\n\n" + + # 총 단어 수 계산 + total_words = sum(s.get('word_count', 0) for s in stages if 'writer' in s.get('name', '')) + markdown += f"**총 단어 수: {total_words:,} / {TARGET_WORDS:,}**\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 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]" + + 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" - 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 + """소설 내용 표시""" + if not novel_text: + return "아직 완성된 내용이 없습니다." + + formatted = "# 📖 완성된 소설\n\n" + + # 단어 수 표시 + word_count = len(novel_text.split()) + formatted += f"**총 분량: {word_count:,}단어 (목표: {TARGET_WORDS:,}단어)**\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 + """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 + """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; + 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); + 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; + 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; + background-color: rgba(0, 255, 127, 0.1); + border-left: 3px solid #00ff7f; + 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); + 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); + 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); + 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); + 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); + 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; + 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; + 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; + 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; + background-color: #4CAF50; + color: white; } .score-medium { - background-color: #FF9800; - color: white; + background-color: #FF9800; + color: white; } .score-low { - background-color: #F44336; - color: white; + background-color: #F44336; + color: white; } """ # Gradio 인터페이스 생성 def create_interface(): - with gr.Blocks(css=custom_css, title="AI 진행형 장편소설 생성 시스템 v2") as interface: - gr.HTML(""" -
-

- 📚 AI 진행형 장편소설 생성 시스템 v2.0 -

-

- 반복 없는 진정한 장편 서사 구조 실현 -

-

- 10개의 유기적으로 연결된 단계를 통해 하나의 완전한 이야기를 만들어냅니다. -
- 각 단계는 이전 단계의 필연적 결과로 이어지며, 인물의 변화와 성장을 추적합니다. -

-
- ⚡ 반복이 아닌 축적, 순환이 아닌 진행을 통한 진정한 장편 서사 -
-
- 🆕 v2.0 개선사항: 강화된 반복 감지 시스템, 편집자 단계 추가, 실시간 진행도 모니터링 -
-
- """) - - # 상태 관리 - 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 - ) - - # 숨겨진 상태 - 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)}") - return gr.update(choices=[]) - - def handle_auto_recover(language): - session_id, message = auto_recover_session(language) - return session_id, message - - # 이벤트 연결 - submit_btn.click( - fn=process_query, - inputs=[query_input, language_select, current_session_id], - outputs=[stages_display, novel_output, status_text, current_session_id] - ) - - 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] - ) - - 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] - ) - - 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] - ) - - 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 + with gr.Blocks(css=custom_css, title="AI 진행형 장편소설 생성 시스템 v2") as interface: + gr.HTML(""" +
+

+ 📚 AI 진행형 장편소설 생성 시스템 v2.0 +

+

+ 반복 없는 진정한 장편 서사 구조 실현 +

+

+ 10개의 유기적으로 연결된 단계를 통해 하나의 완전한 이야기를 만들어냅니다. +
+ 각 단계는 이전 단계의 필연적 결과로 이어지며, 인물의 변화와 성장을 추적합니다. +

+
+ ⚡ 반복이 아닌 축적, 순환이 아닌 진행을 통한 진정한 장편 서사 +
+
+ 🆕 v2.0 개선사항: 강화된 반복 감지 시스템, 편집자 단계 추가, 실시간 진행도 모니터링 +
+
+ """) + + # 상태 관리 + 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 + ) + + # 숨겨진 상태 + 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)}") + return gr.update(choices=[]) + + def handle_auto_recover(language): + session_id, message = auto_recover_session(language) + return session_id, message + + # 이벤트 연결 + submit_btn.click( + fn=process_query, + inputs=[query_input, language_select, current_session_id], + outputs=[stages_display, novel_output, status_text, current_session_id] + ) + + 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] + ) + + 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] + ) + + 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] + ) + + 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 진행형 장편소설 생성 시스템 v2.0 시작...") - logger.info("=" * 60) - - # 환경 확인 - logger.info(f"API 엔드포인트: {API_URL}") - logger.info(f"목표 분량: {TARGET_WORDS:,}단어") - logger.info(f"작가당 최소 분량: {MIN_WORDS_PER_WRITER:,}단어") - 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 - ) \ No newline at end of file + logger.info("AI 진행형 장편소설 생성 시스템 v2.0 시작...") + logger.info("=" * 60) + + # 환경 확인 + logger.info(f"API 엔드포인트: {API_URL}") + logger.info(f"목표 분량: {TARGET_WORDS:,}단어") + logger.info(f"작가당 최소 분량: {MIN_WORDS_PER_WRITER:,}단어") + 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 + ) \ No newline at end of file