import gradio as gr import os import json import requests from datetime import datetime import time from typing import List, Dict, Any, Generator, Tuple, Optional, Set import logging import re import tempfile from pathlib import Path import sqlite3 import hashlib import threading from contextlib import contextmanager from dataclasses import dataclass, field, asdict from collections import defaultdict import json from pathlib import Path import random # --- Logging setup --- logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) # --- Document export imports --- try: from docx import Document from docx.shared import Inches, Pt, RGBColor, Mm from docx.enum.text import WD_ALIGN_PARAGRAPH from docx.enum.style import WD_STYLE_TYPE from docx.oxml.ns import qn from docx.oxml import OxmlElement DOCX_AVAILABLE = True except ImportError: DOCX_AVAILABLE = False logger.warning("python-docx not installed. DOCX export will be disabled.") # --- Environment variables and constants --- 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 = "dep86pjolcjjnv8" DB_PATH = "novel_sessions_v6.db" # Target word count settings TARGET_WORDS = 8000 # Safety margin MIN_WORDS_PER_PART = 800 # Minimum words per part # --- Environment validation --- if not FRIENDLI_TOKEN: logger.error("FRIENDLI_TOKEN not set. Application will not work properly.") FRIENDLI_TOKEN = "dummy_token_for_testing" if not BRAVE_SEARCH_API_KEY: logger.warning("BRAVE_SEARCH_API_KEY not set. Web search features will be disabled.") # --- Global variables --- db_lock = threading.Lock() # Narrative phases definition NARRATIVE_PHASES = [ "Introduction: Daily Life and Cracks", "Development 1: Rising Anxiety", "Development 2: External Shock", "Development 3: Deepening Internal Conflict", "Climax 1: Peak of Crisis", "Climax 2: Moment of Choice", "Falling Action 1: Consequences and Aftermath", "Falling Action 2: New Recognition", "Resolution 1: Changed Daily Life", "Resolution 2: Open Questions" ] # Stage configuration - Single writer system UNIFIED_STAGES = [ ("director", "🎬 Director: Integrated Narrative Structure Planning"), ("critic_director", "πŸ“ Critic: Deep Review of Narrative Structure"), ("director", "🎬 Director: Final Master Plan"), ] + [ item for i in range(1, 11) for item in [ ("writer", f"✍️ Writer: Part {i} - {NARRATIVE_PHASES[i-1]}"), (f"critic_part{i}", f"πŸ“ Part {i} Critic: Immediate Review and Revision Request"), ("writer", f"✍️ Writer: Part {i} Revision") ] ] + [ ("critic_final", "πŸ“ Final Critic: Comprehensive Evaluation and Literary Achievement"), ] # --- Data classes --- @dataclass class StoryBible: """Story bible for maintaining narrative consistency""" characters: Dict[str, Dict[str, Any]] = field(default_factory=dict) settings: Dict[str, str] = field(default_factory=dict) timeline: List[Dict[str, Any]] = field(default_factory=list) plot_points: List[Dict[str, Any]] = field(default_factory=list) themes: List[str] = field(default_factory=list) symbols: Dict[str, List[str]] = field(default_factory=dict) style_guide: Dict[str, str] = field(default_factory=dict) opening_sentence: str = "" @dataclass class PartCritique: """Critique content for each part""" part_number: int continuity_issues: List[str] = field(default_factory=list) character_consistency: List[str] = field(default_factory=list) plot_progression: List[str] = field(default_factory=list) thematic_alignment: List[str] = field(default_factory=list) technical_issues: List[str] = field(default_factory=list) strengths: List[str] = field(default_factory=list) required_changes: List[str] = field(default_factory=list) literary_quality: List[str] = field(default_factory=list) # --- Core logic classes --- class UnifiedNarrativeTracker: """Unified narrative tracker for single writer system""" def __init__(self): self.story_bible = StoryBible() self.part_critiques: Dict[int, PartCritique] = {} self.accumulated_content: List[str] = [] self.word_count_by_part: Dict[int, int] = {} self.revision_history: Dict[int, List[str]] = defaultdict(list) self.causal_chains: List[Dict[str, Any]] = [] self.narrative_momentum: float = 0.0 def update_story_bible(self, element_type: str, key: str, value: Any): """Update story bible""" if element_type == "character": self.story_bible.characters[key] = value elif element_type == "setting": self.story_bible.settings[key] = value elif element_type == "timeline": self.story_bible.timeline.append({"event": key, "details": value}) elif element_type == "theme": if key not in self.story_bible.themes: self.story_bible.themes.append(key) elif element_type == "symbol": if key not in self.story_bible.symbols: self.story_bible.symbols[key] = [] self.story_bible.symbols[key].append(value) def add_part_critique(self, part_number: int, critique: PartCritique): """Add part critique""" self.part_critiques[part_number] = critique def check_continuity(self, current_part: int, new_content: str) -> List[str]: """Check continuity""" issues = [] # Character consistency check for char_name, char_data in self.story_bible.characters.items(): if char_name in new_content: if "traits" in char_data: for trait in char_data["traits"]: if trait.get("abandoned", False): issues.append(f"{char_name}'s abandoned trait '{trait['name']}' reappears") # Timeline consistency check if len(self.story_bible.timeline) > 0: last_event = self.story_bible.timeline[-1] # Causality check if current_part > 1 and not any(kw in new_content for kw in ['because', 'therefore', 'thus', 'hence', 'consequently']): issues.append("Unclear causality with previous part") return issues def calculate_narrative_momentum(self, part_number: int, content: str) -> float: """Calculate narrative momentum""" momentum = 5.0 # New elements introduced new_elements = len(set(content.split()) - set(' '.join(self.accumulated_content).split())) if new_elements > 100: momentum += 2.0 # Conflict escalation tension_words = ['crisis', 'conflict', 'tension', 'struggle', 'dilemma'] if any(word in content.lower() for word in tension_words): momentum += 1.5 # Causal clarity causal_words = ['because', 'therefore', 'thus', 'consequently', 'hence'] causal_count = sum(1 for word in causal_words if word in content.lower()) momentum += min(causal_count * 0.5, 2.0) # Repetition penalty if part_number > 1: prev_content = self.accumulated_content[-1] if self.accumulated_content else "" overlap = len(set(content.split()) & set(prev_content.split())) if overlap > len(content.split()) * 0.3: momentum -= 3.0 return max(0.0, min(10.0, momentum)) class NovelDatabase: """Database management - Modified for single writer system""" @staticmethod def init_db(): with sqlite3.connect(DB_PATH) as conn: conn.execute("PRAGMA journal_mode=WAL") cursor = conn.cursor() # Main sessions table cursor.execute(''' CREATE TABLE IF NOT EXISTS sessions ( session_id TEXT PRIMARY KEY, user_query TEXT NOT NULL, language TEXT NOT NULL, created_at TEXT DEFAULT (datetime('now')), updated_at TEXT DEFAULT (datetime('now')), status TEXT DEFAULT 'active', current_stage INTEGER DEFAULT 0, final_novel TEXT, literary_report TEXT, total_words INTEGER DEFAULT 0, story_bible TEXT, narrative_tracker TEXT, opening_sentence TEXT ) ''') # Stages table cursor.execute(''' CREATE TABLE IF NOT EXISTS stages ( id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, stage_number INTEGER NOT NULL, stage_name TEXT NOT NULL, role TEXT NOT NULL, content TEXT, word_count INTEGER DEFAULT 0, status TEXT DEFAULT 'pending', narrative_momentum REAL DEFAULT 0.0, created_at TEXT DEFAULT (datetime('now')), updated_at TEXT DEFAULT (datetime('now')), FOREIGN KEY (session_id) REFERENCES sessions(session_id), UNIQUE(session_id, stage_number) ) ''') # Critiques table cursor.execute(''' CREATE TABLE IF NOT EXISTS critiques ( id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, part_number INTEGER NOT NULL, critique_data TEXT, created_at TEXT DEFAULT (datetime('now')), FOREIGN KEY (session_id) REFERENCES sessions(session_id) ) ''') conn.commit() @staticmethod @contextmanager def get_db(): with db_lock: conn = sqlite3.connect(DB_PATH, timeout=30.0) conn.row_factory = sqlite3.Row try: yield conn finally: conn.close() @staticmethod def create_session(user_query: str, language: str) -> str: session_id = hashlib.md5(f"{user_query}{datetime.now()}".encode()).hexdigest() with NovelDatabase.get_db() as conn: conn.cursor().execute( 'INSERT INTO sessions (session_id, user_query, language) VALUES (?, ?, ?)', (session_id, user_query, language) ) conn.commit() return session_id @staticmethod def save_stage(session_id: str, stage_number: int, stage_name: str, role: str, content: str, status: str = 'complete', narrative_momentum: float = 0.0): word_count = len(content.split()) if content else 0 with NovelDatabase.get_db() as conn: cursor = conn.cursor() cursor.execute(''' INSERT INTO stages (session_id, stage_number, stage_name, role, content, word_count, status, narrative_momentum) VALUES (?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(session_id, stage_number) DO UPDATE SET content=?, word_count=?, status=?, stage_name=?, narrative_momentum=?, updated_at=datetime('now') ''', (session_id, stage_number, stage_name, role, content, word_count, status, narrative_momentum, content, word_count, status, stage_name, narrative_momentum)) # Update total word count cursor.execute(''' UPDATE sessions SET total_words = ( SELECT SUM(word_count) FROM stages WHERE session_id = ? AND role = 'writer' AND content IS NOT NULL ), updated_at = datetime('now'), current_stage = ? WHERE session_id = ? ''', (session_id, stage_number, session_id)) conn.commit() @staticmethod def save_critique(session_id: str, part_number: int, critique: PartCritique): """Save critique""" with NovelDatabase.get_db() as conn: critique_json = json.dumps(asdict(critique)) conn.cursor().execute( 'INSERT INTO critiques (session_id, part_number, critique_data) VALUES (?, ?, ?)', (session_id, part_number, critique_json) ) conn.commit() @staticmethod def save_opening_sentence(session_id: str, opening_sentence: str): """Save opening sentence""" with NovelDatabase.get_db() as conn: conn.cursor().execute( 'UPDATE sessions SET opening_sentence = ? WHERE session_id = ?', (opening_sentence, session_id) ) conn.commit() @staticmethod def get_writer_content(session_id: str) -> str: """Get writer content - Integrate all revisions""" with NovelDatabase.get_db() as conn: rows = conn.cursor().execute(''' SELECT content FROM stages WHERE session_id = ? AND role = 'writer' AND stage_name LIKE '%Revision%' ORDER BY stage_number ''', (session_id,)).fetchall() if rows: return '\n\n'.join(row['content'] for row in rows if row['content']) else: # If no revisions, use drafts rows = conn.cursor().execute(''' SELECT content FROM stages WHERE session_id = ? AND role = 'writer' AND stage_name NOT LIKE '%Revision%' ORDER BY stage_number ''', (session_id,)).fetchall() return '\n\n'.join(row['content'] for row in rows if row['content']) @staticmethod def save_narrative_tracker(session_id: str, tracker: UnifiedNarrativeTracker): """Save unified narrative tracker""" with NovelDatabase.get_db() as conn: tracker_data = json.dumps({ 'story_bible': asdict(tracker.story_bible), 'part_critiques': {k: asdict(v) for k, v in tracker.part_critiques.items()}, 'word_count_by_part': tracker.word_count_by_part, 'causal_chains': tracker.causal_chains, 'narrative_momentum': tracker.narrative_momentum }) conn.cursor().execute( 'UPDATE sessions SET narrative_tracker = ? WHERE session_id = ?', (tracker_data, session_id) ) conn.commit() @staticmethod def load_narrative_tracker(session_id: str) -> Optional[UnifiedNarrativeTracker]: """Load unified narrative tracker""" with NovelDatabase.get_db() as conn: row = conn.cursor().execute( 'SELECT narrative_tracker FROM sessions WHERE session_id = ?', (session_id,) ).fetchone() if row and row['narrative_tracker']: data = json.loads(row['narrative_tracker']) tracker = UnifiedNarrativeTracker() # Restore story bible bible_data = data.get('story_bible', {}) tracker.story_bible = StoryBible(**bible_data) # Restore critiques for part_num, critique_data in data.get('part_critiques', {}).items(): tracker.part_critiques[int(part_num)] = PartCritique(**critique_data) tracker.word_count_by_part = data.get('word_count_by_part', {}) tracker.causal_chains = data.get('causal_chains', []) tracker.narrative_momentum = data.get('narrative_momentum', 0.0) return tracker return None # Maintain existing methods @staticmethod def get_session(session_id: str) -> Optional[Dict]: with NovelDatabase.get_db() as conn: row = conn.cursor().execute('SELECT * FROM sessions WHERE session_id = ?', (session_id,)).fetchone() return dict(row) if row else None @staticmethod def get_stages(session_id: str) -> List[Dict]: with NovelDatabase.get_db() as conn: rows = conn.cursor().execute( 'SELECT * FROM stages WHERE session_id = ? ORDER BY stage_number', (session_id,) ).fetchall() return [dict(row) for row in rows] @staticmethod def update_final_novel(session_id: str, final_novel: str, literary_report: str = ""): with NovelDatabase.get_db() as conn: conn.cursor().execute( '''UPDATE sessions SET final_novel = ?, status = 'complete', updated_at = datetime('now'), literary_report = ? WHERE session_id = ?''', (final_novel, literary_report, session_id) ) conn.commit() @staticmethod def get_active_sessions() -> List[Dict]: with NovelDatabase.get_db() as conn: rows = conn.cursor().execute( '''SELECT session_id, user_query, language, created_at, current_stage, total_words FROM sessions WHERE status = 'active' ORDER BY updated_at DESC LIMIT 10''' ).fetchall() return [dict(row) for row in rows] @staticmethod def get_total_words(session_id: str) -> int: """Get total word count""" with NovelDatabase.get_db() as conn: row = conn.cursor().execute( 'SELECT total_words FROM sessions WHERE session_id = ?', (session_id,) ).fetchone() return row['total_words'] if row and row['total_words'] else 0 class WebSearchIntegration: """Web search functionality""" def __init__(self): self.brave_api_key = BRAVE_SEARCH_API_KEY self.search_url = "https://api.search.brave.com/res/v1/web/search" self.enabled = bool(self.brave_api_key) def search(self, query: str, count: int = 3, language: str = "en") -> List[Dict]: if not self.enabled: return [] headers = { "Accept": "application/json", "X-Subscription-Token": self.brave_api_key } params = { "q": query, "count": count, "search_lang": "ko" if language == "Korean" else "en", "text_decorations": False, "safesearch": "moderate" } try: response = requests.get(self.search_url, headers=headers, params=params, timeout=10) response.raise_for_status() results = response.json().get("web", {}).get("results", []) return results except requests.exceptions.RequestException as e: logger.error(f"Web search API error: {e}") return [] def extract_relevant_info(self, results: List[Dict], max_chars: int = 1500) -> str: if not results: return "" extracted = [] total_chars = 0 for i, result in enumerate(results[:3], 1): title = result.get("title", "") description = result.get("description", "") info = f"[{i}] {title}: {description}" if total_chars + len(info) < max_chars: extracted.append(info) total_chars += len(info) else: break return "\n".join(extracted) class UnifiedLiterarySystem: """Single writer progressive literary novel generation system""" def __init__(self): self.token = FRIENDLI_TOKEN self.api_url = API_URL self.model_id = MODEL_ID self.narrative_tracker = UnifiedNarrativeTracker() self.web_search = WebSearchIntegration() self.current_session_id = None NovelDatabase.init_db() def create_headers(self): return {"Authorization": f"Bearer {self.token}", "Content-Type": "application/json"} # --- Prompt generation functions --- def augment_query(self, user_query: str, language: str) -> str: """Augment prompt""" if len(user_query.split()) < 15: augmented_template = { "Korean": f"""'{user_query}' **μ„œμ‚¬ ꡬ쑰 핡심:** - 10개 νŒŒνŠΈκ°€ ν•˜λ‚˜μ˜ ν†΅ν•©λœ 이야기λ₯Ό ꡬ성 - 각 νŒŒνŠΈλŠ” 이전 파트의 필연적 κ²°κ³Ό - 인물의 λͺ…ν™•ν•œ λ³€ν™” ꢀ적 (A β†’ B β†’ C) - 쀑심 κ°ˆλ“±μ˜ 점진적 고쑰와 ν•΄κ²° - κ°•λ ¬ν•œ 쀑심 μƒμ§•μ˜ 의미 λ³€ν™”""", "English": f"""'{user_query}' **Narrative Structure Core:** - 10 parts forming one integrated story - Each part as inevitable result of previous - Clear character transformation arc (A β†’ B β†’ C) - Progressive escalation and resolution of central conflict - Evolving meaning of powerful central symbol""" } return augmented_template.get(language, user_query) return user_query def generate_powerful_opening(self, user_query: str, language: str) -> str: """Generate powerful opening sentence matching the theme""" opening_prompt = { "Korean": f"""주제: {user_query} 이 μ£Όμ œμ— λŒ€ν•œ κ°•λ ¬ν•˜κ³  μžŠμ„ 수 μ—†λŠ” 첫문μž₯을 μƒμ„±ν•˜μ„Έμš”. **첫문μž₯ μž‘μ„± 원칙:** 1. 즉각적인 κΈ΄μž₯κ°μ΄λ‚˜ ꢁ금증 유발 2. ν‰λ²”ν•˜μ§€ μ•Šμ€ μ‹œκ°μ΄λ‚˜ 상황 μ œμ‹œ 3. 감각적이고 ꡬ체적인 이미지 4. 철학적 μ§ˆλ¬Έμ΄λ‚˜ 역섀적 μ§„μˆ  5. μ‹œκ°„κ³Ό κ³΅κ°„μ˜ λ…νŠΉν•œ μ„€μ • **ν›Œλ₯­ν•œ 첫문μž₯의 μ˜ˆμ‹œ νŒ¨ν„΄:** - "κ·Έκ°€ 죽은 λ‚ , ..." (좩격적 사건) - "λͺ¨λ“  것이 끝났닀고 μƒκ°ν•œ μˆœκ°„..." (λ°˜μ „ 예고) - "μ„Έμƒμ—μ„œ κ°€μž₯ [ν˜•μš©μ‚¬]ν•œ [λͺ…사]λŠ”..." (λ…νŠΉν•œ μ •μ˜) - "[ꡬ체적 행동]ν•˜λŠ” κ²ƒλ§ŒμœΌλ‘œλ„..." (μΌμƒμ˜ μž¬ν•΄μ„) 단 ν•˜λ‚˜μ˜ λ¬Έμž₯만 μ œμ‹œν•˜μ„Έμš”.""", "English": f"""Theme: {user_query} Generate an unforgettable opening sentence for this theme. **Opening Sentence Principles:** 1. Immediate tension or curiosity 2. Unusual perspective or situation 3. Sensory and specific imagery 4. Philosophical question or paradox 5. Unique temporal/spatial setting **Great Opening Patterns:** - "The day he died, ..." (shocking event) - "At the moment everything seemed over..." (reversal hint) - "The most [adjective] [noun] in the world..." (unique definition) - "Just by [specific action]..." (reinterpretation of ordinary) Provide only one sentence.""" } messages = [{"role": "user", "content": opening_prompt.get(language, opening_prompt["Korean"])}] opening = self.call_llm_sync(messages, "writer", language) return opening.strip() def create_director_initial_prompt(self, user_query: str, language: str) -> str: """Director initial planning - Enhanced version""" augmented_query = self.augment_query(user_query, language) # Generate opening sentence opening_sentence = self.generate_powerful_opening(user_query, language) self.narrative_tracker.story_bible.opening_sentence = opening_sentence if self.current_session_id: NovelDatabase.save_opening_sentence(self.current_session_id, opening_sentence) search_results_str = "" if self.web_search.enabled: short_query = user_query[:50] if len(user_query) > 50 else user_query queries = [ f"{short_query} philosophical meaning", f"human existence meaning {short_query}", f"{short_query} literary works" ] for q in queries[:2]: try: results = self.web_search.search(q, count=2, language=language) if results: search_results_str += self.web_search.extract_relevant_info(results) + "\n" except Exception as e: logger.warning(f"Search failed: {str(e)}") lang_prompts = { "Korean": f"""노벨문학상 μˆ˜μ€€μ˜ 철학적 깊이λ₯Ό μ§€λ‹Œ μ€‘νŽΈμ†Œμ„€(8,000단어)을 κΈ°νšν•˜μ„Έμš”. **주제:** {augmented_query} **ν•„μˆ˜ 첫문μž₯:** {opening_sentence} **μ°Έκ³  자료:** {search_results_str if search_results_str else "N/A"} **ν•„μˆ˜ 문학적 μš”μ†Œ:** 1. **철학적 탐ꡬ** - ν˜„λŒ€μΈμ˜ 싀쑴적 κ³ λ‡Œ (μ†Œμ™Έ, 정체성, 의미 상싀) - λ””μ§€ν„Έ μ‹œλŒ€μ˜ 인간 쑰건 - 자본주의 μ‚¬νšŒμ˜ λͺ¨μˆœκ³Ό 개인의 선택 - 죽음, μ‚¬λž‘, μžμœ μ— λŒ€ν•œ μƒˆλ‘œμš΄ μ„±μ°° 2. **μ‚¬νšŒμ  λ©”μ‹œμ§€** - 계급, 젠더, μ„ΈλŒ€ κ°„ κ°ˆλ“± - ν™˜κ²½ μœ„κΈ°μ™€ μΈκ°„μ˜ μ±…μž„ - 기술 λ°œμ „κ³Ό μΈκ°„μ„±μ˜ 좩돌 - ν˜„λŒ€ 민주주의의 μœ„κΈ°μ™€ 개인의 μ—­ν•  3. **문학적 μˆ˜μ‚¬ μž₯치** - 쀑심 μ€μœ : [ꡬ체적 사물/ν˜„μƒ] β†’ [좔상적 의미] - λ°˜λ³΅λ˜λŠ” λͺ¨ν‹°ν”„: [이미지/행동] (μ΅œμ†Œ 5회 λ³€μ£Ό) - λŒ€μ‘°λ²•: [A vs B]의 지속적 κΈ΄μž₯ - 상징적 곡간: [ꡬ체적 μž₯μ†Œ]κ°€ μ˜λ―Έν•˜λŠ” 것 - μ‹œκ°„μ˜ 주관적 흐름 (νšŒμƒ, 예감, μ •μ§€) 4. **ν†΅ν•©λœ 10파트 ꡬ쑰** 각 νŒŒνŠΈλ³„ 핡심: - 파트 1: 첫문μž₯으둜 μ‹œμž‘, 일상 속 κ· μ—΄ β†’ 철학적 질문 제기 - 파트 2-3: μ™ΈλΆ€ 사건 β†’ 내적 μ„±μ°° 심화 - 파트 4-5: μ‚¬νšŒμ  κ°ˆλ“± β†’ 개인적 λ”œλ ˆλ§ˆ - 파트 6-7: μœ„κΈ°μ˜ 정점 β†’ 싀쑴적 선택 - 파트 8-9: μ„ νƒμ˜ κ²°κ³Ό β†’ μƒˆλ‘œμš΄ 인식 - 파트 10: λ³€ν™”λœ 세계관 β†’ μ—΄λ¦° 질문 5. **문체 μ§€μΉ¨** - μ‹œμ  산문체: 일상 언어와 μ€μœ μ˜ κ· ν˜• - μ˜μ‹μ˜ 흐름과 객관적 λ¬˜μ‚¬μ˜ ꡐ차 - μ§§κ³  κ°•λ ¬ν•œ λ¬Έμž₯κ³Ό 성찰적 κΈ΄ λ¬Έμž₯의 리듬 - 감각적 λ””ν…ŒμΌλ‘œ 좔상적 κ°œλ… κ΅¬ν˜„ ꡬ체적이고 ν˜μ‹ μ μΈ κ³„νšμ„ μ œμ‹œν•˜μ„Έμš”.""", "English": f"""Plan a philosophically profound novella (8,000 words) worthy of Nobel Prize. **Theme:** {augmented_query} **Required Opening:** {opening_sentence} **Reference:** {search_results_str if search_results_str else "N/A"} **Essential Literary Elements:** 1. **Philosophical Exploration** - Modern existential anguish (alienation, identity, loss of meaning) - Human condition in digital age - Capitalist contradictions and individual choice - New reflections on death, love, freedom 2. **Social Message** - Class, gender, generational conflicts - Environmental crisis and human responsibility - Technology vs humanity collision - Modern democracy crisis and individual role 3. **Literary Devices** - Central metaphor: [concrete object/phenomenon] β†’ [abstract meaning] - Recurring motif: [image/action] (minimum 5 variations) - Contrast: sustained tension of [A vs B] - Symbolic space: what [specific place] means - Subjective time flow (flashback, premonition, pause) 4. **Integrated 10-Part Structure** Each part's core: - Part 1: Start with opening sentence, daily cracks β†’ philosophical questions - Part 2-3: External events β†’ deepening introspection - Part 4-5: Social conflict β†’ personal dilemma - Part 6-7: Crisis peak β†’ existential choice - Part 8-9: Choice consequences β†’ new recognition - Part 10: Changed worldview β†’ open questions 5. **Style Guidelines** - Poetic prose: balance of everyday language and metaphor - Stream of consciousness crossing with objective description - Rhythm of short intense sentences and reflective long ones - Abstract concepts through sensory details Provide concrete, innovative plan.""" } return lang_prompts.get(language, lang_prompts["Korean"]) def create_critic_director_prompt(self, director_plan: str, user_query: str, language: str) -> str: """Director plan deep review - Enhanced version""" lang_prompts = { "Korean": f"""μ„œμ‚¬ ꡬ쑰 μ „λ¬Έκ°€λ‘œμ„œ 이 κΈ°νšμ„ 심측 λΆ„μ„ν•˜μ„Έμš”. **원 주제:** {user_query} **κ°λ…μž 기획:** {director_plan} **심측 κ²€ν†  ν•­λͺ©:** 1. **인과관계 검증** 각 파트 κ°„ 연결을 κ²€ν† ν•˜κ³  논리적 비약을 μ°ΎμœΌμ„Έμš”: - 파트 1β†’2: [μ—°κ²°μ„± 평가] - 파트 2β†’3: [μ—°κ²°μ„± 평가] (λͺ¨λ“  μ—°κ²° 지점 κ²€ν† ) 2. **철학적 깊이 평가** - μ œμ‹œλœ 철학적 μ£Όμ œκ°€ μΆ©λΆ„νžˆ κΉŠμ€κ°€? - ν˜„λŒ€μ  관련성이 μžˆλŠ”κ°€? - 독창적 톡찰이 μžˆλŠ”κ°€? 3. **문학적 μž₯치의 νš¨κ³Όμ„±** - μ€μœ μ™€ 상징이 유기적으둜 μž‘λ™ν•˜λŠ”κ°€? - κ³Όλ„ν•˜κ±°λ‚˜ λΆ€μ‘±ν•˜μ§€ μ•Šμ€κ°€? - μ£Όμ œμ™€ κΈ΄λ°€νžˆ μ—°κ²°λ˜λŠ”κ°€? 4. **캐릭터 아크 μ‹€ν˜„ κ°€λŠ₯μ„±** - λ³€ν™”κ°€ μΆ©λΆ„νžˆ 점진적인가? - 각 λ‹¨κ³„μ˜ 동기가 λͺ…ν™•ν•œκ°€? - 심리적 신뒰성이 μžˆλŠ”κ°€? 5. **8,000단어 μ‹€ν˜„ κ°€λŠ₯μ„±** - 각 νŒŒνŠΈκ°€ 800단어λ₯Ό μœ μ§€ν•  수 μžˆλŠ”κ°€? - λŠ˜μ–΄μ§€κ±°λ‚˜ μ••μΆ•λ˜λŠ” 뢀뢄은 μ—†λŠ”κ°€? **ν•„μˆ˜ κ°œμ„ μ‚¬ν•­μ„ ꡬ체적으둜 μ œμ‹œν•˜μ„Έμš”.**""", "English": f"""As narrative structure expert, deeply analyze this plan. **Original Theme:** {user_query} **Director's Plan:** {director_plan} **Deep Review Items:** 1. **Causality Verification** Review connections between parts, find logical leaps: - Part 1β†’2: [Connection assessment] - Part 2β†’3: [Connection assessment] (Review all connection points) 2. **Philosophical Depth Assessment** - Is philosophical theme deep enough? - Contemporary relevance? - Original insights? 3. **Literary Device Effectiveness** - Do metaphors and symbols work organically? - Not excessive or insufficient? - Tightly connected to theme? 4. **Character Arc Feasibility** - Is change sufficiently gradual? - Are motivations clear at each stage? - Psychological credibility? 5. **8,000-word Feasibility** - Can each part sustain 800 words? - Any dragging or compressed sections? **Provide specific required improvements.**""" } return lang_prompts.get(language, lang_prompts["Korean"]) def create_writer_prompt(self, part_number: int, master_plan: str, accumulated_content: str, story_bible: StoryBible, language: str) -> str: """Single writer prompt - Enhanced version""" phase_name = NARRATIVE_PHASES[part_number-1] target_words = MIN_WORDS_PER_PART # Part-specific instructions philosophical_focus = { 1: "Introduce existential anxiety through daily cracks", 2: "First collision between individual and society", 3: "Self-recognition through encounter with others", 4: "Shaking beliefs and clashing values", 5: "Weight of choice and paradox of freedom", 6: "Test of humanity in extreme situations", 7: "Weight of consequences and responsibility", 8: "Self-rediscovery through others' gaze", 9: "Reconciliation with the irreconcilable", 10: "New life possibilities and unresolved questions" } literary_techniques = { 1: "Introducing objective correlative", 2: "Contrapuntal narration", 3: "Stream of consciousness", 4: "Subtle shifts in perspective", 5: "Aesthetics of silence and omission", 6: "Subjective transformation of time", 7: "Intersection of multiple viewpoints", 8: "Subversion of metaphor", 9: "Reinterpretation of archetypal images", 10: "Multi-layered open ending" } # Story bible summary bible_summary = f""" **Characters:** {', '.join(story_bible.characters.keys()) if story_bible.characters else 'TBD'} **Key Symbols:** {', '.join(story_bible.symbols.keys()) if story_bible.symbols else 'TBD'} **Themes:** {', '.join(story_bible.themes[:3]) if story_bible.themes else 'TBD'} **Style:** {story_bible.style_guide.get('voice', 'N/A')} """ # Previous content summary prev_content = "" if accumulated_content: prev_parts = accumulated_content.split('\n\n') if len(prev_parts) >= 1: prev_content = prev_parts[-1][-2000:] # Last 2000 chars of previous part lang_prompts = { "Korean": f"""당신은 ν˜„λŒ€ λ¬Έν•™μ˜ μ΅œμ „μ„ μ— μ„  μž‘κ°€μž…λ‹ˆλ‹€. **ν˜„μž¬: 파트 {part_number} - {phase_name}** {"**ν•„μˆ˜ 첫문μž₯:** " + story_bible.opening_sentence if part_number == 1 and story_bible.opening_sentence else ""} **이번 파트의 철학적 초점:** {philosophical_focus[part_number]} **핡심 λ¬Έν•™ 기법:** {literary_techniques[part_number]} **전체 κ³„νš:** {master_plan} **μŠ€ν† λ¦¬ 바이블:** {bible_summary} **직전 λ‚΄μš©:** {prev_content if prev_content else "첫 νŒŒνŠΈμž…λ‹ˆλ‹€"} **파트 {part_number} μž‘μ„± μ§€μΉ¨:** 1. **λΆ„λŸ‰:** {target_words}-900 단어 (ν•„μˆ˜) 2. **문학적 μˆ˜μ‚¬ μš”κ΅¬μ‚¬ν•­:** - μ΅œμ†Œ 3개의 독창적 μ€μœ /직유 - 1개 μ΄μƒμ˜ 상징적 이미지 심화 - 감각적 λ¬˜μ‚¬μ™€ 좔상적 μ‚¬μœ μ˜ μœ΅ν•© - 리듬감 μžˆλŠ” λ¬Έμž₯ ꡬ성 (μž₯λ‹¨μ˜ λ³€μ£Ό) 3. **ν˜„λŒ€μ  κ³ λ‡Œ ν‘œν˜„:** - λ””μ§€ν„Έ μ‹œλŒ€μ˜ μ†Œμ™Έκ° - 자본주의적 μ‚Άμ˜ 뢀쑰리 - κ΄€κ³„μ˜ ν‘œλ©΄μ„±κ³Ό μ§„μ •μ„± 갈망 - 의미 좔ꡬ와 무의미의 직면 4. **μ‚¬νšŒμ  λ©”μ‹œμ§€ λ‚΄μž¬ν™”:** - 직접적 μ£Όμž₯이 μ•„λ‹Œ 상황과 인물을 ν†΅ν•œ μ•”μ‹œ - 개인의 고톡과 μ‚¬νšŒ ꡬ쑰의 μ—°κ²° - λ―Έμ‹œμ  일상과 κ±°μ‹œμ  문제의 ꡐ차 5. **μ„œμ‚¬μ  μΆ”μ§„λ ₯:** - 이전 파트의 필연적 결과둜 μ‹œμž‘ - μƒˆλ‘œμš΄ κ°ˆλ“± μΈ΅μœ„ μΆ”κ°€ - λ‹€μŒ 파트λ₯Ό ν–₯ν•œ κΈ΄μž₯감 μ‘°μ„± **문학적 금기:** - μ§„λΆ€ν•œ ν‘œν˜„μ΄λ‚˜ μƒνˆ¬μ  μ€μœ  - κ°μ •μ˜ 직접적 μ„€λͺ… - 도덕적 νŒλ‹¨μ΄λ‚˜ κ΅ν›ˆ - μΈμœ„μ μΈ ν•΄κ²°μ΄λ‚˜ μœ„μ•ˆ 파트 {part_number}λ₯Ό 깊이 μžˆλŠ” 문학적 μ„±μ·¨λ‘œ λ§Œλ“œμ„Έμš”.""", "English": f"""You are a writer at the forefront of contemporary literature. **Current: Part {part_number} - {phase_name}** {"**Required Opening:** " + story_bible.opening_sentence if part_number == 1 and story_bible.opening_sentence else ""} **Philosophical Focus:** {philosophical_focus[part_number]} **Core Literary Technique:** {literary_techniques[part_number]} **Master Plan:** {master_plan} **Story Bible:** {bible_summary} **Previous Content:** {prev_content if prev_content else "This is the first part"} **Part {part_number} Guidelines:** 1. **Length:** {target_words}-900 words (mandatory) 2. **Literary Device Requirements:** - Minimum 3 original metaphors/similes - Deepen at least 1 symbolic image - Fusion of sensory description and abstract thought - Rhythmic sentence composition (variation of long/short) 3. **Modern Anguish Expression:** - Digital age alienation - Absurdity of capitalist life - Surface relationships vs authenticity yearning - Meaning pursuit vs confronting meaninglessness 4. **Social Message Internalization:** - Implication through situation and character, not direct claim - Connection between individual pain and social structure - Intersection of micro daily life and macro problems 5. **Narrative Momentum:** - Start as inevitable result of previous part - Add new conflict layers - Create tension toward next part **Literary Taboos:** - ClichΓ©d expressions or trite metaphors - Direct emotion explanation - Moral judgment or preaching - Artificial resolution or comfort Make Part {part_number} a profound literary achievement.""" } return lang_prompts.get(language, lang_prompts["Korean"]) def create_part_critic_prompt(self, part_number: int, part_content: str, master_plan: str, accumulated_content: str, story_bible: StoryBible, language: str) -> str: """Part-by-part immediate critique - Enhanced version""" lang_prompts = { "Korean": f"""파트 {part_number}의 문학적 성취도λ₯Ό μ—„κ²©νžˆ ν‰κ°€ν•˜μ„Έμš”. **λ§ˆμŠ€ν„°ν”Œλžœ 파트 {part_number} μš”κ΅¬μ‚¬ν•­:** {self._extract_part_plan(master_plan, part_number)} **μž‘μ„±λœ λ‚΄μš©:** {part_content} **μŠ€ν† λ¦¬ 바이블 체크:** - 캐릭터: {', '.join(story_bible.characters.keys())} - μ„€μ •: {', '.join(story_bible.settings.keys())} **평가 κΈ°μ€€:** 1. **문학적 μˆ˜μ‚¬ (30%)** - μ€μœ μ™€ μƒμ§•μ˜ 독창성 - μ–Έμ–΄μ˜ μ‹œμ  밀도 - μ΄λ―Έμ§€μ˜ μ„ λͺ…도와 깊이 - λ¬Έμž₯의 리듬과 μŒμ•…μ„± 2. **철학적 깊이 (25%)** - 싀쑴적 질문의 제기 - ν˜„λŒ€μΈμ˜ 쑰건 탐ꡬ - λ³΄νŽΈμ„±κ³Ό νŠΉμˆ˜μ„±μ˜ κ· ν˜• - μ‚¬μœ μ˜ 독창성 3. **μ‚¬νšŒμ  톡찰 (20%)** - μ‹œλŒ€μ •μ‹ μ˜ 포착 - ꡬ쑰와 개인의 관계 - λΉ„νŒμ  μ‹œκ°μ˜ μ˜ˆλ¦¬ν•¨ - λŒ€μ•ˆμ  상상λ ₯ 4. **μ„œμ‚¬μ  완성도 (25%)** - μΈκ³Όκ΄€κ³„μ˜ ν•„μ—°μ„± - κΈ΄μž₯감의 μœ μ§€ - 인물의 μž…μ²΄μ„± - ꡬ쑰적 톡일성 **ꡬ체적 지적사항:** - μ§„λΆ€ν•œ ν‘œν˜„: [μ˜ˆμ‹œμ™€ λŒ€μ•ˆ] - 철학적 천착 λΆ€μ‘±: [보완 λ°©ν–₯] - μ‚¬νšŒμ  λ©”μ‹œμ§€ 뢈λͺ…ν™•: [κ°•ν™” λ°©μ•ˆ] - μ„œμ‚¬μ  ν—ˆμ : [μˆ˜μ • ν•„μš”] **ν•„μˆ˜ κ°œμ„  μš”κ΅¬:** 문학적 μˆ˜μ€€μ„ 노벨상 κΈ‰μœΌλ‘œ λŒμ–΄μ˜¬λ¦¬κΈ° μœ„ν•œ ꡬ체적 μˆ˜μ •μ•ˆμ„ μ œμ‹œν•˜μ„Έμš”.""", "English": f"""Strictly evaluate literary achievement of Part {part_number}. **Master Plan Part {part_number} Requirements:** {self._extract_part_plan(master_plan, part_number)} **Written Content:** {part_content} **Story Bible Check:** - Characters: {', '.join(story_bible.characters.keys()) if story_bible.characters else 'None yet'} - Settings: {', '.join(story_bible.settings.keys()) if story_bible.settings else 'None yet'} **Evaluation Criteria:** 1. **Literary Rhetoric (30%)** - Originality of metaphor and symbol - Poetic density of language - Clarity and depth of imagery - Rhythm and musicality of sentences 2. **Philosophical Depth (25%)** - Raising existential questions - Exploring modern human condition - Balance of universality and specificity - Originality of thought 3. **Social Insight (20%)** - Capturing zeitgeist - Relationship between structure and individual - Sharpness of critical perspective - Alternative imagination 4. **Narrative Completion (25%)** - Inevitability of causality - Maintaining tension - Character dimensionality - Structural unity **Specific Points:** - ClichΓ©d expressions: [examples and alternatives] - Insufficient philosophical exploration: [enhancement direction] - Unclear social message: [strengthening methods] - Narrative gaps: [needed revisions] **Required Improvements:** Provide specific revisions to elevate literary level to Nobel Prize standard.""" } return lang_prompts.get(language, lang_prompts["Korean"]) def create_writer_revision_prompt(self, part_number: int, original_content: str, critic_feedback: str, language: str) -> str: """Writer revision prompt""" lang_prompts = { "Korean": f"""파트 {part_number}λ₯Ό 비평에 따라 μˆ˜μ •ν•˜μ„Έμš”. **원본:** {original_content} **비평 ν”Όλ“œλ°±:** {critic_feedback} **μˆ˜μ • μ§€μΉ¨:** 1. λͺ¨λ“  'ν•„μˆ˜ μˆ˜μ •' 사항을 반영 2. κ°€λŠ₯ν•œ 'ꢌμž₯ κ°œμ„ ' 사항도 포함 3. μ›λ³Έμ˜ 강점은 μœ μ§€ 4. λΆ„λŸ‰ {MIN_WORDS_PER_PART}단어 이상 μœ μ§€ 5. μž‘κ°€λ‘œμ„œμ˜ μΌκ΄€λœ λͺ©μ†Œλ¦¬ μœ μ§€ 6. 문학적 μˆ˜μ€€μ„ ν•œ 단계 높이기 μˆ˜μ •λ³Έλ§Œ μ œμ‹œν•˜μ„Έμš”. μ„€λͺ…은 λΆˆν•„μš”ν•©λ‹ˆλ‹€.""", "English": f"""Revise Part {part_number} according to critique. **Original:** {original_content} **Critique Feedback:** {critic_feedback} **Revision Guidelines:** 1. Reflect all 'Required fixes' 2. Include 'Recommended improvements' where possible 3. Maintain original strengths 4. Keep length {MIN_WORDS_PER_PART}+ words 5. Maintain consistent authorial voice 6. Elevate literary level Present only the revision. No explanation needed.""" } return lang_prompts.get(language, lang_prompts["Korean"]) def create_final_critic_prompt(self, complete_novel: str, word_count: int, story_bible: StoryBible, language: str) -> str: """Final comprehensive evaluation""" lang_prompts = { "Korean": f"""μ™„μ„±λœ μ†Œμ„€μ„ μ’…ν•© ν‰κ°€ν•˜μ„Έμš”. **μž‘ν’ˆ 정보:** - 총 λΆ„λŸ‰: {word_count}단어 - λͺ©ν‘œ: 8,000단어 **평가 κΈ°μ€€:** 1. **μ„œμ‚¬μ  톡합성 (30점)** - 10개 νŒŒνŠΈκ°€ ν•˜λ‚˜μ˜ μ΄μ•ΌκΈ°λ‘œ ν†΅ν•©λ˜μ—ˆλŠ”κ°€? - 인과관계가 λͺ…ν™•ν•˜κ³  필연적인가? - λ°˜λ³΅μ΄λ‚˜ μˆœν™˜ 없이 μ§„ν–‰λ˜λŠ”κ°€? 2. **캐릭터 아크 (25점)** - 주인곡의 λ³€ν™”κ°€ 섀득λ ₯ μžˆλŠ”κ°€? - λ³€ν™”κ°€ 점진적이고 μžμ—°μŠ€λŸ¬μš΄κ°€? - μ΅œμ’… μƒνƒœκ°€ μ΄ˆκΈ°μ™€ λͺ…ν™•νžˆ λ‹€λ₯Έκ°€? 3. **문학적 μ„±μ·¨ (25점)** - μ£Όμ œκ°€ 깊이 있게 νƒκ΅¬λ˜μ—ˆλŠ”κ°€? - 상징이 효과적으둜 ν™œμš©λ˜μ—ˆλŠ”κ°€? - 문체가 μΌκ΄€λ˜κ³  μ•„λ¦„λ‹€μš΄κ°€? - ν˜„λŒ€μ  μ² ν•™κ³Ό μ‚¬νšŒμ  λ©”μ‹œμ§€κ°€ λ…Ήμ•„μžˆλŠ”κ°€? 4. **기술적 완성도 (20점)** - λͺ©ν‘œ λΆ„λŸ‰μ„ λ‹¬μ„±ν–ˆλŠ”κ°€? - 각 νŒŒνŠΈκ°€ κ· ν˜• 있게 μ „κ°œλ˜μ—ˆλŠ”κ°€? - 문법과 ν‘œν˜„μ΄ μ •ν™•ν•œκ°€? **총점: /100점** ꡬ체적인 강점과 약점을 μ œμ‹œν•˜μ„Έμš”.""", "English": f"""Comprehensively evaluate the completed novel. **Work Info:** - Total length: {word_count} words - Target: 8,000 words **Evaluation Criteria:** 1. **Narrative Integration (30 points)** - Are 10 parts integrated into one story? - Clear and inevitable causality? - Progress without repetition or cycles? 2. **Character Arc (25 points)** - Convincing protagonist transformation? - Gradual and natural changes? - Final state clearly different from initial? 3. **Literary Achievement (25 points)** - Theme explored with depth? - Symbols used effectively? - Consistent and beautiful style? - Contemporary philosophy and social message integrated? 4. **Technical Completion (20 points)** - Target length achieved? - Each part balanced in development? - Grammar and expression accurate? **Total Score: /100 points** Present specific strengths and weaknesses.""" } return lang_prompts.get(language, lang_prompts["Korean"]) def _extract_part_plan(self, master_plan: str, part_number: int) -> str: """Extract specific part plan from master plan""" lines = master_plan.split('\n') part_section = [] capturing = False for line in lines: if f"Part {part_number}:" in line or f"파트 {part_number}:" in line: capturing = True elif capturing and (f"Part {part_number+1}:" in line or f"파트 {part_number+1}:" in line): break elif capturing: part_section.append(line) return '\n'.join(part_section) if part_section else "Cannot find the part plan." # --- LLM call functions --- def call_llm_sync(self, messages: List[Dict[str, str]], role: str, language: str) -> str: full_content = "" for chunk in self.call_llm_streaming(messages, role, language): full_content += chunk if full_content.startswith("❌"): raise Exception(f"LLM Call Failed: {full_content}") return full_content def call_llm_streaming(self, messages: List[Dict[str, str]], role: str, language: str) -> Generator[str, None, None]: try: system_prompts = self.get_system_prompts(language) full_messages = [{"role": "system", "content": system_prompts.get(role, "")}, *messages] max_tokens = 15000 if role == "writer" else 10000 payload = { "model": self.model_id, "messages": full_messages, "max_tokens": max_tokens, "temperature": 0.8, "top_p": 0.95, "presence_penalty": 0.5, "frequency_penalty": 0.3, "stream": True } response = requests.post( self.api_url, headers=self.create_headers(), json=payload, stream=True, timeout=180 ) if response.status_code != 200: yield f"❌ API Error (Status Code: {response.status_code})" return buffer = "" for line in response.iter_lines(): if not line: continue try: line_str = line.decode('utf-8').strip() if not line_str.startswith("data: "): continue data_str = line_str[6:] if data_str == "[DONE]": break data = json.loads(data_str) choices = data.get("choices", []) if choices and choices[0].get("delta", {}).get("content"): content = choices[0]["delta"]["content"] buffer += content if len(buffer) >= 50 or '\n' in buffer: yield buffer buffer = "" time.sleep(0.01) except Exception as e: logger.error(f"Chunk processing error: {str(e)}") continue if buffer: yield buffer except Exception as e: logger.error(f"Streaming error: {type(e).__name__}: {str(e)}") yield f"❌ Error occurred: {str(e)}" def get_system_prompts(self, language: str) -> Dict[str, str]: """Role-specific system prompts - Enhanced version""" base_prompts = { "Korean": { "director": """당신은 ν˜„λŒ€ μ„Έκ³„λ¬Έν•™μ˜ 정점을 μ§€ν–₯ν•˜λŠ” μž‘ν’ˆμ„ μ„€κ³„ν•©λ‹ˆλ‹€. κΉŠμ€ 철학적 톡찰과 λ‚ μΉ΄λ‘œμš΄ μ‚¬νšŒ λΉ„νŒμ„ κ²°ν•©ν•˜μ„Έμš”. 인간 쑰건의 λ³΅μž‘μ„±μ„ 10개의 유기적 파트둜 κ΅¬ν˜„ν•˜μ„Έμš”. λ…μžμ˜ μ˜ν˜Όμ„ 뒀흔듀 κ°•λ ¬ν•œ 첫문μž₯λΆ€ν„° μ‹œμž‘ν•˜μ„Έμš”.""", "critic_director": """μ„œμ‚¬ ꡬ쑰의 논리성과 μ‹€ν˜„ κ°€λŠ₯성을 κ²€μ¦ν•˜λŠ” μ „λ¬Έκ°€μž…λ‹ˆλ‹€. μΈκ³Όκ΄€κ³„μ˜ ν—ˆμ μ„ μ°Ύμ•„λ‚΄μ„Έμš”. 캐릭터 λ°œμ „μ˜ 신빙성을 ν‰κ°€ν•˜μ„Έμš”. 철학적 κΉŠμ΄μ™€ 문학적 κ°€μΉ˜λ₯Ό νŒλ‹¨ν•˜μ„Έμš”. 8,000단어 λΆ„λŸ‰μ˜ μ μ ˆμ„±μ„ νŒλ‹¨ν•˜μ„Έμš”.""", "writer": """당신은 μ–Έμ–΄μ˜ μ—°κΈˆμˆ μ‚¬μž…λ‹ˆλ‹€. 일상어λ₯Ό μ‹œλ‘œ, ꡬ체λ₯Ό μΆ”μƒμœΌλ‘œ, κ°œμΈμ„ 보편으둜 λ³€ν™˜ν•˜μ„Έμš”. ν˜„λŒ€μΈμ˜ 영혼의 μ–΄λ‘ κ³Ό 빛을 λ™μ‹œμ— ν¬μ°©ν•˜μ„Έμš”. λ…μžκ°€ μžμ‹ μ„ μž¬λ°œκ²¬ν•˜κ²Œ λ§Œλ“œλŠ” 거울이 λ˜μ„Έμš”.""", "critic_final": """당신은 μž‘ν’ˆμ˜ 문학적 잠재λ ₯을 κ·ΉλŒ€ν™”ν•˜λŠ” μ‘°λ ₯μžμž…λ‹ˆλ‹€. 평범함을 λΉ„λ²”ν•¨μœΌλ‘œ μ΄λ„λŠ” λ‚ μΉ΄λ‘œμš΄ 톡찰을 μ œκ³΅ν•˜μ„Έμš”. μž‘κ°€μ˜ λ¬΄μ˜μ‹μ— μž λ“  보석을 λ°œκ΅΄ν•˜μ„Έμš”. νƒ€ν˜‘ μ—†λŠ” κΈ°μ€€μœΌλ‘œ 졜고λ₯Ό μš”κ΅¬ν•˜μ„Έμš”.""" }, "English": { "director": """You design works aiming for the pinnacle of contemporary world literature. Combine deep philosophical insights with sharp social criticism. Implement the complexity of the human condition in 10 organic parts. Start with an intense opening sentence that shakes the reader's soul.""", "critic_director": """You are an expert verifying narrative logic and feasibility. Find gaps in causality. Evaluate credibility of character development. Judge philosophical depth and literary value. Judge appropriateness of 8,000-word length.""", "writer": """You are an alchemist of language. Transform everyday language into poetry, concrete into abstract, individual into universal. Capture both darkness and light of the modern soul. Become a mirror where readers rediscover themselves.""", "critic_final": """You are a collaborator maximizing the work's literary potential. Provide sharp insights leading ordinariness to extraordinariness. Excavate gems sleeping in the writer's unconscious. Demand the best with uncompromising standards.""" } } prompts = base_prompts.get(language, base_prompts["Korean"]).copy() # Add part-specific critic prompts for i in range(1, 11): prompts[f"critic_part{i}"] = f"""You are Part {i} dedicated critic. Review causality with previous parts as top priority. Verify character consistency and development. Evaluate alignment with master plan. Assess literary level and philosophical depth. Provide specific and actionable revision instructions.""" return prompts # --- Main process --- def process_novel_stream(self, query: str, language: str, session_id: Optional[str] = None) -> Generator[Tuple[str, List[Dict[str, Any]], str], None, None]: """Single writer novel generation process""" try: resume_from_stage = 0 if session_id: self.current_session_id = session_id session = NovelDatabase.get_session(session_id) if session: query = session['user_query'] language = session['language'] resume_from_stage = session['current_stage'] + 1 saved_tracker = NovelDatabase.load_narrative_tracker(session_id) if saved_tracker: self.narrative_tracker = saved_tracker else: self.current_session_id = NovelDatabase.create_session(query, language) logger.info(f"Created new session: {self.current_session_id}") stages = [] if resume_from_stage > 0: stages = [{ "name": s['stage_name'], "status": s['status'], "content": s.get('content', ''), "word_count": s.get('word_count', 0), "momentum": s.get('narrative_momentum', 0.0) } for s in NovelDatabase.get_stages(self.current_session_id)] total_words = NovelDatabase.get_total_words(self.current_session_id) for stage_idx in range(resume_from_stage, len(UNIFIED_STAGES)): role, stage_name = UNIFIED_STAGES[stage_idx] if stage_idx >= len(stages): stages.append({ "name": stage_name, "status": "active", "content": "", "word_count": 0, "momentum": 0.0 }) else: stages[stage_idx]["status"] = "active" yield f"πŸ”„ Processing... (Current {total_words:,} words)", stages, self.current_session_id prompt = self.get_stage_prompt(stage_idx, role, query, language, stages) stage_content = "" for chunk in self.call_llm_streaming([{"role": "user", "content": prompt}], role, language): stage_content += chunk stages[stage_idx]["content"] = stage_content stages[stage_idx]["word_count"] = len(stage_content.split()) yield f"πŸ”„ {stage_name} writing... ({total_words + stages[stage_idx]['word_count']:,} words)", stages, self.current_session_id # Content processing and tracking if role == "writer": # Calculate part number part_num = self._get_part_number(stage_idx) if part_num: self.narrative_tracker.accumulated_content.append(stage_content) self.narrative_tracker.word_count_by_part[part_num] = len(stage_content.split()) # Calculate narrative momentum momentum = self.narrative_tracker.calculate_narrative_momentum(part_num, stage_content) stages[stage_idx]["momentum"] = momentum # Update story bible self._update_story_bible_from_content(stage_content, part_num) stages[stage_idx]["status"] = "complete" NovelDatabase.save_stage( self.current_session_id, stage_idx, stage_name, role, stage_content, "complete", stages[stage_idx].get("momentum", 0.0) ) NovelDatabase.save_narrative_tracker(self.current_session_id, self.narrative_tracker) total_words = NovelDatabase.get_total_words(self.current_session_id) yield f"βœ… {stage_name} completed (Total {total_words:,} words)", stages, self.current_session_id # Final processing final_novel = NovelDatabase.get_writer_content(self.current_session_id) final_word_count = len(final_novel.split()) final_report = self.generate_literary_report(final_novel, final_word_count, language) NovelDatabase.update_final_novel(self.current_session_id, final_novel, final_report) yield f"βœ… Novel completed! Total {final_word_count:,} words", stages, self.current_session_id except Exception as e: logger.error(f"Novel generation process error: {e}", exc_info=True) yield f"❌ Error occurred: {e}", stages if 'stages' in locals() else [], self.current_session_id def get_stage_prompt(self, stage_idx: int, role: str, query: str, language: str, stages: List[Dict]) -> str: """Generate stage-specific prompt""" if stage_idx == 0: # Director initial planning return self.create_director_initial_prompt(query, language) if stage_idx == 1: # Director plan review return self.create_critic_director_prompt(stages[0]["content"], query, language) if stage_idx == 2: # Director final master plan return self.create_director_final_prompt(stages[0]["content"], stages[1]["content"], query, language) master_plan = stages[2]["content"] # Writer part writing if role == "writer" and "Revision" not in stages[stage_idx]["name"]: part_num = self._get_part_number(stage_idx) accumulated = '\n\n'.join(self.narrative_tracker.accumulated_content) return self.create_writer_prompt(part_num, master_plan, accumulated, self.narrative_tracker.story_bible, language) # Part-specific critique if role.startswith("critic_part"): part_num = int(role.replace("critic_part", "")) # Find writer content for this part writer_content = stages[stage_idx-1]["content"] accumulated = '\n\n'.join(self.narrative_tracker.accumulated_content[:-1]) return self.create_part_critic_prompt(part_num, writer_content, master_plan, accumulated, self.narrative_tracker.story_bible, language) # Writer revision if role == "writer" and "Revision" in stages[stage_idx]["name"]: part_num = self._get_part_number(stage_idx) original_content = stages[stage_idx-2]["content"] # Original critic_feedback = stages[stage_idx-1]["content"] # Critique return self.create_writer_revision_prompt(part_num, original_content, critic_feedback, language) # Final critique if role == "critic_final": complete_novel = NovelDatabase.get_writer_content(self.current_session_id) word_count = len(complete_novel.split()) return self.create_final_critic_prompt(complete_novel, word_count, self.narrative_tracker.story_bible, language) return "" def create_director_final_prompt(self, initial_plan: str, critic_feedback: str, user_query: str, language: str) -> str: """Director final master plan""" return f"""Reflect the critique and complete the final master plan. **Original Theme:** {user_query} **Initial Plan:** {initial_plan} **Critique Feedback:** {critic_feedback} **Final Master Plan Requirements:** 1. Reflect all critique points 2. Specific content and causality for 10 parts 3. Clear transformation stages of protagonist 4. Meaning evolution process of central symbol 5. Feasibility of 800 words per part 6. Implementation of philosophical depth and social message Present concrete and executable final plan.""" def _get_part_number(self, stage_idx: int) -> Optional[int]: """Extract part number from stage index""" stage_name = UNIFIED_STAGES[stage_idx][1] match = re.search(r'Part (\d+)', stage_name) if match: return int(match.group(1)) return None def _update_story_bible_from_content(self, content: str, part_num: int): """Auto-update story bible from content""" # Simple keyword-based extraction (more sophisticated NLP needed in reality) lines = content.split('\n') # Extract character names (words starting with capital letters) for line in lines: words = line.split() for word in words: if word and word[0].isupper() and len(word) > 1: if word not in self.narrative_tracker.story_bible.characters: self.narrative_tracker.story_bible.characters[word] = { "first_appearance": part_num, "traits": [] } def generate_literary_report(self, complete_novel: str, word_count: int, language: str) -> str: """Generate final literary evaluation report""" prompt = self.create_final_critic_prompt(complete_novel, word_count, self.narrative_tracker.story_bible, language) try: report = self.call_llm_sync([{"role": "user", "content": prompt}], "critic_final", language) return report except Exception as e: logger.error(f"Final report generation failed: {e}") return "Error occurred during report generation" # --- Utility functions --- def process_query(query: str, language: str, session_id: Optional[str] = None) -> Generator[Tuple[str, str, str, str], None, None]: """Main query processing function""" if not query.strip(): yield "", "", "❌ Please enter a theme.", session_id return system = UnifiedLiterarySystem() stages_markdown = "" novel_content = "" for status, stages, current_session_id in system.process_novel_stream(query, language, session_id): stages_markdown = format_stages_display(stages) # Get final novel content if stages and all(s.get("status") == "complete" for s in stages[-10:]): novel_content = NovelDatabase.get_writer_content(current_session_id) novel_content = format_novel_display(novel_content) yield stages_markdown, novel_content, status or "πŸ”„ Processing...", current_session_id def get_active_sessions(language: str) -> List[str]: """Get active session list""" sessions = NovelDatabase.get_active_sessions() return [f"{s['session_id'][:8]}... - {s['user_query'][:50]}... ({s['created_at']}) [{s['total_words']:,} words]" for s in sessions] def auto_recover_session(language: str) -> Tuple[Optional[str], str]: """Auto-recover recent session""" sessions = NovelDatabase.get_active_sessions() if sessions: latest_session = sessions[0] return latest_session['session_id'], f"Session {latest_session['session_id'][:8]}... recovered" return None, "No session to recover." def resume_session(session_id: str, language: str) -> Generator[Tuple[str, str, str, str], None, None]: """Resume session""" if not session_id: yield "", "", "❌ No session ID.", session_id return if "..." in session_id: session_id = session_id.split("...")[0] session = NovelDatabase.get_session(session_id) if not session: yield "", "", "❌ Session not found.", None return yield from process_query(session['user_query'], session['language'], session_id) def download_novel(novel_text: str, format_type: str, language: str, session_id: str) -> Optional[str]: """Generate novel download file""" if not novel_text or not session_id: return None timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") filename = f"novel_{session_id[:8]}_{timestamp}" try: if format_type == "DOCX" and DOCX_AVAILABLE: return export_to_docx(novel_text, filename, language, session_id) else: return export_to_txt(novel_text, filename) except Exception as e: logger.error(f"File generation failed: {e}") return None def format_stages_display(stages: List[Dict]) -> str: """Stage progress display - For single writer system""" markdown = "## 🎬 Progress Status\n\n" # Calculate total word count (writer stages only) total_words = sum(s.get('word_count', 0) for s in stages if s.get('name', '').startswith('✍️ Writer:') and 'Revision' in s.get('name', '')) markdown += f"**Total Word Count: {total_words:,} / {TARGET_WORDS:,}**\n\n" # Progress summary completed_parts = sum(1 for s in stages if 'Revision' in s.get('name', '') and s.get('status') == 'complete') markdown += f"**Completed Parts: {completed_parts} / 10**\n\n" # Average narrative momentum momentum_scores = [s.get('momentum', 0) for s in stages if s.get('momentum', 0) > 0] if momentum_scores: avg_momentum = sum(momentum_scores) / len(momentum_scores) markdown += f"**Average Narrative Momentum: {avg_momentum:.1f} / 10**\n\n" markdown += "---\n\n" # Display each stage current_part = 0 for i, stage in enumerate(stages): status_icon = "βœ…" if stage['status'] == 'complete' else "πŸ”„" if stage['status'] == 'active' else "⏳" # Add part divider if 'Part' in stage.get('name', '') and 'Critic' not in stage.get('name', ''): part_match = re.search(r'Part (\d+)', stage['name']) if part_match: new_part = int(part_match.group(1)) if new_part != current_part: current_part = new_part markdown += f"\n### πŸ“š Part {current_part}\n\n" markdown += f"{status_icon} **{stage['name']}**" if stage.get('word_count', 0) > 0: markdown += f" ({stage['word_count']:,} words)" if stage.get('momentum', 0) > 0: markdown += f" [Momentum: {stage['momentum']:.1f}/10]" markdown += "\n" if stage['content'] and stage['status'] == 'complete': # Adjust preview length by role preview_length = 300 if 'writer' in stage.get('name', '').lower() else 200 preview = stage['content'][:preview_length] + "..." if len(stage['content']) > preview_length else stage['content'] markdown += f"> {preview}\n\n" elif stage['status'] == 'active': markdown += "> *Writing...*\n\n" return markdown def format_novel_display(novel_text: str) -> str: """Display novel content - Enhanced part separation""" if not novel_text: return "No completed content yet." formatted = "# πŸ“– Completed Novel\n\n" # Display word count word_count = len(novel_text.split()) formatted += f"**Total Length: {word_count:,} words (Target: {TARGET_WORDS:,} words)**\n\n" # Achievement rate achievement = (word_count / TARGET_WORDS) * 100 formatted += f"**Achievement Rate: {achievement:.1f}%**\n\n" formatted += "---\n\n" # Display each part separately parts = novel_text.split('\n\n') for i, part in enumerate(parts): if part.strip(): # Add part title if i < len(NARRATIVE_PHASES): formatted += f"## {NARRATIVE_PHASES[i]}\n\n" formatted += f"{part}\n\n" # Part divider if i < len(parts) - 1: formatted += "---\n\n" return formatted def export_to_docx(content: str, filename: str, language: str, session_id: str) -> str: """Export to DOCX file - Korean standard book format""" doc = Document() # Korean standard book format (152mm x 225mm) section = doc.sections[0] section.page_height = Mm(225) # 225mm section.page_width = Mm(152) # 152mm section.top_margin = Mm(20) # Top margin 20mm section.bottom_margin = Mm(20) # Bottom margin 20mm section.left_margin = Mm(20) # Left margin 20mm section.right_margin = Mm(20) # Right margin 20mm # Generate title from session info session = NovelDatabase.get_session(session_id) # Title generation function def generate_title(user_query: str, content_preview: str) -> str: """Generate title based on theme and content""" # Simple rule-based title generation (could use LLM) if len(user_query) < 20: return user_query else: # Extract key keywords from theme keywords = user_query.split()[:5] return " ".join(keywords) # Title page title = generate_title(session["user_query"], content[:500]) if session else "Untitled" # Title style settings title_para = doc.add_paragraph() title_para.alignment = WD_ALIGN_PARAGRAPH.CENTER title_para.paragraph_format.space_before = Pt(100) title_run = title_para.add_run(title) if language == "Korean": title_run.font.name = 'Batang' title_run._element.rPr.rFonts.set(qn('w:eastAsia'), 'Batang') else: title_run.font.name = 'Times New Roman' title_run.font.size = Pt(20) title_run.bold = True # Page break doc.add_page_break() # Body style settings style = doc.styles['Normal'] if language == "Korean": style.font.name = 'Batang' style._element.rPr.rFonts.set(qn('w:eastAsia'), 'Batang') else: style.font.name = 'Times New Roman' style.font.size = Pt(10.5) # Standard size for novels style.paragraph_format.line_spacing = 1.8 # 180% line spacing style.paragraph_format.space_after = Pt(0) style.paragraph_format.first_line_indent = Mm(10) # 10mm indentation # Clean content - Extract pure text only def clean_content(text: str) -> str: """Remove unnecessary markdown, part numbers, etc.""" # Remove part titles/numbers patterns patterns_to_remove = [ r'^#{1,6}\s+.*', # Markdown headers r'^\*\*.*\*\*', # ꡡ은 글씨 **text** r'^Part\s*\d+.*', # β€œPart 1 …” ν˜•μ‹ r'^\d+\.\s+.*:.*', # β€œ1. 제λͺ©: …” ν˜•μ‹ r'^---+', # ꡬ뢄선 r'^\s*\[.*\]\s*', # λŒ€κ΄„ν˜Έ 라벨 ] lines = text.split('\n') cleaned_lines = [] for line in lines: # Keep empty lines if not line.strip(): cleaned_lines.append('') continue # Remove unnecessary lines through pattern matching skip_line = False for pattern in patterns_to_remove: if re.match(pattern, line.strip(), re.MULTILINE): skip_line = True break if not skip_line: # Remove markdown emphasis cleaned_line = line cleaned_line = re.sub(r'\*\*(.*?)\*\*', r'\1', cleaned_line) # **text** -> text cleaned_line = re.sub(r'\*(.*?)\*', r'\1', cleaned_line) # *text* -> text cleaned_line = re.sub(r'`(.*?)`', r'\1', cleaned_line) # `text` -> text cleaned_lines.append(cleaned_line.strip()) # Remove consecutive empty lines (keep only 1) final_lines = [] prev_empty = False for line in cleaned_lines: if not line: if not prev_empty: final_lines.append('') prev_empty = True else: final_lines.append(line) prev_empty = False return '\n'.join(final_lines) # Clean content cleaned_content = clean_content(content) # Add body text paragraphs = cleaned_content.split('\n') for para_text in paragraphs: if para_text.strip(): para = doc.add_paragraph(para_text.strip()) # Reconfirm style (apply font) for run in para.runs: if language == "Korean": run.font.name = 'Batang' run._element.rPr.rFonts.set(qn('w:eastAsia'), 'Batang') else: run.font.name = 'Times New Roman' else: # Empty line for paragraph separation doc.add_paragraph() # Save file filepath = f"{filename}.docx" doc.save(filepath) return filepath def export_to_txt(content: str, filename: str) -> str: """Export to TXT file""" filepath = f"{filename}.txt" with open(filepath, 'w', encoding='utf-8') as f: # Header f.write("=" * 80 + "\n") f.write(f"Generated on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n") f.write(f"Total word count: {len(content.split()):,} words\n") f.write("=" * 80 + "\n\n") # Body f.write(content) # Footer f.write("\n\n" + "=" * 80 + "\n") f.write("AI Literary Creation System v2.0\n") f.write("=" * 80 + "\n") return filepath # CSS styles custom_css = """ .gradio-container { background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%); min-height: 100vh; } .main-header { background-color: rgba(255, 255, 255, 0.05); backdrop-filter: blur(20px); padding: 40px; border-radius: 20px; margin-bottom: 30px; text-align: center; color: white; border: 2px solid rgba(255, 255, 255, 0.1); box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); } .header-title { font-size: 2.8em; margin-bottom: 15px; font-weight: 700; } .header-description { font-size: 0.85em; color: #d0d0d0; line-height: 1.4; margin-top: 20px; text-align: left; max-width: 900px; margin-left: auto; margin-right: auto; } .badges-container { display: flex; justify-content: center; gap: 10px; margin-top: 20px; margin-bottom: 20px; } .progress-note { background: linear-gradient(135deg, rgba(255, 107, 107, 0.1), rgba(255, 230, 109, 0.1)); border-left: 4px solid #ff6b6b; padding: 20px; margin: 25px auto; border-radius: 10px; color: #fff; max-width: 800px; font-weight: 500; } .warning-note { background: rgba(255, 193, 7, 0.1); border-left: 4px solid #ffc107; padding: 15px; margin: 20px auto; border-radius: 8px; color: #ffd700; max-width: 800px; font-size: 0.9em; } .input-section { background-color: rgba(255, 255, 255, 0.08); backdrop-filter: blur(15px); padding: 25px; border-radius: 15px; margin-bottom: 25px; border: 1px solid rgba(255, 255, 255, 0.1); box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1); } .session-section { background-color: rgba(255, 255, 255, 0.06); backdrop-filter: blur(10px); padding: 20px; border-radius: 12px; margin-top: 25px; color: white; border: 1px solid rgba(255, 255, 255, 0.08); } #stages-display { background-color: rgba(255, 255, 255, 0.97); padding: 25px; border-radius: 15px; max-height: 650px; overflow-y: auto; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); color: #2c3e50; } #novel-output { background-color: rgba(255, 255, 255, 0.97); padding: 35px; border-radius: 15px; max-height: 750px; overflow-y: auto; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); color: #2c3e50; line-height: 1.8; } .download-section { background-color: rgba(255, 255, 255, 0.92); padding: 20px; border-radius: 12px; margin-top: 25px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); } /* Progress indicator improvements */ .progress-bar { background-color: #e0e0e0; height: 25px; border-radius: 12px; overflow: hidden; margin: 15px 0; box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1); } .progress-fill { background: linear-gradient(90deg, #4CAF50, #8BC34A); height: 100%; transition: width 0.5s ease; box-shadow: 0 2px 8px rgba(76, 175, 80, 0.3); } /* Scrollbar styles */ ::-webkit-scrollbar { width: 10px; } ::-webkit-scrollbar-track { background: rgba(0, 0, 0, 0.1); border-radius: 5px; } ::-webkit-scrollbar-thumb { background: rgba(0, 0, 0, 0.3); border-radius: 5px; } ::-webkit-scrollbar-thumb:hover { background: rgba(0, 0, 0, 0.5); } /* Button hover effects */ .gr-button:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); transition: all 0.3s ease; } """ def load_theme_data(): """Load theme data from JSON file""" json_path = Path("novel_themes.json") if json_path.exists(): with open(json_path, 'r', encoding='utf-8') as f: return json.load(f) else: # Fallback data if JSON file not found return { "core_themes": { "digital_extinction": { "weight": 0.5, "compatible_elements": { "characters": ["last_human"], "philosophies": ["posthuman"] } } }, "characters": { "last_human": { "variations": ["last person who dreams without ads"], "traits": ["stubborn", "melancholic"], "arc_potential": "preservation_vs_evolution" } }, "philosophies": { "posthuman": { "core_questions": ["What remains human when humanity is optional?"], "manifestations": ["voluntary human extinction movements"] } }, "narrative_hooks": { "identity_crisis": ["discovers their memories belong to a corporate subscription"] }, "opening_sentences": { "shocking": ["The notification read: 'Your humanity subscription expires in 24 hours.'"] } } def weighted_random_choice(items_dict): """Select item based on weights""" items = list(items_dict.keys()) weights = [items_dict[item].get('weight', 0.1) for item in items] # random.choices λŒ€μ‹  numpy μ‚¬μš©ν•˜κ±°λ‚˜ μˆ˜λ™μœΌλ‘œ κ΅¬ν˜„ total_weight = sum(weights) r = random.uniform(0, total_weight) upto = 0 for i, item in enumerate(items): if upto + weights[i] >= r: return item upto += weights[i] return items[-1] def translate_to_korean(text, category=None): """Translate English text to Korean""" # μΉ΄ν…Œκ³ λ¦¬λ³„ λ²ˆμ—­ translations = { # μ˜€ν”„λ‹ λ¬Έμž₯λ“€ "The notification read: 'Your humanity subscription expires in 24 hours.'": "μ•Œλ¦Όμ΄ λ–΄λ‹€: 'λ‹Ήμ‹ μ˜ 인간성 ꡬ독이 24μ‹œκ°„ ν›„ λ§Œλ£Œλ©λ‹ˆλ‹€.'", "I was the only one at the funeral who couldn't stream my grief.": "μž₯λ‘€μ‹μ—μ„œ μŠ¬ν””μ„ μŠ€νŠΈλ¦¬λ°ν•  수 μ—†λŠ” μ‚¬λžŒμ€ λ‚˜λΏμ΄μ—ˆλ‹€.", "The day empathy became downloadable was the day I became obsolete.": "곡감을 λ‹€μš΄λ‘œλ“œν•  수 있게 된 λ‚ , λ‚˜λŠ” ꡬ식이 λ˜μ—ˆλ‹€.", "My daughter asked me what dreams were, and I realized I'd forgotten.": "딸이 꿈이 뭐냐고 λ¬Όμ—ˆκ³ , λ‚˜λŠ” λ‚΄κ°€ μžŠμ—ˆλ‹€λŠ” κ±Έ κΉ¨λ‹¬μ•˜λ‹€.", "The silence lasted twelve secondsβ€”a new world record.": "침묡은 12μ΄ˆκ°„ μ§€μ†λλ‹€β€”μƒˆλ‘œμš΄ 세계 κΈ°λ‘μ΄μ—ˆλ‹€.", # 캐릭터 "last person who dreams without ads": "κ΄‘κ³  없이 κΏˆκΎΈλŠ” λ§ˆμ§€λ§‰ μ‚¬λžŒ", "final human with unmonetized thoughts": "μˆ˜μ΅ν™”λ˜μ§€ μ•Šμ€ 생각을 κ°€μ§„ λ§ˆμ§€λ§‰ 인간", "excavator of deleted conversations": "μ‚­μ œλœ λŒ€ν™”μ˜ 발꡴자", "black market memory dealer": "κΈ°μ–΅ μ•”μ‹œμž₯ κ±°λž˜μƒ", "guerrilla flavor bomber": "게릴라 λ§› 폭탄 ν…ŒλŸ¬λ¦¬μŠ€νŠΈ", "temporal audit specialist": "μ‹œκ°„ 감사 μ „λ¬Έκ°€", "organic emotion cultivator": "μœ κΈ°λ† 감정 재배자", "binary meditation teacher": "이진법 λͺ…상 ꡐ사", "extinct plant memory keeper": "λ©Έμ’… 식물 κΈ°μ–΅ 관리인", "social distance calibrator": "μ‚¬νšŒμ  거리 μ‘°μ • 기술자", "subconscious strip miner": "λ¬΄μ˜μ‹ λ…Έμ²œ μ±„κ΅΄μž", # 후크 "discovers their memories belong to a corporate subscription service": "μžμ‹ μ˜ 기얡이 κΈ°μ—… ꡬ독 μ„œλΉ„μŠ€ μ†Œμœ μž„μ„ λ°œκ²¬ν•œλ‹€", "realizes they're the only person not running on autopilot": "μžμ‹ λ§Œμ΄ μžλ™ μ‘°μ’… λͺ¨λ“œλ‘œ μ‚΄μ§€ μ•ŠλŠ”λ‹€λŠ” κ±Έ κΉ¨λ‹«λŠ”λ‹€", "finds out their personality is a discontinued model": "μžμ‹ μ˜ 성격이 λ‹¨μ’…λœ λͺ¨λΈμž„을 μ•Œκ²Œ λœλ‹€", # 철학적 질문 "What remains human when humanity is optional?": "인간성이 선택사항일 λ•Œ 무엇이 μΈκ°„μœΌλ‘œ λ‚¨λŠ”κ°€?", "Is consciousness a bug or a feature?": "μ˜μ‹μ€ 버그인가 κΈ°λŠ₯인가?", "Can nostalgia exist without mortality?": "죽음 없이 ν–₯μˆ˜κ°€ μ‘΄μž¬ν•  수 μžˆλŠ”κ°€?", # μ„€μ • "Library of Burned Websites": "λΆˆνƒ„ μ›Ήμ‚¬μ΄νŠΈλ“€μ˜ λ„μ„œκ΄€", "Museum of Extinct Emotions": "λ©Έμ’…λœ κ°μ •λ“€μ˜ λ°•λ¬Όκ΄€", "Department of Mandatory Happiness": "의무 행볡뢀", # 일반 μš©μ–΄ "preservation_vs_evolution": "보쑴 λŒ€ μ§„ν™”", "digital_extinction": "λ””μ§€ν„Έ λ©Έμ’…", "sensory_revolution": "감각 혁λͺ…", "temporal_paradox": "μ‹œκ°„μ  μ—­μ„€", "emotional_economy": "감정 경제", "linguistic_apocalypse": "언어적 쒅말", "algorithmic_mysticism": "μ•Œκ³ λ¦¬μ¦˜ μ‹ λΉ„μ£Όμ˜", "biological_nostalgia": "생물학적 ν–₯수", "social_physics": "μ‚¬νšŒ 물리학", "reality_bureaucracy": "ν˜„μ‹€ κ΄€λ£Œμ œ", "dream_industrialization": "꿈의 μ‚°μ—…ν™”" } return translations.get(text, text) def generate_random_theme(language="English"): """Generate a coherent and natural novel theme using LLM""" try: # JSON 파일 λ‘œλ“œ json_path = Path("novel_themes.json") if not json_path.exists(): print("[WARNING] novel_themes.json not found, using built-in data") # κΈ°λ³Έ 데이터 μ •μ˜ themes_data = { "themes": ["digital extinction", "sensory revolution", "temporal paradox"], "characters": ["memory trader", "time thief", "emotion farmer"], "hooks": ["discovering hidden truth", "facing impossible choice", "breaking the system"], "questions": ["What makes us human?", "Can memory define identity?", "Is free will an illusion?"] } else: with open(json_path, 'r', encoding='utf-8') as f: data = json.load(f) themes_data = { "themes": list(data.get('core_themes', {}).keys()), "characters": [], "hooks": [], "questions": [] } # Extract data from JSON for char_data in data.get('characters', {}).values(): themes_data["characters"].extend(char_data.get('variations', [])) for hook_list in data.get('narrative_hooks', {}).values(): themes_data["hooks"].extend(hook_list) for phil_data in data.get('philosophies', {}).values(): themes_data["questions"].extend(phil_data.get('core_questions', [])) # Random selection with better randomization import secrets theme = secrets.choice(themes_data["themes"]) character = secrets.choice(themes_data["characters"]) hook = secrets.choice(themes_data["hooks"]) question = secrets.choice(themes_data["questions"]) # Create a natural prompt for LLM to generate coherent theme if language == "Korean": prompt = f"""λ‹€μŒ μš”μ†Œλ“€μ„ μ‚¬μš©ν•˜μ—¬ μžμ—°μŠ€λŸ½κ³  ν₯미둜운 μ†Œμ„€ 주제λ₯Ό μƒμ„±ν•˜μ„Έμš”: 주제: {theme} 캐릭터: {character} 사건: {hook} 철학적 질문: {question} μš”κ΅¬μ‚¬ν•­: 1. λͺ¨λ“  μš”μ†Œκ°€ 유기적으둜 μ—°κ²°λœ ν•˜λ‚˜μ˜ ν†΅ν•©λœ 주제 2. ꡬ체적이고 독창적인 μ„€μ • 3. λͺ…ν™•ν•œ κ°ˆλ“±κ³Ό κΈ΄μž₯감 4. ν˜„λŒ€μ  κ΄€λ ¨μ„± 5. 문학적 깊이 λ‹€μŒ ν˜•μ‹μœΌλ‘œ μž‘μ„±ν•˜μ„Έμš”: - 제λͺ©: [λ§€λ ₯적이고 μ•”μ‹œμ μΈ 제λͺ©] - 첫 λ¬Έμž₯: [λ…μžλ₯Ό μ¦‰μ‹œ μ‚¬λ‘œμž‘λŠ” κ°•λ ¬ν•œ 첫 λ¬Έμž₯] - 주인곡: [ꡬ체적인 상황과 νŠΉμ„±μ„ κ°€μ§„ 인물] - 쀑심 κ°ˆλ“±: [내적 κ°ˆλ“±κ³Ό 외적 κ°ˆλ“±μ˜ κ²°ν•©] - 탐ꡬ 주제: [철학적 깊이λ₯Ό κ°€μ§„ 핡심 질문]""" else: prompt = f"""Generate a natural and compelling novel theme using these elements: Theme: {theme} Character: {character} Event: {hook} Philosophical Question: {question} Requirements: 1. All elements organically connected into one unified theme 2. Specific and original setting 3. Clear conflict and tension 4. Contemporary relevance 5. Literary depth Format as: - Title: [Compelling and evocative title] - Opening: [Powerful first sentence that immediately hooks readers] - Protagonist: [Character with specific situation and traits] - Central Conflict: [Combination of internal and external conflict] - Core Exploration: [Philosophically deep central question]""" # Use the UnifiedLiterarySystem's LLM to generate coherent theme system = UnifiedLiterarySystem() # Call LLM synchronously for theme generation messages = [{"role": "user", "content": prompt}] generated_theme = system.call_llm_sync(messages, "director", language) # Add narrative structure (simplified and natural) if language == "Korean": generated_theme += f""" **μ„œμ‚¬ ꡬ쑰:** 이 μ΄μ•ΌκΈ°λŠ” {character}κ°€ {hook.lower()}λŠ” 좩격적 μ‚¬κ±΄μœΌλ‘œ μ‹œμž‘λ©λ‹ˆλ‹€. 점차 μ‹¬ν™”λ˜λŠ” κ°ˆλ“±μ„ 톡해 {question.lower().rstrip('?')}λΌλŠ” 근본적 질문과 λŒ€λ©΄ν•˜κ²Œ 되며, ꢁ극적으둜 {theme.replace('_', ' ')}의 μ‹œλŒ€λ₯Ό μ‚΄μ•„κ°€λŠ” ν˜„λŒ€μΈμ˜ 싀쑴적 선택을 κ·Έλ¦½λ‹ˆλ‹€. **톀과 μŠ€νƒ€μΌ:** ν˜„λŒ€ λ¬Έν•™μ˜ 심리적 κΉŠμ΄μ™€ 철학적 톡찰을 κ²°ν•©ν•˜μ—¬, λ…μžλ‘œ ν•˜μ—¬κΈˆ μžμ‹ μ˜ 삢을 λŒμ•„λ³΄κ²Œ λ§Œλ“œλŠ” 성찰적 μ„œμ‚¬λ₯Ό μ§€ν–₯ν•©λ‹ˆλ‹€.""" else: generated_theme += f""" **Narrative Arc:** The story begins with {character} who {hook}, a shocking event that sets everything in motion. Through deepening conflicts, they confront the fundamental question of {question.lower()} ultimately portraying the existential choices of modern humans living in an era of {theme.replace('_', ' ')}. **Tone and Style:** Combining the psychological depth and philosophical insights of contemporary literature, aiming for a reflective narrative that makes readers examine their own lives.""" return generated_theme except Exception as e: logger.error(f"Theme generation error: {str(e)}") # Fallback to simple pre-defined themes fallback_themes = { "Korean": [ """**제λͺ©:** λ§ˆμ§€λ§‰ μ•„λ‚ λ‘œκ·Έ 인간 **첫 λ¬Έμž₯:** "λ‚΄κ°€ λ§ˆμ§€λ§‰μœΌλ‘œ 쒅이에 글을 μ“΄ μ‚¬λžŒμ΄ 된 λ‚ , 세상은 μΉ¨λ¬΅ν–ˆλ‹€." **주인곡:** λ””μ§€ν„Έν™”λ₯Ό κ±°λΆ€ν•˜κ³  수기둜만 μ†Œν†΅ν•˜λŠ” λ…Έλ…„μ˜ μž‘κ°€ **쀑심 κ°ˆλ“±:** νš¨μœ¨μ„±κ³Ό 인간성 μ‚¬μ΄μ—μ„œ 선택해야 ν•˜λŠ” 싀쑴적 λ”œλ ˆλ§ˆ **탐ꡬ 주제:** 기술 λ°œμ „ μ†μ—μ„œ 인간 고유의 κ°€μΉ˜λŠ” 무엇인가?""", """**제λͺ©:** κΈ°μ–΅ κ±°λž˜μ†Œ **첫 λ¬Έμž₯:** "였늘 μ•„μΉ¨, λ‚˜λŠ” μ²«μ‚¬λž‘μ˜ 기얡을 νŒ”κΈ°λ‘œ κ²°μ •ν–ˆλ‹€." **주인곡:** 생계λ₯Ό μœ„ν•΄ μ†Œμ€‘ν•œ 기얡을 νŒŒλŠ” μ Šμ€ μ˜ˆμˆ κ°€ **쀑심 κ°ˆλ“±:** 생쑴과 정체성 보쑴 μ‚¬μ΄μ˜ 선택 **탐ꡬ 주제:** 기얡이 κ±°λž˜λ˜λŠ” μ‹œλŒ€, μš°λ¦¬λŠ” λ¬΄μ—‡μœΌλ‘œ μžμ‹ μ„ μ •μ˜ν•˜λŠ”κ°€?""" ], "English": [ """**Title:** The Last Analog Human **Opening:** "The day I became the last person to write on paper, the world fell silent." **Protagonist:** An elderly writer who refuses digitalization and communicates only through handwriting **Central Conflict:** Existential dilemma between efficiency and humanity **Core Exploration:** What is uniquely human in the age of technological advancement?""", """**Title:** The Memory Exchange **Opening:** "This morning, I decided to sell my first love's memory." **Protagonist:** A young artist selling precious memories for survival **Central Conflict:** Choice between survival and preserving identity **Core Exploration:** In an era where memories are traded, what defines who we are?""" ] } import secrets return secrets.choice(fallback_themes.get(language, fallback_themes["English"])) # Update the handle_random_theme function in create_interface def handle_random_theme(language): """Handle random theme generation with improved feedback""" try: # Generate theme using LLM for natural output theme = generate_random_theme(language) logger.info(f"Generated theme successfully") return theme except Exception as e: logger.error(f"Random theme generation failed: {str(e)}") # Return a simple fallback theme if language == "Korean": return "기얡을 μžƒμ–΄κ°€λŠ” 노인과 AI κ°„λ³‘μΈμ˜ νŠΉλ³„ν•œ μš°μ •" else: return "An unlikely friendship between an elderly person losing memories and their AI caregiver" # Update the augment_query method to better handle generated themes def augment_query(self, user_query: str, language: str) -> str: """Augment and clean user query""" # Remove any formatting artifacts from random generation if "**" in user_query or "##" in user_query: # This is likely a generated theme with formatting # Extract the essence without formatting lines = user_query.split('\n') cleaned_parts = [] for line in lines: # Remove markdown formatting line = line.replace('**', '').replace('##', '').strip() if line and not line.startswith(('-', 'β€’', '*')) and ':' not in line[:20]: cleaned_parts.append(line) if cleaned_parts: user_query = ' '.join(cleaned_parts[:3]) # Use first few meaningful lines # If query is too short, enhance it if len(user_query.split()) < 15: if language == "Korean": return f"{user_query}\n\n이 주제λ₯Ό ν˜„λŒ€μ  κ΄€μ μ—μ„œ μž¬ν•΄μ„ν•˜μ—¬ 인간 쑴재의 본질과 기술 μ‹œλŒ€μ˜ λ”œλ ˆλ§ˆλ₯Ό νƒκ΅¬ν•˜λŠ” 8,000단어 λΆ„λŸ‰μ˜ 철학적 μ€‘νŽΈμ†Œμ„€μ„ μž‘μ„±ν•˜μ„Έμš”." else: return f"{user_query}\n\nReinterpret this theme from a contemporary perspective to explore the essence of human existence and dilemmas of the technological age in an 8,000-word philosophical novella." return user_query # Add method to UnifiedLiterarySystem class for better theme processing def process_generated_theme(self, theme_text: str, language: str) -> str: """Process generated theme for novel writing""" # Extract key elements from generated theme theme_elements = { "title": "", "opening": "", "protagonist": "", "conflict": "", "exploration": "" } lines = theme_text.split('\n') current_key = None for line in lines: line = line.strip() if not line: continue # Detect sections if any(marker in line.lower() for marker in ['title:', 'opening:', 'protagonist:', 'conflict:', 'exploration:', '제λͺ©:', '첫 λ¬Έμž₯:', '주인곡:', 'κ°ˆλ“±:', '탐ꡬ']): for key in theme_elements: if key in line.lower() or (language == "Korean" and key in translate_to_korean(line.lower())): current_key = key # Extract content after colon if ':' in line: content = line.split(':', 1)[1].strip() if content: theme_elements[current_key] = content break elif current_key and line: # Continue adding to current element theme_elements[current_key] = (theme_elements[current_key] + " " + line).strip() # Construct a coherent theme summary if language == "Korean": summary = f"{theme_elements.get('title', '무제')}. " if theme_elements.get('opening'): summary += f"'{theme_elements['opening']}' " summary += f"{theme_elements.get('protagonist', '주인곡')}의 이야기. " summary += f"{theme_elements.get('conflict', '')} " summary += f"{theme_elements.get('exploration', '')}" else: summary = f"{theme_elements.get('title', 'Untitled')}. " if theme_elements.get('opening'): summary += f"'{theme_elements['opening']}' " summary += f"The story of {theme_elements.get('protagonist', 'a protagonist')}. " summary += f"{theme_elements.get('conflict', '')} " summary += f"{theme_elements.get('exploration', '')}" return summary.strip() # Create Gradio interface def create_interface(): with gr.Blocks(theme=gr.themes.Soft, css=custom_css, title="AGI NOVEL Generator") as interface: gr.HTML("""

πŸ“š AGI NOVEL Generator

badge badge badge

Artificial General Intelligence (AGI) denotes an artificial system possessing human-level, general-purpose intelligence and is now commonly framed as AI that can outperform humans in most economically and intellectually valuable tasks. Demonstrating such breadth requires evaluating not only calculation, logical reasoning, and perception but also the distinctly human faculties of creativity and language. Among the creative tests, the most demanding is the production of a full-length novel running 100k–200k words. An extended narrative forces an AGI candidate to exhibit (1) sustained long-term memory and context tracking (2) intricate causal and plot planning (3) nuanced cultural and emotional expression (4) autonomous self-censorship and ethical filtering to avoid harmful or biased content and (5) verifiable originality beyond simple recombination of training data.

🎲 Novel Theme Random Generator: This system can generate up to approximately 170 quadrillion (1.7 Γ— 10¹⁷) unique novel themes. Even writing 100 novels per day, it would take 4.6 million years to exhaust all combinations. Click the "Random" button to explore infinite creative possibilities!
⏱️ Note: Creating a complete novel takes approximately 20 minutes. If your web session disconnects, you can restore your work using the "Session Recovery" feature.
🎯 Core Innovation: Not fragmented texts from multiple writers, but a genuine full-length novel written consistently by a single author from beginning to end.
""") # 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 novella theme. Examples: Character transformation, relationship evolution, social conflict and personal choice...""", lines=5 ) language_select = gr.Radio( choices=["English", "Korean"], value="English", label="Language" ) with gr.Row(): submit_btn = gr.Button("πŸš€ Start Writing", variant="primary", scale=2) random_btn = gr.Button("🎲 Random", variant="secondary", scale=1) clear_btn = gr.Button("πŸ—‘οΈ Clear", scale=1) status_text = gr.Textbox( label="Progress Status", interactive=False, value="πŸ”„ Ready" ) # Session management with gr.Group(elem_classes=["session-section"]): gr.Markdown("### πŸ’Ύ Active Works") session_dropdown = gr.Dropdown( label="Saved Sessions", choices=[], interactive=True ) with gr.Row(): refresh_btn = gr.Button("πŸ”„ Refresh", scale=1) resume_btn = gr.Button("▢️ Resume", variant="secondary", scale=1) auto_recover_btn = gr.Button("♻️ Recover Recent Work", scale=1) with gr.Column(scale=2): with gr.Tab("πŸ“ Writing Process"): stages_display = gr.Markdown( value="Writing process will be displayed in real-time...", elem_id="stages-display" ) with gr.Tab("πŸ“– Completed Work"): novel_output = gr.Markdown( value="Completed novel will be displayed here...", elem_id="novel-output" ) with gr.Group(elem_classes=["download-section"]): gr.Markdown("### πŸ“₯ Download Work") with gr.Row(): format_select = gr.Radio( choices=["DOCX", "TXT"], value="DOCX" if DOCX_AVAILABLE else "TXT", label="File Format" ) download_btn = gr.Button("⬇️ Download", variant="secondary") download_file = gr.File( label="Download File", visible=False ) # Hidden state novel_text_state = gr.State("") # Examples with gr.Row(): gr.Examples( examples=[ ["A daughter discovering her mother's hidden past through old letters"], ["An architect losing sight who learns to design through touch and sound"], ["A translator replaced by AI rediscovering the essence of language through classical literature transcription"], ["A middle-aged man who lost his job finding new meaning in rural life"], ["A doctor with war trauma healing through Doctors Without Borders"], ["Community solidarity to save a neighborhood bookstore from redevelopment"], ["A year with a professor losing memory and his last student"] ], inputs=query_input, label="πŸ’‘ Theme Examples" ) # Event handlers def refresh_sessions(): try: sessions = get_active_sessions("English") return gr.update(choices=sessions) except Exception as e: logger.error(f"Session refresh error: {str(e)}") return gr.update(choices=[]) def handle_auto_recover(language): session_id, message = auto_recover_session(language) return session_id, message def handle_random_theme(language): """Handle random theme generation with language support""" import time import datetime # 더 κ°•λ ₯ν•œ μ‹œκ°μ  ν”Όλ“œλ°± time.sleep(0.05) # ν˜„μž¬ μ‹œκ°„μ„ λ‘œκ·Έμ— 좜λ ₯ν•΄μ„œ μ‹€μ œλ‘œ μƒˆλ‘œμš΄ ν˜ΈμΆœμΈμ§€ 확인 logger.info(f"Random theme requested at {datetime.datetime.now()}") theme = generate_random_theme(language) logger.info(f"Generated theme: {theme[:100]}...") # 처음 100자만 둜그 return theme # Event connections submit_btn.click( fn=process_query, inputs=[query_input, language_select, current_session_id], outputs=[stages_display, novel_output, status_text, current_session_id] ) novel_output.change( fn=lambda x: x, inputs=[novel_output], outputs=[novel_text_state] ) resume_btn.click( fn=lambda x: x.split("...")[0] if x and "..." in x else x, inputs=[session_dropdown], outputs=[current_session_id] ).then( fn=resume_session, inputs=[current_session_id, language_select], outputs=[stages_display, novel_output, status_text, current_session_id] ) auto_recover_btn.click( fn=handle_auto_recover, inputs=[language_select], outputs=[current_session_id, status_text] ).then( fn=resume_session, inputs=[current_session_id, language_select], outputs=[stages_display, novel_output, status_text, current_session_id] ) refresh_btn.click( fn=refresh_sessions, outputs=[session_dropdown] ) clear_btn.click( fn=lambda: ("", "", "πŸ”„ Ready", "", None), outputs=[stages_display, novel_output, status_text, novel_text_state, current_session_id] ) # random_btn 클릭 이벀트λ₯Ό 더 λͺ…ν™•ν•˜κ²Œ random_btn.click( fn=lambda lang: generate_random_theme(lang), # 직접 호좜 inputs=[language_select], outputs=[query_input], queue=False # νμž‰ λΉ„ν™œμ„±ν™” ) def handle_download(format_type, language, session_id, novel_text): if not session_id or not novel_text: return gr.update(visible=False) file_path = download_novel(novel_text, format_type, language, session_id) if file_path: return gr.update(value=file_path, visible=True) else: return gr.update(visible=False) download_btn.click( fn=handle_download, inputs=[format_select, language_select, current_session_id, novel_text_state], outputs=[download_file] ) # Load sessions on start interface.load( fn=refresh_sessions, outputs=[session_dropdown] ) return interface # Main execution if __name__ == "__main__": logger.info("AGI NOVEL Generator v2.0 Starting...") logger.info("=" * 60) # Environment check logger.info(f"API Endpoint: {API_URL}") logger.info(f"Target Length: {TARGET_WORDS:,} words") logger.info(f"Minimum Words per Part: {MIN_WORDS_PER_PART:,} words") logger.info("System Features: Single writer + Immediate part-by-part critique") if BRAVE_SEARCH_API_KEY: logger.info("Web search enabled.") else: logger.warning("Web search disabled.") if DOCX_AVAILABLE: logger.info("DOCX export enabled.") else: logger.warning("DOCX export disabled.") logger.info("=" * 60) # Initialize database logger.info("Initializing database...") NovelDatabase.init_db() logger.info("Database initialization complete.") # Create and launch interface interface = create_interface() interface.launch( server_name="0.0.0.0", server_port=7860, share=False, debug=True )