diff --git "a/app-backup.py" "b/app-backup.py" --- "a/app-backup.py" +++ "b/app-backup.py" @@ -38,7 +38,7 @@ 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_v5.db" +DB_PATH = "novel_sessions_v6.db" # 목표 분량 설정 TARGET_WORDS = 8000 # 안전 마진을 위해 8000단어 @@ -69,7 +69,7 @@ NARRATIVE_PHASES = [ "결말 2: 열린 질문" ] -# 단계별 구성 +# 단계별 구성 - 편집자 단계 추가 PROGRESSIVE_STAGES = [ ("director", "🎬 감독자: 통합된 서사 구조 기획"), ("critic", "📝 비평가: 서사 진행성과 깊이 검토"), @@ -83,6 +83,7 @@ PROGRESSIVE_STAGES = [ (f"writer{i}", f"✍️ 작가 {i}: 수정본 - {NARRATIVE_PHASES[i-1]}") for i in range(1, 11) ] + [ + ("editor", "✂️ 편집자: 반복 제거 및 서사 재구성"), ("critic", f"📝 비평가: 최종 검토 및 문학적 평가"), ] @@ -115,8 +116,173 @@ class SymbolicEvolution: 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 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 ProgressionMonitor: + """실시간 서사 진행 모니터링""" + def __init__(self): + self.phase_keywords = {} + self.locations = set() + self.characters = set() + + def count_new_characters(self, content: str, phase: int) -> int: + """새로운 인물 등장 횟수""" + # 간단한 고유명사 추출 (대문자로 시작하는 단어) + potential_names = re.findall(r'\b[A-Z가-힣][a-z가-힣]+\b', content) + new_chars = set(potential_names) - self.characters + self.characters.update(new_chars) + return len(new_chars) + + def count_new_locations(self, content: str, phase: int) -> int: + """새로운 장소 등장 횟수""" + # 장소 관련 키워드 + location_markers = ['에서', '으로', '에', '의', 'at', 'in', 'to'] + new_locs = 0 + + for marker in location_markers: + matches = re.findall(rf'(\S+)\s*{marker}', content) + for match in matches: + if match not in self.locations and len(match) > 2: + self.locations.add(match) + new_locs += 1 + + return new_locs + + def calculate_content_difference(self, current_phase: int, content: str, previous_content: str) -> float: + """이전 단계와의 내용 차이 비율""" + if not previous_content: + return 1.0 + + dedup = ContentDeduplicator() + return 1.0 - dedup.check_similarity(content, previous_content) + + def count_repetitions(self, content: str) -> int: + """반복 횟수 계산""" + paragraphs = content.split('\n\n') + repetitions = 0 + + for i, para1 in enumerate(paragraphs): + for para2 in paragraphs[i+1:]: + similarity = ContentDeduplicator().check_similarity(para1, para2) + if similarity > 0.7: + repetitions += 1 + + return repetitions + + def calculate_progression_score(self, current_phase: int, content: str, previous_content: str = "") -> Dict[str, float]: + """진행도 점수 계산""" + + scores = { + "new_elements": 0.0, # 새로운 요소 + "character_growth": 0.0, # 인물 성장 + "plot_advancement": 0.0, # 플롯 진전 + "no_repetition": 0.0 # 반복 없음 + } + + # 새로운 요소 체크 + new_characters = self.count_new_characters(content, current_phase) + new_locations = self.count_new_locations(content, current_phase) + scores["new_elements"] = min(10, (new_characters * 3 + new_locations * 2)) + + # 성장 관련 키워드 + growth_keywords = ["깨달았다", "이제는", "달라졌다", "새롭게", "비로소", "변했다", "더 이상"] + growth_count = sum(1 for k in growth_keywords if k in content) + scores["character_growth"] = min(10, growth_count * 2) + + # 플롯 진전 (이전 단계와의 차이) + if current_phase > 1 and previous_content: + diff_ratio = self.calculate_content_difference(current_phase, content, previous_content) + scores["plot_advancement"] = min(10, diff_ratio * 10) + else: + scores["plot_advancement"] = 8.0 # 첫 단계는 기본 점수 + + # 반복 체크 (역산) + repetition_count = self.count_repetitions(content) + scores["no_repetition"] = max(0, 10 - repetition_count * 2) + + return scores + + class ProgressiveNarrativeTracker: """서사 진행과 누적을 추적하는 시스템""" def __init__(self): @@ -126,10 +292,17 @@ class ProgressiveNarrativeTracker: self.phase_summaries: Dict[int, str] = {} self.accumulated_events: List[Dict[str, Any]] = [] self.thematic_deepening: List[str] = [] + self.philosophical_insights: List[str] = [] # 철학적 통찰 추적 + self.literary_devices: Dict[int, List[str]] = {} # 문학적 기법 사용 추적 + self.character_consistency = CharacterConsistency() # 캐릭터 일관성 추가 + self.content_deduplicator = ContentDeduplicator() # 중복 감지기 추가 + self.progression_monitor = ProgressionMonitor() # 진행도 모니터 추가 + self.used_expressions: Set[str] = set() # 사용된 표현 추적 def register_character_arc(self, name: str, initial_state: Dict[str, Any]): """캐릭터 아크 등록""" self.character_arcs[name] = CharacterArc(name=name, initial_state=initial_state) + self.character_consistency.register_name(0, "protagonist", name) logger.info(f"Character arc registered: {name}") def update_character_state(self, name: str, phase: int, new_state: Dict[str, Any], transformation: str): @@ -185,6 +358,25 @@ class ProgressiveNarrativeTracker: if static_symbols: issues.append(f"의미가 발전하지 않은 상징: {', '.join(static_symbols)}") + # 4. 철학적 깊이 확인 + if len(self.philosophical_insights) < current_phase // 2: + issues.append("철학적 성찰과 인간에 대한 통찰이 부족합니다") + + # 5. 문학적 기법 다양성 + unique_devices = set() + for devices in self.literary_devices.values(): + unique_devices.update(devices) + if len(unique_devices) < 5: + issues.append("문학적 기법이 단조롭습니다. 더 다양한 표현 기법이 필요합니다") + + # 6. 캐릭터 이름 일관성 + name_issues = [] + for phase, role, name in self.character_consistency.name_history: + if not self.character_consistency.validate_name(phase, role, name): + name_issues.append(f"Phase {phase}: {role} 이름 불일치 ({name})") + if name_issues: + issues.extend(name_issues) + return len(issues) == 0, issues def generate_phase_requirements(self, phase: int) -> str: @@ -195,48 +387,81 @@ class ProgressiveNarrativeTracker: if phase > 1 and (phase-1) in self.phase_summaries: requirements.append(f"이전 단계 핵심: {self.phase_summaries[phase-1]}") + # 사용된 표현 목록 + if self.used_expressions: + requirements.append("\n❌ 다음 표현/상황은 이미 사용됨 (재사용 금지):") + for expr in list(self.used_expressions)[-10:]: # 최근 10개 + requirements.append(f"- {expr[:50]}...") + # 단계별 특수 요구사항 phase_name = NARRATIVE_PHASES[phase-1] if phase <= 10 else "수정" if "도입" in phase_name: + requirements.append("\n✅ 필수 포함:") requirements.append("- 일상의 균열을 보여주되, 큰 사건이 아닌 미묘한 변화로 시작") requirements.append("- 주요 인물들의 초기 상태와 관계 설정") requirements.append("- 핵심 상징 도입 (자연스럽게)") + requirements.append("- 주인공 이름 명확히 설정") elif "발전" in phase_name: + requirements.append("\n✅ 필수 포함:") requirements.append("- 이전 단계의 균열/갈등이 구체화되고 심화") requirements.append("- 새로운 사건이나 인식이 추가되어 복잡성 증가") requirements.append("- 인물 간 관계의 미묘한 변화") + requirements.append("- 새로운 공간이나 시간대 탐색") elif "절정" in phase_name: + requirements.append("\n✅ 필수 포함:") requirements.append("- 축적된 갈등이 임계점에 도달") requirements.append("- 인물의 내적 선택이나 인식의 전환점") requirements.append("- 상징의 의미가 전복되거나 심화") + requirements.append("- 이전과는 다른 행동이나 결정") elif "하강" in phase_name: + requirements.append("\n✅ 필수 포함:") requirements.append("- 절정의 여파와 그로 인한 변화") requirements.append("- 새로운 균형점을 찾아가는 과정") requirements.append("- 인물들의 변화된 관계와 인식") + requirements.append("- 회복이나 상실의 구체적 묘사") elif "결말" in phase_name: + requirements.append("\n✅ 필수 포함:") requirements.append("- 변화된 일상의 모습") requirements.append("- 해결되지 않은 질문들") requirements.append("- 여운과 성찰의 여지") + requirements.append("- 처음과 대비되는 마지막") - # ➕ 철학·인간애 강화 체크리스트 (새로 추가) + # 철학·인간애 강화 체크리스트 requirements.append("\n📌 필수 포함 요소:") requirements.append("- 존재의 의미나 삶의 본질에 대한 성찰이 담긴 1문단 이상") requirements.append("- 타인의 고통에 대한 공감이나 연민을 보여주는 구체적 장면 1개 이상") + requirements.append("- '보여주기(showing)' 기법: 직접 설명 대신 감각적 묘사와 행동으로 표현") + requirements.append("- 이 단계만의 독특한 문학적 장치나 은유 1개 이상") # 반복 방지 요구사항 requirements.append("\n⚠️ 절대 금지사항:") requirements.append("- 이전 단계와 동일한 사건이나 갈등 반복") requirements.append("- 인물이 같은 생각이나 감정에 머무르기") requirements.append("- 플롯이 제자리걸음하기") + requirements.append("- '~을 느꼈다', '~였다'와 같은 직접적 설명") + requirements.append("- 이미 얻은 깨달음을 잊고 다시 시작하기") + + # 진행 체크리스트 + requirements.append("\n☑️ 진행 체크리스트:") + requirements.append("□ 이전 단계의 결과가 이번 단계의 원인이 되는가?") + requirements.append("□ 주인공의 내적 변화가 구체적으로 드러나는가?") + requirements.append("□ 플롯이 실제로 전진하는가?") + requirements.append("□ 새로운 정보/사건이 추가되는가?") return "\n".join(requirements) - + def extract_used_elements(self, content: str): + """사용된 핵심 표현 추출 및 저장""" + # 20자 이상의 특징적인 문장들 추출 + sentences = re.findall(r'[^.!?]+[.!?]', content) + for sent in sentences: + if len(sent) > 20 and len(sent) < 100: + self.used_expressions.add(sent.strip()) class NovelDatabase: @@ -247,6 +472,7 @@ class NovelDatabase: conn.execute("PRAGMA journal_mode=WAL") cursor = conn.cursor() + # 기존 테이블들 cursor.execute(''' CREATE TABLE IF NOT EXISTS sessions ( session_id TEXT PRIMARY KEY, @@ -274,6 +500,7 @@ class NovelDatabase: word_count INTEGER DEFAULT 0, status TEXT DEFAULT 'pending', progression_score REAL DEFAULT 0.0, + repetition_score REAL DEFAULT 0.0, created_at TEXT DEFAULT (datetime('now')), updated_at TEXT DEFAULT (datetime('now')), FOREIGN KEY (session_id) REFERENCES sessions(session_id), @@ -294,6 +521,20 @@ class NovelDatabase: ) ''') + # 새로운 테이블: 중복 감지 기록 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS duplicate_detection ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id TEXT NOT NULL, + phase INTEGER NOT NULL, + duplicate_content TEXT, + original_phase INTEGER, + similarity_score REAL, + created_at TEXT DEFAULT (datetime('now')), + FOREIGN KEY (session_id) REFERENCES sessions(session_id) + ) + ''') + conn.commit() # 기존 메서드들 유지 @@ -322,17 +563,17 @@ 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): + progression_score: float = 0.0, repetition_score: float = 0.0): word_count = len(content.split()) if content else 0 with NovelDatabase.get_db() as conn: cursor = conn.cursor() cursor.execute(''' - INSERT INTO stages (session_id, stage_number, stage_name, role, content, word_count, status, progression_score) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO stages (session_id, stage_number, stage_name, role, content, word_count, status, progression_score, repetition_score) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(session_id, stage_number) - DO UPDATE SET content=?, word_count=?, status=?, stage_name=?, progression_score=?, updated_at=datetime('now') - ''', (session_id, stage_number, stage_name, role, content, word_count, status, progression_score, - content, word_count, status, stage_name, progression_score)) + DO UPDATE SET content=?, word_count=?, status=?, stage_name=?, progression_score=?, repetition_score=?, updated_at=datetime('now') + ''', (session_id, stage_number, stage_name, role, content, word_count, status, progression_score, repetition_score, + content, word_count, status, stage_name, progression_score, repetition_score)) # 총 단어 수 업데이트 cursor.execute(''' @@ -395,7 +636,11 @@ class NovelDatabase: '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 + 'thematic_deepening': tracker.thematic_deepening, + 'philosophical_insights': tracker.philosophical_insights, + 'literary_devices': tracker.literary_devices, + 'character_consistency': asdict(tracker.character_consistency), + 'used_expressions': list(tracker.used_expressions) }) conn.cursor().execute( 'UPDATE sessions SET narrative_tracker = ? WHERE session_id = ?', @@ -423,10 +668,32 @@ class NovelDatabase: tracker.plot_threads[thread_id] = PlotThread(**thread_data) tracker.phase_summaries = data.get('phase_summaries', {}) tracker.thematic_deepening = data.get('thematic_deepening', []) + tracker.philosophical_insights = data.get('philosophical_insights', []) + tracker.literary_devices = data.get('literary_devices', {}) + + # 캐릭터 일관성 복원 + if 'character_consistency' in data: + tracker.character_consistency = CharacterConsistency(**data['character_consistency']) + + # 사용된 표현 복원 + if 'used_expressions' in data: + tracker.used_expressions = set(data['used_expressions']) return tracker return None + @staticmethod + def save_duplicate_detection(session_id: str, phase: int, duplicate_content: str, + original_phase: int, similarity_score: float): + """중복 감지 기록 저장""" + with NovelDatabase.get_db() as conn: + conn.cursor().execute(''' + INSERT INTO duplicate_detection + (session_id, phase, duplicate_content, original_phase, similarity_score) + VALUES (?, ?, ?, ?, ?) + ''', (session_id, phase, duplicate_content, original_phase, similarity_score)) + conn.commit() + @staticmethod def get_session(session_id: str) -> Optional[Dict]: with NovelDatabase.get_db() as conn: @@ -523,20 +790,25 @@ class ProgressiveLiterarySystem: """감독자 초기 기획 - 통합된 서사 구조""" search_results_str = "" if self.web_search.enabled: - # 철학적 키워드 추가 (수정됨) + # 철학적 키워드 추가 (쿼리 길이 제한) + short_query = user_query[:50] if len(user_query) > 50 else user_query queries = [ - f"{user_query} 철학적 의미", # ➕ 철학적 관점 - f"인간 존재 의미 {user_query}", # ➕ 실존적 주제 - f"{user_query} 사회 문제", - f"{user_query} 현대 한국" + f"{short_query} 철학적 의미", # 철학적 관점 + f"인간 존재 의미 {short_query}", # 실존적 주제 + f"{short_query} 문학 작품", + f"{short_query} 현대 사회" ] for q in queries[:3]: # 3개까지만 검색 - results = self.web_search.search(q, count=2, language=language) - if results: - search_results_str += self.web_search.extract_relevant_info(results) + "\n" + try: + results = self.web_search.search(q, count=2, language=language) + if results: + search_results_str += self.web_search.extract_relevant_info(results) + "\n" + except Exception as e: + logger.warning(f"검색 쿼리 실패: {q[:50]}... - {str(e)}") + continue lang_prompts = { - "Korean": f"""당신은 현대 한국 문학의 거장입니다. + "Korean": f"""당신은 노벨문학상 수상작가 수준의 한국 문학 거장입니다. 단편이 아닌 중편 소설(8,000단어 이상)을 위한 통합된 서사 구조를 기획하세요. 절대 '수동태'를 사용하지 마세요. **주제:** {user_query} @@ -550,6 +822,7 @@ class ProgressiveLiterarySystem: - 10개 단계가 유기적으로 연결된 단일 서사 - 각 단계는 이전 단계의 결과로 자연스럽게 이어짐 - 반복이 아닌 축적과 발전 + - 주인공 이름은 처음부터 명확히 설정 (예: 나라미) 단계별 서사 진행: 1) 도입: 일상과 균열 - 평범한 일상 속 첫 균열 @@ -567,24 +840,33 @@ class ProgressiveLiterarySystem: - 주인공: 초기 상태 → 중간 변화 → 최종 상태 (명확한 arc) - 주요 인물들도 각자의 변화 경험 - 관계의 역동적 변화 + - 각 단계에서 인물이 어떻게 변화하는지 구체적으로 명시 3. **주요 플롯 라인** (2-3개) - 메인 플롯: 전체를 관통하는 핵심 갈등 - 서브 플롯: 메인과 연결되며 주제를 심화 + - 각 플롯이 어느 단계에서 시작/발전/해결되는지 명시 4. **상징의 진화** - - 핵심 상징 1-2개 설정 + - 핵심 상징 1-2개 설정 ('개구리알' 같은 강렬하고 다층적인 상징) - 단계별로 의미가 변화/심화/전복 5. **사회적 맥락** - 개인의 문제가 사회 구조와 연결 - 구체적인 한국 사회의 현실 반영 +6. **철학적 깊이와 인간애** + - 보편적 인간 조건에 대한 성찰 + - 타인의 고통에 대한 공감과 연민 + - 실존적 질문과 그에 대한 탐구 + - "왜 살아야 하는가"에 대한 나름의 답 + **절대 금지사항:** - 동일한 사건이나 상황의 반복 - 인물이 같은 감정/생각에 머무르기 - 플롯의 리셋이나 순환 구조 - 각 단계가 독립된 에피소드로 존재 +- 주인공 이름의 불일치나 혼동 **분량 계획:** - 총 8,000단어 이상 @@ -593,7 +875,7 @@ class ProgressiveLiterarySystem: 하나의 강력한 서사가 시작부터 끝까지 관통하는 작품을 기획하세요.""", - "English": f"""You are a master of contemporary literary fiction. + "English": f"""You are a Nobel Prize-winning master of contemporary literary fiction. Plan an integrated narrative structure for a novella (8,000+ words), not a collection of short stories. **Theme:** {user_query} @@ -607,6 +889,7 @@ Plan an integrated narrative structure for a novella (8,000+ words), not a colle - Single narrative with 10 organically connected phases - Each phase naturally follows from previous results - Accumulation and development, not repetition + - Protagonist name clearly established from beginning Phase Progression: 1) Introduction: Daily life and first crack @@ -624,24 +907,33 @@ Plan an integrated narrative structure for a novella (8,000+ words), not a colle - Protagonist: Clear progression from initial → middle → final state - Supporting characters also experience change - Dynamic relationship evolution + - Specify how characters change in each phase 3. **Plot Threads** (2-3) - Main plot: Core conflict throughout - Subplots: Connected and deepening themes + - Specify which phase each plot starts/develops/resolves 4. **Symbolic Evolution** - - 1-2 core symbols + - 1-2 core symbols (like 'frog eggs' - intense and multilayered) - Meaning transforms across phases 5. **Social Context** - Individual problems connected to social structures - Specific contemporary realities +6. **Philosophical Depth and Humanity** + - Reflection on universal human condition + - Empathy and compassion for others' suffering + - Existential questions and exploration + - Personal answer to "why should we live?" + **Absolutely Forbidden:** - Repetition of same events/situations - Characters stuck in same emotions - Plot resets or circular structure - Independent episodes +- Protagonist name inconsistency **Length Planning:** - Total 8,000+ words @@ -653,9 +945,6 @@ Create a work with one powerful narrative from beginning to end.""" return lang_prompts.get(language, lang_prompts["Korean"]) - - - def create_critic_director_prompt(self, director_plan: str, user_query: str, language: str) -> str: """비평가의 감독자 기획 검토 - 서사 통합성 중심""" lang_prompts = { @@ -678,6 +967,7 @@ Create a work with one powerful narrative from beginning to end.""" - 주인공이 명확한 변화의 arc를 가지는가? - 변화가 구체적이고 신빙성 있는가? - 관계의 발전이 계획되어 있는가? + - 주인공 이름이 일관되게 설정되어 있는가? 3. **플롯의 축적성** - 갈등이 점진적으로 심화되는가? @@ -688,6 +978,10 @@ Create a work with one powerful narrative from beginning to end.""" - 8,000단어를 채울 충분한 내용인가? - 각 단계가 800단어의 밀도를 가질 수 있는가? +5. **철학적 깊이** + - 인간 존재에 대한 통찰이 계획되어 있는가? + - 단순한 사건 나열이 아닌 의미의 탐구가 있는가? + **판정:** - 통과: 진정한 장편 서사 구조 - 재작성: 반복적/순환적 구조 @@ -713,6 +1007,7 @@ Strictly review whether this plan is a true 'novel' rather than repeated episode - Clear protagonist transformation arc? - Concrete and credible changes? - Planned relationship development? + - Consistent protagonist naming? 3. **Plot Accumulation** - Progressive conflict deepening? @@ -723,6 +1018,10 @@ Strictly review whether this plan is a true 'novel' rather than repeated episode - Sufficient content for 8,000 words? - Can each phase sustain 800 words? +5. **Philosophical Depth** + - Insights into human existence planned? + - Exploration of meaning, not just events? + **Verdict:** - Pass: True novel structure - Rewrite: Repetitive/circular structure @@ -732,10 +1031,11 @@ Provide specific improvements.""" return lang_prompts.get(language, lang_prompts["Korean"]) - def create_writer_prompt(self, writer_number: int, director_plan: str, - previous_content: str, phase_requirements: str, - narrative_summary: str, language: str) -> str: - """작가 프롬프트 - 서사 진행 강제""" + 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 @@ -756,6 +1056,9 @@ Provide specific improvements.""" **이번 단계 필수 요구사항:** {phase_requirements} +**❌ 절대 사용 금지 표현/상황 (이미 사용됨):** +{chr(10).join(f"- {elem[:80]}..." for elem in used_elements[-15:])} + **작성 지침:** 1. **분량**: {target_words}-900 단어 (필수) @@ -766,11 +1069,13 @@ Provide specific improvements.""" - 이전 단계에서 일어난 일의 직접적 결과로 시작 - 새로운 사건/인식/변화를 추가하여 이야기 전진 - 다음 단계로 자연스럽게 연결될 고리 마�� + - 주인공의 깨달음이 리셋되지 않고 축적됨 3. **인물의 변화** - 이 단계에서 인물이 겪는 구체적 변화 묘사 - 내면의 미묘한 변화도 포착 - 관계의 역학 변화 반영 + - 이전 단계보다 성장한 모습 보여주기 4. **문체와 기법** - 한국 현대 문학의 섬세한 심리 묘사 @@ -781,11 +1086,36 @@ Provide specific improvements.""" - 인물의 목소리와 말투 일관성 - 공간과 시간의 연속성 - 상징과 모티프의 발전 + - 주인공 이름 일관성 (반드시 확인) + +6. **문학적 기법 필수 사용** + - "보여주기(showing)" 기법: 직접 설명 대신 감각적 묘사 + - 은유와 상징: 구체적 사물을 통한 추상적 의미 전달 + - 대화를 통한 성격 드러내기 + - 내적 독백과 의식의 흐름 기법 + +7. **철학적 성찰 포함** + - 각 단계마다 인간 존재에 대한 새로운 통찰 1개 이상 + - 구체적 사건 속에서 보편적 진리 발견 + +8. **새로운 요소 필수** + - 새로운 인물, 장소, 또는 상황 중 최소 1개 + - 이전과 다른 시간대나 공간 + - 갈등의 새로운 측면 드러내기 + +**✅ 진행 체크리스트:** +□ 이전 단계의 결과가 명확히 드러나는가? +□ 플롯이 실제로 전진하는가? +□ 인물의 변화가 구체적인가? +□ 새로운 요소가 추가되었는가? +□ 반복되는 상황이 없는가? **절대 금지:** - 이전과 동일한 상황 반복 - 서사의 정체나 후퇴 - 분량 미달 (최소 {target_words}단어) +- "~을 느꼈다", "~였다"와 같은 직접적 설명 +- 이미 얻은 깨달음 잊기 이전의 흐름을 이어받아 새로운 국면으로 발전시키세요.""", @@ -804,6 +1134,9 @@ Provide specific improvements.""" **Phase Requirements:** {phase_requirements} +**❌ Absolutely Forbidden Expressions/Situations (already used):** +{chr(10).join(f"- {elem[:80]}..." for elem in used_elements[-15:])} + **Writing Guidelines:** 1. **Length**: {target_words}-900 words (mandatory) @@ -814,11 +1147,13 @@ Provide specific improvements.""" - Start as direct result of previous phase - Add new events/awareness/changes to advance story - Create natural connection to next phase + - Accumulated insights, not reset 3. **Character Change** - Concrete changes in this phase - Capture subtle interior shifts - Reflect relationship dynamics + - Show growth from previous phase 4. **Style and Technique** - Delicate psychological portrayal @@ -829,28 +1164,61 @@ Provide specific improvements.""" - Consistent character voices - Spatial/temporal continuity - Symbol/motif development + - Consistent protagonist naming + +6. **Literary Techniques Required** + - "Showing" not telling + - Metaphors and symbols + - Character through dialogue + - Stream of consciousness + +7. **Philosophical Reflection** + - New insights about human existence + - Universal truths in specific events + +8. **New Elements Required** + - At least 1 new character, location, or situation + - Different time/space from before + - New aspect of conflict + +**✅ Progress Checklist:** +□ Clear results from previous phase? +□ Plot actually advancing? +□ Concrete character changes? +□ New elements added? +□ No repeated situations? **Absolutely Forbidden:** - Repeating previous situations - Narrative stagnation/regression -- Under word count (minimum {target_words}) +- Under word count +- Direct explanations +- Forgetting gained insights Continue the flow and develop into new phase.""" } return lang_prompts.get(language, lang_prompts["Korean"]) - def create_critic_consistency_prompt(self, all_content: str, - narrative_tracker: ProgressiveNarrativeTracker, - user_query: str, language: str) -> str: - """비평가 중간 검토 - 서사 누적성 확인""" - - # 서사 진행 체크 + def create_critic_consistency_prompt_enhanced(self, all_content: str, + narrative_tracker: ProgressiveNarrativeTracker, + user_query: str, language: str) -> str: + """강화된 비평가 중간 검토 - 반복 검출 강화""" + + # 서사 진행 체크 phase_count = len(narrative_tracker.phase_summaries) progression_ok, issues = narrative_tracker.check_narrative_progression(phase_count) + + # 중복 감지 + duplicates = [] + paragraphs = all_content.split('\n\n') + for i, para1 in enumerate(paragraphs[:20]): # 최근 20개 문단 + for j, para2 in enumerate(paragraphs[i+1:i+21]): + if narrative_tracker.content_deduplicator.check_similarity(para1, para2) > 0.7: + duplicates.append(f"문단 {i+1}과 문단 {i+j+2} 유사") lang_prompts = { - "Korean": f"""서사 진행 전문 비평가로서 작품을 검토하세요. + "Korean": f"""서사 진행 전문 비평가로서 작품을 엄격히 검토하세요. **원 주제:** {user_query} @@ -859,34 +1227,51 @@ Continue the flow and develop into new phase.""" **발견된 진행 문제:** {chr(10).join(issues) if issues else "없음"} +**발견된 중복:** +{chr(10).join(duplicates) if duplicates else "없음"} + **작품 내용 (최근 부분):** {all_content[-4000:]} -**집중 검토 사항:** +**필수 검증 항목:** -1. **서사의 축적과 진행** - - 이야기가 실제로 전진하고 있는가? - - 각 단계가 이전의 결과로 연결되는가? - - 동일한 갈등이나 상황이 반복되지 않는가? +1. **반복 검출 (최우선)** + - 동일/유사 문장이 2회 이상 나타나는가? + - 같은 상황이 변주만 달리해 반복되는가? + - 각 단계별로 실제로 새로운 내용이 추가되었는가? + - "습기가 찬 아침", "나라미 어플", "43만원" 등 반복 표현? -2. **인물의 변화 궤적** - - 주인공이 초기와 비교해 어떻게 변했는가? - - 변화가 설득력 있고 점진적인가? - - 관계가 역동적으로 발전하는가? +2. **서사 진행도 측정** + - 1단계와 현재 단계의 상황이 명확히 다른가? + - 주인공의 심리/인식이 변화했는가? + - 갈등이 심화/전환/해결 방향으로 움직였는가? + - 주인공의 깨달음이 리셋되지 않고 축적되는가? -3. **주제의 심화** - - 초기 주제가 어떻게 발전했는가? - - 새로운 층위가 추가되었는가? - - 복잡성이 증가했는가? +3. **설정 일관성** + - 모든 캐릭터 이름이 일관되는가? (특히 주인공) + - 시공간 설정이 논리적인가? + - 설정이 중간에 바뀌지 않는가? 4. **분량과 밀도** - 현재까지 총 단어 수 확인 - 목표(8,000단어)에 도달 가능한가? +5. **문학적 완성도** + - '보여주기' 기법이 잘 사용되고 있는가? + - 철학적 통찰이 자연스럽게 녹아있는가? + - 은유와 상징이 효과적인가? + +**불합격 기준:** +- 2개 이상 단계에서 유사 내용 발견 시 +- 서사가 제자리걸음하는 구간 2개 이상 +- 캐릭터 이름/설정 오류 발견 시 +- 주인공 깨달음의 반복적 리셋 + **수정 지시:** -각 작가에게 구체적인 진행 방향 제시.""", +각 작가에게 구체적인 진행 방향과 금지사항 제시. +발견된 반복은 모두 제거하도록 명시.""", - "English": f"""As a narrative progression critic, review the work. + "English": f"""As a narrative progression critic, strictly review the work. **Original Theme:** {user_query} @@ -895,42 +1280,60 @@ Continue the flow and develop into new phase.""" **Detected Progression Issues:** {chr(10).join(issues) if issues else "None"} +**Detected Duplications:** +{chr(10).join(duplicates) if duplicates else "None"} + **Work Content (recent):** {all_content[-4000:]} -**Focus Review Areas:** +**Mandatory Verification Items:** -1. **Narrative Accumulation and Progress** - - Is story actually moving forward? - - Does each phase connect as result of previous? - - No repetition of same conflicts/situations? +1. **Duplication Detection (Top Priority)** + - Same/similar sentences appearing 2+ times? + - Same situations with only variations? + - Actually new content in each phase? + - Repeated expressions like specific phrases? -2. **Character Transformation Arcs** - - How has protagonist changed from beginning? - - Are changes credible and gradual? - - Dynamic relationship development? +2. **Narrative Progression Measurement** + - Clear difference between phase 1 and current? + - Protagonist's psychology/perception changed? + - Conflict deepening/turning/resolving? + - Insights accumulating, not resetting? -3. **Thematic Deepening** - - How has initial theme developed? - - New layers added? - - Increased complexity? +3. **Setting Consistency** + - All character names consistent? (especially protagonist) + - Logical space/time settings? + - Settings not changing mid-story? 4. **Length and Density** - Current total word count - Can reach 8,000 word target? +5. **Literary Completion** + - "Showing" technique well used? + - Philosophical insights naturally integrated? + - Effective metaphors and symbols? + +**Failure Criteria:** +- Similar content in 2+ phases +- 2+ sections of narrative stagnation +- Character name/setting errors +- Repeated resetting of insights + **Revision Instructions:** -Specific progression directions for each writer.""" +Specific progression directions and prohibitions for each writer. +All detected repetitions must be removed.""" } - + return lang_prompts.get(language, lang_prompts["Korean"]) + def create_writer_revision_prompt(self, writer_number: int, initial_content: str, - critic_feedback: str, language: str) -> str: - """작가 수정 프롬프트""" - target_words = MIN_WORDS_PER_WRITER - - return f"""작가 {writer_number}번, 비평을 반영하여 수정하세요. + critic_feedback: str, language: str) -> str: + """작가 수정 프롬프트""" + target_words = MIN_WORDS_PER_WRITER + + return f"""작가 {writer_number}번, 비평을 반영하여 수정하세요. **초안:** {initial_content} @@ -943,15 +1346,115 @@ Specific progression directions for each writer.""" 2. 인물 변화 구체화 - 이전과 달라진 모습 명확히 3. 분량 확보 - 최소 {target_words}단어 유지 4. 내면 묘사와 사회적 맥락 심화 +5. '보여주기' 기법 강화 - 직접 설명을 감각적 묘사로 대체 +6. 철학적 통찰 자연스럽게 포함 +7. 반복된 표현/상황 완전 제거 +8. 주인공 이름 일관성 확인 + +**특별 주의사항:** +- 이미 사용된 "습기가 찬 아침", "나라미 어플", "43만원" 등의 표현 변경 +- 주인공의 깨달음이 이전보다 발전된 형태로 표현 +- 새로운 디테일과 감각적 묘사 추가 전면 재작성이 필요하면 과감히 수정하세요. 수정본만 제시하세요.""" + def create_editor_prompt(self, complete_novel: str, issues: List[str], language: str) -> str: + """편집자 프롬프트 - 반복 제거 전문""" + + lang_prompts = { + "Korean": f"""당신은 전문 편집자입니다. +완성된 원고에서 반복을 제거하고 서사를 매끄럽게 연결하세요. + +**발견된 문제:** +{chr(10).join(issues)} + +**편집 지침:** + +1. **반복 제거 (최우선)** + - 동일하거나 유사한 문단은 가장 효과적인 것 하나만 남기고 삭제 + - "습기가 찬 아침", "나라미 어플 43만원" 등 반복 표현 중 하나만 유지 + - 비슷한 장면(연못 응시, 계란 던지기 등)은 가장 강렬한 것만 선택 + +2. **서사 재구성** + - 남은 장면들을 인과관계에 따라 재배열 + - 시간 순서와 감정의 흐름이 자연스럽게 연결되도록 + - 필요시 짧은 전환 문단 추가 (2-3문장) + +3. **캐릭터 일관성** + - 주인공 이름을 '나라미'로 통일 + - 다른 인물들의 이름도 일관성 확인 + - 인물의 성격과 말투 일관성 유지 + +4. **깨달음의 누적** + - 주인공의 각 깨달음이 이전보다 발전된 형태로 표현되도록 + - 동일한 수준의 인식 반복 제거 + - 마지막으로 갈수록 더 깊은 통찰이 되도록 + +5. **분량 조정** + - 반복 제거 후에도 8,000단어 이상 유지 + - 필요시 남은 장면들을 약간 확장 (묘사 추가) + +**편집 규칙:** +- 작가의 원문 스타일과 문체는 최대한 보존 +- 새로운 사건이나 인물 추가 금지 +- 핵심 상징과 주제 손상 금지 +- 원문의 철학적 깊이 유지 + +**결과물:** +반복이 완전히 제거되고 자연스럽게 흐르는 최종 원고를 제시하세요. +편집 전후의 주요 변경사항도 간단히 요약하세요.""", + + "English": f"""You are a professional editor. +Remove repetitions and smooth narrative connections in the completed manuscript. + +**Identified Issues:** +{chr(10).join(issues)} + +**Editing Guidelines:** + +1. **Repetition Removal (Top Priority)** + - Keep only most effective version of similar paragraphs + - Retain only one instance of repeated expressions + - Select most powerful version of similar scenes + +2. **Narrative Reconstruction** + - Rearrange remaining scenes by causality + - Natural flow of time and emotion + - Add brief transitions if needed (2-3 sentences) + +3. **Character Consistency** + - Unify protagonist name + - Check other character name consistency + - Maintain character personality/voice + +4. **Insight Accumulation** + - Each insight more developed than previous + - Remove same-level recognition repetitions + - Deeper insights toward the end + +5. **Length Adjustment** + - Maintain 8,000+ words after cuts + - Slightly expand remaining scenes if needed + +**Editing Rules:** +- Preserve original style and voice +- No new events or characters +- Protect core symbols and themes +- Maintain philosophical depth + +**Output:** +Present final manuscript with repetitions removed and natural flow. +Briefly summarize major changes.""" + } + + return lang_prompts.get(language, lang_prompts["Korean"]) + def create_critic_final_prompt(self, complete_novel: str, word_count: int, language: str) -> str: - """최종 비평 - AGI 평가 기준""" - - lang_prompts = { - "Korean": f"""완성된 소설을 AGI 튜링테스트 기준으로 평가하세요. + """최종 비평 - AGI 평가 기준""" + + lang_prompts = { + "Korean": f"""완성된 소설을 AGI 튜링테스트 기준으로 평가하세요. **작품 정보:** - 총 분량: {word_count}단어 @@ -962,33 +1465,45 @@ Specific progression directions for each writer.""" **평가 기준 (AGI 튜링테스트):** -1. **장편소설로서의 완성도 (30점)** # 40→30점으로 조정 - - 통합된 서사 구조 (반복 없음) - - 인물의 명확한 변화 arc - - 플롯의 축적과 해결 - - 8,000단어 이상 분량 - -2. **문학적 성취 (35점)** # 30→35점으로 상향 - - 주제 의식의 깊이 - - 인물 심리의 설득력 - - 문체의 일관성과 아름다움 - - 상징과 은유의 효과 - -3. **사회적 통찰 (25점)** # 20→25점으로 상향 - - 현대 사회 문제 포착 - - 개인과 구조의 연결 - - 보편성과 특수성 균형 +1. **장편소설로서의 완성도 (30점)** + - 통합된 서사 구조 (반복 없음) + - 인물의 명확한 변화 arc + - 플롯의 축적과 해결 + - 8,000단어 이상 분량 + - 설정 일관성 (특히 인물 이름) + +2. **문학적 성취 (35점)** + - 주제 의식의 깊이 + - 인물 심리의 설득력 + - 문체의 일관성과 아름다움 + - 상징과 은유의 효과 + - '보여주기' 기법의 활용도 + +3. **사회적 통찰 (25점)** + - 현대 사회 문제 포착 + - 개인과 구조의 연결 + - 보편성과 특수성 균형 + - 인간 조건에 대한 통찰 4. **독창성과 인간성 (10점)** - - AI가 아닌 인간 작가의 느낌 - - 독창적 표현과 통찰 - - 감정적 진정성 + - AI가 아닌 인간 작가의 느낌 + - 독창적 표현과 통찰 + - 감정적 진정성 + - 철학적 깊이 + +**특별 감점 요인:** +- 내용/문장 반복 (-5점 per 중대 반복) +- 캐릭터 이름 불일치 (-3점) +- 서사 정체/순환 (-5점) +- 깨달음 리셋 현상 (-3점) **총점: /100점** -특히 '반복 구조' 문제가 있었는지 엄격히 평가하세요.""", +특히 '반복 구조' 문제가 있었는지 엄격히 평가하세요. +'개구리알' 같은 강렬한 중심 상징이 있는지 확인하세요. +편집 후에도 남은 반복이 있는지 세밀히 검토하세요.""", - "English": f"""Evaluate the completed novel by AGI Turing Test standards. + "English": f"""Evaluate the completed novel by AGI Turing Test standards. **Work Information:** - Total length: {word_count} words @@ -999,288 +1514,361 @@ Specific progression directions for each writer.""" **Evaluation Criteria (AGI Turing Test):** -1. **Completion as Novel (30 points)** # 40→30 points adjusted - - Integrated narrative structure (no repetition) - - Clear character transformation arcs - - Plot accumulation and resolution - - 8,000+ word length - -2. **Literary Achievement (35 points)** # 30→35 points increased - - Depth of thematic consciousness - - Persuasiveness of character psychology - - Consistency and beauty of style - - Effectiveness of symbols and metaphors - -3. **Social Insight (25 points)** # 20→25 points increased - - Capturing contemporary social issues - - Connection between individual and structure - - Balance of universality and specificity +1. **Completion as Novel (30 points)** + - Integrated narrative structure (no repetition) + - Clear character transformation arcs + - Plot accumulation and resolution + - 8,000+ word length + - Setting consistency (especially names) + +2. **Literary Achievement (35 points)** + - Depth of thematic consciousness + - Persuasiveness of character psychology + - Consistency and beauty of style + - Effectiveness of symbols and metaphors + - Use of "showing" technique + +3. **Social Insight (25 points)** + - Capturing contemporary social issues + - Connection between individual and structure + - Balance of universality and specificity + - Insights into human condition 4. **Originality and Humanity (10 points)** - - Feeling of human author, not AI - - Original expressions and insights - - Emotional authenticity + - Feeling of human author, not AI + - Original expressions and insights + - Emotional authenticity + - Philosophical depth + +**Special Deductions:** +- Content/sentence repetition (-5 points per major repetition) +- Character name inconsistency (-3 points) +- Narrative stagnation/cycling (-5 points) +- Insight reset phenomenon (-3 points) **Total Score: /100 points** -Strictly evaluate whether there are 'repetitive structure' issues.""" - } - - return lang_prompts.get(language, lang_prompts["Korean"]) +Strictly evaluate whether there are 'repetitive structure' issues. +Check for powerful central symbols like 'frog eggs'. +Carefully review for any remaining repetitions after editing.""" + } + + return lang_prompts.get(language, lang_prompts["Korean"]) - # --- LLM 호출 함수들 --- + # --- 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 + full_content = "" + for chunk in self.call_llm_streaming(messages, role, language): + full_content += chunk + if full_content.startswith("❌"): + raise Exception(f"LLM Call Failed: {full_content}") + return full_content def call_llm_streaming(self, messages: List[Dict[str, str]], role: str, language: str) -> Generator[str, None, None]: - try: - system_prompts = self.get_system_prompts(language) - full_messages = [{"role": "system", "content": system_prompts.get(role, "")}, *messages] - - # 작가 역할일 때는 더 많은 토큰 허용 - max_tokens = 15000 if role.startswith("writer") else 10000 - - payload = { - "model": self.model_id, - "messages": full_messages, - "max_tokens": max_tokens, - "temperature": 0.8, - "top_p": 0.95, - "presence_penalty": 0.5, - "frequency_penalty": 0.3, - "stream": True - } - - response = requests.post( - self.api_url, - headers=self.create_headers(), - json=payload, - stream=True, - timeout=180 - ) - - if response.status_code != 200: - yield f"❌ API 오류 (상태 코드: {response.status_code})" - return - - buffer = "" - for line in response.iter_lines(): - if not line: - continue - - try: - line_str = line.decode('utf-8').strip() - if not line_str.startswith("data: "): - continue - - data_str = line_str[6:] - if data_str == "[DONE]": - break - - data = json.loads(data_str) - choices = data.get("choices", []) - if choices and choices[0].get("delta", {}).get("content"): - content = choices[0]["delta"]["content"] - buffer += content - - if len(buffer) >= 50 or '\n' in buffer: - yield buffer - buffer = "" - time.sleep(0.01) - - except Exception as e: - logger.error(f"청크 처리 오류: {str(e)}") - continue - - if buffer: - yield buffer - - except Exception as e: - logger.error(f"스트리밍 오류: {type(e).__name__}: {str(e)}") - yield f"❌ 오류 발생: {str(e)}" + try: + system_prompts = self.get_system_prompts(language) + full_messages = [{"role": "system", "content": system_prompts.get(role, "")}, *messages] + + # 작가와 편집자 역할일 때는 더 많은 토큰 허용 + max_tokens = 15000 if role.startswith("writer") or role == "editor" else 10000 + + payload = { + "model": self.model_id, + "messages": full_messages, + "max_tokens": max_tokens, + "temperature": 0.8, + "top_p": 0.95, + "presence_penalty": 0.5, + "frequency_penalty": 0.3, + "stream": True + } + + response = requests.post( + self.api_url, + headers=self.create_headers(), + json=payload, + stream=True, + timeout=180 + ) + + if response.status_code != 200: + yield f"❌ API 오류 (상태 코드: {response.status_code})" + return + + buffer = "" + for line in response.iter_lines(): + if not line: + continue + + try: + line_str = line.decode('utf-8').strip() + if not line_str.startswith("data: "): + continue + + data_str = line_str[6:] + if data_str == "[DONE]": + break + + data = json.loads(data_str) + choices = data.get("choices", []) + if choices and choices[0].get("delta", {}).get("content"): + content = choices[0]["delta"]["content"] + buffer += content + + if len(buffer) >= 50 or '\n' in buffer: + yield buffer + buffer = "" + time.sleep(0.01) + + except Exception as e: + logger.error(f"청크 처리 오류: {str(e)}") + continue + + if buffer: + yield buffer + + except Exception as e: + logger.error(f"스트리밍 오류: {type(e).__name__}: {str(e)}") + yield f"❌ 오류 발생: {str(e)}" def get_system_prompts(self, language: str) -> Dict[str, str]: - """역할별 시스템 프롬프트""" - - base_prompts = { - "Korean": { - "director": """당신은 한국 현대 문학의 거장입니다. + """역할별 시스템 프롬프트""" + + base_prompts = { + "Korean": { + "director": """당신은 노벨문학상 수상작가 수준의 한국 문학 거장입니다. +인간 존재의 보편적 조건과 한국 사회의 특수성을 동시에 포착하세요. +'개구리알' 같은 강렬하고 다층적인 중심 상징을 창조하세요. +철학적 깊이와 문학적 아름다움을 동시에 추구하세요. 반복이 아닌 진행, 순환이 아닌 발전을 통해 하나의 강력한 서사를 구축하세요. -개인의 문제를 사회 구조와 연결하며, 인물의 진정한 변화를 그려내세요.""", - - "critic": """당신은 엄격한 문학 비평가입니다. +주인공의 이름과 설정을 명확히 하고 일관성을 유지하세요.""", + + "critic": """당신은 엄격한 문학 비평가입니다. 특히 '반복 구조'와 '서사 정체'를 철저히 감시하세요. -작품이 진정한 장편소설인지, 아니면 반복되는 단편의 집합인지 구별하세요.""", - - "writer_base": """당신은 현대 한국 문학 작가입니다. +작품이 진정한 장편소설인지, 아니면 반복되는 단편의 집합인지 구별하세요. +문학적 기법의 효과성과 철학적 깊이를 평가하세요. +캐릭터 이름과 설정의 일관성을 반드시 확인하세요. +동일한 문장이나 상황의 반복을 절대 용납하지 마세요.""", + + "writer_base": """당신은 현대 한국 문학 작가입니다. +'보여주기' 기법을 사용하여 독자의 상상력을 자극하세요. +직접적 설명보다 감각적 묘사와 행동으로 감정을 전달하세요. +각 장면에서 인간 존재에 대한 새로운 통찰을 담으세요. 이전 단계의 결과를 받아 새로운 국면으로 발전시키세요. 최소 800단어를 작성하며, 내면과 사회를 동시에 포착하세요. -절대 이전과 같은 상황을 반복하지 마세요.""" - }, - "English": { - "director": """You are a master of contemporary literary fiction. +절대 이전과 같은 상황을 반복하지 마세요. +주인공의 이름과 설정을 일관되게 유지하세요. +이미 얻은 깨달음을 잊지 말고 발전시키세요.""", + + "editor": """당신은 경험이 풍부한 문학 편집자입니다. +반복을 철저히 제거하고 서사의 흐름을 매끄럽게 만드세요. +원작의 문체와 주제는 보존하면서 구조적 문제를 해결하세요. +캐릭터 이름과 설정의 일관성을 확보하세요. +깨달음이 누적되고 발전하도록 편집하세요.""" + }, + "English": { + "director": """You are a Nobel Prize-winning master of contemporary literary fiction. +Capture both universal human condition and specific social realities. +Create intense, multilayered central symbols like 'frog eggs'. +Pursue both philosophical depth and literary beauty. Build one powerful narrative through progression not repetition, development not cycles. -Connect individual problems to social structures while depicting genuine character transformation.""", - - "critic": """You are a strict literary critic. +Establish protagonist's name and settings clearly with consistency.""", + + "critic": """You are a strict literary critic. Vigilantly monitor for 'repetitive structure' and 'narrative stagnation'. -Distinguish whether this is a true novel or a collection of repeated episodes.""", - - "writer_base": """You are a contemporary literary writer. +Distinguish whether this is a true novel or a collection of repeated episodes. +Evaluate effectiveness of literary techniques and philosophical depth. +Always check character name and setting consistency. +Never tolerate repetition of same sentences or situations.""", + + "writer_base": """You are a contemporary literary writer. +Use 'showing' technique to stimulate reader's imagination. +Convey emotions through sensory description and action rather than explanation. +Include new insights about human existence in each scene. Take results from previous phase and develop into new territory. Write minimum 800 words, capturing both interior and society. -Never repeat previous situations.""" - } - } - - prompts = base_prompts.get(language, base_prompts["Korean"]).copy() - - # 특수 작가 프롬프트 - for i in range(1, 11): - prompts[f"writer{i}"] = prompts["writer_base"] - - return prompts - - # --- 메인 프로세스 --- +Never repeat previous situations. +Maintain protagonist's name and settings consistently. +Don't forget gained insights, develop them further.""", + + "editor": """You are an experienced literary editor. +Thoroughly remove repetitions and smooth narrative flow. +Preserve original style and themes while solving structural issues. +Ensure character name and setting consistency. +Edit so insights accumulate and develop.""" + } + } + + prompts = base_prompts.get(language, base_prompts["Korean"]).copy() + + # 특수 작가 프롬프트 + for i in range(1, 11): + prompts[f"writer{i}"] = prompts["writer_base"] + + return prompts + + # --- 메인 프로세스 --- 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) - } 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 - }) - else: - stages[stage_idx]["status"] = "active" - - yield f"🔄 진행 중... (현재 {total_words:,}단어)", stages, self.current_session_id - - prompt = self.get_stage_prompt(stage_idx, role, query, language, stages) - stage_content = "" - - for chunk in self.call_llm_streaming([{"role": "user", "content": prompt}], role, language): - stage_content += chunk - stages[stage_idx]["content"] = stage_content - stages[stage_idx]["word_count"] = len(stage_content.split()) - yield f"🔄 {stage_name} 작성 중... ({total_words + stages[stage_idx]['word_count']:,}단어)", stages, self.current_session_id - - # 진행도 평가 - if role.startswith("writer"): - writer_num = int(re.search(r'\d+', role).group()) - progression_score = self.evaluate_progression(stage_content, writer_num) - stages[stage_idx]["progression_score"] = progression_score - - # 서사 추적기 업데이트 - self.update_narrative_tracker(stage_content, writer_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("progression_score", 0.0) - ) - - # 서사 추적기 저장 - NovelDatabase.save_narrative_tracker(self.current_session_id, self.narrative_tracker) - - # 총 단어 수 업데이트 - total_words = NovelDatabase.get_total_words(self.current_session_id) - yield f"✅ {stage_name} 완료 (총 {total_words:,}단어)", stages, self.current_session_id - - # 최종 소설 정리 - final_novel = NovelDatabase.get_writer_content(self.current_session_id) - final_word_count = len(final_novel.split()) - final_report = self.generate_literary_report(final_novel, final_word_count, language) - - NovelDatabase.update_final_novel(self.current_session_id, final_novel, final_report) - yield f"✅ 소설 완성! 총 {final_word_count:,}단어 (목표: {TARGET_WORDS:,}단어)", stages, self.current_session_id - - except Exception as e: - logger.error(f"소설 생성 프로세스 오류: {e}", exc_info=True) - yield f"❌ 오류 발생: {e}", stages if 'stages' in locals() else [], self.current_session_id + """소설 생성 프로세스""" + try: + resume_from_stage = 0 + if session_id: + self.current_session_id = session_id + session = NovelDatabase.get_session(session_id) + if session: + query = session['user_query'] + language = session['language'] + resume_from_stage = session['current_stage'] + 1 + # 서사 추적기 복원 + saved_tracker = NovelDatabase.load_narrative_tracker(session_id) + if saved_tracker: + self.narrative_tracker = saved_tracker + else: + self.current_session_id = NovelDatabase.create_session(query, language) + logger.info(f"Created new session: {self.current_session_id}") + + stages = [] + if resume_from_stage > 0: + stages = [{ + "name": s['stage_name'], + "status": s['status'], + "content": s.get('content', ''), + "word_count": s.get('word_count', 0), + "progression_score": s.get('progression_score', 0.0), + "repetition_score": s.get('repetition_score', 0.0) + } for s in NovelDatabase.get_stages(self.current_session_id)] + + # 총 단어 수 추적 + total_words = NovelDatabase.get_total_words(self.current_session_id) + + for stage_idx in range(resume_from_stage, len(PROGRESSIVE_STAGES)): + role, stage_name = PROGRESSIVE_STAGES[stage_idx] + if stage_idx >= len(stages): + stages.append({ + "name": stage_name, + "status": "active", + "content": "", + "word_count": 0, + "progression_score": 0.0, + "repetition_score": 0.0 + }) + else: + stages[stage_idx]["status"] = "active" + + yield f"🔄 진행 중... (현재 {total_words:,}단어)", stages, self.current_session_id + + prompt = self.get_stage_prompt(stage_idx, role, query, language, stages) + stage_content = "" + + for chunk in self.call_llm_streaming([{"role": "user", "content": prompt}], role, language): + stage_content += chunk + stages[stage_idx]["content"] = stage_content + stages[stage_idx]["word_count"] = len(stage_content.split()) + yield f"🔄 {stage_name} 작성 중... ({total_words + stages[stage_idx]['word_count']:,}단어)", stages, self.current_session_id + + # 진행도 평가 + if role.startswith("writer"): + writer_num = int(re.search(r'\d+', role).group()) + previous_content = self.get_previous_writer_content(stages, writer_num) + + # 진행도 점수 계산 + progression_scores = self.narrative_tracker.progression_monitor.calculate_progression_score( + writer_num, stage_content, previous_content + ) + progression_score = sum(progression_scores.values()) / len(progression_scores) + stages[stage_idx]["progression_score"] = progression_score + + # 반복도 점수 계산 + repetition_score = 10.0 - self.narrative_tracker.progression_monitor.count_repetitions(stage_content) + stages[stage_idx]["repetition_score"] = max(0, repetition_score) + + # 서사 추적기 업데이트 + self.update_narrative_tracker(stage_content, writer_num) + self.narrative_tracker.extract_used_elements(stage_content) + + stages[stage_idx]["status"] = "complete" + NovelDatabase.save_stage( + self.current_session_id, stage_idx, stage_name, role, + stage_content, "complete", + stages[stage_idx].get("progression_score", 0.0), + stages[stage_idx].get("repetition_score", 0.0) + ) + + # 서사 추적기 저장 + NovelDatabase.save_narrative_tracker(self.current_session_id, self.narrative_tracker) + + # 총 단어 수 업데이트 + total_words = NovelDatabase.get_total_words(self.current_session_id) + yield f"✅ {stage_name} 완료 (총 {total_words:,}단어)", stages, self.current_session_id + + # 최종 소설 정리 + final_novel = NovelDatabase.get_writer_content(self.current_session_id) + + # 편집자가 처리한 내용이 있으면 그것을 사용 + edited_content = self.get_edited_content(stages) + if edited_content: + final_novel = edited_content + + final_word_count = len(final_novel.split()) + final_report = self.generate_literary_report(final_novel, final_word_count, language) + + NovelDatabase.update_final_novel(self.current_session_id, final_novel, final_report) + yield f"✅ 소설 완성! 총 {final_word_count:,}단어 (목표: {TARGET_WORDS:,}단어)", stages, self.current_session_id + + except Exception as e: + logger.error(f"소설 생성 프로세스 오류: {e}", exc_info=True) + yield f"❌ 오류 발생: {e}", stages if 'stages' in locals() else [], self.current_session_id def get_stage_prompt(self, stage_idx: int, role: str, query: str, language: str, stages: List[Dict]) -> str: - """단계별 프롬프트 생성""" - if stage_idx == 0: - return self.create_director_initial_prompt(query, language) - if stage_idx == 1: - return self.create_critic_director_prompt(stages[0]["content"], query, language) - if stage_idx == 2: - return self.create_director_revision_prompt(stages[0]["content"], stages[1]["content"], query, language) - - master_plan = stages[2]["content"] - - if 3 <= stage_idx <= 12: # 작가 초안 - writer_num = stage_idx - 2 - previous_content = self.get_previous_writer_content(stages, writer_num) - phase_requirements = self.narrative_tracker.generate_phase_requirements(writer_num) - narrative_summary = self.generate_narrative_summary(stages, writer_num) - - return self.create_writer_prompt( - writer_num, master_plan, previous_content, - phase_requirements, narrative_summary, language - ) - - if stage_idx == 13: # 비평가 중간 검토 - all_content = self.get_all_writer_content(stages, 12) - return self.create_critic_consistency_prompt( - all_content, self.narrative_tracker, query, language - ) - - if 14 <= stage_idx <= 23: # 작가 수정 - writer_num = stage_idx - 13 - initial_content = stages[2 + writer_num]["content"] - feedback = stages[13]["content"] - return self.create_writer_revision_prompt(writer_num, initial_content, feedback, language) - - if stage_idx == 24: # 최종 검토 - complete_novel = self.get_all_writer_content(stages, 23) - word_count = len(complete_novel.split()) - return self.create_critic_final_prompt(complete_novel, word_count, language) - - return "" + """단계별 프롬프트 생성""" + if stage_idx == 0: + return self.create_director_initial_prompt(query, language) + if stage_idx == 1: + return self.create_critic_director_prompt(stages[0]["content"], query, language) + if stage_idx == 2: + return self.create_director_revision_prompt(stages[0]["content"], stages[1]["content"], query, language) + + master_plan = stages[2]["content"] + + if 3 <= stage_idx <= 12: # 작가 초안 + writer_num = stage_idx - 2 + previous_content = self.get_previous_writer_content(stages, writer_num) + phase_requirements = self.narrative_tracker.generate_phase_requirements(writer_num) + narrative_summary = self.generate_narrative_summary(stages, writer_num) + used_elements = list(self.narrative_tracker.used_expressions) + + return self.create_writer_prompt_enhanced( + writer_num, master_plan, previous_content, + phase_requirements, narrative_summary, language, used_elements + ) + + if stage_idx == 13: # 비평가 중간 검토 + all_content = self.get_all_writer_content(stages, 12) + return self.create_critic_consistency_prompt_enhanced( + all_content, self.narrative_tracker, query, language + ) + + if 14 <= stage_idx <= 23: # 작가 수정 + writer_num = stage_idx - 13 + initial_content = stages[2 + writer_num]["content"] + feedback = stages[13]["content"] + return self.create_writer_revision_prompt(writer_num, initial_content, feedback, language) + + if stage_idx == 24: # 편집자 + complete_novel = self.get_all_writer_content(stages, 23) + issues = self.detect_issues(complete_novel) + return self.create_editor_prompt(complete_novel, issues, language) + + if stage_idx == 25: # 최종 검토 + edited_novel = stages[24]["content"] if stages[24]["content"] else self.get_all_writer_content(stages, 23) + word_count = len(edited_novel.split()) + return self.create_critic_final_prompt(edited_novel, word_count, language) + + return "" def create_director_revision_prompt(self, initial_plan: str, critic_feedback: str, user_query: str, language: str) -> str: - """감독자 수정 프롬프트""" - return f"""비평을 반영하여 통합된 서사 구조를 완성하세요. + """감독자 수정 프롬프트""" + return f"""비평을 반영하여 통합된 서사 구조를 완성하세요. **원 주제:** {user_query} @@ -1295,574 +1883,683 @@ Never repeat previous situations.""" 2. 10단계가 하나의 이야기로 연결 3. 인물의 명확한 변화 궤적 4. 8,000단어 분량 계획 -5. 수동태 사용금지지 +5. 수동태 사용금지 +6. 철학적 깊이와 인간애 포함 +7. 강렬한 중심 상징 창조 +8. 주인공 이름 명확히 설정 (일관성 유지) 각 단계가 이전의 필연적 결과가 되도록 수정하세요.""" def get_previous_writer_content(self, stages: List[Dict], current_writer: int) -> str: - """이전 작가의 내용 가져오기""" - if current_writer == 1: - return "" - - # 바로 이전 작가의 내용 - prev_idx = current_writer + 1 # stages 인덱스는 writer_num + 2 - if prev_idx < len(stages) and stages[prev_idx]["content"]: - return stages[prev_idx]["content"] - - return "" + """이전 작가의 내용 가져오기""" + if current_writer == 1: + return "" + + # 바로 이전 작가의 내용 + prev_idx = current_writer + 1 # stages 인덱스는 writer_num + 2 + if prev_idx < len(stages) and stages[prev_idx]["content"]: + return stages[prev_idx]["content"] + + return "" def get_all_writer_content(self, stages: List[Dict], up_to_stage: int) -> str: - """특정 단계까지의 모든 작가 내용""" - contents = [] - for i, s in enumerate(stages): - if i <= up_to_stage and "writer" in s.get("name", "") and s["content"]: - contents.append(s["content"]) - return "\n\n".join(contents) + """특정 단계까지의 모든 작가 내용""" + contents = [] + for i, s in enumerate(stages): + if i <= up_to_stage and "writer" in s.get("name", "") and s["content"]: + contents.append(s["content"]) + return "\n\n".join(contents) + + def get_edited_content(self, stages: List[Dict]) -> str: + """편집된 내용 가져오기""" + for s in stages: + if "편집자" in s.get("name", "") and s["content"]: + return s["content"] + return "" def generate_narrative_summary(self, stages: List[Dict], up_to_writer: int) -> str: - """현재까지의 서사 요약""" - if up_to_writer == 1: - return "첫 시작입니다." - - summary_parts = [] - for i in range(1, up_to_writer): - if i in self.narrative_tracker.phase_summaries: - summary_parts.append(f"[{NARRATIVE_PHASES[i-1]}]: {self.narrative_tracker.phase_summaries[i]}") - - return "\n".join(summary_parts) if summary_parts else "이전 내용을 이어받아 진행하세요." + """현재까지의 서사 요약""" + if up_to_writer == 1: + return "첫 시작입니다." + + summary_parts = [] + for i in range(1, up_to_writer): + if i in self.narrative_tracker.phase_summaries: + summary_parts.append(f"[{NARRATIVE_PHASES[i-1]}]: {self.narrative_tracker.phase_summaries[i]}") + + return "\n".join(summary_parts) if summary_parts else "이전 내용을 이어받아 진행하세요." def update_narrative_tracker(self, content: str, writer_num: int): - """서사 추적기 업데이트""" - # 간단한 요약 생성 (실제로는 더 정교한 분석 필요) - lines = content.split('\n') - key_events = [line.strip() for line in lines if len(line.strip()) > 50][:3] - - if key_events: - summary = " ".join(key_events[:2])[:200] + "..." - self.narrative_tracker.phase_summaries[writer_num] = summary + """서사 추적기 업데이트""" + # 간단한 요약 생성 (실제로는 더 정교한 분석 필요) + lines = content.split('\n') + key_events = [line.strip() for line in lines if len(line.strip()) > 50][:3] + + if key_events: + summary = " ".join(key_events[:2])[:200] + "..." + self.narrative_tracker.phase_summaries[writer_num] = summary + + # 철학적 통찰 추출 (간단한 키워드 기반) + philosophical_keywords = ['존재', '의미', '삶', '죽음', '인간', '고통', '희망', '사랑', + 'existence', 'meaning', 'life', 'death', 'human', 'suffering', 'hope', 'love'] + for keyword in philosophical_keywords: + if keyword in content: + self.narrative_tracker.philosophical_insights.append(f"Phase {writer_num}: {keyword} 탐구") + break + + # 문학적 기법 감지 + literary_devices = [] + if '처럼' in content or 'like' in content or 'as if' in content: + literary_devices.append('비유') + if '...' in content or '—' in content: + literary_devices.append('의식의 흐름') + if content.count('"') > 4: + literary_devices.append('대화') + + if literary_devices: + self.narrative_tracker.literary_devices[writer_num] = literary_devices + + def detect_issues(self, content: str) -> List[str]: + """문제점 감지""" + issues = [] + + # 반복 감지 + duplicates = self.narrative_tracker.content_deduplicator.count_repetitions(content) + if duplicates > 0: + issues.append(f"{duplicates}개의 반복된 문단 발견") + + # 특정 반복 표현 감지 + repetitive_phrases = ["습기가 찬 아침", "나라미 어플", "43만원", "개구리알을 바라보았다"] + for phrase in repetitive_phrases: + count = content.count(phrase) + if count > 2: + issues.append(f"'{phrase}' 표현이 {count}회 반복됨") + + # 캐릭터 이름 일관성 + name_variations = ["나라미", "안정", "나"] + found_names = [name for name in name_variations if name in content] + if len(found_names) > 1: + issues.append(f"주인공 이름 불일치: {', '.join(found_names)}") + + return issues def 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 - - return min(10.0, score) + """서사 진행도 평가""" + score = 5.0 + + # 분량 체크 + word_count = len(content.split()) + if word_count >= MIN_WORDS_PER_WRITER: + score += 2.0 + + # 새로운 요소 체크 + if phase > 1: + prev_summary = self.narrative_tracker.phase_summaries.get(phase-1, "") + if prev_summary and len(set(content.split()) - set(prev_summary.split())) > 100: + score += 1.5 + + # 변화 언급 체크 + change_keywords = ['변했', '달라졌', '새로운', '이제는', '더 이상', + 'changed', 'different', 'new', 'now', 'no longer'] + if any(keyword in content for keyword in change_keywords): + score += 1.5 + + # 철학적 깊이 체크 + philosophical_keywords = ['존재', '의미', '삶의', '인간의', '왜', 'existence', 'meaning', 'life', 'human', 'why'] + if any(keyword in content for keyword in philosophical_keywords): + score += 0.5 + + # 문학적 기법 체크 + if not any(phrase in content for phrase in ['느꼈다', '였다', 'felt', 'was']): + score += 0.5 # 보여주기 기법 사용 + + return min(10.0, score) def generate_literary_report(self, complete_novel: str, word_count: int, language: str) -> str: - """최종 문학적 평가""" - prompt = self.create_critic_final_prompt(complete_novel, word_count, language) - try: - report = self.call_llm_sync([{"role": "user", "content": prompt}], "critic", language) - return report - except Exception as e: - logger.error(f"최종 보고서 생성 실패: {e}") - return "보고서 생성 중 오류 발생" + """최종 문학적 평가""" + prompt = self.create_critic_final_prompt(complete_novel, word_count, language) + try: + report = self.call_llm_sync([{"role": "user", "content": prompt}], "critic", language) + return report + except Exception as e: + logger.error(f"최종 보고서 생성 실패: {e}") + return "보고서 생성 중 오류 발생" # --- 유틸리티 함수들 --- def process_query(query: str, language: str, session_id: Optional[str] = None) -> Generator[Tuple[str, str, str, str], None, None]: - """메인 쿼리 처리 함수""" - if not query.strip(): - yield "", "", "❌ 주제를 입력해주세요.", session_id - return - - system = ProgressiveLiterarySystem() - stages_markdown = "" - novel_content = "" - - for status, stages, current_session_id in system.process_novel_stream(query, language, session_id): - stages_markdown = format_stages_display(stages) - - # 최종 소설 내용 가져오기 - if stages and all(s.get("status") == "complete" for s in stages[-10:]): - novel_content = NovelDatabase.get_writer_content(current_session_id) - novel_content = format_novel_display(novel_content) - - yield stages_markdown, novel_content, status or "🔄 처리 중...", current_session_id + """메인 쿼리 처리 함수""" + if not query.strip(): + yield "", "", "❌ 주제를 입력해주세요.", session_id + return + + system = ProgressiveLiterarySystem() + stages_markdown = "" + novel_content = "" + + for status, stages, current_session_id in system.process_novel_stream(query, language, session_id): + stages_markdown = format_stages_display(stages) + + # 최종 소설 내용 가져오기 + if stages and all(s.get("status") == "complete" for s in stages[-10:]): + novel_content = NovelDatabase.get_writer_content(current_session_id) + # 편집된 내용이 있으면 그것을 사용 + edited = system.get_edited_content(stages) + if edited: + novel_content = edited + novel_content = format_novel_display(novel_content) + + yield stages_markdown, novel_content, status or "🔄 처리 중...", current_session_id def get_active_sessions(language: str) -> List[str]: - """활성 세션 목록""" - sessions = NovelDatabase.get_active_sessions() - return [f"{s['session_id'][:8]}... - {s['user_query'][:50]}... ({s['created_at']}) [{s['total_words']:,}단어]" - for s in sessions] + """활성 세션 목록""" + sessions = NovelDatabase.get_active_sessions() + return [f"{s['session_id'][:8]}... - {s['user_query'][:50]}... ({s['created_at']}) [{s['total_words']:,}단어]" + for s in sessions] def auto_recover_session(language: str) -> Tuple[Optional[str], str]: - """최근 세션 자동 복구""" - sessions = NovelDatabase.get_active_sessions() - if sessions: - latest_session = sessions[0] - return latest_session['session_id'], f"세션 {latest_session['session_id'][:8]}... 복구됨" - return None, "복구할 세션이 없습니다." + """최근 세션 자동 복구""" + sessions = NovelDatabase.get_active_sessions() + if sessions: + latest_session = sessions[0] + return latest_session['session_id'], f"세션 {latest_session['session_id'][:8]}... 복구됨" + return None, "복구할 세션이 없습니다." def resume_session(session_id: str, language: str) -> Generator[Tuple[str, str, str, str], None, None]: - """세션 재개""" - if not session_id: - yield "", "", "❌ 세션 ID가 없습니다.", session_id - return - - if "..." in session_id: - session_id = session_id.split("...")[0] - - session = NovelDatabase.get_session(session_id) - if not session: - yield "", "", "❌ 세션을 찾을 수 없습니다.", None - return - - yield from process_query(session['user_query'], session['language'], session_id) + """세션 재개""" + if not session_id: + yield "", "", "❌ 세션 ID가 없습니다.", session_id + return + + if "..." in session_id: + session_id = session_id.split("...")[0] + + session = NovelDatabase.get_session(session_id) + if not session: + yield "", "", "❌ 세션을 찾을 수 없습니다.", None + return + + yield from process_query(session['user_query'], session['language'], session_id) def download_novel(novel_text: str, format_type: str, language: str, session_id: str) -> Optional[str]: - """소설 다운로드 파일 생성""" - if not novel_text or not session_id: - return None - - timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") - filename = f"novel_{session_id[:8]}_{timestamp}" - - try: - if format_type == "DOCX" and DOCX_AVAILABLE: - return export_to_docx(novel_text, filename, language, session_id) - else: - return export_to_txt(novel_text, filename) - except Exception as e: - logger.error(f"파일 생성 실패: {e}") - return None + """소설 다운로드 파일 생성""" + if not novel_text or not session_id: + return None + + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + filename = f"novel_{session_id[:8]}_{timestamp}" + + try: + if format_type == "DOCX" and DOCX_AVAILABLE: + return export_to_docx(novel_text, filename, language, session_id) + else: + return export_to_txt(novel_text, filename) + except Exception as e: + logger.error(f"파일 생성 실패: {e}") + return None def format_stages_display(stages: List[Dict]) -> str: - """단계별 진행 상황 표시""" - markdown = "## 🎬 진행 상황\n\n" - - # 총 단어 수 계산 - total_words = sum(s.get('word_count', 0) for s in stages if 'writer' in s.get('name', '')) - markdown += f"**총 단어 수: {total_words:,} / {TARGET_WORDS:,}**\n\n" - - for i, stage in enumerate(stages): - status_icon = "✅" if stage['status'] == 'complete' else "🔄" if stage['status'] == 'active' else "⏳" - markdown += f"{status_icon} **{stage['name']}**" - - if stage.get('word_count', 0) > 0: - markdown += f" ({stage['word_count']:,}단어)" - - if stage.get('progression_score', 0) > 0: - markdown += f" [진행도: {stage['progression_score']:.1f}/10]" - - markdown += "\n" - - if stage['content']: - preview = stage['content'][:200] + "..." if len(stage['content']) > 200 else stage['content'] - markdown += f"> {preview}\n\n" - - return markdown + """단계별 진행 상황 표시""" + markdown = "## 🎬 진행 상황\n\n" + + # 총 단어 수 계산 + total_words = sum(s.get('word_count', 0) for s in stages if 'writer' in s.get('name', '')) + markdown += f"**총 단어 수: {total_words:,} / {TARGET_WORDS:,}**\n\n" + + for i, stage in enumerate(stages): + status_icon = "✅" if stage['status'] == 'complete' else "🔄" if stage['status'] == 'active' else "⏳" + markdown += f"{status_icon} **{stage['name']}**" + + if stage.get('word_count', 0) > 0: + markdown += f" ({stage['word_count']:,}단어)" + + # 진행도와 반복도 점수 표시 + if stage.get('progression_score', 0) > 0: + markdown += f" [진행도: {stage['progression_score']:.1f}/10]" + if stage.get('repetition_score', 0) > 0: + markdown += f" [반복도: {stage['repetition_score']:.1f}/10]" + + markdown += "\n" + + if stage['content']: + preview = stage['content'][:200] + "..." if len(stage['content']) > 200 else stage['content'] + markdown += f"> {preview}\n\n" + + return markdown def format_novel_display(novel_text: str) -> str: - """소설 내용 표시""" - if not novel_text: - return "아직 완성된 내용이 없습니다." - - formatted = "# 📖 완성된 소설\n\n" - - # 단어 수 표시 - word_count = len(novel_text.split()) - formatted += f"**총 분량: {word_count:,}단어 (목표: {TARGET_WORDS:,}단어)**\n\n" - formatted += "---\n\n" - - # 각 단계를 구분하여 표시 - sections = novel_text.split('\n\n') - for i, section in enumerate(sections): - if section.strip(): - formatted += f"{section}\n\n" - - return formatted + """소설 내용 표시""" + if not novel_text: + return "아직 완성된 내용이 없습니다." + + formatted = "# 📖 완성된 소설\n\n" + + # 단어 수 표시 + word_count = len(novel_text.split()) + formatted += f"**총 분량: {word_count:,}단어 (목표: {TARGET_WORDS:,}단어)**\n\n" + formatted += "---\n\n" + + # 각 단계를 구분하여 표시 + sections = novel_text.split('\n\n') + for i, section in enumerate(sections): + if section.strip(): + formatted += f"{section}\n\n" + + return formatted def export_to_docx(content: str, filename: str, language: str, session_id: str) -> str: - """DOCX 파일로 내보내기""" - doc = Document() - - # 페이지 설정 - section = doc.sections[0] - section.page_height = Inches(11) - section.page_width = Inches(8.5) - section.top_margin = Inches(1) - section.bottom_margin = Inches(1) - section.left_margin = Inches(1.25) - section.right_margin = Inches(1.25) - - # 세션 정보 - session = NovelDatabase.get_session(session_id) - - # 제목 페이지 - title_para = doc.add_paragraph() - title_para.alignment = WD_ALIGN_PARAGRAPH.CENTER - - if session: - title_run = title_para.add_run(session["user_query"]) - title_run.font.size = Pt(24) - title_run.bold = True - - # 메타 정보 - doc.add_paragraph() - meta_para = doc.add_paragraph() - meta_para.alignment = WD_ALIGN_PARAGRAPH.CENTER - meta_para.add_run(f"생성일: {datetime.now().strftime('%Y년 %m월 %d일')}\n") - meta_para.add_run(f"총 단어 수: {len(content.split()):,}단어") - - # 페이지 나누기 - doc.add_page_break() - - # 본문 스타일 설정 - style = doc.styles['Normal'] - style.font.name = 'Calibri' - style.font.size = Pt(11) - style.paragraph_format.line_spacing = 1.5 - style.paragraph_format.space_after = Pt(6) - - # 본문 추가 - paragraphs = content.split('\n\n') - for para_text in paragraphs: - if para_text.strip(): - para = doc.add_paragraph(para_text.strip()) - - # 파일 저장 - filepath = f"{filename}.docx" - doc.save(filepath) - return filepath + """DOCX 파일로 내보내기""" + doc = Document() + + # 페이지 설정 + section = doc.sections[0] + section.page_height = Inches(11) + section.page_width = Inches(8.5) + section.top_margin = Inches(1) + section.bottom_margin = Inches(1) + section.left_margin = Inches(1.25) + section.right_margin = Inches(1.25) + + # 세션 정보 + session = NovelDatabase.get_session(session_id) + + # 제목 페이지 + title_para = doc.add_paragraph() + title_para.alignment = WD_ALIGN_PARAGRAPH.CENTER + + if session: + title_run = title_para.add_run(session["user_query"]) + title_run.font.size = Pt(24) + title_run.bold = True + + # 메타 정보 + doc.add_paragraph() + meta_para = doc.add_paragraph() + meta_para.alignment = WD_ALIGN_PARAGRAPH.CENTER + meta_para.add_run(f"생성일: {datetime.now().strftime('%Y년 %m월 %d일')}\n") + meta_para.add_run(f"총 단어 수: {len(content.split()):,}단어") + + # 페이지 나누기 + doc.add_page_break() + + # 본문 스타일 설정 + style = doc.styles['Normal'] + style.font.name = 'Calibri' + style.font.size = Pt(11) + style.paragraph_format.line_spacing = 1.5 + style.paragraph_format.space_after = Pt(6) + + # 본문 추가 + paragraphs = content.split('\n\n') + for para_text in paragraphs: + if para_text.strip(): + para = doc.add_paragraph(para_text.strip()) + + # 파일 저장 + filepath = f"{filename}.docx" + doc.save(filepath) + return filepath def export_to_txt(content: str, filename: str) -> str: - """TXT 파일로 내보내기""" - filepath = f"{filename}.txt" - with open(filepath, 'w', encoding='utf-8') as f: - f.write(content) - return filepath + """TXT 파일로 내보내기""" + filepath = f"{filename}.txt" + with open(filepath, 'w', encoding='utf-8') as f: + f.write(content) + return filepath # CSS 스타일 custom_css = """ .gradio-container { - background: linear-gradient(135deg, #1e3c72 0%, #2a5298 50%, #1e3c72 100%); - min-height: 100vh; + background: linear-gradient(135deg, #1e3c72 0%, #2a5298 50%, #1e3c72 100%); + min-height: 100vh; } .main-header { - background-color: rgba(255, 255, 255, 0.1); - backdrop-filter: blur(10px); - padding: 30px; - border-radius: 12px; - margin-bottom: 30px; - text-align: center; - color: white; - border: 1px solid rgba(255, 255, 255, 0.2); + background-color: rgba(255, 255, 255, 0.1); + backdrop-filter: blur(10px); + padding: 30px; + border-radius: 12px; + margin-bottom: 30px; + text-align: center; + color: white; + border: 1px solid rgba(255, 255, 255, 0.2); } .progress-note { - background-color: rgba(255, 223, 0, 0.1); - border-left: 3px solid #ffd700; - padding: 15px; - margin: 20px 0; - border-radius: 8px; - color: #fff; + background-color: rgba(255, 223, 0, 0.1); + border-left: 3px solid #ffd700; + padding: 15px; + margin: 20px 0; + border-radius: 8px; + color: #fff; +} + +.improvement-note { + background-color: rgba(0, 255, 127, 0.1); + border-left: 3px solid #00ff7f; + padding: 15px; + margin: 20px 0; + border-radius: 8px; + color: #fff; } .input-section { - background-color: rgba(255, 255, 255, 0.1); - backdrop-filter: blur(10px); - padding: 20px; - border-radius: 12px; - margin-bottom: 20px; - border: 1px solid rgba(255, 255, 255, 0.2); + background-color: rgba(255, 255, 255, 0.1); + backdrop-filter: blur(10px); + padding: 20px; + border-radius: 12px; + margin-bottom: 20px; + border: 1px solid rgba(255, 255, 255, 0.2); } .session-section { - background-color: rgba(255, 255, 255, 0.1); - backdrop-filter: blur(10px); - padding: 15px; - border-radius: 8px; - margin-top: 20px; - color: white; - border: 1px solid rgba(255, 255, 255, 0.2); + background-color: rgba(255, 255, 255, 0.1); + backdrop-filter: blur(10px); + padding: 15px; + border-radius: 8px; + margin-top: 20px; + color: white; + border: 1px solid rgba(255, 255, 255, 0.2); } #stages-display { - background-color: rgba(255, 255, 255, 0.95); - padding: 20px; - border-radius: 12px; - max-height: 600px; - overflow-y: auto; - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + background-color: rgba(255, 255, 255, 0.95); + padding: 20px; + border-radius: 12px; + max-height: 600px; + overflow-y: auto; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); } #novel-output { - background-color: rgba(255, 255, 255, 0.95); - padding: 30px; - border-radius: 12px; - max-height: 700px; - overflow-y: auto; - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + background-color: rgba(255, 255, 255, 0.95); + padding: 30px; + border-radius: 12px; + max-height: 700px; + overflow-y: auto; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); } .download-section { - background-color: rgba(255, 255, 255, 0.9); - padding: 15px; - border-radius: 8px; - margin-top: 20px; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + background-color: rgba(255, 255, 255, 0.9); + padding: 15px; + border-radius: 8px; + margin-top: 20px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } /* 진행 표시기 스타일 */ .progress-bar { - background-color: #e0e0e0; - height: 20px; - border-radius: 10px; - overflow: hidden; - margin: 10px 0; + background-color: #e0e0e0; + height: 20px; + border-radius: 10px; + overflow: hidden; + margin: 10px 0; } .progress-fill { - background-color: #4CAF50; - height: 100%; - transition: width 0.3s ease; + background-color: #4CAF50; + height: 100%; + transition: width 0.3s ease; +} + +/* 점수 표시 스타일 */ +.score-badge { + display: inline-block; + padding: 2px 8px; + border-radius: 12px; + font-size: 0.9em; + font-weight: bold; + margin-left: 5px; +} + +.score-high { + background-color: #4CAF50; + color: white; +} + +.score-medium { + background-color: #FF9800; + color: white; +} + +.score-low { + background-color: #F44336; + color: white; } """ # Gradio 인터페이스 생성 def create_interface(): - with gr.Blocks(css=custom_css, title="AI 진행형 장편소설 생성 시스템") as interface: - gr.HTML(""" -
-

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

-

- 8,000단어 이상의 통합된 서사 구조를 가진 중편소설 창작 -

-

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

-
- ⚡ 반복이 아닌 축적, 순환이 아닌 진행을 통한 진정한 장편 서사 -
-
- """) - - # 상태 관리 - current_session_id = gr.State(None) - - with gr.Row(): - with gr.Column(scale=1): - with gr.Group(elem_classes=["input-section"]): - query_input = gr.Textbox( - label="소설 주제 / Novel Theme", - placeholder="중편소설의 주제를 입력하세요. 인물의 변화와 성장이 중심이 되는 이야기...\nEnter the theme for your novella. Focus on character transformation and growth...", - lines=4 - ) - - language_select = gr.Radio( - choices=["Korean", "English"], - value="Korean", - label="언어 / Language" - ) - - with gr.Row(): - submit_btn = gr.Button("🚀 소설 생성 시작", variant="primary", scale=2) - clear_btn = gr.Button("🗑️ 초기화", scale=1) - - status_text = gr.Textbox( - label="상태", - interactive=False, - value="🔄 준비 완료" - ) - - # 세션 관리 - with gr.Group(elem_classes=["session-section"]): - gr.Markdown("### 💾 진행 중인 세션") - session_dropdown = gr.Dropdown( - label="세션 선택", - choices=[], - interactive=True - ) - with gr.Row(): - refresh_btn = gr.Button("🔄 목록 새로고침", scale=1) - resume_btn = gr.Button("▶️ 선택 재개", variant="secondary", scale=1) - auto_recover_btn = gr.Button("♻️ 최근 세션 복구", scale=1) - - with gr.Column(scale=2): - with gr.Tab("📝 창작 진행"): - stages_display = gr.Markdown( - value="창작 과정이 여기에 표시됩니다...", - elem_id="stages-display" - ) - - with gr.Tab("📖 완성된 소설"): - novel_output = gr.Markdown( - value="완성된 소설이 여기에 표시됩니다...", - elem_id="novel-output" - ) - - with gr.Group(elem_classes=["download-section"]): - gr.Markdown("### 📥 소설 다운로드") - with gr.Row(): - format_select = gr.Radio( - choices=["DOCX", "TXT"], - value="DOCX" if DOCX_AVAILABLE else "TXT", - label="형식" - ) - download_btn = gr.Button("⬇️ 다운로드", variant="secondary") - - download_file = gr.File( - label="다운로드된 파일", - visible=False - ) - - # 숨겨진 상태 - novel_text_state = gr.State("") - - # 예제 - with gr.Row(): - gr.Examples( - examples=[ - ["실직한 중년 남성이 새로운 삶의 의미를 찾아가는 여정"], - ["도시에서 시골로 이주한 청년의 적응과 성장 이야기"], - ["세 세대가 함께 사는 가족의 갈등과 화해"], - ["A middle-aged woman's journey to rediscover herself after divorce"], - ["The transformation of a cynical journalist through unexpected encounters"], - ["작은 서점을 운영하는 노부부의 마지막 1년"], - ["AI 시대에 일자리를 잃은 번역가의 새로운 도전"] - ], - inputs=query_input, - label="💡 주제 예시" - ) - - # 이벤트 핸들러 - def refresh_sessions(): - try: - sessions = get_active_sessions("Korean") - return gr.update(choices=sessions) - except Exception as e: - logger.error(f"Error refreshing sessions: {str(e)}") - return gr.update(choices=[]) - - def handle_auto_recover(language): - session_id, message = auto_recover_session(language) - return session_id, message - - # 이벤트 연결 - submit_btn.click( - fn=process_query, - inputs=[query_input, language_select, current_session_id], - outputs=[stages_display, novel_output, status_text, current_session_id] - ) - - novel_output.change( - fn=lambda x: x, - inputs=[novel_output], - outputs=[novel_text_state] - ) - - resume_btn.click( - fn=lambda x: x.split("...")[0] if x and "..." in x else x, - inputs=[session_dropdown], - outputs=[current_session_id] - ).then( - fn=resume_session, - inputs=[current_session_id, language_select], - outputs=[stages_display, novel_output, status_text, current_session_id] - ) - - auto_recover_btn.click( - fn=handle_auto_recover, - inputs=[language_select], - outputs=[current_session_id, status_text] - ).then( - fn=resume_session, - inputs=[current_session_id, language_select], - outputs=[stages_display, novel_output, status_text, current_session_id] - ) - - refresh_btn.click( - fn=refresh_sessions, - outputs=[session_dropdown] - ) - - clear_btn.click( - fn=lambda: ("", "", "🔄 준비 완료", "", None), - outputs=[stages_display, novel_output, status_text, novel_text_state, current_session_id] - ) - - def handle_download(format_type, language, session_id, novel_text): - if not session_id or not novel_text: - return gr.update(visible=False) - - file_path = download_novel(novel_text, format_type, language, session_id) - if file_path: - return gr.update(value=file_path, visible=True) - else: - return gr.update(visible=False) - - download_btn.click( - fn=handle_download, - inputs=[format_select, language_select, current_session_id, novel_text_state], - outputs=[download_file] - ) - - # 시작 시 세션 로드 - interface.load( - fn=refresh_sessions, - outputs=[session_dropdown] - ) - - return interface + with gr.Blocks(css=custom_css, title="AI 진행형 장편소설 생성 시스템 v2") as interface: + gr.HTML(""" +
+

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

+

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

+

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

+
+ ⚡ 반복이 아닌 축적, 순환이 아닌 진행을 통한 진정한 장편 서사 +
+
+ 🆕 v2.0 개선사항: 강화된 반복 감지 시스템, 편집자 단계 추가, 실시간 진행도 모니터링 +
+
+ """) + + # 상태 관리 + current_session_id = gr.State(None) + + with gr.Row(): + with gr.Column(scale=1): + with gr.Group(elem_classes=["input-section"]): + query_input = gr.Textbox( + label="소설 주제 / Novel Theme", + placeholder="중편소설의 주제를 입력하세요. 인물의 변화와 성장이 중심이 되는 이야기...\nEnter the theme for your novella. Focus on character transformation and growth...", + lines=4 + ) + + language_select = gr.Radio( + choices=["Korean", "English"], + value="Korean", + label="언어 / Language" + ) + + with gr.Row(): + submit_btn = gr.Button("🚀 소설 생성 시작", variant="primary", scale=2) + clear_btn = gr.Button("🗑️ 초기화", scale=1) + + status_text = gr.Textbox( + label="상태", + interactive=False, + value="🔄 준비 완료" + ) + + # 세션 관리 + with gr.Group(elem_classes=["session-section"]): + gr.Markdown("### 💾 진행 중인 세션") + session_dropdown = gr.Dropdown( + label="세션 선택", + choices=[], + interactive=True + ) + with gr.Row(): + refresh_btn = gr.Button("🔄 목록 새로고침", scale=1) + resume_btn = gr.Button("▶️ 선택 재개", variant="secondary", scale=1) + auto_recover_btn = gr.Button("♻️ 최근 세션 복구", scale=1) + + with gr.Column(scale=2): + with gr.Tab("📝 창작 진행"): + stages_display = gr.Markdown( + value="창작 과정이 여기에 표시됩니다...", + elem_id="stages-display" + ) + + with gr.Tab("📖 완성된 소설"): + novel_output = gr.Markdown( + value="완성된 소설이 여기에 표시됩니다...", + elem_id="novel-output" + ) + + with gr.Group(elem_classes=["download-section"]): + gr.Markdown("### 📥 소설 다운로드") + with gr.Row(): + format_select = gr.Radio( + choices=["DOCX", "TXT"], + value="DOCX" if DOCX_AVAILABLE else "TXT", + label="형식" + ) + download_btn = gr.Button("⬇️ 다운로드", variant="secondary") + + download_file = gr.File( + label="다운로드된 파일", + visible=False + ) + + # 숨겨진 상태 + novel_text_state = gr.State("") + + # 예제 + with gr.Row(): + gr.Examples( + examples=[ + ["실직한 중년 남성이 새로운 삶의 의미를 찾아가는 여정"], + ["도시에서 시골로 이주한 청년의 적응과 성장 이야기"], + ["세 세대가 함께 사는 가족의 갈등과 화해"], + ["A middle-aged woman's journey to rediscover herself after divorce"], + ["The transformation of a cynical journalist through unexpected encounters"], + ["작은 서점을 운영하는 노부부의 마지막 1년"], + ["AI 시대에 일자리를 잃은 번역가의 새로운 도전"], + ["기초생활수급자가 된 청년의 생존과 존엄성 찾기"] + ], + inputs=query_input, + label="💡 주제 예시" + ) + + # 이벤트 핸들러 + def refresh_sessions(): + try: + sessions = get_active_sessions("Korean") + return gr.update(choices=sessions) + except Exception as e: + logger.error(f"Error refreshing sessions: {str(e)}") + return gr.update(choices=[]) + + def handle_auto_recover(language): + session_id, message = auto_recover_session(language) + return session_id, message + + # 이벤트 연결 + submit_btn.click( + fn=process_query, + inputs=[query_input, language_select, current_session_id], + outputs=[stages_display, novel_output, status_text, current_session_id] + ) + + novel_output.change( + fn=lambda x: x, + inputs=[novel_output], + outputs=[novel_text_state] + ) + + resume_btn.click( + fn=lambda x: x.split("...")[0] if x and "..." in x else x, + inputs=[session_dropdown], + outputs=[current_session_id] + ).then( + fn=resume_session, + inputs=[current_session_id, language_select], + outputs=[stages_display, novel_output, status_text, current_session_id] + ) + + auto_recover_btn.click( + fn=handle_auto_recover, + inputs=[language_select], + outputs=[current_session_id, status_text] + ).then( + fn=resume_session, + inputs=[current_session_id, language_select], + outputs=[stages_display, novel_output, status_text, current_session_id] + ) + + refresh_btn.click( + fn=refresh_sessions, + outputs=[session_dropdown] + ) + + clear_btn.click( + fn=lambda: ("", "", "🔄 준비 완료", "", None), + outputs=[stages_display, novel_output, status_text, novel_text_state, current_session_id] + ) + + def handle_download(format_type, language, session_id, novel_text): + if not session_id or not novel_text: + return gr.update(visible=False) + + file_path = download_novel(novel_text, format_type, language, session_id) + if file_path: + return gr.update(value=file_path, visible=True) + else: + return gr.update(visible=False) + + download_btn.click( + fn=handle_download, + inputs=[format_select, language_select, current_session_id, novel_text_state], + outputs=[download_file] + ) + + # 시작 시 세션 로드 + interface.load( + fn=refresh_sessions, + outputs=[session_dropdown] + ) + + return interface # 메인 실행 if __name__ == "__main__": - logger.info("AI 진행형 장편소설 생성 시스템 시작...") - logger.info("=" * 60) - - # 환경 확인 - logger.info(f"API 엔드포인트: {API_URL}") - logger.info(f"목표 분량: {TARGET_WORDS:,}단어") - logger.info(f"작가당 최소 분량: {MIN_WORDS_PER_WRITER:,}단어") - - if BRAVE_SEARCH_API_KEY: - logger.info("웹 검색이 활성화되었습니다.") - else: - logger.warning("웹 검색이 비활성화되었습니다.") - - if DOCX_AVAILABLE: - logger.info("DOCX 내보내기가 활성화되었습니다.") - else: - logger.warning("DOCX 내보내기가 비활성화되었습니다.") - - logger.info("=" * 60) - - # 데이터베이스 초기화 - logger.info("데이터베이스 초기화 중...") - NovelDatabase.init_db() - logger.info("데이터베이스 초기화 완료.") - - # 인터페이스 생성 및 실행 - interface = create_interface() - - interface.launch( - server_name="0.0.0.0", - server_port=7860, - share=False, - debug=True - ) \ No newline at end of file + logger.info("AI 진행형 장편소설 생성 시스템 v2.0 시작...") + logger.info("=" * 60) + + # 환경 확인 + logger.info(f"API 엔드포인트: {API_URL}") + logger.info(f"목표 분량: {TARGET_WORDS:,}단어") + logger.info(f"작가당 최소 분량: {MIN_WORDS_PER_WRITER:,}단어") + logger.info("주요 개선사항: 반복 감지 강화, 편집자 단계 추가, 진행도 모니터링") + + if BRAVE_SEARCH_API_KEY: + logger.info("웹 검색이 활성화되었습니다.") + else: + logger.warning("웹 검색이 비활성화되었습니다.") + + if DOCX_AVAILABLE: + logger.info("DOCX 내보내기가 활성화되었습니다.") + else: + logger.warning("DOCX 내보내기가 비활성화되었습니다.") + + logger.info("=" * 60) + + # 데이터베이스 초기화 + logger.info("데이터베이스 초기화 중...") + NovelDatabase.init_db() + logger.info("데이터베이스 초기화 완료.") + + # 인터페이스 생성 및 실행 + interface = create_interface() + + interface.launch( + server_name="0.0.0.0", + server_port=7860, + share=False, + debug=True + ) \ No newline at end of file