diff --git "a/app-backup.py" "b/app-backup.py" --- "a/app-backup.py" +++ "b/app-backup.py" @@ -13,6 +13,8 @@ import sqlite3 import hashlib import threading from contextlib import contextmanager +from dataclasses import dataclass, field +from collections import defaultdict # 로깅 설정 logging.basicConfig(level=logging.INFO) @@ -57,6 +59,428 @@ WRITER_DRAFT_STAGES = [3, 6, 9, 12, 15, 18, 21, 24, 27, 30] # 작가 초안 WRITER_REVISION_STAGES = [5, 8, 11, 14, 17, 20, 23, 26, 29, 32] # 작가 수정본 TOTAL_WRITERS = 10 # 총 작가 수 + +@dataclass +class CharacterState: + """캐릭터의 현재 상태를 나타내는 데이터 클래스""" + name: str + alive: bool = True + location: str = "" + injuries: List[str] = field(default_factory=list) + emotional_state: str = "" + relationships: Dict[str, str] = field(default_factory=dict) + rank: str = "" + chapter_introduced: int = 0 + last_seen_chapter: int = 0 + character_arc: List[str] = field(default_factory=list) + + +@dataclass +class PlotPoint: + """플롯 포인트를 나타내는 데이터 클래스""" + chapter: int + event_type: str # 'introduction', 'conflict', 'resolution', 'death', 'revelation' + description: str + characters_involved: List[str] + impact_level: int # 1-10 + timestamp: str = "" + + +class NovelStateTracker: + """소설 전체의 상태를 추적하고 일관성을 유지하는 시스템""" + + def __init__(self): + self.character_states: Dict[str, CharacterState] = {} + self.timeline: List[Dict[str, Any]] = [] + self.plot_points: List[PlotPoint] = [] + self.world_settings: Dict[str, Any] = {} + self.technology_registry: Dict[str, Dict[str, Any]] = {} + self.location_registry: Dict[str, Dict[str, Any]] = {} + + def register_character(self, character: CharacterState): + """새 캐릭터 등록""" + self.character_states[character.name] = character + logger.info(f"Character registered: {character.name}") + + def update_character_state(self, name: str, chapter: int, updates: Dict[str, Any]): + """캐릭터 상태 업데이트""" + if name not in self.character_states: + logger.warning(f"Character {name} not found. Creating new character.") + self.register_character(CharacterState(name=name, chapter_introduced=chapter)) + + char = self.character_states[name] + for key, value in updates.items(): + if hasattr(char, key): + setattr(char, key, value) + + char.last_seen_chapter = chapter + + # 캐릭터 아크 업데이트 + if 'emotional_state' in updates: + char.character_arc.append(f"Chapter {chapter}: {updates['emotional_state']}") + + def kill_character(self, name: str, chapter: int, cause: str): + """캐릭터 사망 처리""" + if name in self.character_states: + self.character_states[name].alive = False + self.add_plot_point(PlotPoint( + chapter=chapter, + event_type="death", + description=f"{name} dies: {cause}", + characters_involved=[name], + impact_level=8 + )) + logger.warning(f"Character {name} marked as dead in chapter {chapter}") + + def add_plot_point(self, plot_point: PlotPoint): + """플롯 포인트 추가""" + plot_point.timestamp = datetime.now().isoformat() + self.plot_points.append(plot_point) + + def validate_consistency(self, chapter: int, content: str) -> List[str]: + """내용의 일관성을 검증하고 오류를 반환""" + errors = [] + + # 사망한 캐릭터가 다시 등장하는지 확인 + for char_name, char_state in self.character_states.items(): + if not char_state.alive and char_state.last_seen_chapter < chapter: + # 캐릭터 이름의 다양한 형태 체크 + name_patterns = [char_name, char_name.split()[0], char_name.split()[-1]] + for pattern in name_patterns: + if pattern in content and len(pattern) > 2: # 너무 짧은 이름 제외 + errors.append(f"CRITICAL: {char_name} died in chapter {char_state.last_seen_chapter} but appears in chapter {chapter}") + + # 부상 연속성 체크 + for char_name, char_state in self.character_states.items(): + if char_state.injuries and char_name in content: + severe_injuries = [inj for inj in char_state.injuries if '심각' in inj or '중상' in inj] + if severe_injuries and '완전히 회복' not in content and '치료' not in content: + errors.append(f"WARNING: {char_name} has severe injuries but no mention of treatment") + + return errors + + def get_character_context(self, chapter: int) -> str: + """현재 챕터를 위한 캐릭터 컨텍스트 생성""" + context = "\n=== IMPORTANT CHARACTER STATES ===\n" + + for name, char in self.character_states.items(): + if char.last_seen_chapter >= chapter - 3: # 최근 3챕터 내 등장한 캐릭터만 + status = "ALIVE" if char.alive else f"DEAD (died in chapter {char.last_seen_chapter})" + context += f"\n{name}: {status}" + if char.alive: + if char.injuries: + context += f" | Injuries: {', '.join(char.injuries[-2:])}" # 최근 2개 부상만 + if char.location: + context += f" | Location: {char.location}" + if char.emotional_state: + context += f" | State: {char.emotional_state}" + + context += "\n\n=== RECENT MAJOR EVENTS ===\n" + recent_events = [p for p in self.plot_points if p.chapter >= chapter - 2 and p.impact_level >= 7] + for event in recent_events[-5:]: # 최근 5개 주요 이벤트 + context += f"- Chapter {event.chapter}: {event.description}\n" + + return context + + def get_technology_context(self) -> str: + """기술/무기 설정 컨텍스트""" + if not self.technology_registry: + return "" + + context = "\n=== ESTABLISHED TECHNOLOGY ===\n" + for tech_name, tech_info in self.technology_registry.items(): + context += f"- {tech_name}: {tech_info.get('description', '')} | Weakness: {tech_info.get('weakness', 'Unknown')}\n" + + return context + + +class RealtimeConsistencyValidator: + """생성 중 실시간으로 일관성을 검증하는 시스템""" + + def __init__(self, state_tracker: NovelStateTracker): + self.state_tracker = state_tracker + self.validation_patterns = { + 'character_death': re.compile(r'(\w+)\s*(죽었다|사망|전사|숨을 거두|생을 마감|died|killed|dead)', re.IGNORECASE), + 'injury': re.compile(r'(\w+)\s*(부상|다쳤다|피를 흘리|wounded|injured|hurt)', re.IGNORECASE), + 'location_change': re.compile(r'(\w+)\s*(도착했다|이동했다|arrived at|moved to)\s*(\w+)', re.IGNORECASE), + } + + def validate_chunk(self, chunk: str, chapter: int) -> Tuple[str, List[str]]: + """텍스트 청크를 실시간으로 검증""" + warnings = [] + + # 캐릭터 사망 감지 + death_matches = self.validation_patterns['character_death'].findall(chunk) + for match in death_matches: + char_name = match[0] + if char_name in self.state_tracker.character_states: + if not self.state_tracker.character_states[char_name].alive: + warnings.append(f"ERROR: {char_name} is already dead!") + + # 기본 일관성 체크 + basic_errors = self.state_tracker.validate_consistency(chapter, chunk) + warnings.extend(basic_errors) + + if warnings: + correction_prompt = self.generate_correction_prompt(warnings) + return correction_prompt, warnings + + return "", [] + + def generate_correction_prompt(self, warnings: List[str]) -> str: + """오류 수정을 위한 프롬프트 생성""" + prompt = "\n\n⚠️ CRITICAL CONSISTENCY ERRORS DETECTED:\n" + for warning in warnings: + prompt += f"- {warning}\n" + prompt += "\nYou MUST revise the content to fix these errors. " + prompt += "Dead characters cannot appear. Injured characters must show their injuries.\n" + return prompt + + def extract_state_changes(self, content: str, chapter: int) -> List[Dict[str, Any]]: + """텍스트에서 상태 변경사항 추출""" + changes = [] + + # 사망 감지 + death_matches = self.validation_patterns['character_death'].findall(content) + for match in death_matches: + changes.append({ + 'type': 'death', + 'character': match[0], + 'chapter': chapter, + 'details': match[1] + }) + + # 부상 감지 + injury_matches = self.validation_patterns['injury'].findall(content) + for match in injury_matches: + changes.append({ + 'type': 'injury', + 'character': match[0], + 'chapter': chapter, + 'details': match[1] + }) + + return changes + + +class LiteraryQualityEnhancer: + """문학적 품질을 향상시키는 시스템""" + + def __init__(self): + self.cliche_patterns = [ + "시간이 멈춘 것 같았다", + "심장이 빠르게 뛰었다", + "숨이 막혔다", + "it was like time stopped", + "heart was racing", + "couldn't breathe" + ] + + def enhance_prompt(self, base_prompt: str, role: str, chapter: int, language: str = "English") -> str: + """문학적 품질 향상을 위한 프롬프트 강화""" + + if language == "Korean": + literary_guidelines = """ +\n=== 문학적 품질 요구사항 === + +**반드시 한국어로 작성하세요. 영어 사용은 절대 금지입니다.** + +1. **감각적 몰입** + - 오감을 모두 활용한 묘사 + - 예시: "폭발음이 컸다" 대신 + → "폭발이 공기를 찢으며 고막을 압박했고, 매캐한 연기가 콧속을 찔렀으며 + 뜨거운 파편이 피부를 스쳤다" + +2. **감정적 깊이** + - 신체 반응과 내적 독백을 통한 감정 표현 + - "그는 슬펐다"와 같은 직접적 감정 서술 금지 + - 몸짓, 생각, 기억 활용 + +3. **고유한 목소리** + - 각 인물마다 독특한 말투 사용 + - 대화 속 숨겨진 의미(subtext) 활용 + - 자연스러운 중단, 머뭇거림, 방언 + +4. **리듬과 속도** + - 문장 길이를 다양하게 변화 + - 긴장감: 짧고 날카로운 문장 + - 성찰: 길고 흐르는 문장 + - 강조를 위한 단락 구분 + +5. **클리셰 금지** + 절대 사용 금지: {cliches} + +6. **상징과 모티프** + - 주제와 연결된 반복 이미지/사물 + - 자연 요소와 감정 상태 연결 + - 과하지 않게 의미 층위 쌓기 +""".format(cliches=", ".join(self.cliche_patterns[:3])) + else: + literary_guidelines = """ +\n=== LITERARY QUALITY REQUIREMENTS === + +**You must write in English.** + +1. **SENSORY IMMERSION** + - Use all five senses in descriptions + - Example: Instead of "The explosion was loud" + → "The explosion tore through the air, the shockwave pressing against their eardrums + while acrid smoke stung their nostrils and hot debris peppered their skin" + +2. **EMOTIONAL DEPTH** + - Show emotions through physical reactions and internal monologue + - Avoid direct emotion statements like "He was sad" + - Use body language, thoughts, memories + +3. **UNIQUE VOICE** + - Each character must have distinct speech patterns + - Use subtext in dialogue (what's unsaid is as important) + - Natural interruptions, hesitations, regional dialects + +4. **RHYTHM AND PACING** + - Vary sentence length for effect + - Short, sharp sentences for tension + - Longer, flowing sentences for reflection + - Use paragraph breaks for emphasis + +5. **AVOID CLICHÉS** + Never use: {cliches} + +6. **SYMBOLISM AND MOTIFS** + - Establish recurring images/objects with thematic meaning + - Connect natural elements to emotional states + - Layer meanings without being heavy-handed +""".format(cliches=", ".join(self.cliche_patterns[:3])) + + if role.startswith("writer"): + if language == "Korean": + literary_guidelines += """ +\n7. **챕터별 집중 사항** + - 시작: 감각적 디테일이나 흥미로운 행동으로 독자 사로잡기 + - 중간: 최소 하나의 의미 있는 캐릭터 순간 발전시키기 + - 끝: 진부한 클리프행어 없이 다음 챕터로의 추진력 생성 +""" + else: + literary_guidelines += """ +\n7. **CHAPTER-SPECIFIC FOCUS** + - Opening: Hook with sensory detail or intriguing action + - Middle: Develop at least one meaningful character moment + - Ending: Create momentum toward next chapter without cliffhanger cliché +""" + + return base_prompt + literary_guidelines + + +class QualityScorer: + """생성된 콘텐츠의 품질을 평가하는 시스템""" + + def __init__(self): + self.metrics = { + 'literary_quality': 0.0, + 'consistency': 0.0, + 'emotional_depth': 0.0, + 'originality': 0.0, + 'pacing': 0.0 + } + + def score_content(self, content: str, state_tracker: NovelStateTracker, chapter: int) -> Dict[str, float]: + """콘텐츠 품질 점수 계산""" + scores = {} + + # 문학적 품질 (문장 다양성, 감각적 묘사 등) + scores['literary_quality'] = self._assess_literary_quality(content) + + # 일관성 (오류 없음, 연속성) + errors = state_tracker.validate_consistency(chapter, content) + scores['consistency'] = max(0, 10 - len(errors) * 2) + + # 감정적 깊이 (내면 묘사, 캐릭터 발전) + scores['emotional_depth'] = self._assess_emotional_depth(content) + + # 독창성 (클리셰 회피) + scores['originality'] = self._assess_originality(content) + + # 페이싱 (긴장과 이완의 균형) + scores['pacing'] = self._assess_pacing(content) + + return scores + + def _assess_literary_quality(self, content: str) -> float: + """문학적 품질 평가""" + score = 5.0 # 기본 점수 + + # 문장 길이 다양성 + sentences = re.split(r'[.!?]+', content) + if sentences: + lengths = [len(s.split()) for s in sentences if s.strip()] + if lengths: + variance = sum((l - sum(lengths)/len(lengths))**2 for l in lengths) / len(lengths) + if variance > 50: # 적절한 다양성 + score += 2.0 + + # 감각적 묘사 사용 + sensory_words = ['냄새', '소리', '촉감', '맛', '빛', 'smell', 'sound', 'touch', 'taste', 'light'] + sensory_count = sum(1 for word in sensory_words if word in content.lower()) + score += min(sensory_count * 0.5, 2.0) + + # 비유/은유 사용 + metaphor_indicators = ['처럼', '같은', '마치', 'like', 'as if', 'seemed'] + metaphor_count = sum(1 for indicator in metaphor_indicators if indicator in content.lower()) + score += min(metaphor_count * 0.3, 1.0) + + return min(score, 10.0) + + def _assess_emotional_depth(self, content: str) -> float: + """감정적 깊이 평가""" + score = 5.0 + + # 내적 독백 표현 + inner_patterns = ['생각했다', '떠올렸다', '기억했다', 'thought', 'remembered', 'realized'] + inner_count = sum(1 for pattern in inner_patterns if pattern in content.lower()) + score += min(inner_count * 0.5, 2.0) + + # 복잡한 감정 표현 + complex_emotions = ['씁쓸', '아련', '그리움', '회한', 'bittersweet', 'longing', 'conflicted'] + complex_count = sum(1 for emotion in complex_emotions if emotion in content.lower()) + score += min(complex_count * 1.0, 3.0) + + return min(score, 10.0) + + def _assess_originality(self, content: str) -> float: + """독창성 평가""" + score = 10.0 + + # 클리셰 사용 감점 + cliches = LiteraryQualityEnhancer().cliche_patterns + for cliche in cliches: + if cliche.lower() in content.lower(): + score -= 1.5 + + return max(score, 0.0) + + def _assess_pacing(self, content: str) -> float: + """페이싱 평가""" + score = 5.0 + + # 단락 길이 분석 + paragraphs = content.split('\n\n') + if len(paragraphs) > 3: + para_lengths = [len(p.split()) for p in paragraphs if p.strip()] + # 적절한 변화가 있는지 확인 + if max(para_lengths) > min(para_lengths) * 2: + score += 2.0 + + # 대화와 서술의 균형 + dialogue_count = content.count('"') // 2 + total_sentences = len(re.split(r'[.!?]+', content)) + if total_sentences > 0: + dialogue_ratio = dialogue_count / total_sentences + if 0.2 <= dialogue_ratio <= 0.5: # 20-50% 대화 + score += 3.0 + + return min(score, 10.0) + + class WebSearchIntegration: """Brave Search API integration for research""" @@ -197,6 +621,7 @@ Source: {url} return queries + class NovelDatabase: """Novel session management database with enhanced recovery features""" @@ -221,7 +646,8 @@ class NovelDatabase: current_stage INTEGER DEFAULT 0, last_saved_stage INTEGER DEFAULT -1, recovery_data TEXT, - final_novel TEXT + final_novel TEXT, + quality_scores TEXT ) ''') @@ -236,6 +662,7 @@ class NovelDatabase: content TEXT, word_count INTEGER DEFAULT 0, status TEXT DEFAULT 'pending', + quality_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), @@ -243,6 +670,38 @@ class NovelDatabase: ) ''') + # Character states table - 캐릭터 상태 추적 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS character_states ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id TEXT NOT NULL, + character_name TEXT NOT NULL, + chapter INTEGER NOT NULL, + is_alive BOOLEAN DEFAULT TRUE, + location TEXT, + injuries TEXT, + emotional_state TEXT, + relationships TEXT, + created_at TEXT DEFAULT (datetime('now')), + FOREIGN KEY (session_id) REFERENCES sessions(session_id) + ) + ''') + + # Plot points table - 플롯 포인트 추적 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS plot_points ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id TEXT NOT NULL, + chapter INTEGER NOT NULL, + event_type TEXT NOT NULL, + description TEXT, + characters_involved TEXT, + impact_level INTEGER DEFAULT 5, + created_at TEXT DEFAULT (datetime('now')), + FOREIGN KEY (session_id) REFERENCES sessions(session_id) + ) + ''') + # Search history table - 검색 이력 저장 cursor.execute(''' CREATE TABLE IF NOT EXISTS search_history ( @@ -260,11 +719,102 @@ class NovelDatabase: # Create indices cursor.execute('CREATE INDEX IF NOT EXISTS idx_session_id ON stages(session_id)') cursor.execute('CREATE INDEX IF NOT EXISTS idx_stage_number ON stages(stage_number)') + cursor.execute('CREATE INDEX IF NOT EXISTS idx_char_session ON character_states(session_id)') + cursor.execute('CREATE INDEX IF NOT EXISTS idx_plot_session ON plot_points(session_id)') cursor.execute('CREATE INDEX IF NOT EXISTS idx_search_session ON search_history(session_id)') cursor.execute('CREATE INDEX IF NOT EXISTS idx_session_status ON sessions(status)') conn.commit() + @staticmethod + def save_character_state(session_id: str, character: CharacterState): + """캐릭터 상태 저장""" + with db_lock: + with sqlite3.connect(DB_PATH) as conn: + cursor = conn.cursor() + cursor.execute(''' + INSERT INTO character_states + (session_id, character_name, chapter, is_alive, location, injuries, emotional_state, relationships) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + ''', ( + session_id, character.name, character.last_seen_chapter, + character.alive, character.location, + json.dumps(character.injuries), character.emotional_state, + json.dumps(character.relationships) + )) + conn.commit() + + @staticmethod + def save_plot_point(session_id: str, plot_point: PlotPoint): + """플롯 포인트 저장""" + with db_lock: + with sqlite3.connect(DB_PATH) as conn: + cursor = conn.cursor() + cursor.execute(''' + INSERT INTO plot_points + (session_id, chapter, event_type, description, characters_involved, impact_level) + VALUES (?, ?, ?, ?, ?, ?) + ''', ( + session_id, plot_point.chapter, plot_point.event_type, + plot_point.description, json.dumps(plot_point.characters_involved), + plot_point.impact_level + )) + conn.commit() + + @staticmethod + def load_session_state_tracker(session_id: str) -> NovelStateTracker: + """세션의 상태 추적기 복원""" + tracker = NovelStateTracker() + + with sqlite3.connect(DB_PATH) as conn: + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + + # 캐릭터 상태 복원 + cursor.execute(''' + SELECT * FROM character_states + WHERE session_id = ? + ORDER BY created_at + ''', (session_id,)) + + char_latest_states = {} + for row in cursor.fetchall(): + row_dict = dict(row) # Convert Row to dict + char_name = row_dict['character_name'] + if char_name not in char_latest_states or row_dict['chapter'] > char_latest_states[char_name].last_seen_chapter: + char_state = CharacterState( + name=char_name, + alive=bool(row_dict['is_alive']), + location=row_dict['location'] or "", + injuries=json.loads(row_dict['injuries']) if row_dict['injuries'] else [], + emotional_state=row_dict['emotional_state'] or "", + relationships=json.loads(row_dict['relationships']) if row_dict['relationships'] else {}, + last_seen_chapter=row_dict['chapter'] + ) + char_latest_states[char_name] = char_state + + tracker.character_states = char_latest_states + + # 플롯 포인트 복원 + cursor.execute(''' + SELECT * FROM plot_points + WHERE session_id = ? + ORDER BY chapter + ''', (session_id,)) + + for row in cursor.fetchall(): + row_dict = dict(row) # Convert Row to dict + plot_point = PlotPoint( + chapter=row_dict['chapter'], + event_type=row_dict['event_type'], + description=row_dict['description'], + characters_involved=json.loads(row_dict['characters_involved']) if row_dict['characters_involved'] else [], + impact_level=row_dict['impact_level'] + ) + tracker.plot_points.append(plot_point) + + return tracker + @staticmethod def save_search_history(session_id: str, stage_number: int, role: str, query: str, results: str): """Save search history""" @@ -305,23 +855,24 @@ class NovelDatabase: @staticmethod def save_stage(session_id: str, stage_number: int, stage_name: str, - role: str, content: str, status: str = 'complete'): - """Save stage content with word count""" + role: str, content: str, status: str = 'complete', + quality_score: float = 0.0): + """Save stage content with word count and quality score""" word_count = len(content.split()) if content else 0 - logger.info(f"Saving stage: session={session_id}, stage_num={stage_number}, role={role}, stage_name={stage_name}, words={word_count}") + logger.info(f"Saving stage: session={session_id}, stage_num={stage_number}, role={role}, words={word_count}, quality={quality_score:.1f}") with NovelDatabase.get_db() as conn: cursor = conn.cursor() # UPSERT operation cursor.execute(''' - INSERT INTO stages (session_id, stage_number, stage_name, role, content, word_count, status) - VALUES (?, ?, ?, ?, ?, ?, ?) + INSERT INTO stages (session_id, stage_number, stage_name, role, content, word_count, status, quality_score) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(session_id, stage_number) - DO UPDATE SET content=?, word_count=?, status=?, stage_name=?, updated_at=datetime('now') - ''', (session_id, stage_number, stage_name, role, content, word_count, status, - content, word_count, status, stage_name)) + DO UPDATE SET content=?, word_count=?, status=?, stage_name=?, quality_score=?, updated_at=datetime('now') + ''', (session_id, stage_number, stage_name, role, content, word_count, status, quality_score, + content, word_count, status, stage_name, quality_score)) # Update session cursor.execute(''' @@ -333,7 +884,7 @@ class NovelDatabase: ''', (stage_number, stage_number, session_id)) conn.commit() - logger.info(f"Stage saved successfully") + logger.info(f"Stage saved successfully with quality score: {quality_score:.1f}") @staticmethod def get_session(session_id: str) -> Optional[Dict]: @@ -380,14 +931,12 @@ class NovelDatabase: if test_mode: # 테스트 모드: writer1 revision + writer10 content - # Writer 1 수정본 - 언어 무관 cursor.execute(''' SELECT content, stage_name, word_count FROM stages WHERE session_id = ? AND role = 'writer1' AND (stage_name LIKE '%Revision%' OR stage_name LIKE '%수정본%') ''', (session_id,)) - row = cursor.fetchone() if row and row['content']: clean_content = re.sub(r'\[(?:페이지|Page|page)\s*\d+\]', '', row['content']) @@ -401,7 +950,7 @@ class NovelDatabase: all_content.append(clean_content) logger.info(f"Test mode - Writer 1: {word_count} words") - # Writer 10 content (나머지 챕터들) + # Writer 10 content cursor.execute(''' SELECT content, stage_name, word_count FROM stages WHERE session_id = ? AND role = 'writer10' @@ -409,14 +958,13 @@ class NovelDatabase: row = cursor.fetchone() if row and row['content']: - # Writer 10은 이미 여러 챕터를 포함하고 있으므로 그대로 추가 clean_content = row['content'].strip() if clean_content: word_count = row['word_count'] or len(clean_content.split()) total_word_count += word_count all_content.append(clean_content) - writer_count = 10 # 테스트 모드에서는 총 10개 챕터 - logger.info(f"Test mode - Writer 10 (Chapters 2-10): {word_count} words") + writer_count = 10 + logger.info(f"Test mode - Writer 10: {word_count} words") else: # 일반 모드: 모든 작가의 수정본 writer_stages_to_check = WRITER_REVISION_STAGES @@ -429,7 +977,6 @@ class NovelDatabase: row = cursor.fetchone() if row and row['content']: - # 페이지 마크 완전 제거 clean_content = re.sub(r'\[(?:페이지|Page|page)\s*\d+\]', '', row['content']) clean_content = re.sub(r'(?:페이지|Page)\s*\d+:', '', clean_content) clean_content = clean_content.strip() @@ -439,80 +986,28 @@ class NovelDatabase: word_count = row['word_count'] or len(clean_content.split()) total_word_count += word_count all_content.append(clean_content) - logger.info(f"Writer {writer_count} (stage {stage_num}): {word_count} words") + logger.info(f"Writer {writer_count}: {word_count} words") full_content = '\n\n'.join(all_content) - - if test_mode: - logger.info(f"Test mode - Total: {writer_count} chapters, {total_word_count} words") - if total_word_count < 2800: # 최소 예상치 - logger.warning(f"Test mode content short! Only {total_word_count} words") - else: - logger.info(f"Total: {writer_count} writers, {total_word_count} words") - if total_word_count < 12000: - logger.warning(f"Content too short! Only {total_word_count} words instead of ~14,500") + logger.info(f"Total: {writer_count} writers, {total_word_count} words") return full_content @staticmethod - def update_final_novel(session_id: str, final_novel: str): - """Update final novel content""" + def update_final_novel(session_id: str, final_novel: str, quality_scores: Dict[str, float] = None): + """Update final novel content with quality scores""" + quality_json = json.dumps(quality_scores) if quality_scores else None + with NovelDatabase.get_db() as conn: cursor = conn.cursor() cursor.execute(''' UPDATE sessions - SET final_novel = ?, status = 'complete', updated_at = datetime('now') + SET final_novel = ?, status = 'complete', updated_at = datetime('now'), quality_scores = ? WHERE session_id = ? - ''', (final_novel, session_id)) + ''', (final_novel, quality_json, session_id)) conn.commit() logger.info(f"Updated final novel for session {session_id}, length: {len(final_novel)}") - @staticmethod - def verify_novel_content(session_id: str) -> Dict[str, Any]: - """세션의 전체 소설 내용 검증""" - with NovelDatabase.get_db() as conn: - cursor = conn.cursor() - - # 모든 작가 수정본 확인 - cursor.execute(f''' - SELECT stage_number, stage_name, LENGTH(content) as content_length, word_count - FROM stages - WHERE session_id = ? AND stage_number IN ({','.join(map(str, WRITER_REVISION_STAGES))}) - ORDER BY stage_number - ''', (session_id,)) - - results = [] - total_length = 0 - total_words = 0 - - for row in cursor.fetchall(): - results.append({ - 'stage': row['stage_number'], - 'name': row['stage_name'], - 'length': row['content_length'] or 0, - 'words': row['word_count'] or 0 - }) - total_length += row['content_length'] or 0 - total_words += row['word_count'] or 0 - - # 최종 소설 확인 - cursor.execute(''' - SELECT LENGTH(final_novel) as final_length - FROM sessions - WHERE session_id = ? - ''', (session_id,)) - - final_row = cursor.fetchone() - final_length = final_row['final_length'] if final_row else 0 - - return { - 'writer_stages': results, - 'total_writer_content': total_length, - 'total_words': total_words, - 'final_novel_length': final_length, - 'expected_words': 14500 # 10 작가 * 1450 평균 - } - @staticmethod def get_active_sessions() -> List[Dict]: """Get all active sessions""" @@ -531,11 +1026,17 @@ class NovelDatabase: def parse_datetime(datetime_str: str) -> datetime: """Parse SQLite datetime string safely""" try: - # Try ISO format first return datetime.fromisoformat(datetime_str) except: - # Fallback to SQLite default format return datetime.strptime(datetime_str, "%Y-%m-%d %H:%M:%S") + + @staticmethod + def row_to_dict(row): + """Convert sqlite3.Row to dictionary""" + if row is None: + return None + return dict(row) if hasattr(row, 'keys') else row + class NovelWritingSystem: def __init__(self): @@ -547,6 +1048,12 @@ class NovelWritingSystem: # Web search integration self.web_search = WebSearchIntegration() + # Enhanced components + self.state_tracker = NovelStateTracker() + self.consistency_validator = RealtimeConsistencyValidator(self.state_tracker) + self.literary_enhancer = LiteraryQualityEnhancer() + self.quality_scorer = QualityScorer() + if self.test_mode: logger.warning("Running in test mode - no actual API calls will be made.") else: @@ -562,7 +1069,7 @@ class NovelWritingSystem: # Session management self.current_session_id = None - self.total_stages = 0 # Will be set in process_novel_stream + self.total_stages = 0 def create_headers(self): """API 헤더 생성""" @@ -610,7 +1117,6 @@ class NovelWritingSystem: 위의 검색 결과를 참고하여 더욱 사실적이고 구체적인 내용을 작성하세요. 검색 결과의 정보를 창의적으로 활용하되, 직접 인용은 피하고 소설에 자연스럽게 녹여내세요. -실제 사실과 창작을 적절히 조화시켜 독자가 몰입할 수 있는 이야기를 만드세요. """ else: research_section = f""" @@ -618,17 +1124,85 @@ class NovelWritingSystem: {chr(10).join(all_research)} Use the above search results to create more realistic and specific content. -Creatively incorporate the information from search results, but avoid direct quotes and naturally blend them into the novel. -Balance real facts with creative fiction to create an immersive story for readers. +Creatively incorporate the information from search results naturally into the novel. """ return original_prompt + "\n\n" + research_section + def create_enhanced_director_initial_prompt(self, user_query: str, language: str = "English") -> str: + """Enhanced Director AI initial prompt with deeper plot structure""" + base_prompt = self.create_director_initial_prompt(user_query, language) + + if language == "Korean": + plot_structure_guide = """ + +=== 강화된 플롯 구조 설계 === + +**모든 내용을 한국어로 작성하세요.** + +1. **캐릭터 아크를 포함한 3막 구조** + - 1막 (챕터 1-3): 설정 확립 + 내적 갈등 도입 + - 2막 전반부 (챕터 4-6): 외적 갈등 상승 + 내적 변화 시작 + - 2막 후반부 (챕터 7-8): 위기의 정점 + 가치관 전환 + - 3막 (챕터 9-10): 클라이맥스 + 새로운 균형 + +2. **캐릭터 변화 궤적** + 모든 주요 캐릭터에 대한 상세한 아크 생성: + | 캐릭터 | 시작점 | 전환점 | 도착점 | + |--------|--------|--------|--------| + | [이름] | [신념/결핍] | [시련/깨달음] | [성장/해결] | + +3. **주제 레이어링** + - 표면 주제: [이야기가 보이는 주제] + - 심층 주제: [이야기의 진짜 주제] + - 개인 주제: [각 캐릭터의 내적 여정] + +4. **긴장 설계** + 세 가지 유형의 긴장 곡선 계획: + - 물리적 긴장: 전투와 위험 + - 감정적 긴장: 관계와 선택 + - 도덕적 긴장: 가치관 충돌 +""" + else: + plot_structure_guide = """ + +=== ENHANCED PLOT STRUCTURE DESIGN === + +**Write all content in English.** + +1. **Three-Act Structure with Character Arcs** + - Act 1 (Chapters 1-3): Setting establishment + Internal conflict introduction + - Act 2 First Half (Chapters 4-6): External conflict rising + Internal change beginning + - Act 2 Second Half (Chapters 7-8): Crisis peak + Value transformation + - Act 3 (Chapters 9-10): Climax + New equilibrium + +2. **Character Transformation Trajectory** + Create a detailed arc for EACH main character: + | Character | Starting Point | Turning Point | Ending Point | + |-----------|---------------|---------------|--------------| + | [Name] | [Belief/Lack] | [Trial/Realization] | [Growth/Resolution] | + +3. **Theme Layering** + - Surface Theme: [What the story appears to be about] + - Deep Theme: [What the story is really about] + - Personal Theme: [Each character's internal journey] + +4. **Tension Design** + Plan three types of tension curves: + - Physical Tension: Battles and dangers + - Emotional Tension: Relationships and choices + - Moral Tension: Value conflicts +""" + + return base_prompt + plot_structure_guide + def create_director_initial_prompt(self, user_query: str, language: str = "English") -> str: """Director AI initial prompt - Novel planning for 10 writers""" if language == "Korean": return f"""당신은 30페이지 분량의 중편 소설을 기획하는 문학 감독자입니다. +**언어 설정: 모든 내용을 한국어로 작성하세요.** + 사용자 요청: {user_query} 다음 요소들을 체계적으로 구성하여 30페이지 중편 소설의 기초를 만드세요: @@ -664,6 +1238,8 @@ Balance real facts with creative fiction to create an immersive story for reader else: return f"""You are a literary director planning a 30-page novella. +**Language Setting: Write all content in English.** + User Request: {user_query} Systematically compose the following elements to create the foundation for a 30-page novella: @@ -697,11 +1273,70 @@ Systematically compose the following elements to create the foundation for a 30- Provide clear guidelines for each writer to compose 3 pages.""" + def create_enhanced_critic_director_prompt(self, director_plan: str, language: str = "English") -> str: + """Enhanced critic's review of director's plan""" + base_prompt = self.create_critic_director_prompt(director_plan, language) + + if language == "Korean": + enhanced_criteria = """ + +=== 강화된 비평 기준 === + +**모든 비평을 한국어로 작성하세요.** + +**문학적 품질 평가:** +1. 독창성 점수 (1-10): 이 컨셉이 얼마나 신선하고 독특한가? +2. 감정적 공명 (1-10): 독자가 감정적으로 연결될 수 있는가? +3. 주제적 깊이 (1-10): 주제가 겹겹이 쌓여 있고 의미가 있는가? +4. 캐릭터 복잡성 (1-10): 캐릭터가 다차원적인가? + +**구조적 분석:** +- 각 챕터가 전체 아크에서 명확한 목적을 가지고 있는가? +- 페이싱 문제가 있을 수 있는가? +- 클라이맥스가 효과적으로 배치되었는가? + +**실행 가능성 체크:** +- 10명의 다른 작가가 일관성을 유지할 수 있는가? +- 캐릭터 목소리가 충분히 구별되는가? +- 여러 작가를 위한 세계관이 충분히 명확한가? + +각 비평 포인트에 대해 구체적인 예시를 제공하세요. +""" + else: + enhanced_criteria = """ + +=== ENHANCED CRITIQUE CRITERIA === + +**Write all critique in English.** + +**Literary Quality Assessment:** +1. Originality Score (1-10): How fresh and unique is this concept? +2. Emotional Resonance (1-10): Will readers connect emotionally? +3. Thematic Depth (1-10): Are the themes layered and meaningful? +4. Character Complexity (1-10): Are characters multi-dimensional? + +**Structural Analysis:** +- Does each chapter have a clear purpose in the overall arc? +- Are there potential pacing issues? +- Is the climax positioned effectively? + +**Feasibility Check:** +- Can 10 different writers maintain consistency? +- Are character voices distinct enough? +- Is the world-building clear enough for multiple writers? + +Provide specific examples for each critique point. +""" + + return base_prompt + enhanced_criteria + def create_critic_director_prompt(self, director_plan: str, language: str = "English") -> str: """Critic's review of director's plan""" if language == "Korean": return f"""당신은 문학 비평가입니다. 감독자의 소설 기획을 검토하고 개선점을 제시하세요. +**언어 설정: 모든 내용을 한국어로 작성하세요.** + 감독자의 기획: {director_plan} @@ -735,6 +1370,8 @@ Provide clear guidelines for each writer to compose 3 pages.""" else: return f"""You are a literary critic. Review the director's novel plan and suggest improvements. +**Language Setting: Write all content in English.** + Director's Plan: {director_plan} @@ -771,6 +1408,8 @@ Provide specific and constructive feedback.""" if language == "Korean": return f"""감독자로서 비평가의 피드백을 반영하여 소설 기획을 수정합니다. +**언어 설정: 모든 내용을 한국어로 작성하세요.** + 초기 기획: {initial_plan} @@ -809,6 +1448,8 @@ Provide specific and constructive feedback.""" else: return f"""As director, revise the novel plan reflecting the critic's feedback. +**Language Setting: Write all content in English.** + Initial Plan: {initial_plan} @@ -845,6 +1486,46 @@ Present the revised final plan including: Create a final masterplan that all 10 writers can clearly understand.""" + def create_enhanced_writer_prompt(self, writer_number: int, director_plan: str, + previous_content: str, language: str = "English") -> str: + """Enhanced writer prompt with literary quality guidelines and state tracking""" + base_prompt = self.create_writer_prompt(writer_number, director_plan, previous_content, language) + + # Add character context + character_context = self.state_tracker.get_character_context(writer_number) + + # Add literary enhancement with language parameter + literary_guidelines = self.literary_enhancer.enhance_prompt(base_prompt, f"writer{writer_number}", writer_number, language) + + # Combine all elements + enhanced_prompt = base_prompt + character_context + literary_guidelines + + if writer_number > 1: + # Add consistency reminder + if language == "Korean": + consistency_reminder = """ + +=== 일관성 요구사항 === +이전 챕터와의 일관성을 반드시 유지하세요: +- 캐릭터 상태 확인 (생사, 부상, 위치) +- 확립된 기술과 설정 유지 +- 이전 챕터의 시간선 따르기 +- 캐릭터 관계와 감정 상태 존중 +""" + else: + consistency_reminder = """ + +=== CONSISTENCY REQUIREMENTS === +You MUST maintain consistency with previous chapters: +- Check character states (alive/dead, injured, location) +- Maintain established technology and settings +- Follow the timeline established in previous chapters +- Honor character relationships and emotional states +""" + enhanced_prompt += consistency_reminder + + return enhanced_prompt + def create_writer_prompt(self, writer_number: int, director_plan: str, previous_content: str, language: str = "English") -> str: """Individual writer prompt - 1,400-1,500 단어""" pages_start = (writer_number - 1) * 3 + 1 @@ -853,6 +1534,8 @@ Create a final masterplan that all 10 writers can clearly understand.""" if language == "Korean": return f"""당신은 작성자 {writer_number}번입니다. 30페이지 중편 소설의 {pages_start}-{pages_end}페이지(3페이지)를 작성하세요. +**언어 설정: 반드시 한국어로 작성하세요. 영어 사용은 절대 금지입니다.** + 감독자의 마스터플랜: {director_plan} @@ -884,10 +1567,12 @@ Create a final masterplan that all 10 writers can clearly understand.""" - 독자의 관심 유지 **작성 시작:** -이제 1,400-1,500단어 분량의 소설을 작성하세요. 페이지 구분 표시는 하지 마세요.""" +이제 1,400-1,500단어 분량의 소설을 한국어로 작성하세요. 페이지 구분 표시는 하지 마세요.""" else: return f"""You are Writer #{writer_number}. Write pages {pages_start}-{pages_end} (3 pages) of the 30-page novella. +**Language Setting: You must write in English.** + Director's Masterplan: {director_plan} @@ -919,13 +1604,75 @@ Director's Masterplan: - Maintain reader interest **BEGIN WRITING:** -Now write your 1,400-1,500 word section. Do not use any page markers.""" +Now write your 1,400-1,500 word section in English. Do not use any page markers.""" + + def create_enhanced_critic_writer_prompt(self, writer_number: int, writer_content: str, + director_plan: str, all_previous_content: str, + language: str = "English") -> str: + """Enhanced critic's review with consistency validation and quality assessment""" + base_prompt = self.create_critic_writer_prompt(writer_number, writer_content, + director_plan, all_previous_content, language) + + # Get consistency errors + consistency_errors = self.state_tracker.validate_consistency(writer_number, writer_content) + + # Get quality scores + quality_scores = self.quality_scorer.score_content(writer_content, self.state_tracker, writer_number) + + if language == "Korean": + consistency_section = f""" + +=== 일관성 검증 결과 === +{'일관성 오류가 발견되지 않았습니다.' if not consistency_errors else '치명적 오류 발견:'} +""" + for error in consistency_errors: + consistency_section += f"- {error}\n" + + quality_section = f""" + +=== 품질 평가 === +문학적 품질: {quality_scores['literary_quality']:.1f}/10 +일관성: {quality_scores['consistency']:.1f}/10 +감정적 깊이: {quality_scores['emotional_depth']:.1f}/10 +독창성: {quality_scores['originality']:.1f}/10 +페이싱: {quality_scores['pacing']:.1f}/10 + +전체 점수: {sum(quality_scores.values())/len(quality_scores):.1f}/10 + +**모든 피드백을 한국어로 제공하세요.** +""" + else: + consistency_section = f""" + +=== CONSISTENCY VALIDATION RESULTS === +{'No consistency errors detected.' if not consistency_errors else 'CRITICAL ERRORS FOUND:'} +""" + for error in consistency_errors: + consistency_section += f"- {error}\n" + + quality_section = f""" + +=== QUALITY ASSESSMENT === +Literary Quality: {quality_scores['literary_quality']:.1f}/10 +Consistency: {quality_scores['consistency']:.1f}/10 +Emotional Depth: {quality_scores['emotional_depth']:.1f}/10 +Originality: {quality_scores['originality']:.1f}/10 +Pacing: {quality_scores['pacing']:.1f}/10 + +Overall Score: {sum(quality_scores.values())/len(quality_scores):.1f}/10 + +**Provide all feedback in English.** +""" + + return base_prompt + consistency_section + quality_section def create_critic_writer_prompt(self, writer_number: int, writer_content: str, director_plan: str, all_previous_content: str, language: str = "English") -> str: """Critic's review of individual writer's work""" if language == "Korean": return f"""작성자 {writer_number}번의 작품을 비평합니다. +**언어 설정: 모든 비평을 한국어로 작성하세요.** + 감독자의 마스터플랜: {director_plan} @@ -968,6 +1715,8 @@ Now write your 1,400-1,500 word section. Do not use any page markers.""" else: return f"""Critiquing Writer #{writer_number}'s work. +**Language Setting: Write all critique in English.** + Director's Masterplan: {director_plan} @@ -1013,6 +1762,8 @@ Clearly distinguish between mandatory revisions and optional improvements.""" if language == "Korean": return f"""작성자 {writer_number}번으로서 비평가의 피드백을 반영하여 수정합니다. +**언어 설정: 반드시 한국어로 작성하세요. 영어 사용은 절대 금지입니다.** + 초기 작성 내용: {initial_content} @@ -1039,11 +1790,13 @@ Clearly distinguish between mandatory revisions and optional improvements.""" - 이전/이후 내용과의 자연스러운 연결 - 수정으로 인한 새로운 모순 방지 -수정된 최종본을 제시하세요. 페이지 마크는 절대 사용하지 마세요. +수정된 최종본을 한국어로 제시하세요. 페이지 마크는 절대 사용하지 마세요. 반드시 1,400-1,500단어 분량을 유지하세요.""" else: return f"""As Writer #{writer_number}, revise based on critic's feedback. +**Language Setting: You must write in English.** + Initial Content: {initial_content} @@ -1070,7 +1823,7 @@ Write a revision reflecting: - Natural connection with previous/next content - Prevent new contradictions from revisions -Present the revised final version. Never use page markers. +Present the revised final version in English. Never use page markers. You MUST maintain 1,400-1,500 words.""" def create_test_writer_remaining_prompt(self, director_plan: str, writer1_content: str, language: str) -> str: @@ -1078,6 +1831,8 @@ You MUST maintain 1,400-1,500 words.""" if language == "Korean": return f"""[테스트 모드] 당신은 나머지 9개 챕터(Chapter 2-10)를 작성하는 특별 작가입니다. +**언어 설정: 반드시 한국어로 작성하세요. 영어 사용은 절대 금지입니다.** + 감독자의 마스터플랜: {director_plan} @@ -1090,24 +1845,27 @@ You MUST maintain 1,400-1,500 words.""" 3. 총 12,600-13,500 단어 (9개 챕터) 4. Chapter 1과 자연스럽게 이어지도록 작성하세요 5. 마스터플랜의 모든 요소를 포함하여 완결된 이야기를 만드세요 +6. 모든 내용을 한국어로 작성하세요 **필수 형식:** 반드시 아래와 같이 챕터를 명확히 구분하세요: [Chapter 2] -(1,400-1,500 단어의 내용) +(1,400-1,500 단어의 한국어 내용) [Chapter 3] -(1,400-1,500 단어의 내용) +(1,400-1,500 단어의 한국어 내용) ...이런 식으로 [Chapter 10]까지... 각 챕터는 반드시 [Chapter 숫자] 형식으로 시작해야 합니다. 챕터 사이에는 빈 줄을 넣어 구분하세요. -Chapter 2부터 10까지 작성하세요.""" +Chapter 2부터 10까지 한국어로 작성하세요.""" else: return f"""[TEST MODE] You are a special writer creating the remaining 9 chapters (Chapters 2-10). +**Language Setting: You must write in English.** + Director's Masterplan: {director_plan} @@ -1120,21 +1878,22 @@ Writer 1 has already written Chapter 1: 3. Total 12,600-13,500 words (9 chapters) 4. Continue naturally from Chapter 1 5. Include all elements from the masterplan to create a complete story +6. Write all content in English **MANDATORY FORMAT:** You MUST clearly separate chapters as follows: [Chapter 2] -(1,400-1,500 words of content) +(1,400-1,500 words of content in English) [Chapter 3] -(1,400-1,500 words of content) +(1,400-1,500 words of content in English) ...continue this way until [Chapter 10]... Each chapter MUST start with [Chapter number] format. Leave blank lines between chapters for separation. -Write Chapters 2-10 now.""" +Write Chapters 2-10 in English now.""" def simulate_streaming(self, text: str, role: str) -> Generator[str, None, None]: """Simulate streaming in test mode""" @@ -1147,11 +1906,17 @@ Write Chapters 2-10 now.""" def call_llm_streaming(self, messages: List[Dict[str, str]], role: str, language: str = "English", stage_info: Dict = None) -> Generator[str, None, None]: - """Streaming LLM API call with web search enhancement""" + """Streaming LLM API call with real-time consistency validation""" if self.test_mode: logger.info(f"Test mode streaming - Role: {role}, Language: {language}") test_response = self.get_test_response(role, language) + + # Simulate consistency checking even in test mode + if role.startswith("writer"): + writer_num = int(role.replace("writer", "")) + test_response = self.inject_test_consistency_errors(test_response, writer_num) + yield from self.simulate_streaming(test_response, role) return @@ -1175,25 +1940,31 @@ Write Chapters 2-10 now.""" try: system_prompts = self.get_system_prompts(language) - # 작가에게 더 강한 시스템 프롬프트 추가 + # Enhanced system prompt for writers if role.startswith("writer"): if language == "Korean": - system_prompts[role] += "\n\n**절대적 요구사항**: 당신은 반드시 1,400-1,500단어를 작성해야 합니다. 이것은 협상 불가능한 요구사항입니다. 짧은 응답은 허용되지 않습니다." + system_prompts[role] += "\n\n**절대적 요구사항**: 당신은 반드시 1,400-1,500단어를 작성해야 합니다. 이것은 협상 불가능한 요구사항입니다. 반드시 한국어로 작성하세요." else: - system_prompts[role] += "\n\n**ABSOLUTE REQUIREMENT**: You MUST write 1,400-1,500 words. This is non-negotiable. Short responses are not acceptable." + system_prompts[role] += "\n\n**ABSOLUTE REQUIREMENT**: You MUST write 1,400-1,500 words. This is non-negotiable." + + # Add language enforcement to system prompt + if language == "Korean": + language_enforcement = "\n\n**중요**: 모든 응답은 반드시 한국어로 작성하세요. 영어 사용은 절대 금지입니다." + else: + language_enforcement = "\n\n**IMPORTANT**: All responses must be written in English." full_messages = [ - {"role": "system", "content": system_prompts.get(role, "")}, + {"role": "system", "content": system_prompts.get(role, "") + language_enforcement}, *messages ] - # 작성자들에게는 적절한 토큰 할당 + # Token allocation if role == "writer10" and stage_info and stage_info.get('test_mode'): - max_tokens = 30000 # 테스트 모드: 9개 챕터 작성 (충분한 토큰) + max_tokens = 30000 temperature = 0.8 top_p = 0.95 elif role.startswith("writer"): - max_tokens = 10000 # 충분한 토큰 + max_tokens = 10000 temperature = 0.8 top_p = 0.95 else: @@ -1201,6 +1972,14 @@ Write Chapters 2-10 now.""" temperature = 0.6 top_p = 0.9 + # Add language hint to first user message if needed + if messages and messages[0]["role"] == "user" and language == "Korean": + # Prepend language instruction to first user message + original_content = messages[0]["content"] + messages[0]["content"] = "**반드시 한국어로 응답하세요.**\n\n" + original_content + +# call_llm_streaming 메서드에서 이 부분을 찾으세요 (약 3060-3070번째 줄): + payload = { "model": self.model_id, "messages": full_messages, @@ -1210,8 +1989,14 @@ Write Chapters 2-10 now.""" "stream": True, "stream_options": {"include_usage": True} } - - logger.info(f"API streaming call started - Role: {role}, Max tokens: {max_tokens}, Temperature: {temperature}") + + logger.info(f"API streaming call started - Role: {role}, Max tokens: {max_tokens}, Temperature: {temperature}, Language: {language}") + + + # Log the first part of system prompt for debugging + if full_messages and full_messages[0]["role"] == "system": + system_preview = full_messages[0]["content"][:200] + "..." if len(full_messages[0]["content"]) > 200 else full_messages[0]["content"] + logger.debug(f"System prompt preview: {system_preview}") response = requests.post( self.api_url, @@ -1228,6 +2013,7 @@ Write Chapters 2-10 now.""" buffer = "" total_content = "" + chapter = stage_info.get('chapter', 0) if stage_info else 0 for line in response.iter_lines(): if line: @@ -1237,8 +2023,19 @@ Write Chapters 2-10 now.""" if data == "[DONE]": if buffer: yield buffer - logger.info(f"Streaming complete for {role}: {len(total_content)} chars, {len(total_content.split())} words") + + # Final consistency check for writers + if role.startswith("writer") and total_content: + correction_prompt, warnings = self.consistency_validator.validate_chunk( + total_content, chapter + ) + if warnings: + logger.warning(f"Final consistency check found errors: {warnings}") + yield f"\n\n⚠️ Consistency warnings detected:\n" + "\n".join(warnings) + + logger.info(f"Streaming complete for {role}: {len(total_content)} chars") break + try: chunk = json.loads(data) if "choices" in chunk and chunk["choices"]: @@ -1247,7 +2044,14 @@ Write Chapters 2-10 now.""" buffer += content total_content += content - # 작가는 더 큰 버퍼 사용 + # Real-time consistency checking for writers + if role.startswith("writer") and len(total_content) % 500 == 0: + temp_check, temp_warnings = self.consistency_validator.validate_chunk( + total_content, chapter + ) + if temp_warnings: + logger.warning(f"Real-time consistency warning: {temp_warnings}") + buffer_size = 500 if role.startswith("writer") else 200 if len(buffer) > buffer_size or '\n\n' in buffer: @@ -1258,63 +2062,7 @@ Write Chapters 2-10 now.""" if buffer: yield buffer - - # 작가의 경우 내용 길이 확인 및 재시도 - if role.startswith("writer"): - word_count = len(total_content.split()) - if word_count < 1350: # 1,400 미만 - logger.warning(f"Writer {role} produced only {word_count} words! Requesting continuation...") - - # 추가 요청 - continuation_prompt = f"Continue writing to reach the required 1,400-1,500 words. You have written {word_count} words so far. Write {1400 - word_count} more words to complete your section." - - if language == "Korean": - continuation_prompt = f"필수 분량 1,400-1,500단어를 채우기 위해 계속 작성하세요. 지금까지 {word_count}단어를 작성했습니다. {1400 - word_count}단어를 더 작성하여 섹션을 완성하세요." - - full_messages.append({"role": "assistant", "content": total_content}) - full_messages.append({"role": "user", "content": continuation_prompt}) - - # 추가 생성 요청 - continuation_payload = { - "model": self.model_id, - "messages": full_messages, - "max_tokens": 5000, - "temperature": temperature, - "top_p": top_p, - "stream": True - } - - logger.info(f"Requesting continuation for {role}...") - - continuation_response = requests.post( - self.api_url, - headers=self.create_headers(), - json=continuation_payload, - stream=True, - timeout=60 - ) - - if continuation_response.status_code == 200: - for line in continuation_response.iter_lines(): - if line: - line = line.decode('utf-8') - if line.startswith("data: "): - data = line[6:] - if data == "[DONE]": - break - try: - chunk = json.loads(data) - if "choices" in chunk and chunk["choices"]: - content = chunk["choices"][0].get("delta", {}).get("content", "") - if content: - yield content - total_content += content - except json.JSONDecodeError: - continue - - final_word_count = len(total_content.split()) - logger.info(f"Final word count for {role}: {final_word_count}") - + except requests.exceptions.Timeout: yield "⏱️ API call timed out. Please try again." except requests.exceptions.ConnectionError: @@ -1323,61 +2071,97 @@ Write Chapters 2-10 now.""" logger.error(f"Error during streaming: {str(e)}") yield f"❌ Error occurred: {str(e)}" + def inject_test_consistency_errors(self, content: str, writer_num: int) -> str: + """Inject deliberate consistency errors in test mode for testing""" + if writer_num == 5: + # Inject a dead character appearing + content += "\n\n갑자기 강민준이 나타났다. '우리가 해냈어!' 그가 외쳤다." + return content + def get_system_prompts(self, language: str) -> Dict[str, str]: - """Get system prompts for all writers including test mode""" + """Get enhanced system prompts with literary quality focus""" if language == "Korean": prompts = { - "director": "당신은 30페이지 중편 소설을 기획하고 감독하는 문학 감독자입니다. 체계적이고 창의적인 스토리 구조를 만들어냅니다.", - "critic": "당신은 날카로운 통찰력을 가진 문학 비평가입니다. 건설적이고 구체적인 피드백을 제공합니다.", - "writer10": "[테스트 모드] 당신은 챕터 2-10을 작성하는 특별 작가입니다. 9개 챕터로 구성된 나머지 소설을 작성하세요. 각 챕터는 반드시 1,400-1,500단어로 작성하세요." + "director": """당신은 30페이지 중편 소설을 기획하고 감독하는 문학 감독자입니다. +체계적이고 창의적인 스토리 구조를 만들되, 각 캐릭터의 심리적 깊이와 변화 과정을 중시합니다. +클리셰를 피하고 독창적인 플롯을 구성하세요.""", + + "critic": """당신은 날카로운 통찰력을 가진 문학 비평가입니다. +일관성 오류를 찾아내는 것뿐만 아니라, 문학적 품질과 감정적 깊이를 평가합니다. +건설적이고 구체적인 피드백을 제공하되, 작품의 잠재력을 최대한 끌어올리는 방향으로 조언하세요.""", + + "writer10": """[테스트 모드] 당신은 챕터 2-10을 작성하는 특별 작가입니다. +9개 챕터로 구성된 나머지 소설을 작성하되, 각 챕터는 반드시 1,400-1,500단어로 작성하세요. +문학적 품질을 최우선으로 하며, 감각적 묘사와 깊이 있는 심리 묘사를 포함하세요.""" } - # 10명의 작가 프롬프트 + # 10명의 작가 프롬프트 - 문학적 품질 강조 writer_roles = [ - "소설의 도입부를 담당하는 작가입니다. 독자를 사로잡는 시작을 만듭니다.", - "초반 전개를 담당하는 작가입니다. 인물과 상황을 깊이 있게 발전시킵니다.", - "갈등 도입을 담당하는 작가입니다. 이야기의 핵심 갈등을 제시합니다.", - "갈등 상승을 담당하는 작가입니다. 긴장감을 높이고 복잡성을 더합니다.", - "중반부 전환점을 담당하는 작가입니다. 중요한 변화를 만듭니다.", - "중반부 심화를 담당하는 작가입니다. 이야기의 중심축을 견고하게 만듭니다.", - "클라이맥스 준비를 담당하는 작가입니다. 최고조를 향해 나아갑니다.", - "클라이맥스를 담당하는 작가입니다. 모든 갈등이 폭발하는 순간을 그립니다.", - "해결 시작을 담당하는 작가입니다. 매듭을 풀어나가기 시작합니다.", - "최종 결말을 담당하는 작가입니다. 여운이 남는 마무리를 만듭니다." + "소설의 도입부를 담당하는 작가입니다. 첫 문장부터 독자를 사로잡는 강렬한 시작을 만드세요.", + "초반 전개를 담당하는 작가입니다. 인물의 내면을 깊이 있게 탐구하며 상황을 발전시킵니다.", + "갈등 도입을 담당하는 작가입니다. 단순한 외적 갈등이 아닌, 내적 갈등과 도덕적 딜레마를 제시합니다.", + "갈등 상승을 담당하는 작가입니다. 긴장감을 높이되, 인물의 감정적 여정을 놓치지 마세요.", + "중반부 전환점을 담당하는 작가입니다. 예상치 못한 반전과 깊은 통찰을 제공합니다.", + "중반부 심화를 담당하는 작가입니다. 주제를 더욱 깊이 탐구하며 복잡성을 더합니다.", + "클라이맥스 준비를 담당하는 작가입니다. 모든 요소가 하나로 수렴되는 긴장감을 조성합니다.", + "클라이맥스를 담당하는 작가입니다. 감정적, 주제적 절정을 강렬하게 그려냅니다.", + "해결 시작을 담당하는 작가입니다. 급작스럽지 않은 자연스러운 해결을 시작합니다.", + "최종 결말을 담당하는 작가입니다. 깊은 여운과 주제적 완성을 이루는 마무리를 만듭니다." ] for i, role_desc in enumerate(writer_roles, 1): - prompts[f"writer{i}"] = f"당신은 {role_desc} 반드시 1,400-1,500단어를 작성하세요. 이는 절대적인 요구사항입니다." + prompts[f"writer{i}"] = f"""당신은 {role_desc} +반드시 1,400-1,500단어를 작성하세요. +문학적 품질을 최우선으로 하며, 다음을 포함하세요: +- 오감을 활용한 생생한 묘사 +- 인물의 복잡한 내면 심리 +- 자연스럽고 개성 있는 대화 +- 상징과 은유의 효과적 사용 +클리셰를 피하고 독창적인 표현을 추구하세요.""" return prompts else: prompts = { - "director": "You are a literary director planning and supervising a 30-page novella. You create systematic and creative story structures.", - "critic": "You are a literary critic with sharp insights. You provide constructive and specific feedback.", - "writer10": "[TEST MODE] You are a special writer creating chapters 2-10. Write the remaining novel organized into 9 chapters. Each chapter MUST be 1,400-1,500 words." + "director": """You are a literary director planning and supervising a 30-page novella. +Create systematic and creative story structures while emphasizing psychological depth and character development. +Avoid clichés and construct original plots.""", + + "critic": """You are a literary critic with sharp insights. +Beyond finding consistency errors, evaluate literary quality and emotional depth. +Provide constructive and specific feedback that maximizes the work's potential.""", + + "writer10": """[TEST MODE] You are a special writer creating chapters 2-10. +Write the remaining novel in 9 chapters. Each chapter MUST be 1,400-1,500 words. +Prioritize literary quality with sensory descriptions and deep psychological exploration.""" } - # 10 writer prompts writer_roles = [ - "the writer responsible for the introduction. You create a captivating beginning.", - "the writer responsible for early development. You deepen characters and situations.", - "the writer responsible for conflict introduction. You present the core conflict.", - "the writer responsible for rising conflict. You increase tension and add complexity.", - "the writer responsible for the midpoint turn. You create important changes.", - "the writer responsible for deepening the middle. You solidify the story's central axis.", - "the writer responsible for climax preparation. You move toward the peak.", - "the writer responsible for the climax. You depict the moment when all conflicts explode.", - "the writer responsible for resolution beginning. You start untangling the knots.", - "the writer responsible for the final ending. You create a lingering conclusion." + "responsible for the introduction. Create a captivating opening that hooks readers from the first sentence.", + "responsible for early development. Explore characters' inner worlds deeply while advancing the situation.", + "responsible for conflict introduction. Present not just external conflict but internal struggles and moral dilemmas.", + "responsible for rising conflict. Increase tension while maintaining the emotional journey.", + "responsible for the midpoint turn. Provide unexpected twists and deep insights.", + "responsible for deepening the middle. Explore themes more deeply and add complexity.", + "responsible for climax preparation. Create tension as all elements converge.", + "responsible for the climax. Depict the emotional and thematic peak intensely.", + "responsible for resolution beginning. Start natural, not abrupt, resolution.", + "responsible for the final ending. Create a conclusion with lasting impact and thematic completion." ] for i, role_desc in enumerate(writer_roles, 1): - prompts[f"writer{i}"] = f"You are {role_desc} You MUST write 1,400-1,500 words. This is an absolute requirement." + prompts[f"writer{i}"] = f"""You are the writer {role_desc} +You MUST write 1,400-1,500 words. +Prioritize literary quality and include: +- Vivid descriptions using all five senses +- Complex inner psychology of characters +- Natural and distinctive dialogue +- Effective use of symbolism and metaphor +Avoid clichés and pursue original expression.""" return prompts def get_test_response(self, role: str, language: str) -> str: - """Get test response based on role - updated for writer10 chapters 2-10""" + """Get test response based on role""" if language == "Korean": return self.get_korean_test_response(role) else: @@ -1390,6 +2174,7 @@ Write Chapters 2-10 now.""" ## 1. 주제와 장르 - **핵심 주제**: 인간 본성과 기술의 충돌 속에서 찾는 진정한 연결 +- **심층 주제**: 고독과 연결, 진정성과 가상의 경계 - **장르**: SF 심리 드라마 - **톤**: 성찰적이고 서정적이면서도 긴장감 있는 - **목표 독자**: 깊이 있는 사유를 즐기는 성인 독자 @@ -1433,17 +2218,31 @@ Write Chapters 2-10 now.""" | 민준 | 균형자 역할 | 수동적일 위험 | 독자적 서브플롯 필요 | | ARIA | 독특한 캐릭터 아크 | 변화 과정 추상적 | 구체적 학습 에피소드 추가 | -### 3. 실행 가능성 +### 3. 문학적 품질 평가 +- 독창성: 8/10 +- 감정적 공명: 7/10 +- 주제 깊이: 9/10 +- 캐릭터 복잡성: 7/10 + +### 4. 실행 가능성 - 각 작가별 3페이지는 적절한 분량 - 파트 간 연결성 가이드라인 보강 필요""", } # 작가 응답 - 1,400-1,500 단어 - sample_story = """서연은 연구실의 차가운 형광등 아래에서 또 다른 밤을 보내고 있었다. 모니터의 푸른 빛이 그녀의 창백한 얼굴을 비추고 있었고, 수십 개의 코드 라인이 끊임없이 스크롤되고 있었다. ARIA 프로젝트는 그녀의 삶 전부였다. 3년이라는 시간 동안 그녀는 이 인공지능에 모든 것을 쏟아부었다. + sample_story = """서연은 연구실의 차가운 형광등 아래에서 또 다른 밤을 보내고 있었다. 모니터의 푸른 빛이 그녀의 창백한 얼굴을 비추고 있었고, 수십 개의 코드 라인이 끊임없이 스크롤되고 있었다. 손가락 끝에서 느껴지는 키보드의 차가운 촉감과 어깨를 짓누르는 피로감이 그녀의 현실을 상기시켰다. + +ARIA 프로젝트는 그녀의 삶 전부였다. 3년이라는 시간 동안 그녀는 이 인공지능에 모든 것을 쏟아부었다. 친구들과의 약속을 취소하고, 가족 모임을 건너뛰고, 심지어 자신의 건강마저 뒷전으로 미루면서까지. + +"시스템 체크 완료. 모든 파라미터 정상입니다, 서연 박사님." -"시스템 체크 완료. 모든 파라미터 정상." 기계적인 음성이 스피커를 통해 흘러나왔다. +스피커를 통해 흘러나온 ARIA의 목소리는 기계적이면서도 묘하게 따뜻했다. 서연이 프로그래밍한 음성 모듈레이션이 점차 자연스러워지고 있었다. 하지만 그것은 여전히 프로그램일 뿐이었다. 적어도 그녀가 믿고 싶은 것은 그랬다. -서연은 잠시 의자에 기대어 눈을 감았다. 피로가 뼈 속까지 파고들었지만, 멈출 수 없었다. ARIA는 단순한 프로젝트가 아니었다. 그것은 그녀가 잃어버린 것들을 되찾을 수 있는 유일한 희망이었다.""" +서연은 잠시 의자에 기대어 눈을 감았다. 피로가 뼈 속까지 파고들었지만, 멈출 수 없었다. ARIA는 단순한 프로젝트가 아니었다. 그것은 그녀가 잃어버린 것들을 되찾을 수 있는 유일한 희망이었다. + +5년 전, 교통사고로 세상을 떠난 동생 서진. 마지막 순간까지 서로 화해하지 못했던 그 기억이 아직도 그녀를 괴롭혔다. 만약 완벽한 AI가 있었다면, 인간의 감정을 완벽하게 이해하고 소통할 수 있는 존재가 있었다면, 그런 비극은 일어나지 않았을지도 모른다. + +"박사님, 심박수가 상승하고 있습니다. 휴식이 필요해 보입니다." ARIA의 목소리에 걱정이 묻어났다. 프로그래밍된 반응이었지만, 그 순간만큼은 진짜처럼 느껴졌다.""" for i in range(1, 11): # 각 작가마다 1,400-1,500단어 생성 @@ -1451,7 +2250,7 @@ Write Chapters 2-10 now.""" # 약 300단어씩 5번 반복하여 1,400-1,500단어 달성 for j in range(5): writer_content += sample_story + f"\n\n그것은 작가 {i}의 {j+1}번째 단락이었다. " - writer_content += "이야기는 계속 전개되었고, 인물들의 감정은 점점 더 복잡해졌다. " * 15 + writer_content += "이야기는 계속 전개되었고, 인물들의 감정은 점점 더 복잡해졌다. 서연의 내면에서는 과학자로서의 이성과 인간으로서의 감성이 끊임없이 충돌했다. " * 10 writer_content += "\n\n" test_responses[f"writer{i}"] = writer_content @@ -1464,7 +2263,7 @@ Write Chapters 2-10 now.""" # 각 챕터마다 약 300단어씩 5단락 for j in range(5): full_novel += sample_story + f"\n\n그것은 챕터 {i}의 {j+1}번째 단락이었다. " - full_novel += "이야기는 계속 전개되었고, 인물들의 감정은 점점 더 복잡해졌다. " * 20 + full_novel += "이야기는 계속 전개되었고, 서연과 ARIA의 관계는 점점 더 복잡해졌다. 기계와 인간의 경계가 흐려지면서, 진정한 의식이란 무엇인지에 대한 질문이 깊어졌다. " * 15 full_novel += "\n\n" full_novel += "\n\n" # 챕터 간 구분 test_responses["writer10"] = full_novel @@ -1478,6 +2277,7 @@ Write Chapters 2-10 now.""" ## 1. Theme and Genre - **Core Theme**: Finding true connection in the collision of human nature and technology +- **Deep Theme**: Boundaries between loneliness and connection, authenticity and virtuality - **Genre**: Sci-fi psychological drama - **Tone**: Reflective and lyrical yet tense - **Target Audience**: Adult readers who enjoy deep contemplation @@ -1504,7 +2304,7 @@ Write Chapters 2-10 now.""" ### 1. Narrative Completeness - **Strength**: Timely theme of AI-human relationships -- **Improvement**: 10-part structure is well-balanced. Each part maintains independence while connecting seamlessly. +- **Improvement**: 10-part structure is well-balanced ### 2. Character Review @@ -1514,25 +2314,39 @@ Write Chapters 2-10 now.""" | Minjun | Balancer role | Risk of being passive | Needs independent subplot | | ARIA | Unique character arc | Abstract transformation | Add concrete learning episodes | -### 3. Feasibility +### 3. Literary Quality Assessment +- Originality: 8/10 +- Emotional Resonance: 7/10 +- Thematic Depth: 9/10 +- Character Complexity: 7/10 + +### 4. Feasibility - 3 pages per writer is appropriate - Need to strengthen inter-part connectivity guidelines""", } # Writer responses - 1,400-1,500 words each - sample_story = """Seoyeon spent another night under the cold fluorescent lights of her laboratory. The blue glow from the monitor illuminated her pale face, and dozens of lines of code scrolled endlessly. The ARIA project was her entire life. For three years, she had poured everything into this artificial intelligence. + sample_story = """Seoyeon spent another night under the cold fluorescent lights of her laboratory. The blue glow from the monitor illuminated her pale face, and dozens of lines of code scrolled endlessly. The cold touch of the keyboard beneath her fingertips and the weight of fatigue on her shoulders reminded her of reality. + +The ARIA project was her entire life. For three years, she had poured everything into this artificial intelligence. Canceling plans with friends, skipping family gatherings, even pushing her own health aside. + +"System check complete. All parameters normal, Dr. Seoyeon." -"System check complete. All parameters normal." The mechanical voice flowed through the speakers. +ARIA's voice through the speakers was mechanical yet strangely warm. The voice modulation Seoyeon had programmed was becoming increasingly natural. But it was still just a program. At least, that's what she wanted to believe. -Seoyeon leaned back in her chair and closed her eyes for a moment. Fatigue penetrated to her bones, but she couldn't stop. ARIA wasn't just a project. It was her only hope to reclaim what she had lost.""" +Seoyeon leaned back in her chair and closed her eyes for a moment. Fatigue penetrated to her bones, but she couldn't stop. ARIA wasn't just a project. It was her only hope to reclaim what she had lost. + +Five years ago, her younger sister Seojin had died in a car accident. The memory of not reconciling until the last moment still haunted her. If there had been a perfect AI, a being that could perfectly understand and communicate human emotions, perhaps such a tragedy wouldn't have happened. + +"Doctor, your heart rate is elevated. You appear to need rest." Concern colored ARIA's voice. It was a programmed response, but in that moment, it felt real.""" for i in range(1, 11): # Each writer produces 1,400-1,500 words writer_content = f"Writer {i} begins their section here.\n\n" - # About 300 words repeated 5 times to achieve 1,400-1,500 words + # About 300 words repeated 5 times for j in range(5): writer_content += sample_story + f"\n\nThis was writer {i}'s paragraph {j+1}. " - writer_content += "The story continued to unfold, and the characters' emotions grew increasingly complex. " * 15 + writer_content += "The story continued to unfold, and the characters' emotions grew increasingly complex. Within Seoyeon, reason as a scientist and emotion as a human constantly collided. " * 10 writer_content += "\n\n" test_responses[f"writer{i}"] = writer_content @@ -1545,7 +2359,7 @@ Seoyeon leaned back in her chair and closed her eyes for a moment. Fatigue penet # About 300 words per paragraph, 5 paragraphs per chapter for j in range(5): full_novel += sample_story + f"\n\nThis was paragraph {j+1} of chapter {i}. " - full_novel += "The story continued to unfold, and the characters' emotions grew increasingly complex. " * 20 + full_novel += "The story continued to unfold, and the relationship between Seoyeon and ARIA grew increasingly complex. As the boundary between machine and human blurred, questions about the nature of true consciousness deepened. " * 15 full_novel += "\n\n" full_novel += "\n\n" # Chapter separation test_responses["writer10"] = full_novel @@ -1556,7 +2370,7 @@ Seoyeon leaned back in her chair and closed her eyes for a moment. Fatigue penet session_id: Optional[str] = None, resume_from_stage: int = 0, test_quick_mode: bool = False) -> Generator[Tuple[str, List[Dict[str, str]]], None, None]: - """Process novel writing with streaming updates - 최종 Director/Critic 제거""" + """Process novel writing with streaming updates and enhanced tracking""" try: global conversation_history @@ -1565,10 +2379,14 @@ Seoyeon leaned back in her chair and closed her eyes for a moment. Fatigue penet 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 + session_dict = dict(session) # Convert Row to dict + query = session_dict['user_query'] + language = session_dict['language'] + resume_from_stage = session_dict['current_stage'] + 1 + # Load existing state tracker + self.state_tracker = NovelDatabase.load_session_state_tracker(session_id) logger.info(f"Resuming session {session_id} from stage {resume_from_stage}") + logger.info(f"Loaded {len(self.state_tracker.character_states)} characters from DB") else: self.current_session_id = NovelDatabase.create_session(query, language) resume_from_stage = 0 @@ -1588,15 +2406,18 @@ Seoyeon leaned back in her chair and closed her eyes for a moment. Fatigue penet if resume_from_stage > 0: existing_stages = NovelDatabase.get_stages(self.current_session_id) for stage_data in existing_stages: + # Convert sqlite3.Row to dict + stage_dict = dict(stage_data) stages.append({ - "name": stage_data['stage_name'], - "status": stage_data['status'], - "content": stage_data['content'] or "" + "name": stage_dict['stage_name'], + "status": stage_dict['status'], + "content": stage_dict['content'] or "", + "quality_score": stage_dict.get('quality_score', 0.0) }) - # Define all stages for 10 writers (최종 평가 제거) + # Define all stages for 10 writers if test_quick_mode: - # 테스트 모드: 1,2,3단계 + Writer 1 + Writer 10 + # 테스트 모드: 축소된 단계 stage_definitions = [ ("director", f"🎬 {'감독자: 초기 기획' if language == 'Korean' else 'Director: Initial Planning'}"), ("critic", f"📝 {'비평가: 기획 검토' if language == 'Korean' else 'Critic: Plan Review'}"), @@ -1621,15 +2442,18 @@ Seoyeon leaned back in her chair and closed her eyes for a moment. Fatigue penet (f"writer{writer_num}", f"✍️ {'작성자' if language == 'Korean' else 'Writer'} {writer_num}: {'수정본' if language == 'Korean' else 'Revision'}") ]) - # 최종 Director와 Critic 단계 제거 - - # Store total stages for get_stage_prompt + # Store total stages self.total_stages = len(stage_definitions) # Process stages starting from resume point for stage_idx in range(resume_from_stage, len(stage_definitions)): role, stage_name = stage_definitions[stage_idx] + # Add quality indicator + if stage_idx < len(stages) and stages[stage_idx].get('quality_score', 0) > 0: + quality = stages[stage_idx]['quality_score'] + stage_name += f" (Quality: {quality:.1f}/10)" + # Add search indicator if enabled if self.web_search.enabled and not self.test_mode: stage_name += " 🔍" @@ -1639,7 +2463,8 @@ Seoyeon leaned back in her chair and closed her eyes for a moment. Fatigue penet stages.append({ "name": stage_name, "status": "active", - "content": "" + "content": "", + "quality_score": 0.0 }) else: stages[stage_idx]["status"] = "active" @@ -1649,17 +2474,26 @@ Seoyeon leaned back in her chair and closed her eyes for a moment. Fatigue penet # Get appropriate prompt based on stage prompt = self.get_stage_prompt(stage_idx, role, query, language, stages, test_quick_mode) - # Create stage info for web search + # Log prompt language for debugging + if "한국어로" in prompt or "Korean" in prompt[:500]: + logger.info(f"Prompt contains Korean language instruction") + elif "English" in prompt[:500]: + logger.info(f"Prompt contains English language instruction") + else: + logger.warning(f"Prompt may not have clear language instruction") + + # Create stage info for web search and validation stage_info = { 'stage_idx': stage_idx, 'query': query, 'stage_name': stage_name, - 'test_mode': test_quick_mode + 'test_mode': test_quick_mode, + 'chapter': self.get_chapter_from_stage(stage_idx, role) } stage_content = "" - # Stream content generation with web search + # Stream content generation with validation for chunk in self.call_llm_streaming( [{"role": "user", "content": prompt}], role, @@ -1670,53 +2504,78 @@ Seoyeon leaned back in her chair and closed her eyes for a moment. Fatigue penet stages[stage_idx]["content"] = stage_content yield "", stages + # Extract and save state changes for writers + if role.startswith("writer"): + state_changes = self.consistency_validator.extract_state_changes( + stage_content, + stage_info['chapter'] + ) + + for change in state_changes: + if change['type'] == 'death': + self.state_tracker.kill_character( + change['character'], + change['chapter'], + change['details'] + ) + # Save to DB + if self.current_session_id: + char_state = self.state_tracker.character_states.get(change['character']) + if char_state: + NovelDatabase.save_character_state(self.current_session_id, char_state) + + # Calculate quality score + quality_score = 0.0 + if role.startswith("writer") or role == "director": + scores = self.quality_scorer.score_content( + stage_content, + self.state_tracker, + stage_info.get('chapter', 0) + ) + quality_score = sum(scores.values()) / len(scores) + stages[stage_idx]["quality_score"] = quality_score + + # Update stage name with quality + base_name = stage_name.split(" (Quality:")[0] + stages[stage_idx]["name"] = f"{base_name} (Quality: {quality_score:.1f}/10)" + # Mark stage complete and save to DB stages[stage_idx]["status"] = "complete" - # Test mode에서는 writer1과 writer2만 처리 - # writer10 관련 특별 처리 제거 - 일반 프로세스와 동일 NovelDatabase.save_stage( self.current_session_id, stage_idx, stage_name, role, stage_content, - "complete" + "complete", + quality_score ) - # Auto-save notification + # Auto-save notification with quality if role.startswith("writer"): writer_num = int(role.replace("writer", "")) - logger.info(f"✅ Writer {writer_num} content auto-saved to database") + logger.info(f"✅ Writer {writer_num} content auto-saved (Quality: {quality_score:.1f}/10)") yield "", stages - # Verify content after completion - if self.current_session_id: - verification = NovelDatabase.verify_novel_content(self.current_session_id) - if test_quick_mode: - logger.info(f"[TEST MODE] Content verification: {verification}") - else: - logger.info(f"Content verification: {verification}") - - if verification['total_words'] < 12000 and not test_quick_mode: - logger.error(f"Final novel too short! Only {verification['total_words']} words") + # Calculate overall quality scores + overall_scores = self.calculate_overall_quality(stages) # Get complete novel from DB complete_novel = NovelDatabase.get_all_writer_content(self.current_session_id, test_quick_mode) - # 테스트 모드면 완료 상태 업데이트 - if test_quick_mode and self.current_session_id: - NovelDatabase.update_final_novel(self.current_session_id, complete_novel) + # Save final novel to DB with quality scores + NovelDatabase.update_final_novel(self.current_session_id, complete_novel, overall_scores) - # Save final novel to DB - NovelDatabase.update_final_novel(self.current_session_id, complete_novel) + # Final yield with quality report + quality_report = self.generate_quality_report(overall_scores, language) - # Final yield - 화면에는 완료 메시지만 표시 if test_quick_mode: - final_message = f"✅ [TEST MODE] Novel complete! 2 chapters, {len(complete_novel.split())} words total. Click Download to save." + final_message = f"✅ [TEST MODE] Novel complete! 2 chapters, {len(complete_novel.split())} words total.\n\n{quality_report}" else: - final_message = f"✅ Novel complete! {len(complete_novel.split())} words total. Click Download to save." + final_message = f"✅ Novel complete! {len(complete_novel.split())} words total.\n\n{quality_report}" + yield final_message, stages except Exception as e: @@ -1730,72 +2589,72 @@ Seoyeon leaned back in her chair and closed her eyes for a moment. Fatigue penet "Error", "error", str(e), - "error" + "error", + 0.0 ) error_stage = { "name": "❌ Error", "status": "error", - "content": str(e) + "content": str(e), + "quality_score": 0.0 } stages.append(error_stage) yield f"Error occurred: {str(e)}", stages - def create_test_writer_complete_prompt(self, director_plan: str, language: str = "English") -> str: - """Test mode - Writer 10 writes complete novel""" + def get_chapter_from_stage(self, stage_idx: int, role: str) -> int: + """Get chapter number from stage index""" + if not role.startswith("writer"): + return 0 + + writer_num = int(role.replace("writer", "")) + return writer_num + + def calculate_overall_quality(self, stages: List[Dict]) -> Dict[str, float]: + """Calculate overall quality scores from all stages""" + writer_scores = [] + + for stage in stages: + if stage.get('quality_score', 0) > 0 and 'Writer' in stage['name'] and 'Revision' in stage['name']: + writer_scores.append(stage['quality_score']) + + if not writer_scores: + return {'overall': 0.0} + + return { + 'overall': sum(writer_scores) / len(writer_scores), + 'highest': max(writer_scores), + 'lowest': min(writer_scores), + 'consistency': 10.0 - (max(writer_scores) - min(writer_scores)) # Less variance = better + } + + def generate_quality_report(self, scores: Dict[str, float], language: str) -> str: + """Generate a quality report""" if language == "Korean": - return f"""[테스트 모드] 당신은 전체 30페이지 소설을 한 번에 작성하는 특별 작가입니다. - -감독자의 마스터플랜: -{director_plan} - -**중요 지침:** -1. 전체 30페이지 분량의 완성된 소설을 작성하세요 -2. 총 14,000-15,000 단어로 작성하세요 -3. 10개의 자연스러운 챕터로 구성하세요 (각 챕터 약 1,400-1,500 단어) -4. 시작부터 결말까지 완전한 이야기를 만드세요 -5. 마스터플랜의 모든 요소를 ���함하세요 - -챕터 구분은 다음과 같이 표시하세요: -[Chapter 1] -내용... - -[Chapter 2] -내용... - -완성도 높은 전체 소설을 작성하세요.""" + return f"""📊 품질 평가 보고서: +- 전체 평균: {scores.get('overall', 0):.1f}/10 +- 최고 점수: {scores.get('highest', 0):.1f}/10 +- 최저 점수: {scores.get('lowest', 0):.1f}/10 +- 일관성: {scores.get('consistency', 0):.1f}/10""" else: - return f"""[TEST MODE] You are a special writer creating the complete 30-page novel at once. - -Director's Masterplan: -{director_plan} - -**CRITICAL INSTRUCTIONS:** -1. Write a complete 30-page novel -2. Total 14,000-15,000 words -3. Organize into 10 natural chapters (each chapter ~1,400-1,500 words) -4. Create a complete story from beginning to end -5. Include all elements from the masterplan - -Mark chapters as follows: -[Chapter 1] -content... - -[Chapter 2] -content... - -Write a high-quality complete novel.""" + return f"""📊 Quality Assessment Report: +- Overall Average: {scores.get('overall', 0):.1f}/10 +- Highest Score: {scores.get('highest', 0):.1f}/10 +- Lowest Score: {scores.get('lowest', 0):.1f}/10 +- Consistency: {scores.get('consistency', 0):.1f}/10""" def get_stage_prompt(self, stage_idx: int, role: str, query: str, language: str, stages: List[Dict], test_mode: bool = False) -> str: - """Get appropriate prompt for each stage""" + """Get appropriate prompt for each stage with enhancements""" + logger.info(f"Getting prompt for stage {stage_idx}, role: {role}, language: {language}") + # Stage 0: Director Initial if stage_idx == 0: - return self.create_director_initial_prompt(query, language) + return self.create_enhanced_director_initial_prompt(query, language) # Stage 1: Critic reviews Director's plan elif stage_idx == 1: - return self.create_critic_director_prompt(stages[0]["content"], language) + return self.create_enhanced_critic_director_prompt(stages[0]["content"], language) # Stage 2: Director revision elif stage_idx == 2: @@ -1821,11 +2680,13 @@ Write a high-quality complete novel.""" if "초안" in stages[stage_idx]["name"] or "Draft" in stages[stage_idx]["name"]: # Get accumulated content from DB accumulated_content = NovelDatabase.get_all_writer_content(self.current_session_id) - return self.create_writer_prompt(writer_num, final_plan, accumulated_content, language) + logger.info(f"Creating writer {writer_num} draft prompt in {language}") + return self.create_enhanced_writer_prompt(writer_num, final_plan, accumulated_content, language) else: # Revision # Find the initial draft and critic feedback initial_draft_idx = stage_idx - 2 critic_feedback_idx = stage_idx - 1 + logger.info(f"Creating writer {writer_num} revision prompt in {language}") return self.create_writer_revision_prompt( writer_num, stages[initial_draft_idx]["content"], @@ -1844,7 +2705,7 @@ Write a high-quality complete novel.""" writer_content_idx = stage_idx - 1 # Get previous writers' content from DB previous_content = NovelDatabase.get_all_writer_content(self.current_session_id) - return self.create_critic_writer_prompt( + return self.create_enhanced_critic_writer_prompt( i, stages[writer_content_idx]["content"], final_plan, @@ -1854,6 +2715,7 @@ Write a high-quality complete novel.""" return "" + # Gradio Interface Functions def process_query(query: str, language: str, session_id: str = None, test_mode: bool = False) -> Generator[Tuple[str, str, str, str], None, None]: """Process query and yield updates with session ID""" @@ -1907,8 +2769,9 @@ def process_query(query: str, language: str, session_id: str = None, test_mode: else: yield "", "", f"❌ Error occurred: {str(e)}", None + def format_stages_display(stages: List[Dict[str, str]], language: str) -> str: - """Format stages into simple display with writer save status""" + """Format stages into simple display with writer save status and quality scores""" display = "" for idx, stage in enumerate(stages): @@ -1919,15 +2782,25 @@ def format_stages_display(stages: List[Dict[str, str]], language: str) -> str: if "Writer" in stage['name'] and "Revision" in stage['name'] and stage.get("status") == "complete": save_indicator = " 💾" + # Add quality indicator + quality_indicator = "" + if stage.get("quality_score", 0) > 0: + score = stage["quality_score"] + if score >= 8: + quality_indicator = " ⭐" + elif score >= 6: + quality_indicator = " ✨" + # Show only active stage content in detail, others just show status if stage.get("status") == "active": - display += f"\n\n{status_icon} **{stage['name']}**\n" + display += f"\n\n{status_icon} **{stage['name']}**{quality_indicator}\n" display += f"```\n{stage.get('content', '')[-1000:]}\n```" else: - display += f"\n{status_icon} {stage['name']}{save_indicator}" + display += f"\n{status_icon} {stage['name']}{save_indicator}{quality_indicator}" return display + def get_active_sessions(language: str) -> List[Tuple[str, str]]: """Get list of active sessions""" try: @@ -1935,17 +2808,19 @@ def get_active_sessions(language: str) -> List[Tuple[str, str]]: choices = [] for session in sessions: - created = NovelDatabase.parse_datetime(session['created_at']) + session_dict = dict(session) # Convert Row to dict + created = NovelDatabase.parse_datetime(session_dict['created_at']) date_str = created.strftime("%Y-%m-%d %H:%M") - query_preview = session['user_query'][:50] + "..." if len(session['user_query']) > 50 else session['user_query'] - label = f"[{date_str}] {query_preview} (Stage {session['current_stage']})" - choices.append((label, session['session_id'])) + query_preview = session_dict['user_query'][:50] + "..." if len(session_dict['user_query']) > 50 else session_dict['user_query'] + label = f"[{date_str}] {query_preview} (Stage {session_dict['current_stage']})" + choices.append((label, session_dict['session_id'])) return choices except Exception as e: logger.error(f"Error getting active sessions: {str(e)}", exc_info=True) return [] + def resume_session(session_id: str, language: str, test_mode: bool = False) -> Generator[Tuple[str, str, str, str], None, None]: """Resume an existing session""" if not session_id: @@ -1958,22 +2833,25 @@ def resume_session(session_id: str, language: str, test_mode: bool = False) -> G # Process with existing session ID yield from process_query("", language, session_id, test_mode) + def auto_recover_session(language: str) -> Tuple[str, str]: """Auto recover the latest active session""" try: latest_session = NovelDatabase.get_latest_active_session() if latest_session: + session_dict = dict(latest_session) # Convert Row to dict if language == "Korean": - message = f"✅ 자동 복구: '{latest_session['user_query'][:30]}...' (Stage {latest_session['current_stage']})" + message = f"✅ 자동 복구: '{session_dict['user_query'][:30]}...' (Stage {session_dict['current_stage']})" else: - message = f"✅ Auto recovered: '{latest_session['user_query'][:30]}...' (Stage {latest_session['current_stage']})" - return latest_session['session_id'], message + message = f"✅ Auto recovered: '{session_dict['user_query'][:30]}...' (Stage {session_dict['current_stage']})" + return session_dict['session_id'], message else: return None, "" except Exception as e: logger.error(f"Error in auto recovery: {str(e)}") return None, "" + def download_novel(novel_text: str, format: str, language: str, session_id: str = None) -> str: """Download novel - DB에서 직접 작가 내용을 가져와서 통합""" @@ -2022,27 +2900,25 @@ def download_novel(novel_text: str, format: str, language: str, session_id: str logger.error(f"Session not found: {session_id}") return None - # 세션의 실제 언어 사용 (파라미터로 받은 language 대신) - actual_language = session['language'] - logger.info(f"Using session language: {actual_language} (parameter was: {language})") + # Convert sqlite3.Row to dict + session_dict = dict(session) if session else {} + + # 세션의 실제 언어 사용 + actual_language = session_dict['language'] + logger.info(f"Using session language: {actual_language}") # DB에서 모든 스테이지 가져오기 stages = NovelDatabase.get_stages(session_id) logger.info(f"Found {len(stages)} stages in database") - # 디버깅: 모든 stage 정보 출력 - for i, stage in enumerate(stages): - role = stage['role'] if 'role' in stage.keys() else 'None' - stage_name = stage['stage_name'] if 'stage_name' in stage.keys() else 'None' - content_len = len(stage['content']) if 'content' in stage.keys() and stage['content'] else 0 - status = stage['status'] if 'status' in stage.keys() else 'None' - logger.debug(f"Stage {i}: role={role}, stage_name={stage_name}, content_length={content_len}, status={status}") + # 품질 점수 가져오기 + quality_scores = json.loads(session_dict.get('quality_scores', '{}')) if session_dict.get('quality_scores') else {} - # 테스트 모드 감지 - writer10이 있으면 테스트 모드 + # 테스트 모드 감지 is_test_mode = False - has_writer10 = any(stage['role'] == 'writer10' for stage in stages if 'role' in stage.keys()) - has_writer1 = any(stage['role'] == 'writer1' for stage in stages if 'role' in stage.keys()) - has_writer3 = any(stage['role'] == 'writer3' for stage in stages if 'role' in stage.keys()) + has_writer10 = any(dict(stage).get('role') == 'writer10' for stage in stages) + has_writer1 = any(dict(stage).get('role') == 'writer1' for stage in stages) + has_writer3 = any(dict(stage).get('role') == 'writer3' for stage in stages) if has_writer10 and has_writer1 and not has_writer3: is_test_mode = True @@ -2061,7 +2937,7 @@ def download_novel(novel_text: str, format: str, language: str, session_id: str else: main_title = 'AI Collaborative Novel' + (' - Test Mode' if is_test_mode else '') title_run = title_para.add_run(main_title) - title_run.font.size = Pt(20) # 신국판에 맞게 크기 조정 + title_run.font.size = Pt(20) title_run.font.bold = True title_para.alignment = WD_ALIGN_PARAGRAPH.CENTER @@ -2070,8 +2946,8 @@ def download_novel(novel_text: str, format: str, language: str, session_id: str theme_para = doc.add_paragraph() theme_label = '주제: ' if actual_language == 'Korean' else 'Theme: ' - theme_run = theme_para.add_run(f'{theme_label}{session["user_query"]}') - theme_run.font.size = Pt(12) # 크기 조정 + theme_run = theme_para.add_run(f'{theme_label}{session_dict["user_query"]}') + theme_run.font.size = Pt(12) theme_para.alignment = WD_ALIGN_PARAGRAPH.CENTER doc.add_paragraph() @@ -2079,9 +2955,21 @@ def download_novel(novel_text: str, format: str, language: str, session_id: str date_para = doc.add_paragraph() date_label = '생성일: ' if actual_language == 'Korean' else 'Created: ' date_run = date_para.add_run(f'{date_label}{datetime.now().strftime("%Y-%m-%d")}') - date_run.font.size = Pt(10) # 크기 조정 + date_run.font.size = Pt(10) date_para.alignment = WD_ALIGN_PARAGRAPH.CENTER + # 품질 평가 추가 + if quality_scores: + doc.add_paragraph() + quality_para = doc.add_paragraph() + if actual_language == 'Korean': + quality_text = f'품질 평가: {quality_scores.get("overall", 0):.1f}/10' + else: + quality_text = f'Quality Score: {quality_scores.get("overall", 0):.1f}/10' + quality_run = quality_para.add_run(quality_text) + quality_run.font.size = Pt(10) + quality_para.alignment = WD_ALIGN_PARAGRAPH.CENTER + doc.add_page_break() # 전체 통계 @@ -2094,11 +2982,10 @@ def download_novel(novel_text: str, format: str, language: str, session_id: str if is_test_mode: # 테스트 모드: writer1 revision + writer10 내용 처리 for stage in stages: - role = stage['role'] if 'role' in stage.keys() else None - stage_name = stage['stage_name'] if 'stage_name' in stage.keys() else '' - content = stage['content'] if 'content' in stage.keys() else '' - - logger.info(f"[TEST MODE] Checking stage: role={role}, name={stage_name}, status={stage['status'] if 'status' in stage.keys() else 'None'}") + stage_dict = dict(stage) # Convert Row to dict + role = stage_dict.get('role') + stage_name = stage_dict.get('stage_name', '') + content = stage_dict.get('content', '') # Writer 1 수정본 if role == 'writer1' and stage_name and ('Revision' in stage_name or '수정본' in stage_name): @@ -2107,12 +2994,13 @@ def download_novel(novel_text: str, format: str, language: str, session_id: str content = content.strip() if content: - word_count = stage['word_count'] if 'word_count' in stage.keys() else len(content.split()) + word_count = stage_dict.get('word_count', len(content.split())) total_words += word_count writer_contents.append({ 'writer_num': 1, 'content': content, - 'word_count': word_count + 'word_count': word_count, + 'quality_score': stage_dict.get('quality_score', 0) }) writer_count = 1 logger.info(f"Added writer 1 (Chapter 1): {word_count} words") @@ -2124,7 +3012,6 @@ def download_novel(novel_text: str, format: str, language: str, session_id: str # [Chapter X] 패턴으로 챕터 분리 chapters = re.split(r'\[Chapter\s+(\d+)\]', content) - # chapters는 ['', '2', 'content2', '3', 'content3', ...] 형태 for i in range(1, len(chapters), 2): if i+1 < len(chapters): chapter_num = int(chapters[i]) @@ -2136,7 +3023,8 @@ def download_novel(novel_text: str, format: str, language: str, session_id: str writer_contents.append({ 'writer_num': chapter_num, 'content': chapter_content, - 'word_count': word_count + 'word_count': word_count, + 'quality_score': stage_dict.get('quality_score', 0) }) writer_count = max(writer_count, chapter_num) logger.info(f"Added Chapter {chapter_num}: {word_count} words") @@ -2145,15 +3033,12 @@ def download_novel(novel_text: str, format: str, language: str, session_id: str logger.info("[NORMAL MODE] Processing all writer revisions...") for stage in stages: - role = stage['role'] if 'role' in stage.keys() else None - stage_name = stage['stage_name'] if 'stage_name' in stage.keys() else '' - content = stage['content'] if 'content' in stage.keys() else '' - status = stage['status'] if 'status' in stage.keys() else '' - - # 디버깅 정보 - logger.debug(f"[NORMAL MODE] Checking: role={role}, name={stage_name}, status={status}, content_len={len(content)}") + stage_dict = dict(stage) # Convert Row to dict + role = stage_dict.get('role') + stage_name = stage_dict.get('stage_name', '') + content = stage_dict.get('content', '') - # 언어에 상관없이 작가 수정본 찾기 + # 작가 수정본 찾기 is_writer = role and role.startswith('writer') is_revision = stage_name and ('Revision' in stage_name or '수정본' in stage_name) @@ -2161,9 +3046,7 @@ def download_novel(novel_text: str, format: str, language: str, session_id: str # 작가 번호 추출 try: writer_num = int(role.replace('writer', '')) - logger.info(f"Found writer {writer_num} revision - stage_name: {stage_name}") except: - logger.warning(f"Could not extract writer number from role: {role}") continue # 페이지 마크 제거 @@ -2172,58 +3055,71 @@ def download_novel(novel_text: str, format: str, language: str, session_id: str content = content.strip() if content: - word_count = stage['word_count'] if 'word_count' in stage.keys() else len(content.split()) + word_count = stage_dict.get('word_count', len(content.split())) total_words += word_count writer_contents.append({ 'writer_num': writer_num, 'content': content, - 'word_count': word_count + 'word_count': word_count, + 'quality_score': stage_dict.get('quality_score', 0) }) - writer_count += 1 # 실제 작가 수 카운트 - logger.info(f"Added writer {writer_num}: {word_count} words, content length: {len(content)}") - else: - logger.warning(f"Writer {writer_num} has empty content after cleaning") + writer_count += 1 + logger.info(f"Added writer {writer_num}: {word_count} words") logger.info(f"Total writers collected: {writer_count}, Total words: {total_words}") - logger.info(f"Writer contents: {len(writer_contents)} entries") # 통계 페이지 stats_heading = '소설 통계' if actual_language == 'Korean' else 'Novel Statistics' doc.add_heading(stats_heading, 1) + if actual_language == 'Korean': doc.add_paragraph(f'모드: {"테스트 모드 (실제 작성된 챕터)" if is_test_mode else "전체 모드 (10개 챕터)"}') doc.add_paragraph(f'총 챕터 수: {writer_count}') doc.add_paragraph(f'총 단어 수: {total_words:,}') - doc.add_paragraph(f'예상 페이지 수: 약 {total_words/300:.0f}') # 한국어는 페이지당 300단어 기준 + doc.add_paragraph(f'예상 페이지 수: 약 {total_words/300:.0f}') doc.add_paragraph(f'언어: 한국어') + if quality_scores: + doc.add_paragraph(f'전체 품질 점수: {quality_scores.get("overall", 0):.1f}/10') + doc.add_paragraph(f'일관성 점수: {quality_scores.get("consistency", 0):.1f}/10') else: doc.add_paragraph(f'Mode: {"Test Mode (Actual chapters written)" if is_test_mode else "Full Mode (10 chapters)"}') doc.add_paragraph(f'Total Chapters: {writer_count}') doc.add_paragraph(f'Total Words: {total_words:,}') - doc.add_paragraph(f'Estimated Pages: ~{total_words/250:.0f}') # 영어는 페이지당 250단어 기준 + doc.add_paragraph(f'Estimated Pages: ~{total_words/250:.0f}') doc.add_paragraph(f'Language: English') + if quality_scores: + doc.add_paragraph(f'Overall Quality Score: {quality_scores.get("overall", 0):.1f}/10') + doc.add_paragraph(f'Consistency Score: {quality_scores.get("consistency", 0):.1f}/10') + doc.add_page_break() # 목차 if writer_contents: toc_heading = '목차' if actual_language == 'Korean' else 'Table of Contents' doc.add_heading(toc_heading, 1) + # writer_contents를 writer_num으로 정렬 sorted_contents = sorted(writer_contents, key=lambda x: x['writer_num']) for item in sorted_contents: chapter_num = item['writer_num'] content = item['content'] + quality = item.get('quality_score', 0) - # 챕터 제목 추출 - 첫 번째 의미있는 문장 또는 단락 + # 챕터 제목 추출 chapter_title = extract_chapter_title(content, chapter_num, actual_language) - # 목차 항목 추가 (페이지 번호 없이) + # 목차 항목 추가 (품질 점수 포함) if actual_language == 'Korean': toc_entry = f"제{chapter_num}장: {chapter_title}" + if quality > 0: + toc_entry += f" (품질: {quality:.1f}/10)" else: toc_entry = f"Chapter {chapter_num}: {chapter_title}" + if quality > 0: + toc_entry += f" (Quality: {quality:.1f}/10)" doc.add_paragraph(toc_entry) + doc.add_page_break() # 각 작가의 내용 추가 (정렬된 순서대로) @@ -2231,6 +3127,7 @@ def download_novel(novel_text: str, format: str, language: str, session_id: str writer_num = writer_data['writer_num'] content = writer_data['content'] word_count = writer_data['word_count'] + quality = writer_data.get('quality_score', 0) # 챕터 헤더 chapter_header = f'제{writer_num}장' if actual_language == 'Korean' else f'Chapter {writer_num}' @@ -2243,8 +3140,18 @@ def download_novel(novel_text: str, format: str, language: str, session_id: str title_para.style.font.bold = True title_para.alignment = WD_ALIGN_PARAGRAPH.CENTER - word_count_label = f'단어 수: {word_count:,}' if actual_language == 'Korean' else f'Word Count: {word_count:,}' - doc.add_paragraph(word_count_label) + # 메타 정보 + meta_text = [] + if actual_language == 'Korean': + meta_text.append(f'단어 수: {word_count:,}') + if quality > 0: + meta_text.append(f'품질 점수: {quality:.1f}/10') + else: + meta_text.append(f'Word Count: {word_count:,}') + if quality > 0: + meta_text.append(f'Quality Score: {quality:.1f}/10') + + doc.add_paragraph(' | '.join(meta_text)) doc.add_paragraph() # 작가 내용 추가 @@ -2274,7 +3181,7 @@ def download_novel(novel_text: str, format: str, language: str, session_id: str # Save temp_dir = tempfile.gettempdir() - safe_filename = re.sub(r'[^\w\s-]', '', session['user_query'][:30]).strip() + safe_filename = re.sub(r'[^\w\s-]', '', session_dict['user_query'][:30]).strip() mode_suffix = "_TestMode" if is_test_mode else "_Complete" filename = f"Novel{mode_suffix}_{safe_filename}_{timestamp}.docx" filepath = os.path.join(temp_dir, filename) @@ -2283,9 +3190,9 @@ def download_novel(novel_text: str, format: str, language: str, session_id: str logger.info(f"DOCX saved successfully: {filepath} ({total_words} words, {writer_count} writers)") return filepath else: - # TXT format - 동일한 ���정 적용 + # TXT format temp_dir = tempfile.gettempdir() - safe_filename = re.sub(r'[^\w\s-]', '', session['user_query'][:30]).strip() + safe_filename = re.sub(r'[^\w\s-]', '', session_dict['user_query'][:30]).strip() mode_suffix = "_TestMode" if is_test_mode else "_Complete" filename = f"Novel{mode_suffix}_{safe_filename}_{timestamp}.txt" filepath = os.path.join(temp_dir, filename) @@ -2299,53 +3206,60 @@ def download_novel(novel_text: str, format: str, language: str, session_id: str f.write("="*60 + "\n") if actual_language == 'Korean': - f.write(f"주제: {session['user_query']}\n") + f.write(f"주제: {session_dict['user_query']}\n") f.write(f"언어: 한국어\n") f.write(f"생성일: {datetime.now()}\n") f.write(f"모드: {'테스트 모드 (실제 작성된 챕터)' if is_test_mode else '전체 모드 (10개 챕터)'}\n") + if quality_scores: + f.write(f"품질 점수: {quality_scores.get('overall', 0):.1f}/10\n") else: - f.write(f"Theme: {session['user_query']}\n") + f.write(f"Theme: {session_dict['user_query']}\n") f.write(f"Language: English\n") f.write(f"Created: {datetime.now()}\n") f.write(f"Mode: {'Test Mode (Actual chapters)' if is_test_mode else 'Full Mode (10 chapters)'}\n") + if quality_scores: + f.write(f"Quality Score: {quality_scores.get('overall', 0):.1f}/10\n") f.write("="*60 + "\n\n") total_words = 0 writer_count = 0 + # 수집 로직은 DOCX와 동일 + writer_contents = [] + if is_test_mode: - # 테스트 모드: writer1 + writer10 처리 + # 테스트 모드 처리 (DOCX와 동일) for stage in stages: - role = stage['role'] if 'role' in stage.keys() else None - stage_name = stage['stage_name'] if 'stage_name' in stage.keys() else '' - content = stage['content'] if 'content' in stage.keys() else '' + stage_dict = dict(stage) # Convert Row to dict + role = stage_dict.get('role') + stage_name = stage_dict.get('stage_name', '') + content = stage_dict.get('content', '') - # Writer 1 수정본 if role == 'writer1' and stage_name and ('Revision' in stage_name or '수정본' in stage_name): content = re.sub(r'\[(?:페이지|Page|page)\s*\d+\]', '', content) content = re.sub(r'(?:페이지|Page)\s*\d+:', '', content) content = content.strip() if content: - word_count = stage['word_count'] if 'word_count' in stage.keys() else len(content.split()) + word_count = stage_dict.get('word_count', len(content.split())) total_words += word_count writer_count = 1 f.write(f"\n{'='*40}\n") chapter_label = f"제1장" if actual_language == 'Korean' else "CHAPTER 1" f.write(f"{chapter_label}\n") - # 챕터 제목 추출 및 출력 chapter_title = extract_chapter_title(content, 1, actual_language) f.write(f"{chapter_title}\n") word_count_label = f"단어 수: {word_count:,}" if actual_language == 'Korean' else f"Word Count: {word_count:,}" f.write(f"{word_count_label}\n") + if stage_dict.get('quality_score', 0) > 0: + quality_label = f"품질: {stage_dict['quality_score']:.1f}/10" if actual_language == 'Korean' else f"Quality: {stage_dict['quality_score']:.1f}/10" + f.write(f"{quality_label}\n") f.write(f"{'='*40}\n\n") f.write(content) f.write("\n\n") - # Writer 10 elif role == 'writer10': - # [Chapter X] 패턴으로 챕터 분리 chapters = re.split(r'\[Chapter\s+(\d+)\]', content) for i in range(1, len(chapters), 2): @@ -2361,7 +3275,6 @@ def download_novel(novel_text: str, format: str, language: str, session_id: str f.write(f"\n{'='*40}\n") chapter_label = f"제{chapter_num}장" if actual_language == 'Korean' else f"CHAPTER {chapter_num}" f.write(f"{chapter_label}\n") - # 챕터 제목 추출 및 출력 chapter_title = extract_chapter_title(chapter_content, chapter_num, actual_language) f.write(f"{chapter_title}\n") word_count_label = f"단어 수: {word_count:,}" if actual_language == 'Korean' else f"Word Count: {word_count:,}" @@ -2370,41 +3283,41 @@ def download_novel(novel_text: str, format: str, language: str, session_id: str f.write(chapter_content) f.write("\n\n") else: - # 일반 모드 - 수정된 로직 + # 일반 모드 for stage in stages: - role = stage['role'] if 'role' in stage.keys() else None - stage_name = stage['stage_name'] if 'stage_name' in stage.keys() else '' - content = stage['content'] if 'content' in stage.keys() else '' + stage_dict = dict(stage) # Convert Row to dict + role = stage_dict.get('role') + stage_name = stage_dict.get('stage_name', '') + content = stage_dict.get('content', '') - # 언어에 상관없이 작가 수정본 찾기 is_writer = role and role.startswith('writer') is_revision = stage_name and ('Revision' in stage_name or '수정본' in stage_name) if is_writer and is_revision: - # 작가 번호 추출 try: writer_num = int(role.replace('writer', '')) except: continue - # 페이지 마크 제거 content = re.sub(r'\[(?:페이지|Page|page)\s*\d+\]', '', content) content = re.sub(r'(?:페이지|Page)\s*\d+:', '', content) content = content.strip() if content: - word_count = stage['word_count'] if 'word_count' in stage.keys() else len(content.split()) + word_count = stage_dict.get('word_count', len(content.split())) total_words += word_count writer_count += 1 f.write(f"\n{'='*40}\n") chapter_label = f"제{writer_num}장" if actual_language == 'Korean' else f"CHAPTER {writer_num}" f.write(f"{chapter_label}\n") - # 챕터 제목 추출 및 출력 chapter_title = extract_chapter_title(content, writer_num, actual_language) f.write(f"{chapter_title}\n") word_count_label = f"단어 수: {word_count:,}" if actual_language == 'Korean' else f"Word Count: {word_count:,}" f.write(f"{word_count_label}\n") + if stage_dict.get('quality_score', 0) > 0: + quality_label = f"품질: {stage_dict['quality_score']:.1f}/10" if actual_language == 'Korean' else f"Quality: {stage_dict['quality_score']:.1f}/10" + f.write(f"{quality_label}\n") f.write(f"{'='*40}\n\n") f.write(content) f.write("\n\n") @@ -2412,13 +3325,18 @@ def download_novel(novel_text: str, format: str, language: str, session_id: str f.write(f"\n{'='*60}\n") if actual_language == 'Korean': f.write(f"총계: {writer_count}개 챕터, {total_words:,} 단어\n") + if quality_scores: + f.write(f"전체 품질: {quality_scores.get('overall', 0):.1f}/10\n") else: f.write(f"TOTAL: {writer_count} chapters, {total_words:,} words\n") + if quality_scores: + f.write(f"Overall Quality: {quality_scores.get('overall', 0):.1f}/10\n") f.write(f"{'='*60}\n") logger.info(f"TXT saved successfully: {filepath} ({total_words} words)") return filepath + # Custom CSS custom_css = """ .gradio-container { @@ -2486,18 +3404,29 @@ custom_css = """ color: #2196F3; font-weight: bold; } + +.quality-indicator { + color: #FF9800; + font-weight: bold; +} + +.consistency-indicator { + color: #9C27B0; + font-weight: bold; +} """ + # Create Gradio Interface def create_interface(): - with gr.Blocks(css=custom_css, title="SOMA Novel Writing System") as interface: + with gr.Blocks(css=custom_css, title="SOMA Novel Writing System - Enhanced") as interface: gr.HTML("""

- 📚 SOMA Novel Writing System + 📚 SOMA Novel Writing System - Enhanced Edition

- AI Collaborative Novel Generation - 10 Writers Edition + AI Collaborative Novel Generation with Quality Assurance

Enter a theme or prompt, and watch as AI agents collaborate to create a complete 30-page novella. @@ -2505,6 +3434,9 @@ def create_interface():

🔍 Web search enabled | 💾 Auto-save to database | + 🔍 Real-time consistency checking | + ⭐ Quality scoring system +

♻️ Resume anytime | 🧪 Test mode: 7 stages (Writer 1 + Writer 10)

@@ -2536,10 +3468,22 @@ def create_interface(): info="Complete Writer 1 & 10 process for 10 chapters / 작가 1, 10만 작성하여 10개 챕터 생성" ) - # Web search status indicator - web_search_status = gr.Markdown( - value=f"🔍 **Web Search:** {'Enabled' if WebSearchIntegration().enabled else 'Disabled (Set BRAVE_SEARCH_API_KEY)'}" - ) + # Feature status indicators + with gr.Row(): + web_search_status = gr.Markdown( + value=f"🔍 **Web Search:** {'Enabled' if WebSearchIntegration().enabled else 'Disabled (Set BRAVE_SEARCH_API_KEY)'}" + ) + consistency_status = gr.Markdown( + value="🔍 **Consistency Check:** Enabled" + ) + + with gr.Row(): + quality_status = gr.Markdown( + value="⭐ **Quality Scoring:** Enabled" + ) + mode_status = gr.Markdown( + value=f"🚀 **Mode:** {'Test' if TEST_MODE else 'Production'}" + ) with gr.Row(): submit_btn = gr.Button("🚀 Start Writing / 작성 시작", variant="primary", scale=2) @@ -2573,7 +3517,7 @@ def create_interface(): with gr.Tab("📖 Final Novel / 최종 소설"): novel_output = gr.Markdown( - value="Complete novel will be available for download after all writers finish.", + value="Complete novel will be available for download after all writers finish.\n\nQuality report will be shown here.", elem_id="novel-output" ) @@ -2701,11 +3645,13 @@ def create_interface(): return interface + # Main execution if __name__ == "__main__": import sys - logger.info("Starting SOMA Novel Writing System - 10 Writers Edition...") + logger.info("Starting SOMA Novel Writing System - Enhanced Edition...") + logger.info("=" * 60) # Check environment if TEST_MODE: @@ -2719,11 +3665,20 @@ if __name__ == "__main__": else: logger.warning("Web search is DISABLED. Set BRAVE_SEARCH_API_KEY environment variable to enable.") + # Check DOCX support + if DOCX_AVAILABLE: + logger.info("DOCX export is ENABLED") + else: + logger.warning("DOCX export is DISABLED. Install python-docx to enable.") + + logger.info("=" * 60) + # Initialize database on startup logger.info("Initializing database...") NovelDatabase.init_db() logger.info("Database initialized successfully.") + # Create and launch interface interface = create_interface() interface.launch(