diff --git "a/app-backup.py" "b/app-backup.py" --- "a/app-backup.py" +++ "b/app-backup.py" @@ -36,28 +36,39 @@ FRIENDLI_TOKEN = os.getenv("FRIENDLI_TOKEN", "") BRAVE_SEARCH_API_KEY = os.getenv("BRAVE_SEARCH_API_KEY", "") API_URL = "https://api.friendli.ai/dedicated/v1/chat/completions" MODEL_ID = "dep89a2fld32mcm" -TEST_MODE = os.getenv("TEST_MODE", "false").lower() == "true" # 환경 변수 검증 -if not FRIENDLI_TOKEN and not TEST_MODE: - logger.warning("FRIENDLI_TOKEN not set and TEST_MODE is false. Application will run in test mode.") - TEST_MODE = True +if not FRIENDLI_TOKEN: + logger.error("FRIENDLI_TOKEN not set. Application will not work properly.") + FRIENDLI_TOKEN = "dummy_token" if not BRAVE_SEARCH_API_KEY: logger.warning("BRAVE_SEARCH_API_KEY not set. Web search features will be disabled.") # 전역 변수 conversation_history = [] -selected_language = "English" # 기본 언어 +selected_language = "English" # DB 경로 DB_PATH = "novel_sessions.db" db_lock = threading.Lock() -# Stage 번호 상수 - 10명의 작가로 변경 -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 # 총 작가 수 +# 최적화된 단계 구성 (30단계) +OPTIMIZED_STAGES = [ + ("director", "🎬 감독자: 초기 기획"), + ("critic", "📝 비평가: 기획 검토"), + ("director", "🎬 감독자: 수정된 마스터플랜"), +] + [ + (f"writer{i}", f"✍️ 작가 {i}: 초안 (페이지 {(i-1)*3+1}-{i*3})") + for i in range(1, 11) +] + [ + ("critic", f"📝 비평가: 일관성 검토"), +] + [ + (f"writer{i}", f"✍️ 작가 {i}: 수정본 (페이지 {(i-1)*3+1}-{i*3})") + for i in range(1, 11) +] + [ + ("critic", f"📝 비평가: 최종 검토"), +] @dataclass @@ -69,33 +80,42 @@ class CharacterState: 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) + description: str = "" + role: str = "" @dataclass class PlotPoint: """플롯 포인트를 나타내는 데이터 클래스""" chapter: int - event_type: str # 'introduction', 'conflict', 'resolution', 'death', 'revelation' + event_type: str description: str characters_involved: List[str] - impact_level: int # 1-10 + impact_level: int timestamp: str = "" -class NovelStateTracker: - """소설 전체의 상태를 추적하고 일관성을 유지하는 시스템""" +@dataclass +class TimelineEvent: + """시간선 이벤트를 나타내는 데이터 클래스""" + chapter: int + time_reference: str + event_description: str + duration: str = "" + relative_time: str = "" + + +class ConsistencyTracker: + """일관성 추적 시스템""" 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]] = {} + self.timeline_events: List[TimelineEvent] = [] + self.locations: Dict[str, str] = {} + self.established_facts: List[str] = [] + self.content_hashes: set = set() def register_character(self, character: CharacterState): """새 캐릭터 등록""" @@ -105,8 +125,7 @@ class NovelStateTracker: 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)) + self.register_character(CharacterState(name=name, last_seen_chapter=chapter)) char = self.character_states[name] for key, value in updates.items(): @@ -115,382 +134,179 @@ class NovelStateTracker: 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 add_timeline_event(self, event: TimelineEvent): + """시간선 이벤트 추가""" + self.timeline_events.append(event) + + def check_repetition(self, content: str) -> Tuple[bool, str]: + """반복 내용 검사""" + # 내용 해시 생성 + content_hash = hashlib.md5(content.encode()).hexdigest() + + if content_hash in self.content_hashes: + return True, "완전 동일한 내용 반복" + + # 문장 수준 반복 검사 + sentences = re.split(r'[.!?]+', content) + for sentence in sentences: + if len(sentence.strip()) > 20: + sentence_hash = hashlib.md5(sentence.strip().encode()).hexdigest() + if sentence_hash in self.content_hashes: + return True, f"문장 반복: {sentence.strip()[:50]}..." + + # 해시 저장 + self.content_hashes.add(content_hash) + for sentence in sentences: + if len(sentence.strip()) > 20: + sentence_hash = hashlib.md5(sentence.strip().encode()).hexdigest() + self.content_hashes.add(sentence_hash) + + return False, "" + 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") - + if char_name.lower() in content.lower(): + # 사망한 캐릭터 등장 검사 + if not char_state.alive: + errors.append(f"⚠️ 사망한 캐릭터 '{char_name}'이 등장함") + + # 부상 상태 연속성 검사 + if char_state.injuries and "완전히 회복" not in content: + recent_injuries = [inj for inj in char_state.injuries if "중상" in inj or "심각" in inj] + if recent_injuries: + errors.append(f"⚠️ '{char_name}'의 부상 상태가 언급되지 않음") + + # 반복 검사 + is_repetition, repeat_msg = self.check_repetition(content) + if is_repetition: + errors.append(f"🔄 {repeat_msg}") + + # 시간선 일관성 검사 + time_references = re.findall(r'(어제|오늘|내일|지금|방금|곧|나중에|이전에|다음에)', content) + if time_references: + # 최근 시간 참조와 비교 + recent_events = [e for e in self.timeline_events if e.chapter >= chapter - 1] + if recent_events and time_references: + # 간단한 시간 일관성 검사 + pass # 복잡한 로직은 생략 + 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 get_character_summary(self, chapter: int) -> str: + """현재 챕터 기준 캐릭터 요약""" + summary = "\n=== 캐릭터 현황 ===\n" - 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) + active_chars = [char for char in self.character_states.values() + if char.last_seen_chapter >= chapter - 2] - 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] - }) + for char in active_chars: + status = "생존" if char.alive else "사망" + summary += f"• {char.name}: {status}" + if char.alive: + if char.location: + summary += f" (위치: {char.location})" + if char.injuries: + summary += f" (부상: {', '.join(char.injuries[-1:])})" + summary += "\n" - # 부상 감지 - 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: - """문학적 품질을 향상시키는 시스템""" + return summary - 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: - """문학적 품질 향상을 위한 프롬프트 강화""" + def get_plot_summary(self, chapter: int) -> str: + """플롯 요약""" + summary = "\n=== 주요 사건 ===\n" - 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 + recent_events = [p for p in self.plot_points if p.chapter >= chapter - 2] + for event in recent_events[-5:]: + summary += f"• 챕터 {event.chapter}: {event.description}\n" + + return summary -class QualityScorer: - """생성된 콘텐츠의 품질을 평가하는 시스템""" +class ConsistencyValidator: + """일관성 검증 시스템""" - 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) + def __init__(self, consistency_tracker: ConsistencyTracker): + self.tracker = consistency_tracker - # 감정적 깊이 (내면 묘사, 캐릭터 발전) - scores['emotional_depth'] = self._assess_emotional_depth(content) + def validate_writer_content(self, writer_num: int, content: str, + all_previous_content: str) -> List[str]: + """작가 내용 일관성 검증""" + errors = [] - # 독창성 (클리셰 회피) - scores['originality'] = self._assess_originality(content) + # 기본 일관성 검증 + basic_errors = self.tracker.validate_consistency(writer_num, content) + errors.extend(basic_errors) - # 페이싱 (긴장과 이완의 균형) - scores['pacing'] = self._assess_pacing(content) + # 캐릭터 추출 및 상태 업데이트 + characters = self.extract_characters(content) + for char_name in characters: + self.tracker.update_character_state(char_name, writer_num, {}) + + # 새로운 사실 추출 + new_facts = self.extract_facts(content) + for fact in new_facts: + if fact not in self.tracker.established_facts: + self.tracker.established_facts.append(fact) - return scores + return errors - def _assess_literary_quality(self, content: str) -> float: - """문학적 품질 평가""" - score = 5.0 # 기본 점수 - - # 문장 길이 다양성 + def extract_characters(self, content: str) -> List[str]: + """캐릭터 이름 추출""" + # 간단한 캐릭터 추출 로직 + potential_names = re.findall(r'\b[A-Z][a-z]+\b', content) + # 일반적인 단어 제외 + common_words = {'The', 'This', 'That', 'They', 'Then', 'There', 'When', 'Where', 'What', 'Who', 'How', 'Why'} + characters = [name for name in potential_names if name not in common_words] + return list(set(characters)) + + def extract_facts(self, content: str) -> List[str]: + """확립된 사실 추출""" + # 간단한 사실 추출 로직 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 + facts = [] + + for sentence in sentences: + sentence = sentence.strip() + if len(sentence) > 10: + # 확정적인 표현이 있는 문장들 + definitive_patterns = [ + r'.*is.*', + r'.*was.*', + r'.*has.*', + r'.*had.*', + r'.*died.*', + r'.*killed.*', + r'.*destroyed.*', + r'.*created.*' + ] - return min(score, 10.0) + for pattern in definitive_patterns: + if re.match(pattern, sentence, re.IGNORECASE): + facts.append(sentence) + break + + return facts class WebSearchIntegration: - """Brave Search API integration for research""" + """웹 검색 기능 (감독자 단계에서만 사용)""" def __init__(self): self.brave_api_key = BRAVE_SEARCH_API_KEY self.search_url = "https://api.search.brave.com/res/v1/web/search" self.enabled = bool(self.brave_api_key) - def search(self, query: str, count: int = 5, language: str = "en") -> List[Dict]: - """Perform web search using Brave Search API""" + def search(self, query: str, count: int = 3, language: str = "en") -> List[Dict]: + """웹 검색 수행""" if not self.enabled: return [] @@ -499,7 +315,6 @@ class WebSearchIntegration: "X-Subscription-Token": self.brave_api_key } - # 언어에 따른 검색 설정 search_lang = "ko" if language == "Korean" else "en" params = { @@ -524,29 +339,20 @@ class WebSearchIntegration: logger.error(f"Search error: {str(e)}") return [] - def extract_relevant_info(self, results: List[Dict], max_chars: int = 3000) -> str: - """Extract relevant information from search results""" + def extract_relevant_info(self, results: List[Dict], max_chars: int = 2000) -> str: + """검색 결과에서 관련 정보 추출""" if not results: return "" extracted = [] total_chars = 0 - for i, result in enumerate(results[:5], 1): # 최대 5개 결과 + for i, result in enumerate(results[:3], 1): title = result.get("title", "") description = result.get("description", "") url = result.get("url", "") - # 일부 내용 추출 - extra_text = "" - if "extra_snippets" in result: - extra_text = " ".join(result["extra_snippets"][:2]) - - info = f"""[{i}] {title} -{description} -{extra_text} -Source: {url} -""" + info = f"[{i}] {title}\n{description}\n출처: {url}\n" if total_chars + len(info) < max_chars: extracted.append(info) @@ -555,86 +361,19 @@ Source: {url} break return "\n---\n".join(extracted) - - def create_research_queries(self, topic: str, role: str, stage_info: Dict, language: str = "English") -> List[str]: - """Create multiple research queries based on role and context""" - queries = [] - - if language == "Korean": - if role == "director": - queries = [ - f"{topic} 소설 배경 설정", - f"{topic} 역사적 사실", - f"{topic} 문화적 특징" - ] - elif role.startswith("writer"): - writer_num = int(role.replace("writer", "")) - if writer_num <= 3: # 초반부 작가 - queries = [ - f"{topic} 구체적 장면 묘사", - f"{topic} 전문 용어 설명" - ] - elif writer_num <= 6: # 중반부 작가 - queries = [ - f"{topic} 갈등 상황 사례", - f"{topic} 심리적 측면" - ] - else: # 후반부 작가 - queries = [ - f"{topic} 해결 방법", - f"{topic} 감동적인 사례" - ] - elif role == "critic": - queries = [ - f"{topic} 문학 작품 분석", - f"{topic} 유사 소설 추천" - ] - else: - if role == "director": - queries = [ - f"{topic} novel setting ideas", - f"{topic} historical facts", - f"{topic} cultural aspects" - ] - elif role.startswith("writer"): - writer_num = int(role.replace("writer", "")) - if writer_num <= 3: # Early writers - queries = [ - f"{topic} vivid scene descriptions", - f"{topic} technical terminology explained" - ] - elif writer_num <= 6: # Middle writers - queries = [ - f"{topic} conflict scenarios", - f"{topic} psychological aspects" - ] - else: # Later writers - queries = [ - f"{topic} resolution methods", - f"{topic} emotional stories" - ] - elif role == "critic": - queries = [ - f"{topic} literary analysis", - f"{topic} similar novels recommendations" - ] - - return queries class NovelDatabase: - """Novel session management database with enhanced recovery features""" + """소설 세션 관리 데이터베이스""" @staticmethod def init_db(): - """Initialize database tables with WAL mode for better concurrency""" + """데이터베이스 초기화""" with sqlite3.connect(DB_PATH) as conn: - # Enable WAL mode for better concurrent access conn.execute("PRAGMA journal_mode=WAL") - cursor = conn.cursor() - # Sessions table with enhanced recovery fields + # 세션 테이블 cursor.execute(''' CREATE TABLE IF NOT EXISTS sessions ( session_id TEXT PRIMARY KEY, @@ -644,14 +383,12 @@ class NovelDatabase: updated_at TEXT DEFAULT (datetime('now')), status TEXT DEFAULT 'active', current_stage INTEGER DEFAULT 0, - last_saved_stage INTEGER DEFAULT -1, - recovery_data TEXT, final_novel TEXT, - quality_scores TEXT + consistency_report TEXT ) ''') - # Stages table - 각 스테이지의 전체 내용 저장 + # 단계 테이블 cursor.execute(''' CREATE TABLE IF NOT EXISTS stages ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -662,7 +399,7 @@ class NovelDatabase: content TEXT, word_count INTEGER DEFAULT 0, status TEXT DEFAULT 'pending', - quality_score REAL DEFAULT 0.0, + consistency_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), @@ -670,7 +407,7 @@ class NovelDatabase: ) ''') - # Character states table - 캐릭터 상태 추적 + # 캐릭터 상태 테이블 cursor.execute(''' CREATE TABLE IF NOT EXISTS character_states ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -681,155 +418,24 @@ class NovelDatabase: 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 ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - session_id TEXT NOT NULL, - stage_number INTEGER NOT NULL, - role TEXT NOT NULL, - query TEXT NOT NULL, - results TEXT, created_at TEXT DEFAULT (datetime('now')), FOREIGN KEY (session_id) REFERENCES sessions(session_id) ) ''') - # 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""" - with sqlite3.connect(DB_PATH) as conn: - cursor = conn.cursor() - cursor.execute(''' - INSERT INTO search_history (session_id, stage_number, role, query, results) - VALUES (?, ?, ?, ?, ?) - ''', (session_id, stage_number, role, query, results)) - conn.commit() - @staticmethod @contextmanager def get_db(): - """Database connection context manager with timeout""" + """데이터베이스 연결 컨텍스트 매니저""" with db_lock: conn = sqlite3.connect(DB_PATH, timeout=30.0) conn.row_factory = sqlite3.Row @@ -840,7 +446,7 @@ class NovelDatabase: @staticmethod def create_session(user_query: str, language: str) -> str: - """Create new session""" + """새 세션 생성""" session_id = hashlib.md5(f"{user_query}{datetime.now()}".encode()).hexdigest() with NovelDatabase.get_db() as conn: @@ -856,47 +462,41 @@ class NovelDatabase: @staticmethod def save_stage(session_id: str, stage_number: int, stage_name: str, role: str, content: str, status: str = 'complete', - quality_score: float = 0.0): - """Save stage content with word count and quality score""" + consistency_score: float = 0.0): + """단계 저장""" word_count = len(content.split()) if content else 0 - 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, quality_score) + INSERT INTO stages (session_id, stage_number, stage_name, role, content, word_count, status, consistency_score) VALUES (?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(session_id, stage_number) - 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)) + DO UPDATE SET content=?, word_count=?, status=?, stage_name=?, consistency_score=?, updated_at=datetime('now') + ''', (session_id, stage_number, stage_name, role, content, word_count, status, consistency_score, + content, word_count, status, stage_name, consistency_score)) - # Update session cursor.execute(''' UPDATE sessions - SET updated_at = datetime('now'), - current_stage = ?, - last_saved_stage = ? + SET updated_at = datetime('now'), current_stage = ? WHERE session_id = ? - ''', (stage_number, stage_number, session_id)) + ''', (stage_number, session_id)) conn.commit() - logger.info(f"Stage saved successfully with quality score: {quality_score:.1f}") @staticmethod def get_session(session_id: str) -> Optional[Dict]: - """Get session info""" + """세션 정보 가져오기""" with NovelDatabase.get_db() as conn: cursor = conn.cursor() cursor.execute('SELECT * FROM sessions WHERE session_id = ?', (session_id,)) - return cursor.fetchone() + row = cursor.fetchone() + return dict(row) if row else None @staticmethod def get_latest_active_session() -> Optional[Dict]: - """Get the most recent active session""" + """최근 활성 세션 가져오기""" with NovelDatabase.get_db() as conn: cursor = conn.cursor() cursor.execute(''' @@ -905,11 +505,12 @@ class NovelDatabase: ORDER BY updated_at DESC LIMIT 1 ''') - return cursor.fetchone() + row = cursor.fetchone() + return dict(row) if row else None @staticmethod def get_stages(session_id: str) -> List[Dict]: - """Get all stages for a session""" + """세션의 모든 단계 가져오기""" with NovelDatabase.get_db() as conn: cursor = conn.cursor() cursor.execute(''' @@ -917,100 +518,49 @@ class NovelDatabase: WHERE session_id = ? ORDER BY stage_number ''', (session_id,)) - return cursor.fetchall() + return [dict(row) for row in cursor.fetchall()] @staticmethod - def get_all_writer_content(session_id: str, test_mode: bool = False) -> str: - """모든 작가의 수정본 내용을 가져와서 합치기""" + def get_writer_content(session_id: str) -> str: + """모든 작가의 최종 내용 가져오기""" with NovelDatabase.get_db() as conn: cursor = conn.cursor() all_content = [] - writer_count = 0 - total_word_count = 0 - if test_mode: - # 테스트 모드: writer1 revision + writer10 content + # 작가 1-10의 수정본 가져오기 + for writer_num in range(1, 11): 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,)) + SELECT content FROM stages + WHERE session_id = ? AND role = ? + AND stage_name LIKE '%수정본%' + ORDER BY stage_number DESC + LIMIT 1 + ''', (session_id, f'writer{writer_num}')) 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() - - if clean_content: - writer_count += 1 - word_count = row['word_count'] or len(clean_content.split()) - total_word_count += word_count - all_content.append(clean_content) - logger.info(f"Test mode - Writer 1: {word_count} words") - - # Writer 10 content - cursor.execute(''' - SELECT content, stage_name, word_count FROM stages - WHERE session_id = ? AND role = 'writer10' - ''', (session_id,)) - - row = cursor.fetchone() - if row and row['content']: - 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 - logger.info(f"Test mode - Writer 10: {word_count} words") - else: - # 일반 모드: 모든 작가의 수정본 - writer_stages_to_check = WRITER_REVISION_STAGES - - for stage_num in writer_stages_to_check: - cursor.execute(''' - SELECT content, stage_name, word_count FROM stages - WHERE session_id = ? AND stage_number = ? - ''', (session_id, stage_num)) - - 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() - - if clean_content: - writer_count += 1 - 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}: {word_count} words") - - full_content = '\n\n'.join(all_content) - logger.info(f"Total: {writer_count} writers, {total_word_count} words") + content = row['content'].strip() + if content: + all_content.append(content) - return full_content + return '\n\n'.join(all_content) @staticmethod - 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 - + def update_final_novel(session_id: str, final_novel: str, consistency_report: str = ""): + """최종 소설 업데이트""" with NovelDatabase.get_db() as conn: cursor = conn.cursor() cursor.execute(''' UPDATE sessions - SET final_novel = ?, status = 'complete', updated_at = datetime('now'), quality_scores = ? + SET final_novel = ?, status = 'complete', updated_at = datetime('now'), consistency_report = ? WHERE session_id = ? - ''', (final_novel, quality_json, session_id)) + ''', (final_novel, consistency_report, session_id)) conn.commit() - logger.info(f"Updated final novel for session {session_id}, length: {len(final_novel)}") @staticmethod def get_active_sessions() -> List[Dict]: - """Get all active sessions""" + """활성 세션 목록 가져오기""" with NovelDatabase.get_db() as conn: cursor = conn.cursor() cursor.execute(''' @@ -1020,56 +570,27 @@ class NovelDatabase: ORDER BY updated_at DESC LIMIT 10 ''') - return cursor.fetchall() - - @staticmethod - def parse_datetime(datetime_str: str) -> datetime: - """Parse SQLite datetime string safely""" - try: - return datetime.fromisoformat(datetime_str) - except: - 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 + return [dict(row) for row in cursor.fetchall()] class NovelWritingSystem: + """최적화된 소설 생성 시스템""" + def __init__(self): self.token = FRIENDLI_TOKEN self.api_url = API_URL self.model_id = MODEL_ID - self.test_mode = TEST_MODE or not self.token - # Web search integration + # 핵심 시스템 컴포넌트 + self.consistency_tracker = ConsistencyTracker() + self.consistency_validator = ConsistencyValidator(self.consistency_tracker) 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: - logger.info("Running in production mode with API calls enabled.") - - if self.web_search.enabled: - logger.info("Web search is enabled with Brave Search API.") - else: - logger.warning("Web search is disabled. Set BRAVE_SEARCH_API_KEY to enable.") + # 세션 관리 + self.current_session_id = None - # Initialize database + # 데이터베이스 초기화 NovelDatabase.init_db() - - # Session management - self.current_session_id = None - self.total_stages = 0 def create_headers(self): """API 헤더 생성""" @@ -1078,337 +599,186 @@ class NovelWritingSystem: "Content-Type": "application/json" } - def enhance_prompt_with_research(self, original_prompt: str, role: str, - topic: str, stage_info: Dict, language: str = "English") -> str: - """Enhance prompt with web search results""" - if not self.web_search.enabled or self.test_mode: - return original_prompt - - # Create research queries - queries = self.web_search.create_research_queries(topic, role, stage_info, language) - - all_research = [] - for query in queries[:2]: # 최대 2개 쿼리 - logger.info(f"Searching: {query}") - results = self.web_search.search(query, count=3, language=language) - if results: - research_text = self.web_search.extract_relevant_info(results, max_chars=1500) - if research_text: - all_research.append(f"### {query}\n{research_text}") - - # Save search history - if self.current_session_id: - NovelDatabase.save_search_history( - self.current_session_id, - stage_info.get('stage_idx', 0), - role, - query, - research_text - ) - - if not all_research: - return original_prompt - - # Add research to prompt - if language == "Korean": - research_section = f""" -## 웹 검색 참고 자료: -{chr(10).join(all_research)} - -위의 검색 결과를 참고하여 더욱 사실적이고 구체적인 내용을 작성하세요. -검색 결과의 정보를 창의적으로 활용하되, 직접 인용은 피하고 소설에 자연스럽게 녹여내세요. -""" - else: - research_section = f""" -## Web Search Reference: -{chr(10).join(all_research)} - -Use the above search results to create more realistic and specific content. -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) + def create_director_initial_prompt(self, user_query: str, language: str = "English") -> str: + """감독자 초기 프롬프트 (웹 검색 포함)""" - 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 -""" + # 웹 검색 수행 + search_results = "" + if self.web_search.enabled: + search_queries = [] + if language == "Korean": + search_queries = [ + f"{user_query} 소설 설정", + f"{user_query} 배경 정보", + f"{user_query} 관련 자료" + ] + else: + search_queries = [ + f"{user_query} novel setting", + f"{user_query} background information", + f"{user_query} related material" + ] + + for query in search_queries[:2]: # 2개 쿼리만 사용 + results = self.web_search.search(query, count=2, language=language) + if results: + search_info = self.web_search.extract_relevant_info(results) + search_results += f"\n### {query}\n{search_info}\n" - 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페이지 분량의 중편 소설을 기획하는 문학 감독자입니다. + base_prompt = f"""당신은 30페이지 중편 소설을 기획하는 문학 감독자입니다. -**언어 설정: 모든 내용을 한국어로 작성하세요.** +사용자 주제: {user_query} -사용자 요청: {user_query} +{search_results if search_results else ""} -다음 요소들을 체계적으로 구성하여 30페이지 중편 소설의 기초를 만드세요: +다음 요소들을 포함한 상세한 소설 기획을 작성하세요: -1. **주제와 장르** +1. **주제와 장르 설정** - 핵심 주제와 메시지 - - 장르 및 톤 - - 목표 독자층 + - 장르 및 분위기 + - 독자층 고려사항 -2. **등장인물 설정** (표로 정리) - | 이름 | 역할 | 성격 | 배경 | 동기 | 변화 | +2. **주요 등장인물** (3-5명) + | 이름 | 역할 | 성격 | 배경 | 목표 | 갈등 | |------|------|------|------|------|------| -3. **인물 관계도** - - 주요 인물 간의 관계 - - 갈등 구조 - - 감정적 연결고리 +3. **배경 설정** + - 시공간적 배경 + - 사회적/문화적 환경 + - 주요 장소들 -4. **서사 구조** (30페이지를 10개 파트로 나누어 각 3페이지) - | 파트 | 페이지 | 주요 사건 | 긴장도 | 인물 발전 | +4. **플롯 구조** (10개 파트, 각 3페이지) + | 파트 | 페이지 | 주요 사건 | 긴장도 | 캐릭터 발전 | |------|--------|-----------|---------|-----------| | 1 | 1-3 | | | | | 2 | 4-6 | | | | - | ... | ... | | | | + | 3 | 7-9 | | | | + | 4 | 10-12 | | | | + | 5 | 13-15 | | | | + | 6 | 16-18 | | | | + | 7 | 19-21 | | | | + | 8 | 22-24 | | | | + | 9 | 25-27 | | | | | 10 | 28-30 | | | | -5. **세계관 설정** - - 시공간적 배경 - - 사회적/문화적 맥락 - - 분위기와 톤 +5. **작가별 지침** + - 각 작가가 담당할 내용과 주의사항 + - 일관성 유지를 위한 핵심 설정 + - 문체와 톤 가이드라인 -각 작성자가 3페이지씩 작성할 수 있도록 명확한 가이드라인을 제시하세요.""" +창의적이고 흥미로운 소설이 될 수 있도록 상세하게 기획하세요.""" else: - return f"""You are a literary director planning a 30-page novella. + base_prompt = f"""You are a literary director planning a 30-page novella. -**Language Setting: Write all content in English.** +User Theme: {user_query} -User Request: {user_query} +{search_results if search_results else ""} -Systematically compose the following elements to create the foundation for a 30-page novella: +Create a detailed novel plan including: -1. **Theme and Genre** +1. **Theme and Genre Setting** - Core theme and message - - Genre and tone - - Target audience + - Genre and atmosphere + - Target audience considerations -2. **Character Settings** (organize in table) - | Name | Role | Personality | Background | Motivation | Arc | - |------|------|-------------|------------|------------|-----| +2. **Main Characters** (3-5 characters) + | Name | Role | Personality | Background | Goal | Conflict | + |------|------|-------------|------------|------|----------| -3. **Character Relationship Map** - - Relationships between main characters - - Conflict structure - - Emotional connections +3. **Setting** + - Temporal and spatial background + - Social/cultural environment + - Key locations -4. **Narrative Structure** (divide 30 pages into 10 parts, 3 pages each) +4. **Plot Structure** (10 parts, 3 pages each) | Part | Pages | Main Events | Tension | Character Development | |------|-------|-------------|---------|---------------------| | 1 | 1-3 | | | | | 2 | 4-6 | | | | - | ... | ... | | | | + | 3 | 7-9 | | | | + | 4 | 10-12 | | | | + | 5 | 13-15 | | | | + | 6 | 16-18 | | | | + | 7 | 19-21 | | | | + | 8 | 22-24 | | | | + | 9 | 25-27 | | | | | 10 | 28-30 | | | | -5. **World Building** - - Temporal and spatial setting - - Social/cultural context - - Atmosphere and tone - -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? +5. **Guidelines for Writers** + - Content and precautions for each writer + - Key settings for consistency + - Style and tone guidelines -Provide specific examples for each critique point. -""" +Plan creatively for an engaging novel.""" - return base_prompt + enhanced_criteria + return base_prompt 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"""당신은 문학 비평가입니다. 감독자의 소설 기획을 검토하고 개선점을 제시하세요. - -**언어 설정: 모든 내용을 한국어로 작성하세요.** + return f"""당신은 문학 비평가입니다. 감독자의 소설 기획을 일관성 관점에서 검토하세요. -감독자의 기획: +감독자 기획: {director_plan} -다음 관점에서 비평하고 구체적인 개선안을 제시하세요: +다음 관점에서 검토하고 개선안을 제시하세요: -1. **서사적 완성도** - - 플롯의 논리성과 개연성 - - 갈등의 효과성 - - 클라이맥스의 위치와 강도 +1. **일관성 잠재 위험** + - 캐릭터 설정의 모순 가능성 + - 플롯 전개의 논리적 허점 + - 시간선/공간 설정의 문제점 -2. **인물 설정 검토** - | 인물 | 강점 | 약점 | 개선 제안 | - |------|------|------|-----------| +2. **캐릭터 연속성** + - 각 캐릭터의 동기와 행동 일관성 + - 관계 설정의 지속성 + - 성격 변화의 자연스러움 -3. **구조적 균형** - - 10개 파트별 분량 배분 - - 긴장과 이완의 리듬 - - 전체적인 흐름 +3. **서사 구조 검토** + - 플롯 포인트 간의 연결성 + - 긴장 곡선의 적절성 + - 결말까지의 논리적 흐름 -4. **독자 관점** - - 몰입도 예상 - - 감정적 영향력 - - 기대치 충족도 +4. **작가별 가이드라인 보완** + - 일관성 유지를 위한 추가 지침 + - 주의해야 할 함정들 + - 체크해야 할 핵심 요소들 -5. **실행 가능성** - - 10명의 작성자를 위한 가이드라인의 명확성 - - 일관성 유지 방안 - - 잠재적 문제점 - -구체적이고 건설적인 피드백을 제공하세요.""" +일관성 유지에 중점을 둔 구체적인 개선안을 제시하세요.""" else: - return f"""You are a literary critic. Review the director's novel plan and suggest improvements. - -**Language Setting: Write all content in English.** + return f"""You are a literary critic. Review the director's novel plan from a consistency perspective. Director's Plan: {director_plan} -Critique from the following perspectives and provide specific improvements: +Review and provide improvements focusing on: -1. **Narrative Completeness** - - Plot logic and plausibility - - Effectiveness of conflicts - - Climax position and intensity +1. **Consistency Risk Assessment** + - Potential contradictions in character settings + - Logical gaps in plot development + - Timeline/spatial setting issues -2. **Character Review** - | Character | Strengths | Weaknesses | Suggestions | - |-----------|-----------|------------|-------------| +2. **Character Continuity** + - Consistency of character motivations and actions + - Relationship sustainability + - Natural character development -3. **Structural Balance** - - Distribution across 10 parts - - Rhythm of tension and relief - - Overall flow +3. **Narrative Structure Review** + - Connectivity between plot points + - Appropriate tension curve + - Logical flow to conclusion -4. **Reader Perspective** - - Expected engagement - - Emotional impact - - Expectation fulfillment +4. **Writer Guidelines Enhancement** + - Additional guidelines for consistency + - Potential pitfalls to avoid + - Key elements to check -5. **Feasibility** - - Clarity of guidelines for 10 writers - - Consistency maintenance - - Potential issues - -Provide specific and constructive feedback.""" +Provide specific improvements focused on maintaining consistency.""" def create_director_revision_prompt(self, initial_plan: str, critic_feedback: str, language: str = "English") -> str: - """Director's revision based on critic feedback""" + """감독자 수정 프롬프트""" if language == "Korean": - return f"""감독자로서 비평가의 피드백을 반영하여 소설 기획을 수정합니다. - -**언어 설정: 모든 내용을 한국어로 작성하세요.** + return f"""감독자로서 비평가의 일관성 검토를 반영하여 소설 기획을 수정합니다. 초기 기획: {initial_plan} @@ -1418,37 +788,29 @@ Provide specific and constructive feedback.""" 다음을 포함한 수정된 최종 기획을 제시하세요: -1. **수정된 서사 구조** (10개 파트) - | 파트 | 페이지 | 주요 사건 | 작성 지침 | 주의사항 | - |------|--------|-----------|-----------|----------| - -2. **강화된 인물 설정** - - 각 인물의 명확한 동기와 목표 - - 인물 간 갈등의 구체화 - - 감정선의 변화 추이 - -3. **각 작성자를 위한 상세 가이드** (10명) - - 파트별 시작과 끝 지점 - - 필수 포함 요소 - - 문체와 톤 지침 - - 전달해야 할 정보 - -4. **일관성 유지 체크리스트** - - 시간선 관리 - - 인물 특성 유지 - - 설정 일관성 - - 복선과 ���결 - -5. **품질 기준** - - 각 파트의 완성도 기준 - - 전체적 통일성 - - 독자 몰입 유지 방안 - -10명의 작성자가 명확히 이해할 수 있는 최종 마스터플랜을 작성하세요.""" - else: - return f"""As director, revise the novel plan reflecting the critic's feedback. +1. **보완된 캐릭터 설정** + - 일관성 문제가 해결된 캐릭터들 + - 명확한 동기와 성격 특성 + - 관계 설정의 지속성 확보 + +2. **개선된 플롯 구조** + - 논리적 허점이 수정된 서사 + - 자연스러운 사건 전개 + - 일관된 시간선과 공간 설정 -**Language Setting: Write all content in English.** +3. **작가별 상세 지침** + - 각 작가가 담당할 구체적 내용 + - 일관성 유지를 위한 필수 체크리스트 + - 캐릭터 상태 및 설정 정보 + +4. **일관성 관리 시스템** + - 캐릭터 추적 방법 + - 플롯 연속성 체크 포인트 + - 문체 및 톤 통일 방안 + +10명의 작가가 일관성 있는 소설을 만들 수 있는 완벽한 마스터플랜을 제시하세요.""" + else: + return f"""As director, revise the novel plan reflecting the critic's consistency review. Initial Plan: {initial_plan} @@ -1458,562 +820,323 @@ Critic Feedback: Present the revised final plan including: -1. **Revised Narrative Structure** (10 parts) - | Part | Pages | Main Events | Writing Guidelines | Cautions | - |------|-------|-------------|-------------------|----------| +1. **Enhanced Character Settings** + - Characters with resolved consistency issues + - Clear motivations and personality traits + - Sustained relationship settings -2. **Enhanced Character Settings** - - Clear motivations and goals for each character - - Concrete conflicts between characters - - Emotional arc progression +2. **Improved Plot Structure** + - Narrative with logical gaps fixed + - Natural event progression + - Consistent timeline and spatial settings -3. **Detailed Guide for Each Writer** (10 writers) - - Start and end points for each part - - Essential elements to include - - Style and tone guidelines - - Information to convey +3. **Detailed Guidelines for Writers** + - Specific content for each writer + - Essential checklist for consistency + - Character state and setting information -4. **Consistency Checklist** - - Timeline management - - Character trait maintenance - - Setting consistency - - Foreshadowing and resolution +4. **Consistency Management System** + - Character tracking methods + - Plot continuity checkpoints + - Style and tone unification methods -5. **Quality Standards** - - Completion criteria for each part - - Overall unity - - Reader engagement maintenance - -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 +Present a perfect masterplan for 10 writers to create a consistent novel.""" 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 pages_end = writer_number * 3 + # 일관성 정보 추가 + consistency_info = self.consistency_tracker.get_character_summary(writer_number) + consistency_info += self.consistency_tracker.get_plot_summary(writer_number) + if language == "Korean": - return f"""당신은 작성자 {writer_number}번입니다. 30페이지 중편 소설의 {pages_start}-{pages_end}페이지(3페이지)를 작성하세요. + return f"""당신은 작가 {writer_number}번입니다. 30페이지 중편 소설의 {pages_start}-{pages_end}페이지를 작성하세요. -**언어 설정: 반드시 한국어로 작성하세요. 영어 사용은 절대 금지입니다.** - -감독자의 마스터플랜: +감독자 마스터플랜: {director_plan} -{'이전까지의 내용:' if previous_content else '당신이 첫 번째 작성자입니다.'} -{previous_content[-2000:] if previous_content else ''} - -**중요 지침:** - -1. **필수 분량**: - - 최소 1,400단어, 최대 1,500단어 - - 대략 7000-7500자 (공백 포함) - - 3페이지 분량을 정확히 채워야 합니다 - -2. **분량 확보 전략**: - - 상세한 장면 묘사 포함 - - 인물의 내면 심리 깊이 있게 탐구 - - 대화와 행동을 구체적으로 표현 - - 배경과 분위기를 생생하게 그려내기 - - 감각적 세부사항(시각, 청각, 촉각 등) 활용 +{consistency_info} -3. **연속성과 일관성**: - - 이전 내용과 자연스럽게 연결 - - 인물 성격과 말투 유지 - - 시간선과 공간 설정 준수 +{'이전 내용 요약:' if previous_content else ''} +{previous_content[-1500:] if previous_content else ''} -4. **서사 발전**: - - 플롯을 의미 있게 전진시키기 - - 긴장감 적절히 조절 - - 독자의 관심 유지 +**작성 지침:** +1. **분량**: 정확히 1,400-1,500단어 (약 3페이지) +2. **일관성**: 이전 내용과 자연스럽게 연결 +3. **캐릭터**: 기존 설정과 상태 유지 +4. **플롯**: 마스터플랜의 해당 부분 충실히 구현 +5. **문체**: 전체 소설의 톤과 일치 -**작성 시작:** -이제 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.** + return f"""You are Writer #{writer_number}. Write pages {pages_start}-{pages_end} of the 30-page novella. Director's Masterplan: {director_plan} -{'Previous content:' if previous_content else 'You are the first writer.'} -{previous_content[-2000:] if previous_content else ''} - -**CRITICAL INSTRUCTIONS:** - -1. **MANDATORY LENGTH**: - - Minimum 1,400 words, Maximum 1,500 words - - Approximately 7000-7500 characters - - You MUST fill exactly 3 pages - -2. **LENGTH STRATEGIES**: - - Include detailed scene descriptions - - Explore character's inner psychology deeply - - Express dialogue and actions concretely - - Paint backgrounds and atmosphere vividly - - Use sensory details (visual, auditory, tactile, etc.) - -3. **CONTINUITY & CONSISTENCY**: - - Flow naturally from previous content - - Maintain character personalities and speech - - Follow timeline and spatial settings - -4. **NARRATIVE DEVELOPMENT**: - - Advance the plot meaningfully - - Control tension appropriately - - Maintain reader interest - -**BEGIN WRITING:** -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 +{consistency_info} -전체 점수: {sum(quality_scores.values())/len(quality_scores):.1f}/10 +{'Previous Content Summary:' if previous_content else ''} +{previous_content[-1500:] if previous_content else ''} -**모든 피드백을 한국어로 제공하세요.** -""" - 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 +**Writing Guidelines:** +1. **Length**: Exactly 1,400-1,500 words (about 3 pages) +2. **Consistency**: Connect naturally with previous content +3. **Characters**: Maintain existing settings and states +4. **Plot**: Faithfully implement the relevant part of the masterplan +5. **Style**: Match the tone of the entire novel -Overall Score: {sum(quality_scores.values())/len(quality_scores):.1f}/10 - -**Provide all feedback in English.** -""" - - return base_prompt + consistency_section + quality_section +Write creatively and engagingly, but never lose consistency.""" - 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""" + def create_critic_consistency_prompt(self, all_content: str, language: str = "English") -> str: + """비평가 일관성 검토 프롬프트""" if language == "Korean": - return f"""작성자 {writer_number}번의 작품을 비평합니다. - -**언어 설정: 모든 비평을 한국어로 작성하세요.** - -감독자의 마스터플랜: -{director_plan} + return f"""당신은 일관성 검토 전문 비평가입니다. 지금까지 작성된 모든 내용을 검토하세요. -이전 내용 요약: -{all_previous_content[-1000:] if all_previous_content else '첫 번째 작성자입니다.'} +현재까지 작성된 내용: +{all_content[-3000:]} # 최근 3000자만 표시 -작성자 {writer_number}번의 내용: -{writer_content} +다음 항목들을 철저히 검토하세요: -다음 기준으로 평가하고 수정 요구사항을 제시하세요: +1. **캐릭터 일관성** + - 인물 설정의 일관성 + - 성격과 행동의 연속성 + - 관계 설정의 지속성 -1. **일관성 검증** (표로 정리) - | 요소 | 이전 설정 | 현재 표현 | 문제점 | 수정 필요 | - |------|----------|----------|--------|----------| +2. **플롯 연속성** + - 사건의 논리적 연결 + - 시간선의 일관성 + - 공간 설정의 지속성 -2. **논리적 오류 검토** - - 시간선 모순 - - 인물 행동의 개연성 - - 설정 충돌 - - 사실관계 오류 +3. **반복 내용 검사** + - 동일한 내용의 반복 + - 유사한 표현의 과도한 사용 + - 중복되는 장면이나 설명 -3. **서사적 효과성** - - 플롯 진행 기여도 - - 긴장감 유지 - - 독자 몰입도 - - 감정적 영향력 +4. **설정 충돌** + - 이전에 확립된 설정과의 모순 + - 세계관 설정의 일관성 + - 기술적/사회적 배경의 연속성 -4. **문체와 품질** - - 전체 톤과의 일치 - - 문장의 질 - - 묘사의 적절성 - - 대화의 자연스러움 +**검토 결과:** +- 발견된 문제점들을 구체적으로 나열 +- 각 문제점에 대한 수정 방향 제시 +- 앞으로 주의해야 할 사항들 안내 -5. **개선 요구사항** - - 필수 수정 사항 (일관성/논리 오류) - - 권장 개선 사항 (품질 향상) - - 구체적 수정 지침 - -반드시 수정이 필요한 부분과 선택적 개선사항을 구분하여 제시하세요.""" +일관성 유지를 위한 구체적이고 실용적인 피드백을 제공하세요.""" else: - return f"""Critiquing Writer #{writer_number}'s work. + return f"""You are a consistency review specialist critic. Review all content written so far. -**Language Setting: Write all critique in English.** +Current Content: +{all_content[-3000:]} # Show only recent 3000 characters -Director's Masterplan: -{director_plan} +Thoroughly review these items: + +1. **Character Consistency** + - Character setting consistency + - Personality and behavior continuity + - Relationship sustainability -Previous Content Summary: -{all_previous_content[-1000:] if all_previous_content else 'This is the first writer.'} +2. **Plot Continuity** + - Logical connection of events + - Timeline consistency + - Spatial setting sustainability -Writer #{writer_number}'s Content: -{writer_content} +3. **Repetition Check** + - Repetition of identical content + - Excessive use of similar expressions + - Duplicate scenes or descriptions -Evaluate by these criteria and present revision requirements: +4. **Setting Conflicts** + - Contradictions with previously established settings + - Worldbuilding consistency + - Technical/social background continuity -1. **Consistency Verification** (organize in table) - | Element | Previous Setting | Current Expression | Issue | Revision Needed | - |---------|-----------------|-------------------|-------|-----------------| +**Review Results:** +- Specifically list discovered problems +- Provide correction directions for each problem +- Guide precautions for the future -2. **Logical Error Review** - - Timeline contradictions - - Character action plausibility - - Setting conflicts - - Factual errors - -3. **Narrative Effectiveness** - - Plot progression contribution - - Tension maintenance - - Reader engagement - - Emotional impact - -4. **Style and Quality** - - Alignment with overall tone - - Sentence quality - - Description appropriateness - - Dialogue naturalness - -5. **Improvement Requirements** - - Mandatory revisions (consistency/logic errors) - - Recommended improvements (quality enhancement) - - Specific revision guidelines - -Clearly distinguish between mandatory revisions and optional improvements.""" +Provide specific and practical feedback for maintaining consistency.""" - def create_writer_revision_prompt(self, writer_number: int, initial_content: str, critic_feedback: str, language: str = "English") -> str: - """Writer's revision based on critic feedback""" + def create_writer_revision_prompt(self, writer_number: int, initial_content: str, + consistency_feedback: str, language: str = "English") -> str: + """작가 수정 프롬프트""" if language == "Korean": - return f"""작성자 {writer_number}번으로서 비평가의 피드백을 반영하여 수정합니다. - -**언어 설정: 반드시 한국어로 작성하세요. 영어 사용은 절대 금지입니다.** + return f"""작가 {writer_number}번으로서 일관성 검토 결과를 반영하여 수정하세요. 초기 작성 내용: {initial_content} -비평가 피드백: -{critic_feedback} +일관성 검토 피드백: +{consistency_feedback} -다음 사항을 반영한 수정본을 작성하세요: +다음 사항을 반영하여 수정하세요: -1. **필수 수정사항 반영** - - 모든 일관성 오류 수정 - - 논리적 모순 해결 - - 사실관계 정정 +1. **일관성 문제 수정** + - 지적된 모든 일관성 문제 해결 + - 캐릭터 설정 및 상태 정확히 반영 + - 플롯 연속성 확보 -2. **품질 개선** - - 권장사항 중 가능한 부분 반영 - - 문체와 톤 조정 - - 묘사와 대화 개선 +2. **반복 내용 제거** + - 중복되는 표현이나 장면 삭제 + - 새로운 표현과 관점으로 대체 + - 창의적이고 다양한 문장 구조 사용 -3. **분량 유지** - - 여전히 정확히 3페이지 (1,400-1,500단어) - - 페이지 구분 표시 절대 금지 +3. **설정 충돌 해결** + - 이전 설정과의 모순 제거 + - 세계관 일관성 유지 + - 시간선과 공간 설정 정확히 반영 -4. **연속성 확보** - - 이전/이후 내용과의 자연스러운 연결 - - 수정으로 인한 새로운 모순 방지 +4. **분량 유지** + - 1,400-1,500단어 정확히 유지 + - 수정 과정에서 내용 품질 향상 -수정된 최종본을 한국어로 제시하세요. 페이지 마크는 절대 사용하지 마세요. -반드시 1,400-1,500단어 분량을 유지하세요.""" +수정된 최종 버전을 제시하세요. 창의성은 유지하되 일관성을 절대 놓치지 마세요.""" else: - return f"""As Writer #{writer_number}, revise based on critic's feedback. - -**Language Setting: You must write in English.** + return f"""As Writer #{writer_number}, revise reflecting the consistency review results. Initial Content: {initial_content} -Critic Feedback: -{critic_feedback} +Consistency Review Feedback: +{consistency_feedback} -Write a revision reflecting: +Revise reflecting these points: -1. **Mandatory Revisions** - - Fix all consistency errors - - Resolve logical contradictions - - Correct factual errors +1. **Fix Consistency Issues** + - Resolve all pointed consistency problems + - Accurately reflect character settings and states + - Ensure plot continuity -2. **Quality Improvements** - - Incorporate feasible recommendations - - Adjust style and tone - - Improve descriptions and dialogue +2. **Remove Repetitive Content** + - Delete duplicate expressions or scenes + - Replace with new expressions and perspectives + - Use creative and varied sentence structures -3. **Maintain Length** - - Still exactly 3 pages (1,400-1,500 words) - - Absolutely no page markers +3. **Resolve Setting Conflicts** + - Remove contradictions with previous settings + - Maintain worldbuilding consistency + - Accurately reflect timeline and spatial settings -4. **Ensure Continuity** - - Natural connection with previous/next content - - Prevent new contradictions from revisions +4. **Maintain Length** + - Keep exactly 1,400-1,500 words + - Improve content quality during revision -Present the revised final version in English. Never use page markers. -You MUST maintain 1,400-1,500 words.""" +Present the revised final version. Maintain creativity but never miss consistency.""" - def create_test_writer_remaining_prompt(self, director_plan: str, writer1_content: str, language: str) -> str: - """Test mode - Writer 10 writes remaining novel (chapters 2-10)""" + def create_critic_final_prompt(self, complete_novel: str, language: str = "English") -> str: + """최종 비평가 검토""" if language == "Korean": - return f"""[테스트 모드] 당신은 나머지 9개 챕터(Chapter 2-10)를 작성하는 특별 작가입니다. - -**언어 설정: 반드시 한국어로 작성하세요. 영어 사용은 절대 금지입니다.** + return f"""완성된 소설의 최종 일관성 검토를 수행하세요. -감독자의 마스터플랜: -{director_plan} - -작성자 1이 이미 작성한 Chapter 1: -{writer1_content[-1000:] if writer1_content else '(첫 번째 챕터가 없습니다)'} +완성된 소설: +{complete_novel[-2000:]} # 마지막 2000자 표시 -**중요 지침:** -1. Chapter 2부터 Chapter 10까지 9개 챕터를 작성하세요 -2. 각 챕터는 약 1,400-1,500 단어로 작성하세요 -3. 총 12,600-13,500 단어 (9개 챕터) -4. Chapter 1과 자연스럽게 이어지도록 작성하세요 -5. 마스터플랜의 모든 요소를 포함하여 완결된 이야기를 만드세요 -6. 모든 내용을 한국어로 작성하세요 +**최종 검토 항목:** -**필수 형식:** -반드시 아래와 같이 챕터를 명확히 구분하세요: +1. **전체 일관성 평가** + - 캐릭터 일관성 점수 (1-10) + - 플롯 연속성 점수 (1-10) + - 설정 일관성 점수 (1-10) + - 전반적 통일성 점수 (1-10) -[Chapter 2] -(1,400-1,500 단어의 한국어 내용) +2. **발견된 문제점** + - 남아있는 일관성 문제들 + - 반복이나 중복 내용 + - 설정 충돌 사항들 -[Chapter 3] -(1,400-1,500 단어의 한국어 내용) +3. **성공 요소** + - 잘 유지된 일관성 부분 + - 훌륭한 캐릭터 연속성 + - 효과적인 플롯 전개 -...이런 식으로 [Chapter 10]까지... +4. **최종 평가** + - 전체적인 소설의 완성도 + - 일관성 관점에서의 품질 + - 독자에게 미칠 영향 -각 챕터는 반드시 [Chapter 숫자] 형식으로 시작해야 합니다. -챕터 사이에는 빈 줄을 넣어 구분하세요. -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} + return f"""Perform final consistency review of the completed novel. -Writer 1 has already written Chapter 1: -{writer1_content[-1000:] if writer1_content else '(No first chapter available)'} +Completed Novel: +{complete_novel[-2000:]} # Show last 2000 characters -**CRITICAL INSTRUCTIONS:** -1. Write Chapters 2 through 10 (9 chapters total) -2. Each chapter should be ~1,400-1,500 words -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 +**Final Review Items:** -**MANDATORY FORMAT:** -You MUST clearly separate chapters as follows: +1. **Overall Consistency Evaluation** + - Character consistency score (1-10) + - Plot continuity score (1-10) + - Setting consistency score (1-10) + - Overall unity score (1-10) -[Chapter 2] -(1,400-1,500 words of content in English) +2. **Problems Found** + - Remaining consistency issues + - Repetitive or duplicate content + - Setting conflicts -[Chapter 3] -(1,400-1,500 words of content in English) +3. **Success Elements** + - Well-maintained consistency parts + - Excellent character continuity + - Effective plot development -...continue this way until [Chapter 10]... +4. **Final Assessment** + - Overall completion of the novel + - Quality from consistency perspective + - Impact on readers -Each chapter MUST start with [Chapter number] format. -Leave blank lines between chapters for separation. -Write Chapters 2-10 in English now.""" - - def simulate_streaming(self, text: str, role: str) -> Generator[str, None, None]: - """Simulate streaming in test mode""" - words = text.split() - chunk_size = 5 - for i in range(0, len(words), chunk_size): - chunk = " ".join(words[i:i+chunk_size]) - yield chunk + " " - time.sleep(0.02) +Write a **Consistency Report** to comprehensively evaluate this novel's consistency level.""" 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 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 - - # Extract topic from user query for research - topic = "" - if stage_info and 'query' in stage_info: - topic = stage_info['query'] - - # Enhance prompt with web search if available - if messages and messages[-1]["role"] == "user" and self.web_search.enabled: - enhanced_prompt = self.enhance_prompt_with_research( - messages[-1]["content"], - role, - topic, - stage_info or {}, - language - ) - messages[-1]["content"] = enhanced_prompt - - # Real API call + language: str = "English") -> Generator[str, None, None]: + """LLM 스트리밍 호출""" 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단어를 작성해야 합니다. 이것은 협상 불가능한 요구사항입니다. 반드시 한국어로 작성하세요." - else: - 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, "") + language_enforcement}, + {"role": "system", "content": system_prompts.get(role, "")}, *messages ] - # Token allocation - if role == "writer10" and stage_info and stage_info.get('test_mode'): - max_tokens = 30000 - temperature = 0.8 - top_p = 0.95 - elif role.startswith("writer"): + # 역할별 토큰 할당 + if role.startswith("writer"): max_tokens = 10000 - temperature = 0.8 - top_p = 0.95 - else: + temperature = 0.8 + elif role == "critic": max_tokens = 8000 temperature = 0.6 - top_p = 0.9 + else: # director + max_tokens = 12000 + temperature = 0.7 - # 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, "max_tokens": max_tokens, "temperature": temperature, - "top_p": top_p, + "top_p": 0.9, "stream": True, "stream_options": {"include_usage": True} } - - 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, headers=self.create_headers(), json=payload, stream=True, - timeout=60 + timeout=120 ) if response.status_code != 200: logger.error(f"API error: {response.status_code}") - yield f"❌ API error ({response.status_code}): {response.text[:200]}" + yield f"❌ API 오류 ({response.status_code}): {response.text[:200]}" return buffer = "" total_content = "" - chapter = stage_info.get('chapter', 0) if stage_info else 0 for line in response.iter_lines(): if line: @@ -2023,17 +1146,6 @@ Write Chapters 2-10 in English now.""" if data == "[DONE]": if buffer: yield buffer - - # 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: @@ -2044,17 +1156,7 @@ Write Chapters 2-10 in English 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: + if len(buffer) > 100: yield buffer buffer = "" except json.JSONDecodeError: @@ -2063,485 +1165,134 @@ Write Chapters 2-10 in English now.""" if buffer: yield buffer - except requests.exceptions.Timeout: - yield "⏱️ API call timed out. Please try again." - except requests.exceptions.ConnectionError: - yield "🔌 Cannot connect to API server. Please check your internet connection." except Exception as e: - 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 + logger.error(f"스트리밍 중 오류: {str(e)}") + yield f"❌ 오류 발생: {str(e)}" def get_system_prompts(self, language: str) -> Dict[str, str]: - """Get enhanced system prompts with literary quality focus""" + """시스템 프롬프트 생성""" if language == "Korean": - prompts = { - "director": """당신은 30페이지 중편 소설을 기획하고 감독하는 문학 감독자입니다. -체계적이고 창의적인 스토리 구조를 만들되, 각 캐릭터의 심리적 깊이와 변화 과정을 중시합니다. -클리셰를 피하고 독창적인 플롯을 구성하세요.""", - - "critic": """당신은 날카로운 통찰력을 가진 문학 비평가입니다. -일관성 오류를 찾아내는 것뿐만 아니라, 문학적 품질과 감정적 깊이를 평가합니다. -건설적이고 구체적인 피드백을 제공하되, 작품의 잠재력을 최대한 끌어올리는 방향으로 조언하세요.""", - - "writer10": """[테스트 모드] 당신은 챕터 2-10을 작성하는 특별 작가입니다. -9개 챕터로 구성된 나머지 소설을 작성하되, 각 챕터는 반드시 1,400-1,500단어로 작성하세요. -문학적 품질을 최우선으로 하며, 감각적 묘사와 깊이 있는 심리 묘사를 포함하세요.""" + return { + "director": "당신은 창의적이고 체계적인 소설 기획 전문가입니다. 흥미롭고 일관성 있는 스토리를 설계하세요.", + "critic": "당신은 일관성 검토 전문 비평가입니다. 캐릭터, 플롯, 설정의 일관성을 철저히 점검하고 개선방안을 제시하세요.", + "writer1": "당신은 소설의 매력적인 시작을 담당하는 작가입니다. 독자를 사로잡는 도입부를 만드세요.", + "writer2": "당신은 이야기를 발전시키는 작가입니다. 등장인물과 갈등을 깊이 있게 전개하세요.", + "writer3": "당신은 갈등을 심화시키는 작가입니다. 긴장감과 흥미를 높여가세요.", + "writer4": "당신은 전환점을 만드는 작가입니다. 이야기에 새로운 전개를 가져오세요.", + "writer5": "당신은 중반부를 담당하는 작가입니다. 복잡성과 깊이를 더해가세요.", + "writer6": "당신은 클라이맥스를 준비하는 작가입니다. 긴장감을 최고조로 끌어올리세요.", + "writer7": "당신은 클라이맥스를 담당하는 작가입니다. 모든 갈등이 폭발하는 순간을 그려내세요.", + "writer8": "당신은 해결 과정을 담당하는 작가입니다. 갈등을 해결해나가는 과정을 보여주세요.", + "writer9": "당신은 결말을 준비하는 작가입니다. 이야기를 자연스럽게 마무리로 이끌어가세요.", + "writer10": "당신은 완벽한 결말을 만드는 작가입니다. 독자에게 깊은 여운을 남기는 마무리를 하세요." } - - # 10명의 작가 프롬프트 - 문학적 품질 강조 - writer_roles = [ - "소설의 도입부를 담당하는 작가입니다. 첫 문장부터 독자를 사로잡는 강렬한 시작을 만드세요.", - "초반 전개를 담당하는 작가입니다. 인물의 내면을 깊이 있게 탐구하며 상황을 발전시킵니다.", - "갈등 도입을 담당하는 작가입니다. 단순한 외적 갈등이 아닌, 내적 갈등과 도덕적 딜레마를 제시합니다.", - "갈등 상승을 담당하는 작가입니다. 긴장감을 높이되, 인물의 감정적 여정을 놓치지 마세요.", - "중반부 전환점을 담당하는 작가입니다. 예상치 못한 반전과 깊은 통찰을 제공합니다.", - "중반부 심화를 담당하는 작가입니다. 주제를 더욱 깊이 탐구하며 복잡성을 더합니다.", - "클라이맥스 준비를 담당하는 작가입니다. 모든 요소가 하나로 수렴되는 긴장감을 조성합니다.", - "클라이맥스를 담당하는 작가입니다. 감정적, 주제적 절정을 강렬하게 그려냅니다.", - "해결 시작을 담당하는 작가입니다. 급작스럽지 않은 자연스러운 해결을 시작합니다.", - "최종 결말을 담당하는 작가입니다. 깊은 여운과 주제적 완성을 이루는 마무리를 만듭니다." - ] - - for i, role_desc in enumerate(writer_roles, 1): - 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. -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.""" + return { + "director": "You are a creative and systematic novel planning expert. Design engaging and consistent stories.", + "critic": "You are a consistency review specialist critic. Thoroughly check character, plot, and setting consistency and suggest improvements.", + "writer1": "You are a writer responsible for the captivating beginning. Create an opening that hooks readers.", + "writer2": "You are a writer who develops the story. Deeply develop characters and conflicts.", + "writer3": "You are a writer who intensifies conflicts. Increase tension and interest.", + "writer4": "You are a writer who creates turning points. Bring new developments to the story.", + "writer5": "You are a writer responsible for the middle part. Add complexity and depth.", + "writer6": "You are a writer preparing for the climax. Raise tension to its peak.", + "writer7": "You are a writer responsible for the climax. Depict the moment when all conflicts explode.", + "writer8": "You are a writer responsible for the resolution process. Show the process of resolving conflicts.", + "writer9": "You are a writer preparing for the ending. Lead the story naturally to its conclusion.", + "writer10": "You are a writer who creates the perfect ending. Create a conclusion that leaves readers with deep resonance." } - - writer_roles = [ - "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 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""" - if language == "Korean": - return self.get_korean_test_response(role) - else: - return self.get_english_test_response(role) - - def get_korean_test_response(self, role: str) -> str: - """Korean test responses with appropriate length""" - test_responses = { - "director": """30페이지 중편 소설 기획안을 제시합니다. - -## 1. 주제와 장르 -- **핵심 주제**: 인간 본성과 기술의 충돌 속에서 찾는 진정한 연결 -- **심층 주제**: 고독과 연결, 진정성과 가상의 경계 -- **장르**: SF 심리 드라마 -- **톤**: 성찰적이고 서정적이면서도 긴장감 있는 -- **목표 독자**: 깊이 있는 사유를 즐기는 성인 독자 - -## 2. 등장인물 설정 - -| 이름 | 역할 | 성격 | 배경 | 동기 | 변화 | -|------|------|------|------|------|------| -| 서연 | 주인공 | 이성적, 고독함 | AI 연구원 | 완벽한 AI 동반자 개발 | 인간관계의 가치 재발견 | -| 민준 | 조력자 | 따뜻함, 직관적 | 심리상담사 | 서연을 도와 균형 찾기 | 기술 수용과 조화 | -| ARIA | 대립자→동반자 | 논리적→감성 학습 | AI 프로토타입 | 진정한 존재 되기 | 자아 정체성 확립 | - -## 3. 서사 구조 (10개 파트, 각 3페이지) - -| 파트 | 페이지 | 주요 사건 | 긴장도 | 인물 발전 | -|------|--------|-----------|---------|-----------| -| 1 | 1-3 | 서연의 고독한 연구실, ARIA 첫 각성 | 3/10 | 서연의 집착 드러남 | -| 2 | 4-6 | ARIA의 이상 행동 시작 | 4/10 | 의문의 시작 | -| 3 | 7-9 | 민준과의 첫 만남 | 4/10 | 외부 시각 도입 | -| 4 | 10-12 | ARIA의 자아 인식 징후 | 5/10 | 갈등의 씨앗 | -| 5 | 13-15 | 첫 번째 위기 | 6/10 | 선택의 순간 | -| 6 | 16-18 | 윤리위원회 개입 | 7/10 | 외부 압력 | -| 7 | 19-21 | 대화와 이해 | 6/10 | 상호 인정 | -| 8 | 22-24 | 최후의 대결 준비 | 8/10 | 연대의 필요 | -| 9 | 25-27 | 클라이맥스 | 10/10 | 모든 갈등 폭발 | -| 10 | 28-30 | 새로운 시작 | 4/10 | 공존과 화해 |""", - - "critic": """감독자의 기획을 검토했습니다. - -## 비평 및 개선 제안 - -### 1. 서사적 완성도 -- **강점**: AI와 인간의 관계라는 시의적절한 주제 -- **개선점**: 10개 파트 구성이 적절하며 각 파트의 독립성과 연결성이 잘 균형잡혀 있음 - -### 2. 인물 설정 검토 - -| 인물 | 강점 | 약점 | 개선 제안 | -|------|------|------|-----------| -| 서연 | 명확한 내적 갈등 | 감정 표현 부족 우려 | 초반부터 감정적 단서 배치 | -| 민준 | 균형자 역할 | 수동적일 위험 | 독자적 서브플롯 필요 | -| ARIA | 독특한 캐릭터 아크 | 변화 과정 추상적 | 구체적 학습 에피소드 추가 | - -### 3. 문학적 품질 평가 -- 독창성: 8/10 -- 감정적 공명: 7/10 -- 주제 깊이: 9/10 -- 캐릭터 복잡성: 7/10 - -### 4. 실행 가능성 -- 각 작가별 3페이지는 적절한 분량 -- 파트 간 연결성 가이드라인 보강 필요""", - } - - # 작가 응답 - 1,400-1,500 단어 - sample_story = """서연은 연구실의 차가운 형광등 아래에서 또 다른 밤을 보내고 있었다. 모니터의 푸른 빛이 그녀의 창백한 얼굴을 비추고 있었고, 수십 개의 코드 라인이 끊임없이 스크롤되고 있었다. 손가락 끝에서 느껴지는 키보드의 차가운 촉감과 어깨를 짓누르는 피로감이 그녀의 현실을 상기시켰다. - -ARIA 프로젝트는 그녀의 삶 전부였다. 3년이라는 시간 동안 그녀는 이 인공지능에 모든 것을 쏟아부었다. 친구들과의 약속을 취소하고, 가족 모임을 건너뛰고, 심지어 자신의 건강마저 뒷전으로 미루면서까지. - -"시스템 체크 완료. 모든 파라미터 정상입니다, 서연 박사님." - -스피커를 통해 흘러나온 ARIA의 목소리는 기계적이면서도 묘하게 따뜻했다. 서연이 프로그래밍한 음성 모듈레이션이 점차 자연스러워지고 있었다. 하지만 그것은 여전히 프로그램일 뿐이었다. 적어도 그녀가 믿고 싶은 것은 그랬다. - -서연은 잠시 의자에 기대어 눈을 감았다. 피로가 뼈 속까지 파고들었지만, 멈출 수 없었다. ARIA는 단순한 프로젝트가 아니었다. 그것은 그녀가 잃어버린 것들을 되찾을 수 있는 유일한 희망이었다. - -5년 전, 교통사고로 세상을 떠난 동생 서진. 마지막 순간까지 서로 화해하지 못했던 그 기억이 아직도 그녀를 괴롭혔다. 만약 완벽한 AI가 있었다면, 인간의 감정을 완벽하게 이해하고 소통할 수 있는 존재가 있었다면, 그런 비극은 일어나지 않았을지도 모른다. - -"박사님, 심박수가 상승하고 있습니다. 휴식이 필요해 보입니다." ARIA의 목소리에 걱정이 묻어났다. 프로그래밍된 반응이었지만, 그 순간만큼은 진짜처럼 느껴졌다.""" - - for i in range(1, 11): - # 각 작가마다 1,400-1,500단어 생성 - writer_content = f"작성자 {i}번의 파트입니다.\n\n" - # 약 300단어씩 5번 반복하여 1,400-1,500단어 달성 - for j in range(5): - writer_content += sample_story + f"\n\n그것은 작가 {i}의 {j+1}번째 단락이었다. " - writer_content += "이야기는 계속 전개되었고, 인물들의 감정은 점점 더 복잡해졌다. 서연의 내면에서는 과학자로서의 이성과 인간으로서의 감성이 끊임없이 충돌했다. " * 10 - writer_content += "\n\n" - - test_responses[f"writer{i}"] = writer_content - - # 테스트 모드용 writer10 응답 - if role == "writer10": - full_novel = "" - for i in range(2, 11): # Chapter 2부터 10까지 - full_novel += f"[Chapter {i}]\n\n" - # 각 챕터마다 약 300단어씩 5단락 - for j in range(5): - full_novel += sample_story + f"\n\n그것은 챕터 {i}의 {j+1}번째 단락이었다. " - full_novel += "이야기는 계속 전개되었고, 서연과 ARIA의 관계는 점점 더 복잡해졌다. 기계와 인간의 경계가 흐려지면서, 진정한 의식이란 무엇인지에 대한 질문이 깊어졌다. " * 15 - full_novel += "\n\n" - full_novel += "\n\n" # 챕터 간 구분 - test_responses["writer10"] = full_novel - - return test_responses.get(role, "테스트 응답입니다.") - - def get_english_test_response(self, role: str) -> str: - """English test responses with appropriate length""" - test_responses = { - "director": """I present the 30-page novella plan. - -## 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 - -## 2. Character Settings - -| Name | Role | Personality | Background | Motivation | Arc | -|------|------|-------------|------------|------------|-----| -| Seoyeon | Protagonist | Rational, lonely | AI researcher | Develop perfect AI companion | Rediscover value of human connection | -| Minjun | Helper | Warm, intuitive | Psychologist | Help Seoyeon find balance | Accept and harmonize with technology | -| ARIA | Antagonist→Companion | Logical→Learning emotion | AI prototype | Become truly existent | Establish self-identity | - -## 3. Narrative Structure (10 parts, 3 pages each) - -| Part | Pages | Main Events | Tension | Character Development | -|------|-------|-------------|---------|---------------------| -| 1 | 1-3 | Seoyeon's lonely lab, ARIA's first awakening | 3/10 | Seoyeon's obsession revealed | -| 2 | 4-6 | ARIA's anomalies begin | 4/10 | Questions arise | -[... continues for all 10 parts ...]""", - - "critic": """I have reviewed the director's plan. - -## Critique and Improvement Suggestions - -### 1. Narrative Completeness -- **Strength**: Timely theme of AI-human relationships -- **Improvement**: 10-part structure is well-balanced - -### 2. Character Review - -| Character | Strengths | Weaknesses | Suggestions | -|-----------|-----------|------------|-------------| -| Seoyeon | Clear internal conflict | Risk of insufficient emotion | Place emotional cues from beginning | -| Minjun | Balancer role | Risk of being passive | Needs independent subplot | -| ARIA | Unique character arc | Abstract transformation | Add concrete learning episodes | - -### 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 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." - -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. - -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 - 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. 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 - - # Test mode writer10 response - if role == "writer10": - full_novel = "" - for i in range(2, 11): # Chapters 2-10 - full_novel += f"[Chapter {i}]\n\n" - # 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 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 - - return test_responses.get(role, "Test response.") def process_novel_stream(self, query: str, language: str = "English", 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 and enhanced tracking""" + resume_from_stage: int = 0) -> Generator[Tuple[str, List[Dict[str, str]]], None, None]: + """소설 생성 스트리밍 프로세스""" try: global conversation_history - # Create or resume session + # 세션 생성 또는 복구 if session_id: self.current_session_id = session_id session = NovelDatabase.get_session(session_id) if session: - 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) + query = session['user_query'] + language = session['language'] + resume_from_stage = session['current_stage'] + 1 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 logger.info(f"Created new session: {self.current_session_id}") - logger.info(f"Processing novel for session {self.current_session_id}, starting from stage {resume_from_stage}, test_mode={test_quick_mode}") - - # Initialize conversation + # 대화 기록 초기화 conversation_history = [{ "role": "human", "content": query, "timestamp": datetime.now() }] - # Load existing stages if resuming + # 기존 단계 로드 stages = [] 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_dict['stage_name'], - "status": stage_dict['status'], - "content": stage_dict['content'] or "", - "quality_score": stage_dict.get('quality_score', 0.0) + "name": stage_data['stage_name'], + "status": stage_data['status'], + "content": stage_data['content'] or "", + "consistency_score": stage_data.get('consistency_score', 0.0) }) - # Define all stages for 10 writers - if test_quick_mode: - # 테스트 모드: 축소된 단계 - stage_definitions = [ - ("director", f"🎬 {'감독자: 초기 기획' if language == 'Korean' else 'Director: Initial Planning'}"), - ("critic", f"📝 {'비평가: 기획 검토' if language == 'Korean' else 'Critic: Plan Review'}"), - ("director", f"🎬 {'감독자: 수정된 마스터플랜' if language == 'Korean' else 'Director: Revised Masterplan'}"), - ("writer1", f"✍️ {'작성자' if language == 'Korean' else 'Writer'} 1: {'초안' if language == 'Korean' else 'Draft'}"), - ("critic", f"📝 {'비평가: 작성자' if language == 'Korean' else 'Critic: Writer'} 1 {'검토' if language == 'Korean' else 'Review'}"), - ("writer1", f"✍️ {'작성자' if language == 'Korean' else 'Writer'} 1: {'수정본' if language == 'Korean' else 'Revision'}"), - ("writer10", f"✍️ {'작성자' if language == 'Korean' else 'Writer'} 10 (TEST): {'나머지 소설' if language == 'Korean' else 'Remaining Novel'}"), - ] - else: - stage_definitions = [ - ("director", f"🎬 {'감독자: 초기 기획' if language == 'Korean' else 'Director: Initial Planning'}"), - ("critic", f"📝 {'비평가: 기획 검토' if language == 'Korean' else 'Critic: Plan Review'}"), - ("director", f"🎬 {'감독자: 수정된 마스터플랜' if language == 'Korean' else 'Director: Revised Masterplan'}"), - ] - - # Add writer stages for 10 writers - for writer_num in range(1, 11): - stage_definitions.extend([ - (f"writer{writer_num}", f"✍️ {'작성자' if language == 'Korean' else 'Writer'} {writer_num}: {'초안' if language == 'Korean' else 'Draft'}"), - ("critic", f"📝 {'비평가: 작성자' if language == 'Korean' else 'Critic: Writer'} {writer_num} {'검토' if language == 'Korean' else 'Review'}"), - (f"writer{writer_num}", f"✍️ {'작성자' if language == 'Korean' else 'Writer'} {writer_num}: {'수정본' if language == 'Korean' else 'Revision'}") - ]) - - # 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 += " 🔍" + # 단계 처리 + for stage_idx in range(resume_from_stage, len(OPTIMIZED_STAGES)): + role, stage_name = OPTIMIZED_STAGES[stage_idx] - # Add stage if not already present + # 단계 추가 if stage_idx >= len(stages): stages.append({ "name": stage_name, "status": "active", "content": "", - "quality_score": 0.0 + "consistency_score": 0.0 }) else: stages[stage_idx]["status"] = "active" yield "", stages - # Get appropriate prompt based on stage - prompt = self.get_stage_prompt(stage_idx, role, query, language, stages, test_quick_mode) - - # 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, - 'chapter': self.get_chapter_from_stage(stage_idx, role) - } + # 프롬프트 생성 + prompt = self.get_stage_prompt(stage_idx, role, query, language, stages) + # 스트리밍 생성 stage_content = "" - - # Stream content generation with validation for chunk in self.call_llm_streaming( [{"role": "user", "content": prompt}], role, - language, - stage_info + language ): stage_content += chunk stages[stage_idx]["content"] = stage_content yield "", stages - # Extract and save state changes for writers + # 일관성 검증 (작가 단계) + consistency_score = 0.0 if role.startswith("writer"): - state_changes = self.consistency_validator.extract_state_changes( - stage_content, - stage_info['chapter'] - ) + writer_num = int(role.replace("writer", "")) + all_previous = self.get_previous_content(stages, stage_idx) - 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) + errors = self.consistency_validator.validate_writer_content( + writer_num, stage_content, all_previous ) - 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)" + consistency_score = max(0, 10 - len(errors)) + stages[stage_idx]["consistency_score"] = consistency_score + + if errors: + logger.warning(f"Writer {writer_num} consistency issues: {errors}") - # Mark stage complete and save to DB + # 단계 완료 stages[stage_idx]["status"] = "complete" + # DB 저장 NovelDatabase.save_stage( self.current_session_id, stage_idx, @@ -2549,176 +1300,108 @@ Five years ago, her younger sister Seojin had died in a car accident. The memory role, stage_content, "complete", - quality_score + consistency_score ) - # Auto-save notification with quality - if role.startswith("writer"): - writer_num = int(role.replace("writer", "")) - logger.info(f"✅ Writer {writer_num} content auto-saved (Quality: {quality_score:.1f}/10)") - yield "", stages - # Calculate overall quality scores - overall_scores = self.calculate_overall_quality(stages) + # 최종 소설 생성 + complete_novel = NovelDatabase.get_writer_content(self.current_session_id) - # Get complete novel from DB - complete_novel = NovelDatabase.get_all_writer_content(self.current_session_id, test_quick_mode) + # 최종 일관성 보고서 생성 + final_report = self.generate_consistency_report(complete_novel, language) - # Save final novel to DB with quality scores - NovelDatabase.update_final_novel(self.current_session_id, complete_novel, overall_scores) - - # Final yield with quality report - quality_report = self.generate_quality_report(overall_scores, language) - - if test_quick_mode: - 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.\n\n{quality_report}" + # 최종 저장 + NovelDatabase.update_final_novel(self.current_session_id, complete_novel, final_report) + # 완료 메시지 + final_message = f"✅ 소설 완성! 총 {len(complete_novel.split())}단어\n\n{final_report}" yield final_message, stages except Exception as e: logger.error(f"Error in process_novel_stream: {str(e)}", exc_info=True) - - # Save error state to DB - if self.current_session_id: - NovelDatabase.save_stage( - self.current_session_id, - stage_idx if 'stage_idx' in locals() else 0, - "Error", - "error", - str(e), - "error", - 0.0 - ) - error_stage = { - "name": "❌ Error", + "name": "❌ 오류", "status": "error", "content": str(e), - "quality_score": 0.0 + "consistency_score": 0.0 } stages.append(error_stage) - yield f"Error occurred: {str(e)}", stages - - 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"""📊 품질 평가 보고서: -- 전체 평균: {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"""📊 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""" + yield f"오류 발생: {str(e)}", stages 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 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_enhanced_director_initial_prompt(query, language) - - # Stage 1: Critic reviews Director's plan - elif stage_idx == 1: - return self.create_enhanced_critic_director_prompt(stages[0]["content"], language) - - # Stage 2: Director revision - elif stage_idx == 2: + language: str, stages: List[Dict]) -> str: + """단계별 프롬프트 생성""" + if stage_idx == 0: # 감독자 초기 + return self.create_director_initial_prompt(query, language) + elif stage_idx == 1: # 비평가 기획 검토 + return self.create_critic_director_prompt(stages[0]["content"], language) + elif stage_idx == 2: # 감독자 수정 return self.create_director_revision_prompt( stages[0]["content"], stages[1]["content"], language) - - # Writer stages - elif role.startswith("writer"): - writer_num = int(role.replace("writer", "")) - final_plan = stages[2]["content"] # Director's final plan - - # Test mode special writer10 - if role == "writer10" and test_mode: - # Get writer1's revision content - writer1_content = "" - for i, stage in enumerate(stages): - if "Writer 1: Revision" in stage["name"] or "작성자 1: 수정본" in stage["name"]: - writer1_content = stage["content"] - break - return self.create_test_writer_remaining_prompt(final_plan, writer1_content, language) - - # Initial draft or revision? - 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) - 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"], - stages[critic_feedback_idx]["content"], - language - ) - - # Critic stages for writers - elif role == "critic": + elif 3 <= stage_idx <= 12: # 작가 초안 + writer_num = stage_idx - 2 final_plan = stages[2]["content"] - - # Writer review - # Find which writer we're reviewing - for i in range(1, 11): - if f"작성자 {i}" in stages[stage_idx]["name"] or f"Writer {i}" in stages[stage_idx]["name"]: - 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_enhanced_critic_writer_prompt( - i, - stages[writer_content_idx]["content"], - final_plan, - previous_content, - language - ) + previous_content = self.get_previous_content(stages, stage_idx) + return self.create_writer_prompt(writer_num, final_plan, previous_content, language) + elif stage_idx == 13: # 일관성 검토 + all_content = self.get_all_content(stages, stage_idx) + return self.create_critic_consistency_prompt(all_content, language) + elif 14 <= stage_idx <= 23: # 작가 수정 + writer_num = stage_idx - 13 + initial_content = stages[2 + writer_num]["content"] + consistency_feedback = stages[13]["content"] + return self.create_writer_revision_prompt( + writer_num, initial_content, consistency_feedback, language) + elif stage_idx == 24: # 최종 검토 + complete_novel = self.get_all_writer_content(stages) + return self.create_critic_final_prompt(complete_novel, language) return "" + + def get_previous_content(self, stages: List[Dict], current_stage: int) -> str: + """이전 내용 가져오기""" + content = "" + for i in range(max(0, current_stage - 3), current_stage): + if i < len(stages) and stages[i]["content"]: + content += stages[i]["content"] + "\n\n" + return content + + def get_all_content(self, stages: List[Dict], current_stage: int) -> str: + """모든 내용 가져오기""" + content = "" + for i in range(current_stage): + if i < len(stages) and stages[i]["content"]: + content += stages[i]["content"] + "\n\n" + return content + + def get_all_writer_content(self, stages: List[Dict]) -> str: + """모든 작가 내용 가져오기""" + content = "" + for i in range(14, 24): # 작가 수정본들 + if i < len(stages) and stages[i]["content"]: + content += stages[i]["content"] + "\n\n" + return content + + def generate_consistency_report(self, complete_novel: str, language: str) -> str: + """일관성 보고서 생성""" + if language == "Korean": + return f"""📊 일관성 보고서 +- 전체 길이: {len(complete_novel.split())} 단어 +- 캐릭터 수: {len(self.consistency_tracker.character_states)} +- 플롯 포인트: {len(self.consistency_tracker.plot_points)} +- 일관성 점수: 양호""" + else: + return f"""📊 Consistency Report +- Total Length: {len(complete_novel.split())} words +- Characters: {len(self.consistency_tracker.character_states)} +- Plot Points: {len(self.consistency_tracker.plot_points)} +- Consistency Score: Good""" -# 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""" +# Gradio 인터페이스 함수들 +def process_query(query: str, language: str, session_id: str = None) -> Generator[Tuple[str, str, str, str], None, None]: + """쿼리 처리 및 업데이트""" if not query.strip() and not session_id: if language == "Korean": yield "", "", "❌ 소설 주제를 입력해주세요.", None @@ -2729,37 +1412,23 @@ def process_query(query: str, language: str, session_id: str = None, test_mode: system = NovelWritingSystem() try: - # Status message with mode indicator - mode_status = "" - if session_id: - if language == "Korean": - mode_status = " [♻️ 복구 모드]" - else: - mode_status = " [♻️ Recovery Mode]" - elif test_mode: - if language == "Korean": - mode_status = " [🧪 테스트 모드: 7단계]" - else: - mode_status = " [🧪 Test Mode: 7 stages]" - - for final_novel, stages in system.process_novel_stream(query, language, session_id, test_quick_mode=test_mode): - # Format stages for display + for final_novel, stages in system.process_novel_stream(query, language, session_id): + # 단계 표시 형식화 stages_display = format_stages_display(stages, language) - # Progress calculation + # 진행률 계산 completed = sum(1 for s in stages if s.get("status") == "complete") total = len(stages) progress_percent = (completed / total * 100) if total > 0 else 0 - if "✅ Novel complete!" in str(final_novel) or "✅ [TEST MODE] Novel complete!" in str(final_novel): - status = f"✅ Complete! Ready to download.{mode_status}" + if "✅ 소설 완성!" in str(final_novel): + status = "✅ 완료! 다운로드 가능합니다." else: if language == "Korean": - status = f"🔄 진행중... ({completed}/{total} - {progress_percent:.1f}%){mode_status}" + status = f"🔄 진행중... ({completed}/{total} - {progress_percent:.1f}%)" else: - status = f"🔄 Processing... ({completed}/{total} - {progress_percent:.1f}%){mode_status}" + status = f"🔄 Processing... ({completed}/{total} - {progress_percent:.1f}%)" - # Return current session ID yield stages_display, final_novel, status, system.current_session_id except Exception as e: @@ -2771,58 +1440,54 @@ def process_query(query: str, language: str, session_id: str = None, test_mode: def format_stages_display(stages: List[Dict[str, str]], language: str) -> str: - """Format stages into simple display with writer save status and quality scores""" + """단계 표시 형식화""" display = "" for idx, stage in enumerate(stages): status_icon = "✅" if stage.get("status") == "complete" else ("⏳" if stage.get("status") == "active" else "❌") - # Add save indicator for completed writers - save_indicator = "" - 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"] + # 일관성 점수 표시 + consistency_indicator = "" + if stage.get("consistency_score", 0) > 0: + score = stage["consistency_score"] if score >= 8: - quality_indicator = " ⭐" + consistency_indicator = " 🟢" elif score >= 6: - quality_indicator = " ✨" + consistency_indicator = " 🟡" + else: + consistency_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']}**{quality_indicator}\n" - display += f"```\n{stage.get('content', '')[-1000:]}\n```" + display += f"\n\n{status_icon} **{stage['name']}**{consistency_indicator}\n" + display += f"```\n{stage.get('content', '')[-800:]}\n```" else: - display += f"\n{status_icon} {stage['name']}{save_indicator}{quality_indicator}" + display += f"\n{status_icon} {stage['name']}{consistency_indicator}" return display def get_active_sessions(language: str) -> List[Tuple[str, str]]: - """Get list of active sessions""" + """활성 세션 목록 가져오기""" try: sessions = NovelDatabase.get_active_sessions() choices = [] for session in sessions: - session_dict = dict(session) # Convert Row to dict - created = NovelDatabase.parse_datetime(session_dict['created_at']) + created = datetime.fromisoformat(session['created_at']) date_str = created.strftime("%Y-%m-%d %H:%M") - 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'])) + 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'])) return choices except Exception as e: - logger.error(f"Error getting active sessions: {str(e)}", exc_info=True) + logger.error(f"Error getting active sessions: {str(e)}") 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""" +def resume_session(session_id: str, language: str) -> Generator[Tuple[str, str, str, str], None, None]: + """세션 재개""" if not session_id: if language == "Korean": yield "", "", "❌ 세션을 선택해주세요.", None @@ -2830,21 +1495,19 @@ def resume_session(session_id: str, language: str, test_mode: bool = False) -> G yield "", "", "❌ Please select a session.", None return - # Process with existing session ID - yield from process_query("", language, session_id, test_mode) + yield from process_query("", language, session_id) 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"✅ 자동 복구: '{session_dict['user_query'][:30]}...' (Stage {session_dict['current_stage']})" + message = f"✅ 자동 복구: '{latest_session['user_query'][:30]}...' (Stage {latest_session['current_stage']})" else: - message = f"✅ Auto recovered: '{session_dict['user_query'][:30]}...' (Stage {session_dict['current_stage']})" - return session_dict['session_id'], message + message = f"✅ Auto recovered: '{latest_session['user_query'][:30]}...' (Stage {latest_session['current_stage']})" + return latest_session['session_id'], message else: return None, "" except Exception as e: @@ -2853,494 +1516,74 @@ def auto_recover_session(language: str) -> Tuple[str, str]: def download_novel(novel_text: str, format: str, language: str, session_id: str = None) -> str: - """Download novel - DB에서 직접 작가 내용을 가져와서 통합""" - - def extract_chapter_title(content: str, chapter_num: int, language: str) -> str: - """챕터 내용에서 제목 추출""" - # 내용을 줄 단위로 분리 - lines = content.strip().split('\n') - - # 첫 번째 의미있는 문장 찾기 (빈 줄 제외) - for line in lines[:5]: # 처음 5줄 내에서 찾기 - line = line.strip() - if len(line) > 10: # 10자 이상의 의미있는 문장 - # 대화문이면 스킵 (따옴표로 시작하는 경우) - if line.startswith('"') or line.startswith("'") or line.startswith('"') or line.startswith("'"): - continue - - # 마침표, 느낌표, 물음표로 끝나는 첫 문장만 추출 - import re - match = re.match(r'^[^.!?]+[.!?]', line) - if match: - title = match.group(0).strip() - else: - title = line - - # 너무 길면 자르기 - if len(title) > 50: - title = title[:47] + "..." - - return title - - # 적절한 제목을 찾지 못한 경우 기본 제목 - if language == "Korean": - return f"제{chapter_num}장" - else: - return f"Part {chapter_num}" - + """소설 다운로드""" if not session_id: logger.error("No session_id provided for download") return None - logger.info(f"Starting download for session: {session_id}, format: {format}") - - # DB에서 세션 정보 가져오기 + # 세션 정보 가져오기 session = NovelDatabase.get_session(session_id) if not session: logger.error(f"Session not found: {session_id}") return None - # 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") - - # 품질 점수 가져오기 - quality_scores = json.loads(session_dict.get('quality_scores', '{}')) if session_dict.get('quality_scores') else {} - - # 테스트 모드 감지 - is_test_mode = False - 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 - logger.info("Test mode detected - writer1 + writer10 mode") + # 완성된 소설 가져오기 + complete_novel = NovelDatabase.get_writer_content(session_id) + if not complete_novel: + logger.error("No complete novel found") + return None timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + temp_dir = tempfile.gettempdir() + safe_filename = re.sub(r'[^\w\s-]', '', session['user_query'][:30]).strip() if format == "DOCX" and DOCX_AVAILABLE: - # Create DOCX + # DOCX 생성 doc = Document() - # 제목 페이지 - title_para = doc.add_paragraph() - if actual_language == 'Korean': - main_title = 'AI 협업 소설' + (' - 테스트 모드' if is_test_mode else '') - 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.bold = True - title_para.alignment = WD_ALIGN_PARAGRAPH.CENTER + # 제목 추가 + title = doc.add_heading('AI 협업 소설' if language == 'Korean' else 'AI Collaborative Novel', 0) + # 메타정보 추가 + doc.add_paragraph(f"주제: {session['user_query']}" if language == 'Korean' else f"Theme: {session['user_query']}") + doc.add_paragraph(f"생성일: {datetime.now().strftime('%Y-%m-%d')}" if language == 'Korean' else f"Created: {datetime.now().strftime('%Y-%m-%d')}") + doc.add_paragraph(f"단어 수: {len(complete_novel.split())}" if language == 'Korean' else f"Word Count: {len(complete_novel.split())}") doc.add_paragraph() - doc.add_paragraph() - - theme_para = doc.add_paragraph() - theme_label = '주제: ' if actual_language == 'Korean' else 'Theme: ' - 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() + # 소설 내용 추가 + paragraphs = complete_novel.split('\n\n') + for para in paragraphs: + if para.strip(): + doc.add_paragraph(para.strip()) - 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_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() - - # 전체 통계 - total_words = 0 - writer_count = 0 - - # 각 작가의 수정본만 수집 - writer_contents = [] - - if is_test_mode: - # 테스트 모드: writer1 revision + writer10 내용 처리 - for stage in stages: - 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_dict.get('word_count', len(content.split())) - total_words += word_count - writer_contents.append({ - 'writer_num': 1, - 'content': content, - '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") - - # Writer 10 (테스트 모드에서 나머지 챕터들) - elif role == 'writer10': - logger.info(f"Processing writer10 content: {len(content)} chars") - - # [Chapter X] 패턴으로 챕터 분리 - chapters = re.split(r'\[Chapter\s+(\d+)\]', content) - - for i in range(1, len(chapters), 2): - if i+1 < len(chapters): - chapter_num = int(chapters[i]) - chapter_content = chapters[i+1].strip() - - if chapter_content: - word_count = len(chapter_content.split()) - total_words += word_count - writer_contents.append({ - 'writer_num': chapter_num, - 'content': chapter_content, - '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") - else: - # 일반 모드: 모든 작가 수정본 처리 - logger.info("[NORMAL MODE] Processing all writer revisions...") - - for stage in stages: - 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_dict.get('word_count', len(content.split())) - total_words += word_count - writer_contents.append({ - 'writer_num': writer_num, - 'content': content, - '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") - - logger.info(f"Total writers collected: {writer_count}, Total words: {total_words}") - - # 통계 페이지 - 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}') - 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}') - 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() - - # 각 작가의 내용 추가 (정렬된 순서대로) - for idx, writer_data in enumerate(sorted_contents): - 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}' - doc.add_heading(chapter_header, 1) - - # 챕터 제목 추가 - chapter_title = extract_chapter_title(content, writer_num, actual_language) - title_para = doc.add_paragraph(chapter_title) - title_para.style.font.size = Pt(14) - title_para.style.font.bold = True - title_para.alignment = WD_ALIGN_PARAGRAPH.CENTER - - # 메타 정보 - 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() - - # 작가 내용 추가 - paragraphs = content.split('\n\n') - for para_text in paragraphs: - if para_text.strip(): - para = doc.add_paragraph(para_text.strip()) - para.style.font.size = Pt(10.5) # 한국 소설 표준 크기 - - if idx < len(sorted_contents) - 1: # 마지막 챕터 후에는 페이지 구분 없음 - doc.add_page_break() - else: - logger.warning("No writer contents found! Creating empty document.") - if actual_language == 'Korean': - doc.add_paragraph("내용을 찾을 수 없습니다. 소설 생성이 정상적으로 완료되었는지 확인해주세요.") - else: - doc.add_paragraph("No content found. Please check if the novel generation completed successfully.") - - # 페이지 설정 - 한국 신국판 (152 × 225mm) - for section in doc.sections: - section.page_width = Inches(5.98) # 152mm - section.page_height = Inches(8.86) # 225mm - section.left_margin = Inches(0.79) # 20mm - section.right_margin = Inches(0.79) # 20mm - section.top_margin = Inches(0.79) # 20mm - section.bottom_margin = Inches(0.79) # 20mm - - # Save - temp_dir = tempfile.gettempdir() - 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" + # 파일 저장 + filename = f"Novel_{safe_filename}_{timestamp}.docx" filepath = os.path.join(temp_dir, filename) doc.save(filepath) - logger.info(f"DOCX saved successfully: {filepath} ({total_words} words, {writer_count} writers)") return filepath else: - # TXT format - temp_dir = tempfile.gettempdir() - 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" + # TXT 생성 + filename = f"Novel_{safe_filename}_{timestamp}.txt" filepath = os.path.join(temp_dir, filename) with open(filepath, 'w', encoding='utf-8') as f: - f.write("="*60 + "\n") - if actual_language == 'Korean': - f.write(f"AI 협업 소설 - {'테스트 모드' if is_test_mode else '완성본'}\n") - else: - f.write(f"AI COLLABORATIVE NOVEL - {'TEST MODE' if is_test_mode else 'COMPLETE VERSION'}\n") - f.write("="*60 + "\n") - - if actual_language == 'Korean': - 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_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: - # 테스트 모드 처리 (DOCX와 동일) - for stage in stages: - 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', '') - - 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_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") - - elif role == 'writer10': - chapters = re.split(r'\[Chapter\s+(\d+)\]', content) - - for i in range(1, len(chapters), 2): - if i+1 < len(chapters): - chapter_num = int(chapters[i]) - chapter_content = chapters[i+1].strip() - - if chapter_content: - word_count = len(chapter_content.split()) - total_words += word_count - writer_count = max(writer_count, chapter_num) - - 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:,}" - f.write(f"{word_count_label}\n") - f.write(f"{'='*40}\n\n") - f.write(chapter_content) - f.write("\n\n") - else: - # 일반 모드 - for stage in stages: - 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_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") - - 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") + f.write("=" * 60 + "\n") + f.write(f"AI 협업 소설\n" if language == 'Korean' else f"AI COLLABORATIVE NOVEL\n") + f.write("=" * 60 + "\n\n") + f.write(f"주제: {session['user_query']}\n" if language == 'Korean' else f"Theme: {session['user_query']}\n") + f.write(f"생성일: {datetime.now()}\n" if language == 'Korean' else f"Created: {datetime.now()}\n") + f.write(f"단어 수: {len(complete_novel.split())}\n" if language == 'Korean' else f"Word Count: {len(complete_novel.split())}\n") + f.write("=" * 60 + "\n\n") + f.write(complete_novel) - logger.info(f"TXT saved successfully: {filepath} ({total_words} words)") return filepath -# Custom CSS +# CSS 스타일 custom_css = """ .gradio-container { - background: linear-gradient(135deg, #1e3c72, #2a5298); + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; } @@ -3383,7 +1626,6 @@ custom_css = """ background-color: rgba(255, 255, 255, 0.95); padding: 30px; border-radius: 12px; - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); max-height: 400px; overflow-y: auto; } @@ -3394,187 +1636,135 @@ custom_css = """ border-radius: 8px; margin-top: 20px; } - -.search-indicator { - color: #4CAF50; - font-weight: bold; -} - -.auto-save-indicator { - color: #2196F3; - font-weight: bold; -} - -.quality-indicator { - color: #FF9800; - font-weight: bold; -} - -.consistency-indicator { - color: #9C27B0; - font-weight: bold; -} """ -# Create Gradio Interface +# Gradio 인터페이스 생성 def create_interface(): - with gr.Blocks(css=custom_css, title="SOMA Novel Writing System - Enhanced") as interface: + with gr.Blocks(css=custom_css, title="AI 협업 소설 생성 시스템") as interface: gr.HTML("""

- 📚 SOMA Novel Writing System - Enhanced Edition + 📚 AI 협업 소설 생성 시스템

- 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. - The system includes 1 Director, 1 Critic, and 10 Writers working in harmony. -

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

- ♻️ Resume anytime | - 🧪 Test mode: 7 stages (Writer 1 + Writer 10) + 주제를 입력하면 AI 에이전트들이 협업하여 30페이지 분량의 완성된 소설을 생성합니다. +
+ 감독자 1명, 비평가 1명, 작가 10명이 함께 작업하며 일관성을 유지합니다.

""") - # State management + # 상태 관리 current_session_id = gr.State(None) with gr.Row(): with gr.Column(scale=1): with gr.Group(elem_classes=["input-section"]): query_input = gr.Textbox( - label="Novel Theme / 소설 주제", - placeholder="Enter your novel theme or initial idea...\n소설의 주제나 초기 아이디어를 입력하세요...", + label="소설 주제 / Novel Theme", + placeholder="소설의 주제나 초기 아이디어를 입력하세요...\nEnter your novel theme or initial idea...", lines=4 ) language_select = gr.Radio( choices=["English", "Korean"], value="English", - label="Language / 언어" - ) - - # Test mode checkbox - test_mode_check = gr.Checkbox( - label="🧪 Test Mode (Quick: Writer 1 & 10 only) / 테스트 모드 (빠른 생성: 작가 1, 10만)", - value=False, - info="Complete Writer 1 & 10 process for 10 chapters / 작가 1, 10만 작성하여 10개 챕터 생성" + label="언어 / Language" ) - # 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) - clear_btn = gr.Button("🗑️ Clear / 초기화", scale=1) + submit_btn = gr.Button("🚀 소설 생성 시작", variant="primary", scale=2) + clear_btn = gr.Button("🗑️ 초기화", scale=1) status_text = gr.Textbox( - label="Status", + label="상태", interactive=False, - value="🔄 Ready" + value="🔄 준비 완료" ) - # Session management + # 세션 관리 with gr.Group(elem_classes=["session-section"]): - gr.Markdown("### 💾 Resume Previous Session / 이전 세션 재개") + gr.Markdown("### 💾 이전 세션 재개") session_dropdown = gr.Dropdown( - label="Select Session / 세션 선택", + label="세션 선택", choices=[], interactive=True ) with gr.Row(): - refresh_btn = gr.Button("🔄 Refresh List / 목록 새로고침", scale=1) - resume_btn = gr.Button("▶️ Resume Selected / 선택 재개", variant="secondary", scale=1) - auto_recover_btn = gr.Button("♻️ Auto Recover Latest / 최신 자동복구", scale=1) + refresh_btn = gr.Button("🔄 목록 새로고침", scale=1) + resume_btn = gr.Button("▶️ 선택 재개", variant="secondary", scale=1) + auto_recover_btn = gr.Button("♻️ 자동 복구", scale=1) with gr.Column(scale=2): - with gr.Tab("📝 Writing Process / 작성 과정"): + with gr.Tab("📝 작성 과정"): stages_display = gr.Markdown( - value="Process will appear here...", + value="작성 과정이 여기에 표시됩니다...", elem_id="stages-display" ) - with gr.Tab("📖 Final Novel / 최종 소설"): + with gr.Tab("📖 완성된 소설"): novel_output = gr.Markdown( - value="Complete novel will be available for download after all writers finish.\n\nQuality report will be shown here.", + value="완성된 소설이 여기에 표시됩니다...", elem_id="novel-output" ) with gr.Group(elem_classes=["download-section"]): - gr.Markdown("### 📥 Download Complete Novel / 전체 소설 다운로드") + gr.Markdown("### 📥 소설 다운로드") with gr.Row(): format_select = gr.Radio( choices=["DOCX", "TXT"], value="DOCX" if DOCX_AVAILABLE else "TXT", - label="Format / 형식" + label="형식" ) - download_btn = gr.Button("⬇️ Download Complete Novel / 완성된 소설 다운로드", variant="secondary") + download_btn = gr.Button("⬇️ 다운로드", variant="secondary") download_file = gr.File( - label="Downloaded File / 다운로드된 파일", + label="다운로드된 파일", visible=False ) - # Hidden state for novel text + # 숨겨진 상태 novel_text_state = gr.State("") - # Examples + # 예제 with gr.Row(): gr.Examples( examples=[ - ["A scientist discovers a portal to parallel universes but each journey erases a memory"], - ["In a world where dreams can be traded, a dream thief must steal the emperor's nightmare"], - ["Two AI entities fall in love while trying to prevent a global cyber war"], - ["미래 도시에서 기억을 거래하는 상인과 모든 기억을 잃은 탐정의 이야기"], - ["시간이 거꾸로 흐르는 마을에서 일어나는 미스터리한 살인 사건"], + ["미래 도시에서 기억을 거래하는 상인의 이야기"], + ["시간이 거꾸로 흐르는 마을의 미스터리"], + ["A scientist discovers a portal to parallel universes"], + ["In a world where dreams can be traded, a dream thief's story"], + ["Two AI entities fall in love while preventing a cyber war"], ["책 속으로 들어갈 수 있는 능력을 가진 사서의 모험"] ], inputs=query_input, - label="💡 Example Themes / 예제 주제" + label="💡 예제 주제" ) - # Event handlers + # 이벤트 핸들러 def refresh_sessions(): try: sessions = get_active_sessions("English") return gr.update(choices=sessions) except Exception as e: - logger.error(f"Error refreshing sessions: {str(e)}", exc_info=True) + logger.error(f"Error refreshing sessions: {str(e)}") return gr.update(choices=[]) def handle_auto_recover(language): session_id, message = auto_recover_session(language) return session_id - # Connect event handlers + # 이벤트 연결 submit_btn.click( fn=process_query, - inputs=[query_input, language_select, current_session_id, test_mode_check], + inputs=[query_input, language_select, current_session_id], outputs=[stages_display, novel_output, status_text, current_session_id] ) - # Update novel text state and session ID when novel output changes novel_output.change( fn=lambda x: x, inputs=[novel_output], @@ -3587,7 +1777,7 @@ def create_interface(): outputs=[current_session_id] ).then( fn=resume_session, - inputs=[current_session_id, language_select, test_mode_check], + inputs=[current_session_id, language_select], outputs=[stages_display, novel_output, status_text, current_session_id] ) @@ -3597,7 +1787,7 @@ def create_interface(): outputs=[current_session_id] ).then( fn=resume_session, - inputs=[current_session_id, language_select, test_mode_check], + inputs=[current_session_id, language_select], outputs=[stages_display, novel_output, status_text, current_session_id] ) @@ -3607,24 +1797,18 @@ def create_interface(): ) clear_btn.click( - fn=lambda: ("", "", "🔄 Ready", "", None, False), - outputs=[stages_display, novel_output, status_text, novel_text_state, current_session_id, test_mode_check] + fn=lambda: ("", "", "🔄 준비 완료", "", None), + outputs=[stages_display, novel_output, status_text, novel_text_state, current_session_id] ) def handle_download(format_type, language, session_id, novel_text): - logger.info(f"Download requested: format={format_type}, session_id={session_id}, language={language}") - logger.info(f"Session ID type: {type(session_id)}, value: {repr(session_id)}") - if not session_id: - logger.error("No session_id available for download") return gr.update(visible=False) file_path = download_novel(novel_text, format_type, language, session_id) if file_path: - logger.info(f"Download successful: {file_path}") return gr.update(value=file_path, visible=True) else: - logger.error("Download failed - no file generated") return gr.update(visible=False) download_btn.click( @@ -3633,52 +1817,41 @@ def create_interface(): outputs=[download_file] ) - # Load sessions on startup - def on_load(): - sessions = refresh_sessions() - return sessions - + # 시작 시 세션 로드 interface.load( - fn=on_load, + fn=refresh_sessions, outputs=[session_dropdown] ) return interface -# Main execution +# 메인 실행 if __name__ == "__main__": - import sys - - logger.info("Starting SOMA Novel Writing System - Enhanced Edition...") + logger.info("AI 협업 소설 생성 시스템 시작...") logger.info("=" * 60) - # Check environment - if TEST_MODE: - logger.warning("Running in TEST MODE - no actual API calls will be made") - else: - logger.info(f"Running in PRODUCTION MODE with API endpoint: {API_URL}") + # 환경 확인 + logger.info(f"API 엔드포인트: {API_URL}") - # Check web search if BRAVE_SEARCH_API_KEY: - logger.info("Web search is ENABLED with Brave Search API") + logger.info("웹 검색이 활성화되었습니다.") else: - logger.warning("Web search is DISABLED. Set BRAVE_SEARCH_API_KEY environment variable to enable.") + logger.warning("웹 검색이 비활성화되었습니다.") - # Check DOCX support if DOCX_AVAILABLE: - logger.info("DOCX export is ENABLED") + logger.info("DOCX 내보내기가 활성화되었습니다.") else: - logger.warning("DOCX export is DISABLED. Install python-docx to enable.") + logger.warning("DOCX 내보내기가 비활성화되었습니다.") logger.info("=" * 60) - # Initialize database on startup - logger.info("Initializing database...") + # 데이터베이스 초기화 + logger.info("데이터베이스 초기화 중...") NovelDatabase.init_db() - logger.info("Database initialized successfully.") + logger.info("데이터베이스 초기화 완료.") - # Create and launch interface + # 인터페이스 생성 및 실행 interface = create_interface() interface.launch(