diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -1,6 +1,3 @@ -# 2점대 평가에 불과하다. - - import gradio as gr import os import json @@ -26,7 +23,7 @@ logger = logging.getLogger(__name__) # --- Document export imports --- try: from docx import Document - from docx.shared import Inches, Pt, RGBColor + from docx.shared import Inches, Pt, RGBColor, Mm from docx.enum.text import WD_ALIGN_PARAGRAPH from docx.enum.style import WD_STYLE_TYPE from docx.oxml.ns import qn @@ -41,11 +38,11 @@ FRIENDLI_TOKEN = os.getenv("FRIENDLI_TOKEN", "") BRAVE_SEARCH_API_KEY = os.getenv("BRAVE_SEARCH_API_KEY", "") API_URL = "https://api.friendli.ai/dedicated/v1/chat/completions" MODEL_ID = "dep89a2fld32mcm" -DB_PATH = "novel_sessions_v7.db" +DB_PATH = "novel_sessions_v6.db" -# 목표 분량 설정 (기존 값으로 복원) +# 목표 분량 설정 TARGET_WORDS = 8000 # 안전 마진을 위해 8000단어 -MIN_WORDS_PER_WRITER = 800 # 각 작가 최소 분량 800단어 +MIN_WORDS_PER_PART = 800 # 각 파트 최소 분량 # --- 환경 변수 검증 --- if not FRIENDLI_TOKEN: @@ -72,499 +69,142 @@ NARRATIVE_PHASES = [ "결말 2: 열린 질문" ] -# 편집자 단계 사용 여부 (False로 설정하면 편집자 단계 건너뛰기) -USE_EDITOR_STAGE = False # 편집자가 과도하게 삭제하는 문제로 비활성화 - -# 단계별 구성 - 편집자 단계 조건부 포함 -PROGRESSIVE_STAGES = [ +# 새로운 단계별 구성 - 단일 작가 시스템 +UNIFIED_STAGES = [ ("director", "🎬 감독자: 통합된 서사 구조 기획"), - ("critic", "📝 비평가: 서사 진행성과 깊이 검토"), - ("director", "🎬 감독자: 수정된 마스터플랜"), -] + [ - (f"writer{i}", f"✍️ 작가 {i}: 초안 - {NARRATIVE_PHASES[i-1]}") - for i in range(1, 11) + ("critic_director", "📝 비평가: 서사 구조 심층 검토"), + ("director", "🎬 감독자: 최종 마스터플랜"), ] + [ - ("critic", "📝 비평가: 중간 검토 (서사 누적성과 변화)"), + item for i in range(1, 11) + for item in [ + ("writer", f"✍️ 작가: 파트 {i} - {NARRATIVE_PHASES[i-1]}"), + (f"critic_part{i}", f"📝 파트 {i} 비평가: 즉시 검토 및 수정 요청"), + ("writer", f"✍️ 작가: 파트 {i} 수정본") + ] ] + [ - (f"writer{i}", f"✍️ 작가 {i}: 수정본 - {NARRATIVE_PHASES[i-1]}") - for i in range(1, 11) + ("critic_final", "📝 최종 비평가: 종합 평가 및 문학적 성취도"), ] -# 편집자 단계 조건부 추가 -if USE_EDITOR_STAGE: - PROGRESSIVE_STAGES.extend([ - ("editor", "✂️ 편집자: 반복 제거 및 서사 재구성"), - ("critic", f"📝 비평가: 최종 검토 및 문학적 평가"), - ]) -else: - PROGRESSIVE_STAGES.append( - ("critic", f"📝 비평가: 최종 검토 및 문학적 평가"), - ) - -# 절대 금지사항 - 언어 오류 방지 (간소화) -STRICT_RULES = """ -금지사항: -1. 일본어, 중국어 등 외국어 문자 사용 금지 -2. 이전 단계 사건의 단순 반복 금지 -3. 캐릭터 이름 변경 금지 -""" - # --- 데이터 클래스 --- @dataclass -class CharacterArc: - """인물의 변화 궤적 추적""" - name: str - initial_state: Dict[str, Any] # 초기 상태 - phase_states: Dict[int, Dict[str, Any]] = field(default_factory=dict) # 단계별 상태 - transformations: List[str] = field(default_factory=list) # 주요 변화들 - relationships_evolution: Dict[str, List[str]] = field(default_factory=dict) # 관계 변화 - -@dataclass -class PlotThread: - """플롯 라인 추적""" - thread_id: str - description: str - introduction_phase: int - development_phases: List[int] - resolution_phase: Optional[int] - status: str = "active" # active, resolved, suspended +class StoryBible: + """전체 이야기의 일관성을 유지하는 스토리 바이블""" + characters: Dict[str, Dict[str, Any]] = field(default_factory=dict) + settings: Dict[str, str] = field(default_factory=dict) + timeline: List[Dict[str, Any]] = field(default_factory=list) + plot_points: List[Dict[str, Any]] = field(default_factory=list) + themes: List[str] = field(default_factory=list) + symbols: Dict[str, List[str]] = field(default_factory=dict) + style_guide: Dict[str, str] = field(default_factory=dict) + opening_sentence: str = "" # 첫문장 추가 @dataclass -class SymbolicEvolution: - """상징의 의미 변화 추적""" - symbol: str - initial_meaning: str - phase_meanings: Dict[int, str] = field(default_factory=dict) - transformation_complete: bool = False - -@dataclass -class CharacterConsistency: - """캐릭터 일관성 관리""" - primary_names: Dict[str, str] = field(default_factory=dict) # role -> canonical name - aliases: Dict[str, List[str]] = field(default_factory=dict) # canonical -> aliases - name_history: List[Tuple[int, str, str]] = field(default_factory=list) # (phase, role, used_name) - - def validate_name(self, phase: int, role: str, name: str) -> bool: - """이름 일관성 검증""" - if role in self.primary_names: - canonical = self.primary_names[role] - if name != canonical and name not in self.aliases.get(canonical, []): - return False - return True - - def register_name(self, phase: int, role: str, name: str): - """이름 등록""" - if role not in self.primary_names: - self.primary_names[role] = name - self.name_history.append((phase, role, name)) - +class PartCritique: + """각 파트별 비평 내용""" + part_number: int + continuity_issues: List[str] = field(default_factory=list) + character_consistency: List[str] = field(default_factory=list) + plot_progression: List[str] = field(default_factory=list) + thematic_alignment: List[str] = field(default_factory=list) + technical_issues: List[str] = field(default_factory=list) + strengths: List[str] = field(default_factory=list) + required_changes: List[str] = field(default_factory=list) + literary_quality: List[str] = field(default_factory=list) # 문학성 평가 추가 # --- 핵심 로직 클래스 --- -class LanguageFilter: - """언어 혼입 및 특수기호 오류 방지""" +class UnifiedNarrativeTracker: + """단일 작가 시스템을 위한 통합 서사 추적기""" def __init__(self): - self.forbidden_patterns = [ - r'[ぁ-ん]+', # 히라가나 - r'[ァ-ヶ]+', # 가타카나 - r'[\u4e00-\u9fff]+', # 한자 - r'\$\s*[.,]', # 특수기호 오류 - r'[는은이가을를에의와과도로부터까지만든한다]\s*\$' # 조사 뒤 특수기호 - ] + self.story_bible = StoryBible() + self.part_critiques: Dict[int, PartCritique] = {} + self.accumulated_content: List[str] = [] + self.word_count_by_part: Dict[int, int] = {} + self.revision_history: Dict[int, List[str]] = defaultdict(list) + self.causal_chains: List[Dict[str, Any]] = [] + self.narrative_momentum: float = 0.0 - def clean_text(self, text: str) -> str: - """생성된 텍스트 정제""" - import re - cleaned = text - - # 일본어 문자 제거 - for pattern in self.forbidden_patterns: - cleaned = re.sub(pattern, '', cleaned) + def update_story_bible(self, element_type: str, key: str, value: Any): + """스토리 바이블 업데이트""" + if element_type == "character": + self.story_bible.characters[key] = value + elif element_type == "setting": + self.story_bible.settings[key] = value + elif element_type == "timeline": + self.story_bible.timeline.append({"event": key, "details": value}) + elif element_type == "theme": + if key not in self.story_bible.themes: + self.story_bible.themes.append(key) + elif element_type == "symbol": + if key not in self.story_bible.symbols: + self.story_bible.symbols[key] = [] + self.story_bible.symbols[key].append(value) - # 연속 공백 정리 - cleaned = re.sub(r'\s+', ' ', cleaned) - - # 문장 끝 정리 - cleaned = re.sub(r'([.!?])\s*\$', r'\1', cleaned) - - return cleaned.strip() - - -class ContentDeduplicator: - """중복 콘텐츠 감지 및 제거""" - def __init__(self): - self.seen_paragraphs = set() - self.seen_key_phrases = set() - self.similarity_threshold = 0.85 - - def check_similarity(self, text1: str, text2: str) -> float: - """두 텍스트의 유사도 측정""" - # 간단한 Jaccard 유사도 구현 - words1 = set(text1.lower().split()) - words2 = set(text2.lower().split()) - - intersection = words1.intersection(words2) - union = words1.union(words2) - - return len(intersection) / len(union) if union else 0 - - def extract_key_phrases(self, text: str) -> List[str]: - """핵심 문구 추출""" - # 20자 이상의 문장들을 핵심 문구로 간주 - sentences = [s.strip() for s in re.split(r'[.!?]', text) if len(s.strip()) > 20] - return sentences[:5] # 상위 5개만 - - def is_duplicate(self, paragraph: str) -> bool: - """중복 문단 감지""" - # 핵심 문구 체크 - key_phrases = self.extract_key_phrases(paragraph) - for phrase in key_phrases: - if phrase in self.seen_key_phrases: - return True - - # 전체 문단 유사도 체크 - for seen in self.seen_paragraphs: - if self.check_similarity(paragraph, seen) > self.similarity_threshold: - return True - - # 중복이 아니면 저장 - self.seen_paragraphs.add(paragraph) - self.seen_key_phrases.update(key_phrases) - return False - - def get_used_elements(self) -> List[str]: - """사용된 핵심 요소 반환""" - return list(self.seen_key_phrases)[:10] # 최근 10개 - - def count_repetitions(self, content: str) -> int: - """텍스트 내의 반복 횟수 계산""" - paragraphs = content.split('\n\n') - repetitions = 0 - - for i, para1 in enumerate(paragraphs): - for para2 in paragraphs[i+1:]: - if self.check_similarity(para1, para2) > 0.7: - repetitions += 1 - - return repetitions - - -class RealTimeConsistencyChecker: - """실시간 서사 일관성 검증""" - def __init__(self): - self.plot_graph = {} # 인과관계 그래프 - self.character_states = {} # 캐릭터 상태 추적 - self.event_timeline = [] # 시간순 이벤트 - self.resolved_conflicts = set() # 해결된 갈등 + def add_part_critique(self, part_number: int, critique: PartCritique): + """파트별 비평 추가""" + self.part_critiques[part_number] = critique - def validate_new_content(self, phase: int, content: str, - previous_contents: List[str]) -> Tuple[bool, List[str]]: - """새 콘텐츠의 일관성 검증""" + def check_continuity(self, current_part: int, new_content: str) -> List[str]: + """연속성 검사""" issues = [] - # 1. 시간 역행 검사 - time_markers = self.extract_time_markers(content) - if self.check_time_contradiction(time_markers): - issues.append("시간 순서 모순 발견") - - # 2. 캐릭터 상태 모순 검사 - character_actions = self.extract_character_actions(content) - for char, action in character_actions.items(): - if not self.is_action_possible(char, action, phase): - issues.append(f"{char}의 행동이 이전 상태와 모순됨") - - # 3. 이미 해결된 갈등의 재등장 검사 - conflicts = self.extract_conflicts(content) - for conflict in conflicts: - if conflict in self.resolved_conflicts: - issues.append(f"이미 해결된 갈등 '{conflict}'이 다시 등장") - - return len(issues) == 0, issues - - def extract_time_markers(self, content: str) -> List[str]: - """시간 표지 추출""" - markers = re.findall(r'(아침|점심|저녁|밤|새벽|오전|오후|다음날|며칠 후|일주일 후)', content) - return markers - - def extract_character_actions(self, content: str) -> Dict[str, str]: - """캐릭터 행동 추출""" - actions = {} - # 간단한 패턴 매칭 (실제로는 더 정교한 NLP 필요) - patterns = re.findall(r'(\w+)(?:이|가|은|는)\s+(\w+(?:했다|한다|하다))', content) - for char, action in patterns: - actions[char] = action - return actions - - def extract_conflicts(self, content: str) -> List[str]: - """갈등 요소 추출""" - conflict_keywords = ['갈등', '대립', '충돌', '문제', '위기'] - conflicts = [] - for keyword in conflict_keywords: - if keyword in content: - # 주변 문맥 추출 - idx = content.index(keyword) - context = content[max(0, idx-20):min(len(content), idx+20)] - conflicts.append(context) - return conflicts - - def check_time_contradiction(self, markers: List[str]) -> bool: - """시간 모순 검사""" - # 간단한 시간 순서 체크 - time_order = ['새벽', '아침', '오전', '점심', '오후', '저녁', '밤'] - prev_idx = -1 - for marker in markers: - if marker in time_order: - curr_idx = time_order.index(marker) - if curr_idx < prev_idx: - return True - prev_idx = curr_idx - return False - - def is_action_possible(self, character: str, action: str, phase: int) -> bool: - """캐릭터 행동 가능성 검사""" - # 캐릭터 상태 기반 검증 - if character not in self.character_states: - self.character_states[character] = {"phase": phase, "status": "active"} - return True + # 캐릭터 일관성 체크 + for char_name, char_data in self.story_bible.characters.items(): + if char_name in new_content: + # 캐릭터 특성이 유지되는지 확인 + if "traits" in char_data: + for trait in char_data["traits"]: + if trait.get("abandoned", False): + issues.append(f"{char_name}의 버려진 특성 '{trait['name']}'이 다시 나타남") + + # 시간선 일관성 체크 + if len(self.story_bible.timeline) > 0: + last_event = self.story_bible.timeline[-1] + # 시간 역행 체크 등 - char_state = self.character_states[character] - # 예: 죽은 캐릭터가 다시 등장하는 경우 등 - if char_state.get("status") == "dead" and "했다" in action: - return False - - return True - - -class ProgressionMonitor: - """실시간 서사 진행 모니터링""" - def __init__(self): - self.phase_keywords = {} - self.locations = set() - self.characters = set() - - def count_new_characters(self, content: str, phase: int) -> int: - """새로운 인물 등장 횟수""" - # 간단한 고유명사 추출 (대문자로 시작하는 단어) - potential_names = re.findall(r'\b[A-Z가-힣][a-z가-힣]+\b', content) - new_chars = set(potential_names) - self.characters - self.characters.update(new_chars) - return len(new_chars) - - def count_new_locations(self, content: str, phase: int) -> int: - """새로운 장소 등장 횟수""" - # 장소 관련 키워드 - location_markers = ['에서', '으로', '에', '의', 'at', 'in', 'to'] - new_locs = 0 - - for marker in location_markers: - matches = re.findall(rf'(\S+)\s*{marker}', content) - for match in matches: - if match not in self.locations and len(match) > 2: - self.locations.add(match) - new_locs += 1 - - return new_locs - - def calculate_content_difference(self, current_phase: int, content: str, previous_content: str) -> float: - """이전 단계와의 내용 차이 비율""" - if not previous_content: - return 1.0 + # 인과관계 체크 + if current_part > 1 and not any(kw in new_content for kw in + ['때문에', '그래서', '결과', '이로 인해', 'because', 'therefore']): + issues.append("이전 파트와의 인과관계가 불명확함") - 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 + return issues - def calculate_progression_score(self, current_phase: int, content: str, previous_content: str = "") -> Dict[str, float]: - """진행도 점수 계산""" - - scores = { - "new_elements": 0.0, # 새로운 요소 - "character_growth": 0.0, # 인물 성장 - "plot_advancement": 0.0, # 플롯 진전 - "no_repetition": 0.0 # 반복 없음 - } - - # 새로운 요소 체크 - new_characters = self.count_new_characters(content, current_phase) - new_locations = self.count_new_locations(content, current_phase) - scores["new_elements"] = min(10, (new_characters * 3 + new_locations * 2)) - - # 성장 관련 키워드 - growth_keywords = ["깨달았다", "이제는", "달라졌다", "새롭게", "비로소", "변했다", "더 이상"] - growth_count = sum(1 for k in growth_keywords if k in content) - scores["character_growth"] = min(10, growth_count * 2) - - # 플롯 진전 (이전 단계와의 차이) - if current_phase > 1 and previous_content: - diff_ratio = self.calculate_content_difference(current_phase, content, previous_content) - scores["plot_advancement"] = min(10, diff_ratio * 10) - else: - scores["plot_advancement"] = 8.0 # 첫 단계는 기본 점수 - - # 반복 체크 (역산) - repetition_count = self.count_repetitions(content) - scores["no_repetition"] = max(0, 10 - repetition_count * 2) - - return scores - - -class ProgressiveNarrativeTracker: - """서사 진행과 누적을 추적하는 시스템""" - def __init__(self): - self.character_arcs: Dict[str, CharacterArc] = {} - self.plot_threads: Dict[str, PlotThread] = {} - self.symbolic_evolutions: Dict[str, SymbolicEvolution] = {} - self.phase_summaries: Dict[int, str] = {} - self.accumulated_events: List[Dict[str, Any]] = [] - self.thematic_deepening: List[str] = [] - self.philosophical_insights: List[str] = [] # 철학적 통찰 추적 - self.literary_devices: Dict[int, List[str]] = {} # 문학적 기법 사용 추적 - self.character_consistency = CharacterConsistency() # 캐릭터 일관성 추가 - self.content_deduplicator = ContentDeduplicator() # 중복 감지기 추가 - self.progression_monitor = ProgressionMonitor() # 진행도 모니터 추가 - self.used_expressions: Set[str] = set() # 사용된 표현 추적 - self.consistency_checker = RealTimeConsistencyChecker() # 실시간 일관성 체커 추가 - - def register_character_arc(self, name: str, initial_state: Dict[str, Any]): - """캐릭터 아크 등록""" - self.character_arcs[name] = CharacterArc(name=name, initial_state=initial_state) - self.character_consistency.register_name(0, "protagonist", name) - logger.info(f"Character arc registered: {name}") + def calculate_narrative_momentum(self, part_number: int, content: str) -> float: + """서사적 추진력 계산""" + momentum = 5.0 - 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}") + # 새로운 요소 도입 + new_elements = len(set(content.split()) - set(' '.join(self.accumulated_content).split())) + if new_elements > 100: + momentum += 2.0 - 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) + # 갈등의 고조 + tension_words = ['위기', '갈등', '충돌', '대립', 'crisis', 'conflict'] + if any(word in content for word in tension_words): + momentum += 1.5 - def check_narrative_progression(self, current_phase: int) -> Tuple[bool, List[str]]: - """서사가 실제로 진행되고 있는지 확인""" - issues = [] + # 인과관계 명확성 + causal_words = ['때문에', '그래서', '따라서', 'because', 'therefore'] + causal_count = sum(1 for word in causal_words if word in content) + momentum += min(causal_count * 0.5, 2.0) - # 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 part_number > 1: + prev_content = self.accumulated_content[-1] if self.accumulated_content else "" + overlap = len(set(content.split()) & set(prev_content.split())) + if overlap > len(content.split()) * 0.3: + momentum -= 3.0 - if static_symbols: - issues.append(f"의미가 발전하지 않은 상징: {', '.join(static_symbols)}") - - # 4. 철학적 깊이 확인 - if len(self.philosophical_insights) < current_phase // 2: - issues.append("철학적 성찰과 인간에 대한 통찰이 부족합니다") - - # 5. 문학적 기법 다양성 - unique_devices = set() - for devices in self.literary_devices.values(): - unique_devices.update(devices) - if len(unique_devices) < 5: - issues.append("문학적 기법이 단조롭습니다. 더 다양한 표현 기법이 필요합니다") - - # 6. 캐릭터 이름 일관성 - name_issues = [] - for phase, role, name in self.character_consistency.name_history: - if not self.character_consistency.validate_name(phase, role, name): - name_issues.append(f"Phase {phase}: {role} 이름 불일치 ({name})") - if name_issues: - issues.extend(name_issues) - - # 7. 인과관계 체크 (추가) - if current_phase > 3: - # 사건의 연쇄성 확인 - if len(self.accumulated_events) < current_phase - 1: - issues.append("사건 간 인과관계가 불명확합니다. 각 사건이 다음 사건의 원인이 되어야 합니다") - - return len(issues) == 0, issues - - def generate_phase_requirements(self, phase: int) -> str: - """각 단계별 필수 요구사항 생성 (간소화)""" - requirements = [] - - # 이전 단계 요약 - if phase > 1 and (phase-1) in self.phase_summaries: - requirements.append(f"이전 단계 핵심: {self.phase_summaries[phase-1][:200]}...") - - # 사용된 표현 목록 (5개만) - if self.used_expressions: - requirements.append("\n❌ 다음 표현은 이미 사용됨:") - for expr in list(self.used_expressions)[-5:]: # 최근 5개만 - requirements.append(f"- {expr[:50]}...") - - # 단계별 특수 요구사항 - phase_name = NARRATIVE_PHASES[phase-1] if phase <= 10 else "수정" - - requirements.append(f"\n✅ {phase_name} 필수 포함:") - requirements.append(f"- 최소 {MIN_WORDS_PER_WRITER}단어 작성") - requirements.append("- 구체적인 장면 묘사와 대화") - requirements.append("- 인물의 내면 탐구") - requirements.append("- 이전 단계의 결과로 시작") - - return "\n".join(requirements) - - def extract_used_elements(self, content: str): - """사용된 핵심 표현 추출 및 저장""" - # 20자 이상의 특징적인 문장들 추출 - sentences = re.findall(r'[^.!?]+[.!?]', content) - for sent in sentences: - if len(sent) > 20 and len(sent) < 100: - self.used_expressions.add(sent.strip()) - + return max(0.0, min(10.0, momentum)) 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, @@ -577,10 +217,13 @@ class NovelDatabase: final_novel TEXT, literary_report TEXT, total_words INTEGER DEFAULT 0, - narrative_tracker TEXT + story_bible TEXT, + narrative_tracker TEXT, + opening_sentence TEXT ) ''') + # 스테이지 테이블 cursor.execute(''' CREATE TABLE IF NOT EXISTS stages ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -591,9 +234,7 @@ class NovelDatabase: content TEXT, word_count INTEGER DEFAULT 0, status TEXT DEFAULT 'pending', - progression_score REAL DEFAULT 0.0, - repetition_score REAL DEFAULT 0.0, - consistency_check TEXT, + narrative_momentum REAL DEFAULT 0.0, created_at TEXT DEFAULT (datetime('now')), updated_at TEXT DEFAULT (datetime('now')), FOREIGN KEY (session_id) REFERENCES sessions(session_id), @@ -601,41 +242,13 @@ class NovelDatabase: ) ''') + # 비평 테이블 cursor.execute(''' - CREATE TABLE IF NOT EXISTS plot_threads ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - session_id TEXT NOT NULL, - thread_id TEXT NOT NULL, - description TEXT, - introduction_phase INTEGER, - status TEXT DEFAULT 'active', - created_at TEXT DEFAULT (datetime('now')), - FOREIGN KEY (session_id) REFERENCES sessions(session_id) - ) - ''') - - # 새로운 테이블: 중복 감지 기록 - cursor.execute(''' - CREATE TABLE IF NOT EXISTS duplicate_detection ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - session_id TEXT NOT NULL, - phase INTEGER NOT NULL, - duplicate_content TEXT, - original_phase INTEGER, - similarity_score REAL, - created_at TEXT DEFAULT (datetime('now')), - FOREIGN KEY (session_id) REFERENCES sessions(session_id) - ) - ''') - - # 새로운 테이블: 품질 평가 기록 - cursor.execute(''' - CREATE TABLE IF NOT EXISTS quality_evaluations ( + CREATE TABLE IF NOT EXISTS critiques ( id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, - evaluation_type TEXT NOT NULL, - score REAL, - details TEXT, + part_number INTEGER NOT NULL, + critique_data TEXT, created_at TEXT DEFAULT (datetime('now')), FOREIGN KEY (session_id) REFERENCES sessions(session_id) ) @@ -643,7 +256,6 @@ class NovelDatabase: conn.commit() - # 기존 메서드들 유지 @staticmethod @contextmanager def get_db(): @@ -669,18 +281,20 @@ class NovelDatabase: @staticmethod def save_stage(session_id: str, stage_number: int, stage_name: str, role: str, content: str, status: str = 'complete', - progression_score: float = 0.0, repetition_score: float = 0.0, - consistency_check: str = ""): + narrative_momentum: 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, consistency_check) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO stages (session_id, stage_number, stage_name, role, content, + word_count, status, narrative_momentum) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(session_id, stage_number) - DO UPDATE SET content=?, word_count=?, status=?, stage_name=?, progression_score=?, repetition_score=?, consistency_check=?, updated_at=datetime('now') - ''', (session_id, stage_number, stage_name, role, content, word_count, status, progression_score, repetition_score, consistency_check, - content, word_count, status, stage_name, progression_score, repetition_score, consistency_check)) + DO UPDATE SET content=?, word_count=?, status=?, stage_name=?, + narrative_momentum=?, updated_at=datetime('now') + ''', (session_id, stage_number, stage_name, role, content, word_count, + status, narrative_momentum, content, word_count, status, stage_name, + narrative_momentum)) # 총 단어 수 업데이트 cursor.execute(''' @@ -688,7 +302,7 @@ class NovelDatabase: SET total_words = ( SELECT SUM(word_count) FROM stages - WHERE session_id = ? AND role LIKE 'writer%' AND content IS NOT NULL + WHERE session_id = ? AND role = 'writer' AND content IS NOT NULL ), updated_at = datetime('now'), current_stage = ? @@ -698,56 +312,59 @@ class NovelDatabase: conn.commit() @staticmethod - def get_writer_content(session_id: str) -> str: - """작가 콘텐츠 가져오기 (수정본 우선)""" + def save_critique(session_id: str, part_number: int, critique: PartCritique): + """비평 저장""" 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) + critique_json = json.dumps(asdict(critique)) + conn.cursor().execute( + 'INSERT INTO critiques (session_id, part_number, critique_data) VALUES (?, ?, ?)', + (session_id, part_number, critique_json) + ) + conn.commit() @staticmethod - def get_total_words(session_id: str) -> int: - """총 단어 수 가져오기""" + def save_opening_sentence(session_id: str, opening_sentence: str): + """첫문장 저장""" 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 + conn.cursor().execute( + 'UPDATE sessions SET opening_sentence = ? WHERE session_id = ?', + (opening_sentence, session_id) + ) + conn.commit() @staticmethod - def save_narrative_tracker(session_id: str, tracker: ProgressiveNarrativeTracker): - """서사 추적기 저장""" + def get_writer_content(session_id: str) -> str: + """작가 콘텐츠 가져오기 - 모든 수정본 통합""" + with NovelDatabase.get_db() as conn: + rows = conn.cursor().execute(''' + SELECT content FROM stages + WHERE session_id = ? AND role = 'writer' + AND stage_name LIKE '%수정본%' + ORDER BY stage_number + ''', (session_id,)).fetchall() + + if rows: + return '\n\n'.join(row['content'] for row in rows if row['content']) + else: + # 수정본이 없으면 초안들을 사용 + rows = conn.cursor().execute(''' + SELECT content FROM stages + WHERE session_id = ? AND role = 'writer' + AND stage_name NOT LIKE '%수정본%' + ORDER BY stage_number + ''', (session_id,)).fetchall() + return '\n\n'.join(row['content'] for row in rows if row['content']) + + @staticmethod + def save_narrative_tracker(session_id: str, tracker: UnifiedNarrativeTracker): + """통합 서사 추적기 저장""" 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) + 'story_bible': asdict(tracker.story_bible), + 'part_critiques': {k: asdict(v) for k, v in tracker.part_critiques.items()}, + 'word_count_by_part': tracker.word_count_by_part, + 'causal_chains': tracker.causal_chains, + 'narrative_momentum': tracker.narrative_momentum }) conn.cursor().execute( 'UPDATE sessions SET narrative_tracker = ? WHERE session_id = ?', @@ -756,8 +373,8 @@ class NovelDatabase: conn.commit() @staticmethod - def load_narrative_tracker(session_id: str) -> Optional[ProgressiveNarrativeTracker]: - """서사 추적기 로드""" + def load_narrative_tracker(session_id: str) -> Optional[UnifiedNarrativeTracker]: + """통합 서사 추적기 로드""" with NovelDatabase.get_db() as conn: row = conn.cursor().execute( 'SELECT narrative_tracker FROM sessions WHERE session_id = ?', @@ -766,69 +383,46 @@ class NovelDatabase: if row and row['narrative_tracker']: data = json.loads(row['narrative_tracker']) - tracker = ProgressiveNarrativeTracker() + tracker = UnifiedNarrativeTracker() - # 데이터 복원 - 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', {}) + # 스토리 바이블 복원 + bible_data = data.get('story_bible', {}) + tracker.story_bible = StoryBible(**bible_data) - # 캐릭터 일관성 복원 - if 'character_consistency' in data: - tracker.character_consistency = CharacterConsistency(**data['character_consistency']) - - # 사용된 표현 복원 - if 'used_expressions' in data: - tracker.used_expressions = set(data['used_expressions']) + # 비평 복원 + for part_num, critique_data in data.get('part_critiques', {}).items(): + tracker.part_critiques[int(part_num)] = PartCritique(**critique_data) + + tracker.word_count_by_part = data.get('word_count_by_part', {}) + tracker.causal_chains = data.get('causal_chains', []) + tracker.narrative_momentum = data.get('narrative_momentum', 0.0) return tracker return None - @staticmethod - def save_duplicate_detection(session_id: str, phase: int, duplicate_content: str, - original_phase: int, similarity_score: float): - """중복 감지 기록 저장""" - with NovelDatabase.get_db() as conn: - conn.cursor().execute(''' - INSERT INTO duplicate_detection - (session_id, phase, duplicate_content, original_phase, similarity_score) - VALUES (?, ?, ?, ?, ?) - ''', (session_id, phase, duplicate_content, original_phase, similarity_score)) - conn.commit() - - @staticmethod - def save_quality_evaluation(session_id: str, evaluation_type: str, score: float, details: str): - """품질 평가 저장""" - with NovelDatabase.get_db() as conn: - conn.cursor().execute(''' - INSERT INTO quality_evaluations - (session_id, evaluation_type, score, details) - VALUES (?, ?, ?, ?) - ''', (session_id, evaluation_type, score, details)) - conn.commit() - + # 기존 메서드들 유지 @staticmethod def get_session(session_id: str) -> Optional[Dict]: with NovelDatabase.get_db() as conn: - row = conn.cursor().execute('SELECT * FROM sessions WHERE session_id = ?', (session_id,)).fetchone() + 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() + 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 = ?", + '''UPDATE sessions SET final_novel = ?, status = 'complete', + updated_at = datetime('now'), literary_report = ? WHERE session_id = ?''', (final_novel, literary_report, session_id) ) conn.commit() @@ -837,15 +431,20 @@ class NovelDatabase: def get_active_sessions() -> List[Dict]: with NovelDatabase.get_db() as conn: rows = conn.cursor().execute( - """SELECT session_id, user_query, language, created_at, current_stage, - COALESCE(total_words, 0) as total_words - FROM sessions - WHERE status = 'active' - ORDER BY updated_at DESC - LIMIT 10""" + '''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_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 class WebSearchIntegration: """웹 검색 기능""" @@ -894,1371 +493,1072 @@ class WebSearchIntegration: return "\n".join(extracted) -class ProgressiveLiterarySystem: - """진행형 문학 소설 생성 시스템""" +class UnifiedLiterarySystem: + """단일 작가 진행형 문학 소설 생성 시스템""" def __init__(self): self.token = FRIENDLI_TOKEN self.api_url = API_URL self.model_id = MODEL_ID - self.narrative_tracker = ProgressiveNarrativeTracker() + self.narrative_tracker = UnifiedNarrativeTracker() self.web_search = WebSearchIntegration() - self.language_filter = LanguageFilter() # 언어 필터 추가 self.current_session_id = None NovelDatabase.init_db() - def augment_prompt_if_needed(self, user_query: str, language: str) -> str: - """프롬프트 증강 - 특별한 지시가 없을 때 예시 형식으로 증강""" - # 특별한 지시 키워드 체크 - special_keywords = ['인과관계', '캐릭터 일관성', '설정 일관성', '문체 일관성', - 'causality', 'character consistency', 'setting consistency', 'style consistency'] - - has_special_instruction = any(keyword in user_query.lower() for keyword in special_keywords) - - if has_special_instruction: - return user_query - - # 특별한 지시가 없으면 예시 형식으로 증강 - example_augmentation = { - "Korean": f"""인과관계 치밀함: {user_query}. 모든 사건은 필연적 인과관계로 연결되며, 외부 충격이 내적 갈등을 심화시키고, 각 선택이 다음 국면을 결정한다. 우연은 없고 모든 장면이 결말을 향한 필수 요소가 된다. -캐릭터 일관성: 주인공은 처음부터 끝까지 핵심 가치관을 유지하되, 사건을 통해 그 가치관의 새로운 면을 발견한다. 각 인물은 고유한 말투와 행동 패턴을 가지며, 갈등 상황에서도 캐릭터의 본질에서 벗어나지 않는다. -설정 일관성: 핵심 상징물은 이야기 전체를 관통하며 점진적으로 의미가 확장된다. 공간과 시간 설정은 단순한 배경이 아니라 서사의 필수 요소로 기능한다. -문체 일관성: 선택한 서술 시점과 문체 톤을 끝까지 유지하되, 서사의 흐름에 따라 리듬과 호흡을 조절한다. 장르적 특성을 살린 문체로 독자를 몰입시킨다.""", - - "English": f"""Causal Tightness: {user_query}. All events connect through necessary causality, external shocks deepen internal conflicts, each choice determines the next phase. No coincidences - every scene is essential to the conclusion. -Character Consistency: Protagonist maintains core values throughout while discovering new facets through events. Each character has unique speech patterns and behaviors, staying true to essence even in conflict. -Setting Consistency: Core symbols pervade the entire story with gradually expanding meanings. Space and time settings function as narrative necessities, not mere backdrops. -Style Consistency: Maintain chosen POV and tone throughout while adjusting rhythm and pacing to narrative flow. Genre-appropriate style immerses readers.""" - } - - return example_augmentation.get(language, example_augmentation["Korean"]) - def create_headers(self): return {"Authorization": f"Bearer {self.token}", "Content-Type": "application/json"} # --- 프롬프트 생성 함수들 --- + def augment_query(self, user_query: str, language: str) -> str: + """프롬프트 증강""" + if len(user_query.split()) < 15: + augmented_template = { + "Korean": f"""'{user_query}' + +**서사 구조 핵심:** +- 10개 파트가 하나의 통합된 이야기를 구성 +- 각 파트는 이전 파트의 필연적 결과 +- 인물의 명확한 변화 궤적 (A → B → C) +- 중심 갈등의 점진적 고조와 해결 +- 강렬한 중심 상징의 의미 변화""", + + "English": f"""'{user_query}' + +**Narrative Structure Core:** +- 10 parts forming one integrated story +- Each part as inevitable result of previous +- Clear character transformation arc (A → B → C) +- Progressive escalation and resolution of central conflict +- Evolving meaning of powerful central symbol""" + } + return augmented_template.get(language, user_query) + return user_query + + def generate_powerful_opening(self, user_query: str, language: str) -> str: + """주제에 맞는 강렬한 첫문장 생성""" + + opening_prompt = { + "Korean": f"""주제: {user_query} + +이 주제에 대한 강렬하고 잊을 수 없는 첫문장을 생성하세요. + +**첫문장 작성 원칙:** +1. 즉각적인 긴장감이나 궁금증 유발 +2. 평범하지 않은 시각이나 상황 제시 +3. 감각적이고 구체적인 이미지 +4. 철학적 질문이나 역설적 진술 +5. 시간과 공간의 독특한 설정 + +**훌륭한 첫문장의 예시 패턴:** +- "그가 죽은 날, ..." (충격적 사건) +- "모든 것이 끝났다고 생각한 순간..." (반전 예고) +- "세상에서 가장 [형용사]한 [명사]는..." (독특한 정의) +- "[구체적 행동]하는 것만으로도..." (일상의 재해석) + +단 하나의 문장만 제시하세요.""", + + "English": f"""Theme: {user_query} + +Generate an unforgettable opening sentence for this theme. + +**Opening Sentence Principles:** +1. Immediate tension or curiosity +2. Unusual perspective or situation +3. Sensory and specific imagery +4. Philosophical question or paradox +5. Unique temporal/spatial setting + +**Great Opening Patterns:** +- "The day he died, ..." (shocking event) +- "At the moment everything seemed over..." (reversal hint) +- "The most [adjective] [noun] in the world..." (unique definition) +- "Just by [specific action]..." (reinterpretation of ordinary) + +Provide only one sentence.""" + } + + messages = [{"role": "user", "content": opening_prompt.get(language, opening_prompt["Korean"])}] + opening = self.call_llm_sync(messages, "writer", language) + return opening.strip() + def create_director_initial_prompt(self, user_query: str, language: str) -> str: - """감독자 초기 기획 - 통합된 서사 구조""" - # 프롬프트 증강 - 특별한 지시가 없을 때 예시 형식으로 증강 - augmented_query = self.augment_prompt_if_needed(user_query, language) + """감독자 초기 기획 - 강화된 버전""" + augmented_query = self.augment_query(user_query, language) + + # 첫문장 생성 + opening_sentence = self.generate_powerful_opening(user_query, language) + self.narrative_tracker.story_bible.opening_sentence = opening_sentence + if self.current_session_id: + NovelDatabase.save_opening_sentence(self.current_session_id, opening_sentence) 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} 철학적 의미", + f"인간 존재 의미 {short_query}", + f"{short_query} 문학 작품" ] - for q in queries[:2]: # 2개까지만 검색 + for q in queries[:2]: try: results = self.web_search.search(q, count=2, language=language) if results: - search_results_str += self.web_search.extract_relevant_info(results)[:500] + "\n" + search_results_str += self.web_search.extract_relevant_info(results) + "\n" except Exception as e: - logger.warning(f"검색 쿼리 실패: {q[:50]}... - {str(e)}") - continue + logger.warning(f"검색 실패: {str(e)}") lang_prompts = { - "Korean": f"""당신은 노벨문학상 수상작가 수준의 한국 문학 거장입니다. -중편 소설(8,000단어)을 위한 통합된 서사 구조를 기획하세요. + "Korean": f"""노벨문학상 수준의 철학적 깊이를 지닌 중편소설(8,000단어)을 기획하세요. **주제:** {augmented_query} -**참고 자료:** -{search_results_str[:500] if search_results_str else "N/A"} - -**필수 요구사항:** +**필수 첫문장:** {opening_sentence} -1. **인과관계가 치밀한 통합 서사** - - 10개 단계가 필연적으로 연결된 단일 서사 - - 각 단계는 이전 단계의 직접적 결과로 이어짐 - - 외부 충격과 내적 갈등의 인과관계 명확히 - - 주인공과 조연들의 이름/설정 처음부터 고정 - - 단계별 진행: - 1) 도입: 일상과 균열 - 2) 발전 1: 불안의 고조 - 3) 발전 2: 외부 충격 - 4) 발전 3: 내적 갈등 심화 - 5) 절정 1: 위기의 정점 - 6) 절정 2: 선택의 순간 - 7) 하강 1: 결과와 여파 - 8) 하강 2: 새로운 인식 - 9) 결말 1: 변화된 일상 - 10) 결말 2: 열린 질문 - -2. **캐릭터 일관성과 입체성** - - 각 인물의 핵심 가치관/성격 명시 - - 주인공: 초기 → 중간 → 최종 상태의 변화 궤적 - - 조연들: 고유한 말투와 행동 패턴 설정 - - 갈등이 인물의 본질을 드러내도록 설계 - -3. **설정과 상징의 일관된 활용** - - 핵심 상징물(1-2개)의 의미 변화 추적 - - 공간/시간 설정의 서사적 기능 명시 - - 반복되는 모티프의 점진적 의미 확장 - -4. **문체 일관성 계획** - - 서술 시점과 문체 톤 결정 - - 장르적 특성을 살린 문체 전략 - - 리듬과 호흡의 변주 계획 - -5. **분량 계획** - - 총 8,000단어 - - 각 단계 800단어 - -치밀하고 유기적인 계획을 제시하세요.""", - - "English": f"""You are a Nobel Prize-winning literary master. -Plan an integrated narrative structure for a novella (8,000 words). +**참고 자료:** +{search_results_str if search_results_str else "N/A"} + +**필수 문학적 요소:** + +1. **철학적 탐구** + - 현대인의 실존적 고뇌 (소외, 정체성, 의미 상실) + - 디지털 시대의 인간 조건 + - 자본주의 사회의 모순과 개인의 선택 + - 죽음, 사랑, 자유에 대한 새��운 성찰 + +2. **사회적 메시지** + - 계급, 젠더, 세대 간 갈등 + - 환경 위기와 인간의 책임 + - 기술 발전과 인간성의 충돌 + - 현대 민주주의의 위기와 개인의 역할 + +3. **문학적 수사 장치** + - 중심 은유: [구체적 사물/현상] → [추상적 의미] + - 반복되는 모티프: [이미지/행동] (최소 5회 변주) + - 대조법: [A vs B]의 지속적 긴장 + - 상징적 공간: [구체적 장소]가 의미하는 것 + - 시간의 주관적 흐름 (회상, 예감, 정지) + +4. **통합된 10파트 구조** + 각 파트별 핵심: + - 파트 1: 첫문장으로 시작, 일상 속 균열 → 철학적 질문 제기 + - 파트 2-3: 외부 사건 → 내적 성찰 심화 + - 파트 4-5: 사회적 갈등 → 개인적 딜레마 + - 파트 6-7: 위기의 정점 → 실존적 선택 + - 파트 8-9: 선택의 결과 → 새로운 인식 + - 파트 10: 변화된 세계관 → 열린 질문 + +5. **문체 지침** + - 시적 산문체: 일상 언어와 은유의 균형 + - 의식의 흐름과 객관적 묘사의 교차 + - 짧고 강렬한 문장과 성찰적 긴 문장의 리듬 + - 감각적 디테일로 추상적 개념 구현 + +구체적이고 혁신적인 계획을 제시하세요.""", + + "English": f"""Plan a philosophically profound novella (8,000 words) worthy of Nobel Prize. **Theme:** {augmented_query} -**Reference:** -{search_results_str[:500] if search_results_str else "N/A"} - -**Requirements:** - -1. **Causally Tight Integrated Structure** - - 10 phases connected by necessity - - Each phase as direct result of previous - - Clear external conflicts and internal struggles - - Fixed protagonist/supporting cast names - -2. **Character Consistency & Depth** - - Core values/personality for each character - - Protagonist's transformation trajectory - - Unique speech patterns and behaviors - - Conflicts reveal character essence - -3. **Consistent Settings & Symbols** - - 1-2 core symbols with evolving meanings - - Narrative function of space/time settings - - Progressive expansion of recurring motifs +**Required Opening:** {opening_sentence} -4. **Stylistic Consistency Plan** - - Narrative POV and tone decisions - - Genre-appropriate style strategy - - Rhythm and pacing variations - -5. **Length Plan** - - Total 8,000 words - - 800 words per phase - -Present precise, organic plan.""" +**Reference:** +{search_results_str if search_results_str else "N/A"} + +**Essential Literary Elements:** + +1. **Philosophical Exploration** + - Modern existential anguish (alienation, identity, loss of meaning) + - Human condition in digital age + - Capitalist contradictions and individual choice + - New reflections on death, love, freedom + +2. **Social Message** + - Class, gender, generational conflicts + - Environmental crisis and human responsibility + - Technology vs humanity collision + - Modern democracy crisis and individual role + +3. **Literary Devices** + - Central metaphor: [concrete object/phenomenon] → [abstract meaning] + - Recurring motif: [image/action] (minimum 5 variations) + - Contrast: sustained tension of [A vs B] + - Symbolic space: what [specific place] means + - Subjective time flow (flashback, premonition, pause) + +4. **Integrated 10-Part Structure** + [Details as above] + +5. **Style Guidelines** + - Poetic prose: balance of everyday language and metaphor + - Stream of consciousness crossing with objective description + - Rhythm of short intense sentences and reflective long ones + - Abstract concepts through sensory details + +Provide concrete, innovative plan.""" } return lang_prompts.get(language, lang_prompts["Korean"]) - def create_writer_prompt_enhanced(self, writer_number: int, director_plan: str, - previous_content: str, phase_requirements: str, - narrative_summary: str, language: str, - used_elements: List[str]) -> str: - """강화된 작가 프롬프트 - 분량 문제 해결""" - - phase_name = NARRATIVE_PHASES[writer_number-1] - target_words = MIN_WORDS_PER_WRITER - - # 현재까지 총 단어 수 - total_words = NovelDatabase.get_total_words(self.current_session_id) if self.current_session_id else 0 - remaining_words = TARGET_WORDS - total_words - + def create_critic_director_prompt(self, director_plan: str, user_query: str, language: str) -> str: + """감독자 기획 심층 검토 - 강화된 버전""" lang_prompts = { - "Korean": f"""당신은 작가 {writer_number}번입니다. -**현재 단계: {phase_name}** - -**전체 계획 (요약):** -{director_plan[:800]} - -**이전 내용:** -{previous_content[-800:] if previous_content else "시작"} + "Korean": f"""서사 구조 전문가로서 이 기획을 심층 분석하세요. -**현재 진행 상황:** -- 현재까지 총 단어: {total_words} -- 목표까지 남은 단어: {remaining_words} -- 이번 단계 최소 단어: {target_words} - -**작성 지침:** +**원 주제:** {user_query} -1. **필수 분량: {target_words}-1000 단어** - -2. **서사의 필연성 확보:** - - 이전 단계의 직접적 결과로 시작 - - 새로운 사건은 기존 갈등의 논리적 귀결 - - 인물의 선택이 다음 국면을 결정 - - 우연이 아닌 필연으로 전개 +**감독자 기획:** +{director_plan} -3. **캐릭터 일관성 유지:** - - 설정된 인물명과 관계 엄수 - - 각 인물의 고유 말투 유지 - - 성격의 일관성 속에서 변화 표현 - - 행동이 캐릭터의 본질에서 출발 +**심층 검토 항목:** -4. **구체적 장면 구성:** - - 감각적 묘사 (시각, 청각, 촉각 등) - - 생생한 대화 (인물별 어투 차별화) - - 내면 심리의 구체적 표현 - - 공간과 시간의 명확한 설정 +1. **인과관계 검증** + 각 파트 간 연결을 검토하고 논리적 비약을 찾으세요: + - 파트 1→2: [연결성 평가] + - 파트 2→3: [연결성 평가] + (모든 연결 지점 검토) -5. **금지사항:** - - 이전 상황 단순 반복 - - 캐릭터 이름/설정 변경 - - 갑작스러운 설정 변화 - - 분량 미달 +2. **철학적 깊이 평가** + - 제시된 철학적 주제가 충분히 깊은가? + - 현대적 관련성이 있는가? + - 독창적 통찰이 있는가? -**중요: 반드시 {target_words}단어 이상, 인과관계가 명확한 서사를 작성하세요!** +3. **문학적 장치의 효과성** + - 은유와 상징이 유기적으로 작동하는가? + - 과도하거나 부족하지 않은가? + - 주제와 긴밀히 연결되는가? -이제 시작하세요:""", +4. **캐릭터 아크 실현 가능성** + - 변화가 충분히 점진적인가? + - 각 단계의 동기가 명확한가? + - 심리적 신뢰성이 있는가? - "English": f"""You are Writer #{writer_number}. -**Current Phase: {phase_name}** +5. **8,000단어 실현 가능성** + - 각 파트가 800단어를 유지할 수 있는가? + - 늘어지거나 압축되는 부분은 없는가? -**Overall Plan (Summary):** -{director_plan[:800]} +**필수 개선사항을 구체적으로 제시하세요.**""", -**Previous Content:** -{previous_content[-800:] if previous_content else "Beginning"} + "English": f"""As narrative structure expert, deeply analyze this plan. -**Progress Status:** -- Total words so far: {total_words} -- Words remaining to target: {remaining_words} -- Minimum words this phase: {target_words} +**Original Theme:** {user_query} -**Writing Guidelines:** +**Director's Plan:** +{director_plan} -1. **Required Length: {target_words}-1000 words** +**Deep Review Items:** -2. **Narrative Necessity:** - - Start from direct results of previous - - New events as logical consequences - - Character choices determine next phase - - Necessity, not coincidence +1. **Causality Verification** + Review connections between parts, find logical leaps: + - Part 1→2: [Connection assessment] + - Part 2→3: [Connection assessment] + (Review all connection points) -3. **Character Consistency:** - - Maintain established names/relationships - - Keep unique speech patterns - - Express change within consistent personality - - Actions stem from character essence +2. **Philosophical Depth Assessment** + - Is philosophical theme deep enough? + - Contemporary relevance? + - Original insights? -4. **Concrete Scene Construction:** - - Sensory descriptions (visual, auditory, tactile) - - Vivid dialogue (differentiated speech) - - Specific psychological expression - - Clear spatial/temporal settings +3. **Literary Device Effectiveness** + - Do metaphors and symbols work organically? + - Not excessive or insufficient? + - Tightly connected to theme? -5. **Forbidden:** - - Simple repetition - - Character name/setting changes - - Sudden setting shifts - - Under word count +4. **Character Arc Feasibility** + - Is change sufficiently gradual? + - Are motivations clear at each stage? + - Psychological credibility? -**IMPORTANT: Must write {target_words}+ words with clear causality!** +5. **8,000-word Feasibility** + - Can each part sustain 800 words? + - Any dragging or compressed sections? -Begin now:""" +**Provide specific required improvements.**""" } return lang_prompts.get(language, lang_prompts["Korean"]) - def create_critic_consistency_prompt_enhanced(self, all_content: str, - narrative_tracker: ProgressiveNarrativeTracker, - user_query: str, language: str) -> str: - """강화된 비평가 중간 검토""" - - # 서사 진행 체크 - phase_count = len(narrative_tracker.phase_summaries) - progression_ok, issues = narrative_tracker.check_narrative_progression(phase_count) - - # 중복 감지 - duplicates = [] - paragraphs = all_content.split('\n\n') - for i, para1 in enumerate(paragraphs[:10]): # 최근 10개만 - for j, para2 in enumerate(paragraphs[i+1:i+11]): - if narrative_tracker.content_deduplicator.check_similarity(para1, para2) > 0.7: - duplicates.append(f"문단 {i+1}과 문단 {i+j+2} 유사") + def create_writer_prompt(self, part_number: int, master_plan: str, + accumulated_content: str, story_bible: StoryBible, + language: str) -> str: + """단일 작가 프롬프트 - 강화된 버전""" + + phase_name = NARRATIVE_PHASES[part_number-1] + target_words = MIN_WORDS_PER_PART - # 인과관계 체크 (추가) - causality_issues = [] - if phase_count > 3: - # 각 단계가 이전 단계의 결과인지 확인 - if "그러나 갑자기" in all_content or "우연히" in all_content or "뜻밖에" in all_content: - causality_issues.append("우연적 사건 발생 - 필연적 전개 필요") + # 파트별 특별 지침 + philosophical_focus = { + 1: "일상의 균열을 통해 실존적 불안 도입", + 2: "개인과 사회의 첫 충돌", + 3: "타자와의 만남을 통한 자아 인식", + 4: "신념의 흔들림과 가치관의 충돌", + 5: "선택의 무게와 자유의 역설", + 6: "극한 상황에서의 인간성 시험", + 7: "행동의 결과와 책임의 무게", + 8: "타자의 시선을 통한 자기 재발견", + 9: "화해 불가능한 것과의 화해", + 10: "새로운 삶의 가능성과 미해결 질문" + } + + literary_techniques = { + 1: "객관적 상관물 도입", + 2: "대위법적 서술", + 3: "의식의 흐름", + 4: "시점의 미묘한 전환", + 5: "침묵과 생략의 미학", + 6: "시간의 주관적 변형", + 7: "복수 시점의 교차", + 8: "메타포의 전복", + 9: "원형적 이미지의 재해석", + 10: "열린 결말의 다층성" + } + + # 스토리 바이블 요약 + bible_summary = f""" +**등장인물:** {', '.join(story_bible.characters.keys())} +**핵심 상징:** {', '.join(story_bible.symbols.keys())} +**주제:** {', '.join(story_bible.themes[:3])} +**문체:** {story_bible.style_guide.get('voice', 'N/A')} +""" + + # 직전 내용 요약 (더 많은 컨텍스트 제공) + prev_content = "" + if accumulated_content: + prev_parts = accumulated_content.split('\n\n') + if len(prev_parts) >= 1: + prev_content = prev_parts[-1][-2000:] # 마지막 파트의 끝부분 2000자 lang_prompts = { - "Korean": f"""서사 진행을 검토하세요. + "Korean": f"""당신은 현대 문학의 최전선에 선 작가입니다. +**현재: 파트 {part_number} - {phase_name}** -**원 주제:** {user_query} +{"**필수 첫문장:** " + story_bible.opening_sentence if part_number == 1 and story_bible.opening_sentence else ""} -**현재 진행:** {phase_count}/10 단계 +**이번 파트의 철학적 초점:** {philosophical_focus[part_number]} +**핵심 문학 기법:** {literary_techniques[part_number]} -**발견된 문제:** -{chr(10).join(issues[:5]) if issues else "없음"} +**전체 계획:** +{master_plan} -**중복 발견:** -{chr(10).join(duplicates[:3]) if duplicates else "없음"} +**스토리 바이블:** +{bible_summary} -**인과관계 문제:** -{chr(10).join(causality_issues) if causality_issues else "없음"} +**직전 내용:** +{prev_content if prev_content else "첫 파트입니다"} -**검토 항목:** -1. 서사가 필연적으로 진행되는가? -2. 인물이 일관되게 변화하는가? -3. 반복이나 자기복제가 있는가? -4. 각 단계가 이전의 결과인가? -5. 분량이 충분한가? +**파트 {part_number} 작성 지침:** -**판정:** 통과/재작성 필요 +1. **분량:** {target_words}-900 단어 (필수) -치밀한 인과관계와 캐릭터 일관성을 중심으로 평가하세요.""", +2. **문학적 수사 요구사항:** + - 최소 3개의 독창적 은유/직유 + - 1개 이상의 상징적 이미지 심화 + - 감각적 묘사와 추상적 사유의 융합 + - 리듬감 있는 문장 구성 (장단의 변주) - "English": f"""Review narrative progression. +3. **현대적 고뇌 표현:** + - 디지털 시대의 소외감 + - 자본주의적 삶의 부조리 + - 관계의 표면성과 진정성 갈망 + - 의미 추구와 무의미의 직면 -**Theme:** {user_query} +4. **사회적 메시지 내재화:** + - 직접적 주장이 아닌 상황과 인물을 통한 암시 + - 개인의 고통과 사회 구조의 연결 + - 미시적 일상과 거시적 문제의 교차 -**Progress:** {phase_count}/10 phases +5. **서사적 추진력:** + - 이전 파트의 필연적 결과로 시작 + - 새로운 갈등 층위 추가 + - 다음 파트를 향한 긴장감 조성 -**Issues Found:** -{chr(10).join(issues[:5]) if issues else "None"} +**문학적 금기:** +- 진부한 표현이나 상투적 은유 +- 감정의 직접적 설명 +- 도덕적 판단이나 교훈 +- 인위적인 해결이나 위안 -**Duplications:** -{chr(10).join(duplicates[:3]) if duplicates else "None"} +파트 {part_number}를 깊이 있는 문학적 성취로 만드세요.""", -**Causality Issues:** -{chr(10).join(causality_issues) if causality_issues else "None"} + "English": f"""You are a writer at the forefront of contemporary literature. +**Current: Part {part_number} - {phase_name}** -**Review Items:** -1. Is narrative progressing necessarily? -2. Are characters changing consistently? -3. Any repetitions or self-copying? -4. Does each phase result from previous? -5. Sufficient length? +{"**Required Opening:** " + story_bible.opening_sentence if part_number == 1 and story_bible.opening_sentence else ""} -**Verdict:** Pass/Rewrite needed +**Philosophical Focus:** {philosophical_focus[part_number]} +**Core Literary Technique:** {literary_techniques[part_number]} -Evaluate focusing on tight causality and character consistency.""" - } - - return lang_prompts.get(language, lang_prompts["Korean"]) +**Master Plan:** +{master_plan} - - def create_writer_revision_prompt(self, writer_number: int, initial_content: str, - critic_feedback: str, language: str) -> str: - """작가 수정 프롬프트 (간소화)""" - target_words = MIN_WORDS_PER_WRITER - - lang_prompts = { - "Korean": f"""작가 {writer_number}번, 수정하세요. +**Story Bible:** +{bible_summary} + +**Previous Content:** +{prev_content if prev_content else "This is the first part"} + +**Part {part_number} Guidelines:** -**비평 요점:** -{critic_feedback[:500]} +1. **Length:** {target_words}-900 words (mandatory) -**수정 방향:** -1. 반복 제거, 새로운 전개 추가 -2. 최소 {target_words}단어 유지 -3. 인물 변화 구체화 -4. 대화와 묘사 추가 +2. **Literary Device Requirements:** + - Minimum 3 original metaphors/similes + - Deepen at least 1 symbolic image + - Fusion of sensory description and abstract thought + - Rhythmic sentence composition (variation of long/short) -전면 재작성이 필요하면 과감히 수정하세요. -수정본만 제시하세요.""", +3. **Modern Anguish Expression:** + - Digital age alienation + - Absurdity of capitalist life + - Surface relationships vs authenticity yearning + - Meaning pursuit vs confronting meaninglessness - "English": f"""Writer #{writer_number}, revise. +4. **Social Message Internalization:** + - Implication through situation and character, not direct claim + - Connection between individual pain and social structure + - Intersection of micro daily life and macro problems -**Critique Points:** -{critic_feedback[:500]} +5. **Narrative Momentum:** + - Start as inevitable result of previous part + - Add new conflict layers + - Create tension toward next part -**Revision Direction:** -1. Remove repetition, add new development -2. Maintain {target_words} words minimum -3. Specify character changes -4. Add dialogue and description +**Literary Taboos:** +- Clichéd expressions or trite metaphors +- Direct emotion explanation +- Moral judgment or preaching +- Artificial resolution or comfort -Boldly rewrite if needed. -Present only revised version.""" +Make Part {part_number} a profound literary achievement.""" } return lang_prompts.get(language, lang_prompts["Korean"]) - def create_editor_prompt(self, complete_novel: str, issues: List[str], language: str) -> str: - """편집자 프롬프트 - 보존 중심 편집""" - - current_word_count = len(complete_novel.split()) - min_words = int(current_word_count * 0.95) # 95% 이상 유지 - - lang_prompts = { - "Korean": f"""당신은 보존을 중시하는 편집자입니다. - -**원고 분량: {current_word_count}단어** -**편집 후 최소 분량: {min_words}단어 (필수!)** - -**편집 규칙:** - -1. **원고 보존이 최우선** - - 원고의 95% 이상을 반드시 유지 - - 삭제보다는 수정을 우선 - - 전체 문단 삭제는 절대 금지 - -2. **허용되는 편집:** - - 완전히 동일한 문장이 연속으로 나올 때만 하나 제거 - - 문법 오류나 오타 수정 - - 연결이 어색한 부분에 접속사 추가 (1-2단어) - -3. **절대 금지사항:** - - 문단 통째로 삭제 ❌ - - 내용 요약이나 축약 ❌ - - 작가의 문체 변경 ❌ - - 줄거리 재구성 ❌ + def create_part_critic_prompt(self, part_number: int, part_content: str, + master_plan: str, accumulated_content: str, + story_bible: StoryBible, language: str) -> str: + """파트별 즉시 비평 - 강화된 버전""" + + lang_prompts = { + "Korean": f"""파트 {part_number}의 문학적 성취도를 엄격히 평가하세요. -4. **편집 방법:** - - 중복 문장: 두 번째 것만 제거 - - 어색한 연결: 접속사로 연결 - - 오타: 최소한의 수정 +**마스터플랜 파트 {part_number} 요구사항:** +{self._extract_part_plan(master_plan, part_number)} -**중요: 거의 모든 내용을 그대로 유지하면서 아주 작은 문제만 수정하세요.** +**작성된 내용:** +{part_content} -원고 전체를 다시 작성하지 말고, 원본을 복사한 후 최소한의 수정만 가하세요. +**스토리 바이블 체크:** +- 캐릭터: {', '.join(story_bible.characters.keys())} +- 설정: {', '.join(story_bible.settings.keys())} -편집된 전체 원고를 제시하세요. ({min_words}단어 이상 필수!)""", +**평가 기준:** - "English": f"""You are a preservation-focused editor. +1. **문학적 수사 (30%)** + - 은유와 상징의 독창성 + - 언어의 시적 밀도 + - 이미지의 선명도와 깊이 + - 문장의 리듬과 음악성 + +2. **철학적 깊이 (25%)** + - 실존적 질문의 제기 + - 현대인의 조건 탐구 + - 보편성과 특수성의 균형 + - 사유의 독창성 + +3. **사회적 통찰 (20%)** + - 시대정신의 포착 + - 구조와 개인의 관계 + - 비판적 시각의 예리함 + - 대안적 상상력 + +4. **서사적 완성도 (25%)** + - 인과관계의 필연성 + - 긴장감의 유지 + - 인물의 입체성 + - 구조적 통일성 + +**구체적 지적사항:** +- 진부한 표현: [예시와 대안] +- 철학적 천착 부족: [보완 방향] +- 사회적 메시지 불명확: [강화 방안] +- 서사적 허점: [수정 필요] + +**필수 개선 요구:** +문학적 수준을 노벨상 급으로 끌어올리기 위한 구체적 수정안을 제시하세요.""", + + "English": f"""Strictly evaluate literary achievement of Part {part_number}. + +**Master Plan Part {part_number} Requirements:** +{self._extract_part_plan(master_plan, part_number)} + +**Written Content:** +{part_content} + +**Story Bible Check:** +- Characters: {', '.join(story_bible.characters.keys())} +- Settings: {', '.join(story_bible.settings.keys())} + +**Evaluation Criteria:** + +1. **Literary Rhetoric (30%)** + - Originality of metaphor and symbol + - Poetic density of language + - Clarity and depth of imagery + - Rhythm and musicality of sentences + +2. **Philosophical Depth (25%)** + - Raising existential questions + - Exploring modern human condition + - Balance of universality and specificity + - Originality of thought + +3. **Social Insight (20%)** + - Capturing zeitgeist + - Relationship between structure and individual + - Sharpness of critical perspective + - Alternative imagination + +4. **Narrative Completion (25%)** + - Inevitability of causality + - Maintaining tension + - Character dimensionality + - Structural unity + +**Specific Points:** +- Clichéd expressions: [examples and alternatives] +- Insufficient philosophical exploration: [enhancement direction] +- Unclear social message: [strengthening methods] +- Narrative gaps: [needed revisions] + +**Required Improvements:** +Provide specific revisions to elevate literary level to Nobel Prize standard.""" + } + + return lang_prompts.get(language, lang_prompts["Korean"]) -**Manuscript length: {current_word_count} words** -**Minimum after editing: {min_words} words (REQUIRED!)** + def create_writer_revision_prompt(self, part_number: int, original_content: str, + critic_feedback: str, language: str) -> str: + """작가 수정 프롬프트""" + + lang_prompts = { + "Korean": f"""파트 {part_number}를 비평에 따라 수정하세요. -**Editing Rules:** +**원본:** +{original_content} -1. **Preservation is Priority** - - Must keep 95%+ of manuscript - - Prefer correction over deletion - - Never delete whole paragraphs +**비평 피드백:** +{critic_feedback} -2. **Allowed Edits:** - - Remove only when exact same sentence appears consecutively - - Fix grammar errors or typos - - Add conjunctions for awkward connections (1-2 words) +**수정 지침:** +1. 모든 '필수 수정' 사항을 반영 +2. 가능한 '권장 개선' 사항도 포함 +3. 원본의 강점은 유지 +4. 분량 {MIN_WORDS_PER_PART}단어 이상 유지 +5. 작가로서의 일관된 목소리 유지 +6. 문학적 수준을 한 단계 높이기 -3. **Absolutely Forbidden:** - - Deleting whole paragraphs ❌ - - Summarizing or abbreviating ❌ - - Changing author's style ❌ - - Restructuring plot ❌ +수정본만 제시하세요. 설명은 불필요합니다.""", -4. **Editing Method:** - - Duplicates: Remove only second occurrence - - Awkward connections: Connect with conjunctions - - Typos: Minimal fixes + "English": f"""Revise Part {part_number} according to critique. -**IMPORTANT: Keep almost everything while fixing only tiny issues.** +**Original:** +{original_content} -Don't rewrite the manuscript, copy the original and apply minimal edits. +**Critique Feedback:** +{critic_feedback} -Present the full edited manuscript. ({min_words}+ words required!)""" - } - - return lang_prompts.get(language, lang_prompts["Korean"]) +**Revision Guidelines:** +1. Reflect all 'Required fixes' +2. Include 'Recommended improvements' where possible +3. Maintain original strengths +4. Keep length {MIN_WORDS_PER_PART}+ words +5. Maintain consistent authorial voice +6. Elevate literary level - def create_critic_final_prompt(self, content: str, query: str, language: str) -> str: - """최종 비평""" - - word_count = len(content.split()) +Present only the revision. No explanation needed.""" + } - # 서사 구조 분석 (추가) - has_single_plot = self.check_single_narrative(content) - character_consistency = self.check_character_consistency_in_final(content) + return lang_prompts.get(language, lang_prompts["Korean"]) + + def create_final_critic_prompt(self, complete_novel: str, word_count: int, + story_bible: StoryBible, language: str) -> str: + """최종 종합 평가""" lang_prompts = { - "Korean": f"""완성된 소설을 평가하세요. + "Korean": f"""완성된 소설을 종합 평가하세요. -**주제:** {query} -**분량:** {word_count}단어 (목표: 8,000) +**작품 정보:** +- 총 분량: {word_count}단어 +- 목표: 8,000단어 **평가 기준:** -1. 통합된 서사 구조 (30점) - - 단일한 플롯 존재 여부 - - 인과관계의 치밀함 - - 각 단계의 필연성 - -2. 캐릭터 일관성 (25점) - - 이름과 설정의 통일성 - - 성격과 행동의 일관성 - - 변화의 설득력 - -3. 문학적 완성도 (25점) - - 문체의 일관성 - - 상징과 모티프 활용 - - 문학적 기법의 다양성 - -4. 주제 의식과 통찰 (20점) - - 철학적 깊이 - - 인간에 대한 통찰 - - 독창적 해석 -**감점 요인:** -- 플롯 자기복제 (-10점) -- 캐릭터 설정 충돌 (-10점) -- 단순 반복 (-5점) -- 분량 미달 (-5점) +1. **서사적 통합성 (30점)** + - 10개 파트가 하나의 이야기로 통합되었는가? + - 인과관계가 명확하고 필연적인가? + - 반복이나 순환 없이 진행되는가? -**종합 평가:** -작품이 하나의 완결된 장편소설로 기능하는가? +2. **캐릭터 아크 (25점)** + - 주인공의 변화가 설득력 있는가? + - 변화가 점진적이고 자연스러운가? + - 최종 상태가 초기와 명확히 다른가? -총점: /100점 +3. **문학적 성취 (25점)** + - 주제가 깊이 있게 탐구되었는가? + - 상징이 효과적으로 활용되었는가? + - 문체가 일관되고 아름다운가? + - 현대적 철학과 사회적 메시지가 녹아있는가? -작품의 문학적 가치와 구조적 완성도를 종합적으로 평가하세요.""", +4. **기술적 완성도 (20점)** + - 목표 분량을 달성했는가? + - 각 파트가 균형 있게 전개되었는가? + - 문법과 표현이 정확한가? - "English": f"""Evaluate the completed novel. +**총점: /100점** -**Theme:** {query} -**Length:** {word_count} words (target: 8,000) +구체적인 강점과 약점을 제시하세요.""", -**Criteria:** -1. Integrated Narrative Structure (30 pts) - - Single plot existence - - Causal tightness - - Necessity of each phase - -2. Character Consistency (25 pts) - - Name/setting unity - - Personality/behavior consistency - - Convincing transformation - -3. Literary Quality (25 pts) - - Stylistic consistency - - Symbol/motif usage - - Literary technique variety - -4. Thematic Insight (20 pts) - - Philosophical depth - - Human insight - - Original interpretation + "English": f"""Comprehensively evaluate the completed novel. -**Deductions:** -- Plot self-replication (-10 pts) -- Character setting conflicts (-10 pts) -- Simple repetition (-5 pts) -- Under length (-5 pts) +**Work Info:** +- Total length: {word_count} words +- Target: 8,000 words -**Overall Assessment:** -Does the work function as a complete novel? +**Evaluation Criteria:** -Total: /100 points +1. **Narrative Integration (30 points)** + - Are 10 parts integrated into one story? + - Clear and inevitable causality? + - Progress without repetition or cycles? -Comprehensively evaluate literary value and structural completeness.""" - } - - return lang_prompts.get(language, lang_prompts["Korean"]) +2. **Character Arc (25 points)** + - Convincing protagonist transformation? + - Gradual and natural changes? + - Final state clearly different from initial? - # --- LLM 호출 함수들 --- - def call_llm_sync(self, messages: List[Dict[str, str]], role: str, language: str) -> str: - full_content = "" - for chunk in self.call_llm_streaming(messages, role, language): - full_content += chunk - if full_content.startswith("❌"): - raise Exception(f"LLM Call Failed: {full_content}") - - # 언어 필터 적용 - filtered_content = self.language_filter.clean_text(full_content) - return filtered_content - - def call_llm_streaming(self, messages: List[Dict[str, str]], role: str, language: str) -> Generator[str, None, None]: - try: - system_prompts = self.get_system_prompts(language) - full_messages = [{"role": "system", "content": system_prompts.get(role, "")}, *messages] - - # 작가와 편집자 역할일 때는 더 많은 토큰 허용 - if role.startswith("writer"): - max_tokens = 25000 - elif role == "editor": - # 편집자는 원본 길이 + 여유분 - max_tokens = 30000 # 편집자에게 더 많은 토큰 할당 - else: - max_tokens = 10000 - - payload = { - "model": self.model_id, - "messages": full_messages, - "max_tokens": max_tokens, - "temperature": 0.8, - "top_p": 0.95, - "presence_penalty": 0.5, - "frequency_penalty": 0.3, - "stream": True - } - - response = requests.post( - self.api_url, - headers=self.create_headers(), - json=payload, - stream=True, - timeout=180 - ) - - if response.status_code != 200: - yield f"❌ API 오류 (상태 코드: {response.status_code})" - return - - buffer = "" - for line in response.iter_lines(): - if not line: - continue - - try: - line_str = line.decode('utf-8').strip() - if not line_str.startswith("data: "): - continue - - data_str = line_str[6:] - if data_str == "[DONE]": - break - - data = json.loads(data_str) - choices = data.get("choices", []) - if choices and choices[0].get("delta", {}).get("content"): - content = choices[0]["delta"]["content"] - buffer += content - - if len(buffer) >= 50 or '\n' in buffer: - # 언어 필터 적용 - clean_buffer = self.language_filter.clean_text(buffer) - yield clean_buffer - buffer = "" - time.sleep(0.01) - - except Exception as e: - logger.error(f"청크 처리 오류: {str(e)}") - continue - - if buffer: - clean_buffer = self.language_filter.clean_text(buffer) - yield clean_buffer - - except Exception as e: - logger.error(f"스트리밍 오류: {type(e).__name__}: {str(e)}") - yield f"❌ 오류 발생: {str(e)}" - - def get_system_prompts(self, language: str) -> Dict[str, str]: - """역할별 시스템 프롬프트 (간소화)""" - - base_prompts = { - "Korean": { - "director": """당신은 노벨문학상 수상 작가입니다. -치밀한 인과관계의 단일 서사를 설계하세요. -캐릭터 이름과 설정을 처음부터 명확히 고정하세요. -플롯의 자기복제 없이 필연적 전개를 구축하세요.""", - - "critic": """당신은 문학 비평가입니다. -서사의 인과관계와 캐릭터 일관성을 엄격히 검토하세요. -플롯 자기복제와 설정 충돌을 찾아내세요. -작품이 하나의 완결된 소설인지 평가하세요.""", - - "writer_base": f"""당신은 한국 문학 작가입니다. -반드시 {MIN_WORDS_PER_WRITER}단어 이상 작성하세요. -이전 단계의 필연적 결과로 시작하세요. -캐릭터 이름과 설정을 절대 변경하지 마세요. -구체적 장면과 생생한 대화로 서사를 전개하세요.""", - - "editor": """당신은 원고 보존을 최우선으로 하는 편집자입니다. -원고의 95% 이상을 반드시 유지하세요. -전체 문단을 삭제하지 마세요. -원본을 복사한 후 최소한의 수정만 하세요. -편집 후에도 거의 모든 내용이 남아있어야 합니다.""" - }, - "English": { - "director": """You are a Nobel Prize-winning author. -Design a single narrative with tight causality. -Fix character names and settings clearly from start. -Build necessary development without plot self-replication.""", - - "critic": """You are a literary critic. -Strictly review narrative causality and character consistency. -Find plot self-replication and setting conflicts. -Evaluate if work is one complete novel.""", - - "writer_base": f"""You are a literary writer. -Must write at least {MIN_WORDS_PER_WRITER} words. -Start as necessary result of previous phase. -Never change character names or settings. -Develop narrative with concrete scenes and vivid dialogue.""", - - "editor": """You are a preservation-focused editor. -Must maintain 95%+ of manuscript. -Never delete whole paragraphs. -Copy original and apply minimal edits only. -Almost all content must remain after editing.""" - } - } - - prompts = base_prompts.get(language, base_prompts["Korean"]).copy() - - # 특수 작가 프롬프트 - for i in range(1, 11): - prompts[f"writer{i}"] = prompts["writer_base"] - - return prompts - - # --- 메인 프로세스 --- - def process_novel_stream(self, query: str, language: str, session_id: Optional[str] = None) -> Generator[Tuple[str, List[Dict[str, Any]], str], None, None]: - """소설 생성 프로세스""" - try: - resume_from_stage = 0 - if session_id: - self.current_session_id = session_id - session = NovelDatabase.get_session(session_id) - if session: - query = session['user_query'] - language = session['language'] - resume_from_stage = session['current_stage'] + 1 - # 서사 추적기 복원 - saved_tracker = NovelDatabase.load_narrative_tracker(session_id) - if saved_tracker: - self.narrative_tracker = saved_tracker - else: - self.current_session_id = NovelDatabase.create_session(query, language) - logger.info(f"Created new session: {self.current_session_id}") - - stages = [] - if resume_from_stage > 0: - stages = [{ - "name": s['stage_name'], - "status": s['status'], - "content": s.get('content', ''), - "word_count": s.get('word_count', 0), - "progression_score": s.get('progression_score', 0.0), - "repetition_score": s.get('repetition_score', 0.0), - "consistency_check": s.get('consistency_check', '') - } for s in NovelDatabase.get_stages(self.current_session_id)] - - # 총 단어 수 추적 - total_words = NovelDatabase.get_total_words(self.current_session_id) - - for stage_idx in range(resume_from_stage, len(PROGRESSIVE_STAGES)): - role, stage_name = PROGRESSIVE_STAGES[stage_idx] - if stage_idx >= len(stages): - stages.append({ - "name": stage_name, - "status": "active", - "content": "", - "word_count": 0, - "progression_score": 0.0, - "repetition_score": 0.0, - "consistency_check": "" - }) - else: - stages[stage_idx]["status"] = "active" - - yield f"🔄 진행 중... (현재 {total_words:,}단어)", stages, self.current_session_id - - prompt = self.get_stage_prompt(stage_idx, role, query, language, stages) - stage_content = "" - - for chunk in self.call_llm_streaming([{"role": "user", "content": prompt}], role, language): - stage_content += chunk - stages[stage_idx]["content"] = stage_content - stages[stage_idx]["word_count"] = len(stage_content.split()) - yield f"🔄 {stage_name} 작성 중... ({total_words + stages[stage_idx]['word_count']:,}단어)", stages, self.current_session_id - - # 작가 역할일 때 단어 수 체크 및 재생성 - if role.startswith("writer"): - word_count = len(stage_content.split()) - writer_num = int(re.search(r'\d+', role).group()) - - # 단어 수가 부족하면 추가 생성 요청 - if word_count < MIN_WORDS_PER_WRITER * 0.9: # 90% 미만이면 - retry_prompt = f""" -현재 {word_count}단어만 작성되었습니다. -최소 {MIN_WORDS_PER_WRITER}단어가 필요합니다. - -다음을 추가하여 {MIN_WORDS_PER_WRITER - word_count}단어 이상 더 작성하세요: -- 더 자세한 인물 묘사 -- 추가 대화 장면 -- 배경과 분위기 묘사 -- 인물의 과거 회상 -- 내적 독백 확대 - -기존 내용에 자연스럽게 이어서 작성하세요: -""" - - additional_content = self.call_llm_sync( - [{"role": "user", "content": retry_prompt}], - role, - language - ) - stage_content += "\n\n" + additional_content - stages[stage_idx]["content"] = stage_content - stages[stage_idx]["word_count"] = len(stage_content.split()) - - # 진행도 평가 - previous_content = self.get_previous_writer_content(stages, writer_num) - - # 진행도 점수 계산 - progression_scores = self.narrative_tracker.progression_monitor.calculate_progression_score( - writer_num, stage_content, previous_content - ) - progression_score = sum(progression_scores.values()) / len(progression_scores) - stages[stage_idx]["progression_score"] = progression_score - - # 반복도 점수 계산 - repetition_score = 10.0 - self.narrative_tracker.progression_monitor.count_repetitions(stage_content) - stages[stage_idx]["repetition_score"] = max(0, repetition_score) - - # 일관성 체크 - consistency_ok, consistency_issues = self.narrative_tracker.consistency_checker.validate_new_content( - writer_num, stage_content, [s["content"] for s in stages[:stage_idx] if s["content"]] - ) - stages[stage_idx]["consistency_check"] = "통과" if consistency_ok else "; ".join(consistency_issues) - - # 서사 추적기 업데이트 - self.update_narrative_tracker(stage_content, writer_num) - self.narrative_tracker.extract_used_elements(stage_content) - - # 편집 단계 특별 처리 - if role == "editor" and stage_content: - # 편집 전후 단어 수 비교 - original_novel = "" - for i in range(1, 11): - for s in stages: - if f"writer{i}" in s.get("name", "") and "수정본" in s.get("name", "") and s["content"]: - original_novel += s["content"] + "\n\n" - - original_words = len(original_novel.split()) - edited_words = len(stage_content.split()) - - logger.info(f"편집 결과: {original_words}단어 → {edited_words}단어") - - # 편집으로 20% 이상 삭제되었다면 원본 사용 - if edited_words < original_words * 0.8: - logger.warning(f"편집자가 과도하게 삭제함 ({100 - (edited_words/original_words*100):.1f}% 삭제). 원본 유지.") - stage_content = original_novel - stages[stage_idx]["content"] = stage_content - stages[stage_idx]["word_count"] = original_words - stages[stage_idx]["note"] = "편집 과도로 원본 유지" - - stages[stage_idx]["status"] = "complete" - NovelDatabase.save_stage( - self.current_session_id, stage_idx, stage_name, role, - stage_content, "complete", - stages[stage_idx].get("progression_score", 0.0), - stages[stage_idx].get("repetition_score", 0.0), - stages[stage_idx].get("consistency_check", "") - ) - - # 서사 추적기 저장 - NovelDatabase.save_narrative_tracker(self.current_session_id, self.narrative_tracker) - - # 총 단어 수 업데이트 - total_words = NovelDatabase.get_total_words(self.current_session_id) - - # 단어 수 경고 - if role.startswith("writer") and stages[stage_idx]["word_count"] < MIN_WORDS_PER_WRITER: - logger.warning(f"단어 수 부족: {stage_name} - {stages[stage_idx]['word_count']}단어") - - yield f"✅ {stage_name} 완료 (총 {total_words:,}단어)", stages, self.current_session_id +3. **Literary Achievement (25 points)** + - Theme explored with depth? + - Symbols used effectively? + - Consistent and beautiful style? + - Contemporary philosophy and social message integrated? - # 최종 소설 정리 - final_novel = NovelDatabase.get_writer_content(self.current_session_id) - - # 편집자가 처리한 내용이 있으면 그것을 사용 - edited_content = self.get_edited_content(stages) - if edited_content: - final_novel = edited_content - - final_word_count = len(final_novel.split()) - final_report = self.generate_literary_report(final_novel, query, language) - - # 품질 평가 저장 - self.save_quality_evaluation(final_report) - - NovelDatabase.update_final_novel(self.current_session_id, final_novel, final_report) - - # 최종 단어 수 확인 - if final_word_count < TARGET_WORDS: - yield f"⚠️ 소설 완성! 총 {final_word_count:,}단어 (목표 미달: {TARGET_WORDS - final_word_count:,}단어 부족)", stages, self.current_session_id - else: - yield f"✅ 소설 완성! 총 {final_word_count:,}단어 (목표: {TARGET_WORDS:,}단어 달성!)", stages, self.current_session_id +4. **Technical Completion (20 points)** + - Target length achieved? + - Each part balanced in development? + - Grammar and expression accurate? - except Exception as e: - logger.error(f"소설 생성 프로세스 오류: {e}", exc_info=True) - yield f"❌ 오류 발생: {e}", stages if 'stages' in locals() else [], self.current_session_id +**Total Score: /100 points** - def get_stage_prompt(self, stage_idx, role, query, language, stages): - """단계별 프롬프트 생성""" - if stage_idx == 0: - # 첫 번째 단계 - 감독자 초기 기획 - return self.create_director_initial_prompt(query, language) - - elif stage_idx == 1: - # 두 번째 단계 - 비평가가 감독자 기획 검토 - if role == "critic": - return self.create_critic_consistency_prompt_enhanced( - stages[0]["content"], - self.narrative_tracker, - query, - language - ) +Present specific strengths and weaknesses.""" + } - elif stage_idx == 2: - # 세 번째 단계 - 감독자 수정 - if role == "director": - return self.create_director_revision_prompt( - stages[0]["content"], # 초기 기획 - stages[1]["content"], # 비평 - query, - language - ) + return lang_prompts.get(language, lang_prompts["Korean"]) + + def _extract_part_plan(self, master_plan: str, part_number: int) -> str: + """마스터플랜에서 특정 파트 계획 추출""" + lines = master_plan.split('\n') + part_section = [] + capturing = False - else: - # 작가 단계들 - if role.startswith("writer"): - writer_num = int(re.search(r'\d+', role).group()) - - # 이전 작가들의 내용 가져오기 - previous_content = "" - if writer_num > 1: - # writer_num - 1까지의 모든 작가 내용 - for i in range(1, writer_num): - for stage in stages: - if f"writer{i}" in stage.get("name", "") and stage["content"]: - previous_content += stage["content"] + "\n\n" - - # 감독자 플랜 가져오기 - director_plan = "" - for stage in stages[:3]: # 처음 3개 단계에서 감독자 플랜 찾기 - if "감독자" in stage.get("name", "") and "수정" in stage.get("name", "") and stage["content"]: - director_plan = stage["content"] - break - if not director_plan: # 수정된 플랜이 없으면 초기 플랜 사용 - director_plan = stages[0]["content"] if stages and stages[0]["content"] else "" - - # 서사 요약 생성 - narrative_summary = self.generate_narrative_summary(stages, writer_num) - - # 단계별 요구사항 생성 - phase_requirements = self.narrative_tracker.generate_phase_requirements(writer_num) - - # 사용된 요소들 - used_elements = list(self.narrative_tracker.used_expressions) + for line in lines: + if f"파트 {part_number}:" in line or f"Part {part_number}:" in line: + capturing = True + elif capturing and (f"파트 {part_number+1}:" in line or f"Part {part_number+1}:" in line): + break + elif capturing: + part_section.append(line) - # 초안인지 수정본인지 확인 - stage_name = stages[stage_idx]["name"] if stage_idx < len(stages) else "" - if "초안" in stage_name: - return self.create_writer_prompt_enhanced( - writer_num, - director_plan, - previous_content, - phase_requirements, - narrative_summary, - language, - used_elements - ) - else: # 수정본 - # 해당 작가의 초안 찾기 - initial_content = "" - critic_feedback = "" - - # 초안 찾기 - for i, stage in enumerate(stages): - if f"writer{writer_num}" in stage.get("name", "") and "초안" in stage.get("name", ""): - initial_content = stage["content"] - break - - # 중간 비평 찾기 - for i, stage in enumerate(stages): - if "critic" in stage.get("name", "") and "중간 검토" in stage.get("name", ""): - critic_feedback = stage["content"] - break - - return self.create_writer_revision_prompt( - writer_num, - initial_content, - critic_feedback, - language - ) + return '\n'.join(part_section) if part_section else "해당 파트 계획을 찾을 수 없습니다." + + # --- 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 == "writer" 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 - elif role == "critic": - # 중간 비평가 검토 - if "중간 검토" in stages[stage_idx]["name"]: - # 모든 작가 콘텐츠 수집 - all_content = "" - for stage in stages: - if "writer" in stage.get("name", "") and "초안" in stage.get("name", "") and stage["content"]: - all_content += stage["content"] + "\n\n" + 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 - return self.create_critic_consistency_prompt_enhanced( - all_content, - self.narrative_tracker, - query, - language - ) - # 최종 비평 - else: - # 편집된 내용이 있으면 그것을, 없으면 모든 작가 수정본 - final_content = "" - for stage in stages: - if "편집자" in stage.get("name", "") and stage["content"]: - final_content = stage["content"] - break + data_str = line_str[6:] + if data_str == "[DONE]": + break - if not final_content: - # 편집된 내용이 없으면 모든 작가 수정본 수집 - for i in range(1, 11): - for stage in stages: - if f"writer{i}" in stage.get("name", "") and "수정본" in stage.get("name", "") and stage["content"]: - final_content += stage["content"] + "\n\n" + 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) - return self.create_critic_final_prompt(final_content, query, language) + except Exception as e: + logger.error(f"청크 처리 오류: {str(e)}") + continue - elif role == "editor": - # 편집자 - 모든 작가 수정본 수집 - complete_novel = "" - writer_contents = [] - for i in range(1, 11): - for stage in stages: - if f"writer{i}" in stage.get("name", "") and "수정본" in stage.get("name", "") and stage["content"]: - writer_contents.append(stage["content"]) - complete_novel += stage["content"] + "\n\n" + 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": """당신은 현대 세계문학의 정점을 지향하는 작품을 설계합니다. +깊은 철학적 통찰과 날카로운 사회 비판을 결합하세요. +인간 조건의 복잡성을 10개의 유기적 파트로 구현하세요. +독자의 영혼을 뒤흔들 강렬한 첫문장부터 시작하세요.""", + + "critic_director": """서사 구조의 논리성과 실현 가능성을 검증하는 전문가입니다. +인과관계의 허점을 찾아내세요. +캐릭터 발전의 신빙성을 평가하세요. +철학적 깊이와 문학적 가치를 판단하세요. +8,000단어 분량의 적절성을 판단하세요.""", - # 문제점 감지 - 매우 심각한 문제만 - issues = [] + "writer": """당신은 언어의 연금술사입니다. +일상어를 시로, 구체를 추상으로, 개인을 보편으로 변환하세요. +현대인의 영혼의 어둠과 빛을 동시에 포착하세요. +독자가 자신을 재발견하게 만드는 거울이 되세요.""", - # 완전히 동일한 문단 체크 - paragraphs = complete_novel.split('\n\n') - seen_paragraphs = set() - exact_duplicates = 0 - for para in paragraphs: - if para.strip() in seen_paragraphs and len(para.strip()) > 100: - exact_duplicates += 1 - seen_paragraphs.add(para.strip()) + "critic_final": """당신은 작품의 문학적 잠재력을 극대화하는 조력자입니다. +평범함을 비범함으로 이끄는 날카로운 통찰을 제공하세요. +작가의 무의식에 잠든 보석을 발굴하세요. +타협 없는 기준으로 최고를 요구하세요.""" + }, + "English": { + "director": """You design works aiming for the pinnacle of contemporary world literature. +Combine deep philosophical insights with sharp social criticism. +Implement the complexity of the human condition in 10 organic parts. +Start with an intense opening sentence that shakes the reader's soul.""", - if exact_duplicates > 5: - issues.append(f"{exact_duplicates}개의 완전히 동일한 문단 발견") + "critic_director": """You are an expert verifying narrative logic and feasibility. +Find gaps in causality. +Evaluate credibility of character development. +Judge philosophical depth and literary value. +Judge appropriateness of 8,000-word length.""", - # 문제가 거의 없으면 편집 최소화 - if len(issues) == 0: - issues = ["원고 상태가 양호합니다. 편집을 최소화하세요."] + "writer": """You are an alchemist of language. +Transform everyday language into poetry, concrete into abstract, individual into universal. +Capture both darkness and light of the modern soul. +Become a mirror where readers rediscover themselves.""", - return self.create_editor_prompt(complete_novel, issues, language) + "critic_final": """You are a collaborator maximizing the work's literary potential. +Provide sharp insights leading ordinariness to extraordinariness. +Excavate gems sleeping in the writer's unconscious. +Demand the best with uncompromising standards.""" + } + } - # 기본값 (에러 방지) - return f"역할 {role}에 대한 프롬프트가 정의되지 않았습니다." - - - def create_director_revision_prompt(self, initial_plan: str, critic_feedback: str, user_query: str, language: str) -> str: - """감독자 수정 프롬프트""" - lang_prompts = { - "Korean": f"""비평을 반영하여 수정하세요. - -**원 주제:** {user_query} + prompts = base_prompts.get(language, base_prompts["Korean"]).copy() + + # 파트별 비평가 프롬프트 추가 + for i in range(1, 11): + prompts[f"critic_part{i}"] = f"""파트 {i} 전담 비평가입니다. +이전 파트와의 인과관계를 최우선으로 검토하세요. +캐릭터 일관성과 발전을 확인하세요. +마스터플랜과의 일치도를 평가하세요. +문학적 수준과 철학적 깊이를 평가하세요. +구체적이고 실행 가능한 수정 지시를 제공하세요.""" + + return prompts -**비평 요점:** -{critic_feedback[:500]} + # --- 메인 프로세스 --- + 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), + "momentum": s.get('narrative_momentum', 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(UNIFIED_STAGES)): + role, stage_name = UNIFIED_STAGES[stage_idx] + if stage_idx >= len(stages): + stages.append({ + "name": stage_name, + "status": "active", + "content": "", + "word_count": 0, + "momentum": 0.0 + }) + else: + stages[stage_idx]["status"] = "active" -**수정 핵심:** -1. 10단계가 인과관계로 긴밀히 연결 -2. 캐릭터 이름과 설정 처음부터 고정 -3. 각 단계가 이전의 필연적 결과 -4. 플롯의 자기복제 방지 -5. 8,000단어 달성 전략 + 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 == "writer": + # 파트 번호 계산 + part_num = self._get_part_number(stage_idx) + if part_num: + self.narrative_tracker.accumulated_content.append(stage_content) + self.narrative_tracker.word_count_by_part[part_num] = len(stage_content.split()) + + # 서사 추진력 계산 + momentum = self.narrative_tracker.calculate_narrative_momentum(part_num, stage_content) + stages[stage_idx]["momentum"] = momentum + + # 스토리 바이블 업데이트 + self._update_story_bible_from_content(stage_content, part_num) + + stages[stage_idx]["status"] = "complete" + NovelDatabase.save_stage( + self.current_session_id, stage_idx, stage_name, role, + stage_content, "complete", stages[stage_idx].get("momentum", 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) + 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:,}단어", 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 -치밀한 인과관계의 수정 계획을 제시하세요.""", - - "English": f"""Revise based on critique. + 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_final_prompt(stages[0]["content"], stages[1]["content"], query, language) -**Original Theme:** {user_query} + master_plan = stages[2]["content"] + + # 작가 파트 작성 + if role == "writer" and "수정본" not in stages[stage_idx]["name"]: + part_num = self._get_part_number(stage_idx) + accumulated = '\n\n'.join(self.narrative_tracker.accumulated_content) + return self.create_writer_prompt(part_num, master_plan, accumulated, + self.narrative_tracker.story_bible, language) + + # 파트별 비평 + if role.startswith("critic_part"): + part_num = int(role.replace("critic_part", "")) + # 해당 파트의 작가 내용 찾기 + writer_content = stages[stage_idx-1]["content"] + accumulated = '\n\n'.join(self.narrative_tracker.accumulated_content[:-1]) + return self.create_part_critic_prompt(part_num, writer_content, master_plan, + accumulated, self.narrative_tracker.story_bible, language) + + # 작가 수정본 + if role == "writer" and "수정본" in stages[stage_idx]["name"]: + part_num = self._get_part_number(stage_idx) + original_content = stages[stage_idx-2]["content"] # 원본 + critic_feedback = stages[stage_idx-1]["content"] # 비평 + return self.create_writer_revision_prompt(part_num, original_content, + critic_feedback, language) + + # 최종 비평 + if role == "critic_final": + complete_novel = NovelDatabase.get_writer_content(self.current_session_id) + word_count = len(complete_novel.split()) + return self.create_final_critic_prompt(complete_novel, word_count, + self.narrative_tracker.story_bible, language) + + return "" -**Critique Points:** -{critic_feedback[:500]} + def create_director_final_prompt(self, initial_plan: str, critic_feedback: str, + user_query: str, language: str) -> str: + """감독자 최종 마스터플랜""" + return f"""비평을 반영하여 최종 마스터플랜을 완성하세요. -**Revision Focus:** -1. 10 phases tightly connected by causality -2. Character names/settings fixed from start -3. Each phase as necessary result of previous -4. Prevent plot self-replication -5. Strategy to achieve 8,000 words +**원 주제:** {user_query} -**Structure Enhancement:** -- External events trigger internal changes -- Choices determine next phase -- Necessary, not coincidental development +**초기 기획:** +{initial_plan} -Present revision plan with tight causality.""" - } - - return lang_prompts.get(language, lang_prompts["Korean"]) +**비평 피드백:** +{critic_feedback} - def get_previous_writer_content(self, stages: List[Dict], current_writer: int) -> str: - """이전 작가의 내용 가져오기""" - if current_writer == 1: - return "" - - # 이전 작가들의 내용 수집 - previous_content = [] - for i in range(1, current_writer): - for stage in stages: - if f"writer{i}" in stage.get("name", "") and stage["content"]: - previous_content.append(stage["content"]) - break - - return "\n\n".join(previous_content) - - def get_all_writer_content(self, stages: List[Dict], up_to_stage: int) -> str: - """특정 단계까지의 모든 작가 내용""" - contents = [] - for i, s in enumerate(stages): - if i <= up_to_stage and "writer" in s.get("name", "") and s["content"]: - contents.append(s["content"]) - return "\n\n".join(contents) - - def get_edited_content(self, stages: List[Dict]) -> str: - """편집된 내용 가져오기""" - for s in stages: - if "편집자" in s.get("name", "") and s["content"]: - return s["content"] - return "" - - def generate_narrative_summary(self, stages: List[Dict], up_to_writer: int) -> str: - """현재까지의 서사 요약""" - if up_to_writer == 1: - return "첫 시작입니다." - - summary_parts = [] - for i in range(1, up_to_writer): - if i in self.narrative_tracker.phase_summaries: - summary_parts.append(f"[{NARRATIVE_PHASES[i-1]}]: {self.narrative_tracker.phase_summaries[i][:100]}...") - - return "\n".join(summary_parts) if summary_parts else "이전 내용을 이어받아 진행하세요." +**최종 마스터플랜 요구사항:** +1. 모든 비평 지적사항 반영 +2. 10개 파트의 구체적 내용과 인과관계 +3. 주인공의 명확한 변화 단계 +4. 중심 상징의 의미 변화 과정 +5. 각 파트 800단어 실현 가능성 +6. 철학적 깊이와 사회적 메시지 구현 방안 - def update_narrative_tracker(self, content: str, writer_num: int): - """서사 추적기 업데이트""" - # 간단한 요약 생성 (실제로는 더 정교한 분석 필요) - lines = content.split('\n') - key_events = [line.strip() for line in lines if len(line.strip()) > 50][:3] - - if key_events: - summary = " ".join(key_events[:2])[:200] + "..." - self.narrative_tracker.phase_summaries[writer_num] = summary - - # 철학적 통찰 추출 (간단한 키워드 기반) - philosophical_keywords = ['존재', '의미', '삶', '죽음', '인간', '고통', '희망', '사랑', - 'existence', 'meaning', 'life', 'death', 'human', 'suffering', 'hope', 'love'] - for keyword in philosophical_keywords: - if keyword in content: - self.narrative_tracker.philosophical_insights.append(f"Phase {writer_num}: {keyword} 탐구") - break - - # 문학적 기법 감지 - literary_devices = [] - if '처럼' in content or 'like' in content or 'as if' in content: - literary_devices.append('비유') - if '...' in content or '—' in content: - literary_devices.append('의식의 흐름') - if content.count('"') > 4: - literary_devices.append('대화') - - if literary_devices: - self.narrative_tracker.literary_devices[writer_num] = literary_devices +구체적이고 실행 가능한 최종 계획을 제시하세요.""" - def detect_issues(self, content: str) -> List[str]: - """문제점 감지 - 심각한 문제만""" - issues = [] - - # 완전히 동일한 문단만 감지 - paragraphs = content.split('\n\n') - exact_duplicates = 0 - for i, para1 in enumerate(paragraphs): - for j, para2 in enumerate(paragraphs[i+1:]): - if para1.strip() == para2.strip() and len(para1.strip()) > 50: - exact_duplicates += 1 - - if exact_duplicates > 0: - issues.append(f"{exact_duplicates}개의 완전히 동일한 문단 발견") - - # 5회 이상 반복되는 표현만 감지 - repetitive_phrases = ["습기가 찬 아침", "나라미 어플", "43만원", "개구리알을 바라보았다"] - for phrase in repetitive_phrases: - count = content.count(phrase) - if count > 5: # 임계값을 3에서 5로 상향 - issues.append(f"'{phrase}' 표현이 {count}회 과도하게 반복됨") - - # 심각한 캐릭터 이름 불일치만 - name_variations = ["나라미", "안정", "나"] - found_names = [name for name in name_variations if content.count(name) > 10] - if len(found_names) > 2: - issues.append(f"주인공 이름 심각한 불일치: {', '.join(found_names)}") - - # 언어 오류 감지 - if re.search(r'[ぁ-んァ-ヶ一-龯]', content): - issues.append("일본어/중국어 문자 발견") - - return issues[:3] # 가장 심각한 3개만 반환 + def _get_part_number(self, stage_idx: int) -> Optional[int]: + """스테이지 인덱스에서 파트 번호 추출""" + stage_name = UNIFIED_STAGES[stage_idx][1] + match = re.search(r'파트 (\d+)', stage_name) + if match: + return int(match.group(1)) + return None - def evaluate_progression(self, content: str, phase: int) -> float: - """서사 진행도 평가""" - score = 5.0 - - # 분량 체크 - word_count = len(content.split()) - if word_count >= MIN_WORDS_PER_WRITER: - score += 2.0 - - # 새로운 요소 체크 - if phase > 1: - prev_summary = self.narrative_tracker.phase_summaries.get(phase-1, "") - if prev_summary and len(set(content.split()) - set(prev_summary.split())) > 100: - score += 1.5 - - # 변화 언급 체크 - change_keywords = ['변했', '달라졌', '새로운', '이제는', '더 이상', - 'changed', 'different', 'new', 'now', 'no longer'] - if any(keyword in content for keyword in change_keywords): - score += 1.5 - - # 철학적 깊이 체크 - philosophical_keywords = ['존재', '의미', '삶의', '인간의', '왜', 'existence', 'meaning', 'life', 'human', 'why'] - if any(keyword in content for keyword in philosophical_keywords): - score += 0.5 - - # 문학적 기법 체크 - if not any(phrase in content for phrase in ['느꼈다', '였다', 'felt', 'was']): - score += 0.5 # 보여주기 기법 사용 - - return min(10.0, score) - - def generate_literary_report(self, complete_novel: str, query: str, language: str) -> str: - """최종 문학적 평가""" - prompt = self.create_critic_final_prompt(complete_novel, query, language) - try: - report = self.call_llm_sync([{"role": "user", "content": prompt}], "critic", language) - return report - except Exception as e: - logger.error(f"최종 보고서 생성 실패: {e}") - return "보고서 생성 중 오류 발생" - - def check_single_narrative(self, content: str) -> bool: - """단일 서사 구조 확인""" - # 주인공 이름의 일관성 체크 - names = re.findall(r'[가-힣]{2,4}(?=이|가|은|는|을|를)', content) - name_counts = {} - for name in names: - name_counts[name] = name_counts.get(name, 0) + 1 - - # 가장 많이 등장하는 이름이 전체의 50% 이상인지 - if name_counts: - max_count = max(name_counts.values()) - total_count = sum(name_counts.values()) - return max_count / total_count > 0.5 - return False - - def check_character_consistency_in_final(self, content: str) -> float: - """최종 작품의 캐릭터 일관성 점수""" - # 캐릭터 이름과 직업/설정의 일관성 체크 - consistency_score = 1.0 - - # 같은 이름이 다른 설정으로 등장하는지 체크 - name_professions = {} - patterns = [ - r'([가-힣]{2,4})(?:은|는|이|가)\s+(\w+(?:작가|기자|교사|의사|변호사))', - r'(\w+(?:작가|기자|교사|의사|변호사))\s+([가-힣]{2,4})' - ] - - for pattern in patterns: - matches = re.findall(pattern, content) - for match in matches: - name = match[0] if '가' <= match[0][0] <= '힣' else match[1] - profession = match[1] if '가' <= match[0][0] <= '힣' else match[0] - - if name in name_professions and name_professions[name] != profession: - consistency_score -= 0.2 # 불일치마다 감점 - else: - name_professions[name] = profession + def _update_story_bible_from_content(self, content: str, part_num: int): + """컨텐츠에서 스토리 바이블 자동 업데이트""" + # 간단한 키워드 기반 추출 (실제로는 더 정교한 NLP 필요) + lines = content.split('\n') - return max(0, consistency_score) - - def save_quality_evaluation(self, report: str): - """품질 평가 저장""" - try: - # 점수 추출 (간단한 패턴 매칭) - score_match = re.search(r'총점:\s*(\d+(?:\.\d+)?)/100', report) - score = float(score_match.group(1)) if score_match else 0.0 - - NovelDatabase.save_quality_evaluation( - self.current_session_id, - "final_evaluation", - score, - report - ) - except Exception as e: - logger.error(f"품질 평가 저장 ��패: {e}") + # 캐릭터 이름 추출 (대문자로 시작하는 단어들) + for line in lines: + words = line.split() + for word in words: + if word and word[0].isupper() and len(word) > 1: + if word not in self.narrative_tracker.story_bible.characters: + self.narrative_tracker.story_bible.characters[word] = { + "first_appearance": part_num, + "traits": [] + } + + def generate_literary_report(self, complete_novel: str, word_count: int, language: str) -> str: + """최종 문학적 평가 보고서 생성""" + prompt = self.create_final_critic_prompt(complete_novel, word_count, + self.narrative_tracker.story_bible, language) + try: + report = self.call_llm_sync([{"role": "user", "content": prompt}], + "critic_final", 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 = UnifiedLiterarySystem() + 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) + novel_content = format_novel_display(novel_content) + + yield stages_markdown, novel_content, status or "🔄 처리 중...", current_session_id def get_active_sessions(language: str) -> List[str]: """활성 세션 목록""" sessions = NovelDatabase.get_active_sessions() - result = [] - for s in sessions: - # None 값 체크 추가 - session_id = s.get('session_id', '') - user_query = s.get('user_query', '') - created_at = s.get('created_at', '') - total_words = s.get('total_words', 0) or 0 # None일 경우 0으로 처리 - - if session_id and user_query: # 필수 값이 있는 경우만 추가 - result.append( - f"{session_id[:8]}... - {user_query[:50]}... ({created_at}) [{total_words:,}단어]" - ) - return result + 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]: """소설 다운로드 파일 생성""" @@ -2278,51 +1578,63 @@ def download_novel(novel_text: str, format_type: str, language: str, session_id: return None def format_stages_display(stages: List[Dict]) -> str: - """단계별 진행 상황 표시""" + """단계별 진행 상황 표시 - 단일 작가 시스템용""" markdown = "## 🎬 진행 상황\n\n" - # 총 단어 수 계산 - total_words = sum(s.get('word_count', 0) for s in stages if 'writer' in s.get('name', '')) - progress_percent = (total_words / TARGET_WORDS) * 100 if TARGET_WORDS > 0 else 0 + # 총 단어 수 계산 (작가 스테이지만) + total_words = sum(s.get('word_count', 0) for s in stages + if s.get('name', '').startswith('✍️ 작가:') and '수정본' in s.get('name', '')) + markdown += f"**총 단어 수: {total_words:,} / {TARGET_WORDS:,}**\n\n" + + # 진행 상황 요약 + completed_parts = sum(1 for s in stages + if '수정본' in s.get('name', '') and s.get('status') == 'complete') + markdown += f"**완성된 파트: {completed_parts} / 10**\n\n" - markdown += f"**총 단어 수: {total_words:,} / {TARGET_WORDS:,}**\n" - markdown += f"**진행률: {progress_percent:.1f}%**\n" - markdown += f"{'█' * int(progress_percent // 5)}{'░' * (20 - int(progress_percent // 5))}\n\n" + # 서사 추진력 평균 + momentum_scores = [s.get('momentum', 0) for s in stages if s.get('momentum', 0) > 0] + if momentum_scores: + avg_momentum = sum(momentum_scores) / len(momentum_scores) + markdown += f"**평균 서사 추진력: {avg_momentum:.1f} / 10**\n\n" + markdown += "---\n\n" + + # 각 스테이지 표시 + current_part = 0 for i, stage in enumerate(stages): status_icon = "✅" if stage['status'] == 'complete' else "🔄" if stage['status'] == 'active' else "⏳" + + # 파트 구분선 추가 + if '파트' in stage.get('name', '') and '비평가' not in stage.get('name', ''): + part_match = re.search(r'파트 (\d+)', stage['name']) + if part_match: + new_part = int(part_match.group(1)) + if new_part != current_part: + current_part = new_part + markdown += f"\n### 📚 파트 {current_part}\n\n" + markdown += f"{status_icon} **{stage['name']}**" if stage.get('word_count', 0) > 0: markdown += f" ({stage['word_count']:,}단어)" - - # 작가 단계에서 단어 수 부족 경고 - if 'writer' in stage.get('name', '') and stage['word_count'] < MIN_WORDS_PER_WRITER: - markdown += f" ⚠️ **분량 부족!**" - # 진행도와 반복도 점수 표시 - if stage.get('progression_score', 0) > 0: - markdown += f" [진행도: {stage['progression_score']:.1f}/10]" - if stage.get('repetition_score', 0) > 0: - markdown += f" [반복도: {stage['repetition_score']:.1f}/10]" - - # 일관성 체크 표시 - if stage.get('consistency_check'): - if stage['consistency_check'] == "통과": - markdown += " ✔️" - else: - markdown += f" ⚠️ {stage['consistency_check']}" + if stage.get('momentum', 0) > 0: + markdown += f" [추진력: {stage['momentum']:.1f}/10]" markdown += "\n" - if stage['content']: - preview = stage['content'][:200] + "..." if len(stage['content']) > 200 else stage['content'] + if stage['content'] and stage['status'] == 'complete': + # 미리보기 길이를 역할에 따라 조정 + preview_length = 300 if 'writer' in stage.get('name', '').lower() else 200 + preview = stage['content'][:preview_length] + "..." if len(stage['content']) > preview_length else stage['content'] markdown += f"> {preview}\n\n" + elif stage['status'] == 'active': + markdown += "> *작성 중...*\n\n" return markdown def format_novel_display(novel_text: str) -> str: - """소설 내용 표시""" + """소설 내용 표시 - 파트별 구분 강화""" if not novel_text: return "아직 완성된 내용이 없습니다." @@ -2332,277 +1644,316 @@ def format_novel_display(novel_text: str) -> str: word_count = len(novel_text.split()) formatted += f"**총 분량: {word_count:,}단어 (목표: {TARGET_WORDS:,}단어)**\n\n" - if word_count < TARGET_WORDS: - shortage = TARGET_WORDS - word_count - formatted += f"⚠️ **주의: 목표 분량에 {shortage:,}단어 부족합니다.**\n\n" - else: - formatted += f"✅ **목표 분량 달성!**\n\n" - + # 달성률 + achievement = (word_count / TARGET_WORDS) * 100 + formatted += f"**달성률: {achievement:.1f}%**\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" + # 각 파트를 구분하여 표시 + parts = novel_text.split('\n\n') + + for i, part in enumerate(parts): + if part.strip(): + # 파트 제목 추가 + if i < len(NARRATIVE_PHASES): + formatted += f"## {NARRATIVE_PHASES[i]}\n\n" + + formatted += f"{part}\n\n" + + # 파트 사이 구분선 + if i < len(parts) - 1: + formatted += "---\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() + + # 한국 신국판 규격 설정 (152mm x 225mm) + section = doc.sections[0] + section.page_height = Mm(225) # 225mm + section.page_width = Mm(152) # 152mm + section.top_margin = Mm(20) # 상단 여백 20mm + section.bottom_margin = Mm(20) # 하단 여백 20mm + section.left_margin = Mm(20) # 좌측 여백 20mm + section.right_margin = Mm(20) # 우측 여백 20mm + + # 세션 정보로부터 제목 생성 + session = NovelDatabase.get_session(session_id) + + # 제목 생성 함수 + def generate_title(user_query: str, content_preview: str) -> str: + """주제와 내용을 기반으로 제목 생성""" + # 간단한 규칙 기반 제목 생성 (실제로는 LLM 호출 가능) + if len(user_query) < 20: + return user_query + else: + # 주제에서 핵심 키워드 추출 + keywords = user_query.split()[:5] + return " ".join(keywords) + + # 제목 페이지 + title = generate_title(session["user_query"], content[:500]) if session else "무제" + + # 제목 스타일 설정 + title_para = doc.add_paragraph() + title_para.alignment = WD_ALIGN_PARAGRAPH.CENTER + title_para.paragraph_format.space_before = Pt(100) + + title_run = title_para.add_run(title) + title_run.font.name = '바탕' + title_run._element.rPr.rFonts.set(qn('w:eastAsia'), '바탕') + title_run.font.size = Pt(20) + title_run.bold = True + + # 페이지 구분 + doc.add_page_break() + + # 본문 스타일 설정 + style = doc.styles['Normal'] + style.font.name = '바탕' + style._element.rPr.rFonts.set(qn('w:eastAsia'), '바탕') + style.font.size = Pt(10.5) # 한국 소설 표준 크기 + style.paragraph_format.line_spacing = 1.8 # 행간 180% + style.paragraph_format.space_after = Pt(0) + style.paragraph_format.first_line_indent = Mm(10) # 들여쓰기 10mm + + # 본문 내용 정제 - 순수 텍스트만 추출 + def clean_content(text: str) -> str: + """불필요한 마크다운, 파트 번호 등 제거""" + # 파트 제목/번호 패턴 제거 + patterns_to_remove = [ + r'^#{1,6}\s+.*$', # 마크다운 헤더 + r'^\*\*.*\*\*$', # 굵은 글씨 라인 + r'^파트\s*\d+.*$', # 파트 번호 + r'^Part\s*\d+.*$', # Part 번호 + r'^\d+\.\s+.*:.*$', # 번호가 있는 제목 + r'^---+$', # 구분선 + r'^\s*\[.*\]\s*$', # 대괄호로 둘러싸인 라벨 + ] + + lines = text.split('\n') + cleaned_lines = [] + + for line in lines: + # 빈 줄은 유지 + if not line.strip(): + cleaned_lines.append('') + continue + + # 패턴 매칭으로 불필요한 라인 제거 + skip_line = False + for pattern in patterns_to_remove: + if re.match(pattern, line.strip(), re.MULTILINE): + skip_line = True + break + + if not skip_line: + # 마크다운 강조 표시 제거 + cleaned_line = line + cleaned_line = re.sub(r'\*\*(.*?)\*\*', r'\1', cleaned_line) # **text** -> text + cleaned_line = re.sub(r'\*(.*?)\*', r'\1', cleaned_line) # *text* -> text + cleaned_line = re.sub(r'`(.*?)`', r'\1', cleaned_line) # `text` -> text + cleaned_lines.append(cleaned_line.strip()) + + # 연속된 빈 줄 제거 (최대 1개만 유지) + final_lines = [] + prev_empty = False + for line in cleaned_lines: + if not line: + if not prev_empty: + final_lines.append('') + prev_empty = True + else: + final_lines.append(line) + prev_empty = False + + return '\n'.join(final_lines) + + # 내용 정제 + cleaned_content = clean_content(content) + + # 본문 추가 + paragraphs = cleaned_content.split('\n') + for para_text in paragraphs: + if para_text.strip(): + para = doc.add_paragraph(para_text.strip()) + # 스타일 재확인 (한글 폰트 적용) + for run in para.runs: + run.font.name = '바탕' + run._element.rPr.rFonts.set(qn('w:eastAsia'), '바탕') + else: + # 문단 구분을 위한 빈 줄 + doc.add_paragraph() + + # 파일 저장 + 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("=" * 80 + "\n") + f.write(f"생성일: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n") + f.write(f"총 단어 수: {len(content.split()):,}단어\n") + f.write("=" * 80 + "\n\n") + + # 본문 f.write(content) + + # 푸터 + f.write("\n\n" + "=" * 80 + "\n") + f.write("AI 문학 창작 시스템 v2.0\n") + f.write("=" * 80 + "\n") + return filepath - # CSS 스타일 custom_css = """ .gradio-container { - background: linear-gradient(135deg, #1e3c72 0%, #2a5298 50%, #1e3c72 100%); + background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%); min-height: 100vh; } .main-header { - background-color: rgba(255, 255, 255, 0.1); - backdrop-filter: blur(10px); - padding: 30px; - border-radius: 12px; + background-color: rgba(255, 255, 255, 0.05); + backdrop-filter: blur(20px); + padding: 40px; + border-radius: 20px; margin-bottom: 30px; text-align: center; color: white; - border: 1px solid rgba(255, 255, 255, 0.2); + border: 2px solid rgba(255, 255, 255, 0.1); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); } .progress-note { - background-color: rgba(255, 223, 0, 0.1); - border-left: 3px solid #ffd700; - padding: 15px; - margin: 20px 0; - border-radius: 8px; - color: #fff; -} - -.improvement-note { - background-color: rgba(0, 255, 127, 0.1); - border-left: 3px solid #00ff7f; - padding: 15px; - margin: 20px 0; - border-radius: 8px; - color: #fff; -} - -.warning-note { - background-color: rgba(255, 69, 0, 0.1); - border-left: 3px solid #ff4500; - padding: 15px; - margin: 20px 0; - border-radius: 8px; + background: linear-gradient(135deg, rgba(255, 107, 107, 0.1), rgba(255, 230, 109, 0.1)); + border-left: 4px solid #ff6b6b; + padding: 20px; + margin: 25px auto; + border-radius: 10px; color: #fff; + max-width: 800px; + font-weight: 500; } .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.08); + backdrop-filter: blur(15px); + padding: 25px; + border-radius: 15px; + margin-bottom: 25px; + border: 1px solid rgba(255, 255, 255, 0.1); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1); } .session-section { - background-color: rgba(255, 255, 255, 0.1); + background-color: rgba(255, 255, 255, 0.06); backdrop-filter: blur(10px); - padding: 15px; - border-radius: 8px; - margin-top: 20px; + padding: 20px; + border-radius: 12px; + margin-top: 25px; color: white; - border: 1px solid rgba(255, 255, 255, 0.2); + border: 1px solid rgba(255, 255, 255, 0.08); } #stages-display { - background-color: rgba(255, 255, 255, 0.95); - padding: 20px; - border-radius: 12px; - max-height: 600px; + background-color: rgba(255, 255, 255, 0.97); + padding: 25px; + border-radius: 15px; + max-height: 650px; overflow-y: auto; - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); + color: #2c3e50; } #novel-output { - background-color: rgba(255, 255, 255, 0.95); - padding: 30px; - border-radius: 12px; - max-height: 700px; + background-color: rgba(255, 255, 255, 0.97); + padding: 35px; + border-radius: 15px; + max-height: 750px; overflow-y: auto; - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); + color: #2c3e50; + line-height: 1.8; } .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.92); + padding: 20px; + border-radius: 12px; + margin-top: 25px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); } -/* 진행 표시기 스타일 */ +/* 진행 표시기 개선 */ .progress-bar { background-color: #e0e0e0; - height: 20px; - border-radius: 10px; + height: 25px; + border-radius: 12px; overflow: hidden; - margin: 10px 0; + margin: 15px 0; + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1); } .progress-fill { - background-color: #4CAF50; + background: linear-gradient(90deg, #4CAF50, #8BC34A); height: 100%; - transition: width 0.3s ease; + transition: width 0.5s ease; + box-shadow: 0 2px 8px rgba(76, 175, 80, 0.3); } -/* 점수 표시 스타일 */ -.score-badge { - display: inline-block; - padding: 2px 8px; - border-radius: 12px; - font-size: 0.9em; - font-weight: bold; - margin-left: 5px; -} - -.score-high { - background-color: #4CAF50; - color: white; +/* 스크롤바 스타일 */ +::-webkit-scrollbar { + width: 10px; } -.score-medium { - background-color: #FF9800; - color: white; +::-webkit-scrollbar-track { + background: rgba(0, 0, 0, 0.1); + border-radius: 5px; } -.score-low { - background-color: #F44336; - color: white; +::-webkit-scrollbar-thumb { + background: rgba(0, 0, 0, 0.3); + border-radius: 5px; } -/* 일관성 체크 표시 */ -.consistency-pass { - color: #4CAF50; - font-weight: bold; +::-webkit-scrollbar-thumb:hover { + background: rgba(0, 0, 0, 0.5); } -.consistency-fail { - color: #F44336; - font-weight: bold; -} - -/* 보고서 표시 스타일 */ -#report-display { - background-color: rgba(255, 255, 255, 0.95); - padding: 20px; - border-radius: 12px; - max-height: 600px; - overflow-y: auto; - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); -} - -/* 분량 경고 스타일 */ -.word-count-warning { - background-color: #FFF3CD; - border-left: 4px solid #FFC107; - padding: 10px 15px; - margin: 10px 0; - border-radius: 4px; +/* 버튼 호버 효과 */ +.gr-button:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); + transition: all 0.3s ease; } """ # Gradio 인터페이스 생성 def create_interface(): - with gr.Blocks(css=custom_css, title="AI 진행형 장편소설 생성 시스템 v3.1") as interface: + with gr.Blocks(css=custom_css, title="AI 단일 작가 장편소설 시스템 v2.0") as interface: gr.HTML("""
-

- 📚 AI 진행형 장편소설 생성 시스템 v3.1 +

+ 📚 AI 단일 작가 장편소설 시스템 v2.0

-

- 목표 분량 달성을 위한 최적화 버전 +

+ 하나의 일관된 목소리로 만드는 8,000단어 통합 서사

-

- 10개의 유기적으로 연결된 단계를 통해 하나의 완전한 이야기를 만들어냅니다. +

+ 단일 작가가 10개 파트를 순차적으로 집필하며, 각 파트는 전담 비평가의 즉각적 피드백을 받아 수정됩��다.
- 각 단계는 이전 단계의 필연적 결과로 이어지며, 인물의 변화와 성장을 추적합니다. + 인과관계의 명확성서사의 유기적 진행을 최우선으로 추구합니다.

- ⚡ 반복이 아닌 축적, 순환이 아닌 진행을 통한 진정한 장편 서사 -
-
- 🆕 v3.1 핵심 개선사항: - -
-
- ⚠️ 분량 목표: 각 작가당 최소 800단어, 전체 8,000단어 이상 + 🎯 핵심 혁신: 여러 작가의 파편화된 텍스트가 아닌, + 한 명의 작가가 처음부터 끝까지 일관되게 집필하는 진정한 장편소설
""") @@ -2615,8 +1966,11 @@ def create_interface(): 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 + placeholder="""중편소설의 주제를 입력하세요. +예: 인물의 내적 변화, 관계의 발전, 사회적 갈등과 개인의 선택... +Enter your novella theme. +Ex: Character transformation, relationship evolution, social conflict and personal choice...""", + lines=5 ) language_select = gr.Radio( @@ -2626,61 +1980,55 @@ def create_interface(): ) with gr.Row(): - submit_btn = gr.Button("🚀 소설 생성 시작", variant="primary", scale=2) + submit_btn = gr.Button("🚀 집필 시작", variant="primary", scale=2) clear_btn = gr.Button("🗑️ 초기화", scale=1) status_text = gr.Textbox( - label="상태", + label="진행 상태", interactive=False, value="🔄 준비 완료" ) # 세션 관리 with gr.Group(elem_classes=["session-section"]): - gr.Markdown("### 💾 진행 중인 세션") + gr.Markdown("### 💾 진행 중인 작품") session_dropdown = gr.Dropdown( - label="세션 선택", + 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) + 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("📝 창작 진행"): + with gr.Tab("📝 집필 과정"): stages_display = gr.Markdown( - value="창작 과정이 여기에 표시됩니다...", + value="집필 과정이 실시간으로 표시됩니다...", elem_id="stages-display" ) - with gr.Tab("📖 완성된 소설"): + with gr.Tab("📖 완성 작품"): novel_output = gr.Markdown( value="완성된 소설이 여기에 표시됩니다...", elem_id="novel-output" ) with gr.Group(elem_classes=["download-section"]): - gr.Markdown("### 📥 소설 다운로드") + gr.Markdown("### 📥 작품 다운로드") with gr.Row(): format_select = gr.Radio( choices=["DOCX", "TXT"], value="DOCX" if DOCX_AVAILABLE else "TXT", - label="형식" + label="파일 형식" ) download_btn = gr.Button("⬇️ 다운로드", variant="secondary") download_file = gr.File( - label="다운로드된 파일", + label="다운로드 파일", visible=False ) - - with gr.Tab("📊 평가 보고서"): - report_display = gr.Markdown( - value="평가 보고서가 여기에 표시됩니다...", - elem_id="report-display" - ) # 숨겨진 상태 novel_text_state = gr.State("") @@ -2689,15 +2037,13 @@ def create_interface(): 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 시대에 일자리를 잃은 번역가의 새로운 도전"], - ["재개발로 사라져가는 동네에서의 마지막 계절"] + ["실직한 중년 남성이 도시를 떠나 시골에서 새로운 삶의 의미를 찾아가는 과정"], + ["전쟁 트라우마를 가진 의사가 국경없는의사회 활동을 통해 치유되는 이야기"], + ["AI에게 일자리를 빼앗긴 번역가가 고전 문학 필사를 통해 언어의 본질을 재발견하는 여정"], + ["A daughter discovering her mother's hidden past through old letters"], + ["An architect losing sight who learns to design through touch and sound"], + ["재개발로 사라질 동네 서점을 지키려는 주민들의 연대"], + ["기억을 잃어가는 노교수와 그의 마지막 제자의 일 년"] ], inputs=query_input, label="💡 주제 예시" @@ -2709,34 +2055,18 @@ def create_interface(): sessions = get_active_sessions("Korean") return gr.update(choices=sessions) except Exception as e: - logger.error(f"Error refreshing sessions: {str(e)}") - logger.error(f"Full error: {e}", exc_info=True) # 전체 스택 트레이스 로깅 + logger.error(f"세션 새로고침 오류: {str(e)}") return gr.update(choices=[]) def handle_auto_recover(language): session_id, message = auto_recover_session(language) return session_id, message - def update_displays(stages_md, novel_md, status, session_id): - """모든 디스플레이 업데이트""" - # 평가 보고서 가져오기 - report = "" - if session_id: - session = NovelDatabase.get_session(session_id) - if session and session.get('literary_report'): - report = session['literary_report'] - - return stages_md, novel_md, status, session_id, report - # 이벤트 연결 submit_btn.click( fn=process_query, inputs=[query_input, language_select, current_session_id], outputs=[stages_display, novel_output, status_text, current_session_id] - ).then( - fn=lambda s, n, st, sid: (s, n, st, sid, NovelDatabase.get_session(sid).get('literary_report', '') if sid and NovelDatabase.get_session(sid) else ''), - inputs=[stages_display, novel_output, status_text, current_session_id], - outputs=[stages_display, novel_output, status_text, current_session_id, report_display] ) novel_output.change( @@ -2753,10 +2083,6 @@ def create_interface(): fn=resume_session, inputs=[current_session_id, language_select], outputs=[stages_display, novel_output, status_text, current_session_id] - ).then( - fn=lambda s, n, st, sid: (s, n, st, sid, NovelDatabase.get_session(sid).get('literary_report', '') if sid and NovelDatabase.get_session(sid) else ''), - inputs=[stages_display, novel_output, status_text, current_session_id], - outputs=[stages_display, novel_output, status_text, current_session_id, report_display] ) auto_recover_btn.click( @@ -2767,10 +2093,6 @@ def create_interface(): fn=resume_session, inputs=[current_session_id, language_select], outputs=[stages_display, novel_output, status_text, current_session_id] - ).then( - fn=lambda s, n, st, sid: (s, n, st, sid, NovelDatabase.get_session(sid).get('literary_report', '') if sid and NovelDatabase.get_session(sid) else ''), - inputs=[stages_display, novel_output, status_text, current_session_id], - outputs=[stages_display, novel_output, status_text, current_session_id, report_display] ) refresh_btn.click( @@ -2779,8 +2101,8 @@ def create_interface(): ) clear_btn.click( - fn=lambda: ("", "", "🔄 준비 완료", "", None, ""), - outputs=[stages_display, novel_output, status_text, novel_text_state, current_session_id, report_display] + 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): @@ -2807,23 +2129,16 @@ def create_interface(): return interface - # 메인 실행 if __name__ == "__main__": - logger.info("AI 진행형 장편소설 생성 시스템 v3.1 시작...") + 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("주요 개선사항:") - logger.info("- 분량 목표 8,000단어로 조정") - logger.info("- 프롬프트 간소화") - logger.info("- 단어 수 부족 시 자동 재생성") - logger.info("- 실시간 진행률 표시") - logger.info("- 프롬프트 자동 증강 기능") - logger.info("- 인과관계와 캐릭터 일관성 강화") + logger.info(f"파트당 최소 분량: {MIN_WORDS_PER_PART:,}단어") + logger.info("시스템 특징: 단일 작가 + 파트별 즉시 비평") if BRAVE_SEARCH_API_KEY: logger.info("웹 검색이 활성화되었습니다.") @@ -2850,4 +2165,4 @@ if __name__ == "__main__": server_port=7860, share=False, debug=True - ) \ No newline at end of file + ) \ No newline at end of file