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 # --- 로깅 설정 --- 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 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.") # --- 환경 변수 및 상수 --- FRIENDLI_TOKEN = os.getenv("FRIENDLI_TOKEN", "") BRAVE_SEARCH_API_KEY = os.getenv("BRAVE_SEARCH_API_KEY", "") API_URL = "https://api.friendli.ai/dedicated/v1/chat/completions" MODEL_ID = "dep89a2fld32mcm" DB_PATH = "novel_sessions_v5.db" # 목표 분량 설정 TARGET_WORDS = 8000 # 안전 마진을 위해 8000단어 MIN_WORDS_PER_WRITER = 800 # 각 작가 최소 분량 # --- 환경 변수 검증 --- 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.") # --- 전역 변수 --- db_lock = threading.Lock() # 서사 진행 단계 정의 NARRATIVE_PHASES = [ "도입: 일상과 균열", "발전 1: 불안의 고조", "발전 2: 외부 충격", "발전 3: 내적 갈등 심화", "절정 1: 위기의 정점", "절정 2: 선택의 순간", "하강 1: 결과와 여파", "하강 2: 새로운 인식", "결말 1: 변화된 일상", "결말 2: 열린 질문" ] # 단계별 구성 PROGRESSIVE_STAGES = [ ("director", "🎬 감독자: 통합된 서사 구조 기획"), ("critic", "📝 비평가: 서사 진행성과 깊이 검토"), ("director", "🎬 감독자: 수정된 마스터플랜"), ] + [ (f"writer{i}", f"✍️ 작가 {i}: 초안 - {NARRATIVE_PHASES[i-1]}") for i in range(1, 11) ] + [ ("critic", "📝 비평가: 중간 검토 (서사 누적성과 변화)"), ] + [ (f"writer{i}", f"✍️ 작가 {i}: 수정본 - {NARRATIVE_PHASES[i-1]}") for i in range(1, 11) ] + [ ("critic", f"📝 비평가: 최종 검토 및 문학적 평가"), ] # --- 데이터 클래스 --- @dataclass class CharacterArc: """인물의 변화 궤적 추적""" name: str initial_state: Dict[str, Any] # 초기 상태 phase_states: Dict[int, Dict[str, Any]] = field(default_factory=dict) # 단계별 상태 transformations: List[str] = field(default_factory=list) # 주요 변화들 relationships_evolution: Dict[str, List[str]] = field(default_factory=dict) # 관계 변화 @dataclass class PlotThread: """플롯 라인 추적""" thread_id: str description: str introduction_phase: int development_phases: List[int] resolution_phase: Optional[int] status: str = "active" # active, resolved, suspended @dataclass class SymbolicEvolution: """상징의 의미 변화 추적""" symbol: str initial_meaning: str phase_meanings: Dict[int, str] = field(default_factory=dict) transformation_complete: bool = False # --- 핵심 로직 클래스 --- class ProgressiveNarrativeTracker: """서사 진행과 누적을 추적하는 시스템""" def __init__(self): self.character_arcs: Dict[str, CharacterArc] = {} self.plot_threads: Dict[str, PlotThread] = {} self.symbolic_evolutions: Dict[str, SymbolicEvolution] = {} self.phase_summaries: Dict[int, str] = {} self.accumulated_events: List[Dict[str, Any]] = [] self.thematic_deepening: List[str] = [] def register_character_arc(self, name: str, initial_state: Dict[str, Any]): """캐릭터 아크 등록""" self.character_arcs[name] = CharacterArc(name=name, initial_state=initial_state) logger.info(f"Character arc registered: {name}") def update_character_state(self, name: str, phase: int, new_state: Dict[str, Any], transformation: str): """캐릭터 상태 업데이트 및 변화 기록""" if name in self.character_arcs: arc = self.character_arcs[name] arc.phase_states[phase] = new_state arc.transformations.append(f"Phase {phase}: {transformation}") logger.info(f"Character {name} transformed in phase {phase}: {transformation}") def add_plot_thread(self, thread_id: str, description: str, intro_phase: int): """새로운 플롯 라인 추가""" self.plot_threads[thread_id] = PlotThread( thread_id=thread_id, description=description, introduction_phase=intro_phase, development_phases=[] ) def develop_plot_thread(self, thread_id: str, phase: int): """플롯 라인 발전""" if thread_id in self.plot_threads: self.plot_threads[thread_id].development_phases.append(phase) def check_narrative_progression(self, current_phase: int) -> Tuple[bool, List[str]]: """서사가 실제로 진행되고 있는지 확인""" issues = [] # 1. 캐릭터 변화 확인 static_characters = [] for name, arc in self.character_arcs.items(): if len(arc.transformations) < current_phase // 3: # 최소 3단계마다 변화 필요 static_characters.append(name) if static_characters: issues.append(f"다음 인물들의 변화가 부족합니다: {', '.join(static_characters)}") # 2. 플롯 진행 확인 unresolved_threads = [] for thread_id, thread in self.plot_threads.items(): if thread.status == "active" and len(thread.development_phases) < 2: unresolved_threads.append(thread.description) if unresolved_threads: issues.append(f"진전되지 않은 플롯: {', '.join(unresolved_threads)}") # 3. 상징 발전 확인 static_symbols = [] for symbol, evolution in self.symbolic_evolutions.items(): if len(evolution.phase_meanings) < current_phase // 4: static_symbols.append(symbol) if static_symbols: issues.append(f"의미가 발전하지 않은 상징: {', '.join(static_symbols)}") return len(issues) == 0, issues def generate_phase_requirements(self, phase: int) -> str: """각 단계별 필수 요구사항 생성""" requirements = [] # 이전 단계 요약 if phase > 1 and (phase-1) in self.phase_summaries: requirements.append(f"이전 단계 핵심: {self.phase_summaries[phase-1]}") # 단계별 특수 요구사항 phase_name = NARRATIVE_PHASES[phase-1] if phase <= 10 else "수정" if "도입" in phase_name: requirements.append("- 일상의 균열을 보여주되, 큰 사건이 아닌 미묘한 변화로 시작") requirements.append("- 주요 인물들의 초기 상태와 관계 설정") requirements.append("- 핵심 상징 도입 (자연스럽게)") elif "발전" in phase_name: requirements.append("- 이전 단계의 균열/갈등이 구체화되고 심화") requirements.append("- 새로운 사건이나 인식이 추가되어 복잡성 증가") requirements.append("- 인물 간 관계의 미묘한 변화") elif "절정" in phase_name: requirements.append("- 축적된 갈등이 임계점에 도달") requirements.append("- 인물의 내적 선택이나 인식의 전환점") requirements.append("- 상징의 의미가 전복되거나 심화") elif "하강" in phase_name: requirements.append("- 절정의 여파와 그로 인한 변화") requirements.append("- 새로운 균형점을 찾아가는 과정") requirements.append("- 인물들의 변화된 관계와 인식") elif "결말" in phase_name: requirements.append("- 변화된 일상의 모습") requirements.append("- 해결되지 않은 질문들") requirements.append("- 여운과 성찰의 여지") # 반복 방지 요구사항 requirements.append("\n⚠️ 절대 금지사항:") requirements.append("- 이전 단계와 동일한 사건이나 갈등 반복") requirements.append("- 인물이 같은 생각이나 감정에 머무르기") requirements.append("- 플롯이 제자리걸음하기") return "\n".join(requirements) class NovelDatabase: """데이터베이스 관리""" @staticmethod def init_db(): with sqlite3.connect(DB_PATH) as conn: conn.execute("PRAGMA journal_mode=WAL") cursor = conn.cursor() cursor.execute(''' CREATE TABLE IF NOT EXISTS sessions ( session_id TEXT PRIMARY KEY, 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, narrative_tracker TEXT ) ''') 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', progression_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), UNIQUE(session_id, stage_number) ) ''') cursor.execute(''' CREATE TABLE IF NOT EXISTS plot_threads ( id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, thread_id TEXT NOT NULL, description TEXT, introduction_phase INTEGER, status TEXT DEFAULT 'active', created_at TEXT DEFAULT (datetime('now')), FOREIGN KEY (session_id) REFERENCES sessions(session_id) ) ''') 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', progression_score: float = 0.0): word_count = len(content.split()) if content else 0 with NovelDatabase.get_db() as conn: cursor = conn.cursor() cursor.execute(''' INSERT INTO stages (session_id, stage_number, stage_name, role, content, word_count, status, progression_score) VALUES (?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(session_id, stage_number) DO UPDATE SET content=?, word_count=?, status=?, stage_name=?, progression_score=?, updated_at=datetime('now') ''', (session_id, stage_number, stage_name, role, content, word_count, status, progression_score, content, word_count, status, stage_name, progression_score)) # 총 단어 수 업데이트 cursor.execute(''' UPDATE sessions SET total_words = ( SELECT SUM(word_count) FROM stages WHERE session_id = ? AND role LIKE '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 get_writer_content(session_id: str) -> str: """작가 콘텐츠 가져오기 (수정본 우선)""" with NovelDatabase.get_db() as conn: all_content = [] for writer_num in range(1, 11): # 수정본이 있으면 수정본을, 없으면 초안을 row = conn.cursor().execute(''' SELECT content FROM stages WHERE session_id = ? AND role = ? AND stage_name LIKE '%수정본%' ORDER BY stage_number DESC LIMIT 1 ''', (session_id, f'writer{writer_num}')).fetchone() if not row or not row['content']: # 수정본이 없으면 초안 사용 row = conn.cursor().execute(''' SELECT content FROM stages WHERE session_id = ? AND role = ? AND stage_name LIKE '%초안%' ORDER BY stage_number DESC LIMIT 1 ''', (session_id, f'writer{writer_num}')).fetchone() if row and row['content']: all_content.append(row['content'].strip()) return '\n\n'.join(all_content) @staticmethod def get_total_words(session_id: str) -> int: """총 단어 수 가져오기""" with NovelDatabase.get_db() as conn: row = conn.cursor().execute( 'SELECT total_words FROM sessions WHERE session_id = ?', (session_id,) ).fetchone() return row['total_words'] if row and row['total_words'] else 0 @staticmethod def save_narrative_tracker(session_id: str, tracker: ProgressiveNarrativeTracker): """서사 추적기 저장""" with NovelDatabase.get_db() as conn: tracker_data = json.dumps({ 'character_arcs': {k: asdict(v) for k, v in tracker.character_arcs.items()}, 'plot_threads': {k: asdict(v) for k, v in tracker.plot_threads.items()}, 'phase_summaries': tracker.phase_summaries, 'thematic_deepening': tracker.thematic_deepening }) 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[ProgressiveNarrativeTracker]: """서사 추적기 로드""" 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 = ProgressiveNarrativeTracker() # 데이터 복원 for name, arc_data in data.get('character_arcs', {}).items(): tracker.character_arcs[name] = CharacterArc(**arc_data) for thread_id, thread_data in data.get('plot_threads', {}).items(): tracker.plot_threads[thread_id] = PlotThread(**thread_data) tracker.phase_summaries = data.get('phase_summaries', {}) tracker.thematic_deepening = data.get('thematic_deepening', []) return tracker return None @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] class WebSearchIntegration: """웹 검색 기능""" 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"웹 검색 API 오류: {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 ProgressiveLiterarySystem: """진행형 문학 소설 생성 시스템""" def __init__(self): self.token = FRIENDLI_TOKEN self.api_url = API_URL self.model_id = MODEL_ID self.narrative_tracker = ProgressiveNarrativeTracker() 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"} # --- 프롬프트 생성 함수들 --- def create_director_initial_prompt(self, user_query: str, language: str) -> str: """감독자 초기 기획 - 통합된 서사 구조""" search_results_str = "" if self.web_search.enabled: queries = [f"{user_query} 사회 문제", f"{user_query} 현대 한국"] for q in queries[:1]: results = self.web_search.search(q, count=2, language=language) if results: search_results_str += self.web_search.extract_relevant_info(results) + "\n" lang_prompts = { "Korean": f"""당신은 현대 한국 문학의 거장입니다. 단편이 아닌 중편 소설(8,000단어 이상)을 위한 통합된 서사 구조를 기획하세요. **주제:** {user_query} **참고 자료:** {search_results_str if search_results_str else "N/A"} **필수 요구사항:** 1. **통합된 서사 구조 (가장 중요)** - 10개 단계가 유기적으로 연결된 단일 서사 - 각 단계는 이전 단계의 결과로 자연스럽게 이어짐 - 반복이 아닌 축적과 발전 단계별 서사 진행: 1) 도입: 일상과 균열 - 평범한 일상 속 첫 균열 2) 발전 1: 불안의 고조 - 균열이 확대되며 불안 증폭 3) 발전 2: 외부 충격 - 예상치 못한 외부 사건 4) 발전 3: 내적 갈등 심화 - 가치관의 충돌 5) 절정 1: 위기의 정점 - 모든 갈등이 극대화 6) 절정 2: 선택의 순간 - 결정적 선택 7) 하강 1: 결과와 여파 - 선택의 직접적 결과 8) 하강 2: 새로운 인식 - 변화된 세계관 9) 결말 1: 변화된 일상 - 새로운 균형 10) 결말 2: 열린 질문 - 독자에게 던지는 질문 2. **인물의 변화 궤적** - 주인공: 초기 상태 → 중간 변화 → 최종 상태 (명확한 arc) - 주요 인물들도 각자의 변화 경험 - 관계의 역동적 변화 3. **주요 플롯 라인** (2-3개) - 메인 플롯: 전체를 관통하는 핵심 갈등 - 서브 플롯: 메인과 연결되며 주제를 심화 4. **상징의 진화** - 핵심 상징 1-2개 설정 - 단계별로 의미가 변화/심화/전복 5. **사회적 맥락** - 개인의 문제가 사회 구조와 연결 - 구체적인 한국 사회의 현실 반영 **절대 금지사항:** - 동일한 사건이나 상황의 반복 - 인물이 같은 감정/생각에 머무르기 - 플롯의 리셋이나 순환 구조 - 각 단계가 독립된 에피소드로 존재 **분량 계획:** - 총 8,000단어 이상 - 각 단계 평균 800단어 - 균형 잡힌 서사 전개 하나의 강력한 서사가 시작부터 끝까지 관통하는 작품을 기획하세요.""", "English": f"""You are a master of contemporary literary fiction. Plan an integrated narrative structure for a novella (8,000+ words), not a collection of short stories. **Theme:** {user_query} **Reference:** {search_results_str if search_results_str else "N/A"} **Essential Requirements:** 1. **Integrated Narrative Structure (Most Important)** - Single narrative with 10 organically connected phases - Each phase naturally follows from previous results - Accumulation and development, not repetition Phase Progression: 1) Introduction: Daily life and first crack 2) Development 1: Rising anxiety 3) Development 2: External shock 4) Development 3: Deepening internal conflict 5) Climax 1: Peak crisis 6) Climax 2: Moment of choice 7) Falling Action 1: Direct consequences 8) Falling Action 2: New awareness 9) Resolution 1: Changed daily life 10) Resolution 2: Open questions 2. **Character Transformation Arcs** - Protagonist: Clear progression from initial → middle → final state - Supporting characters also experience change - Dynamic relationship evolution 3. **Plot Threads** (2-3) - Main plot: Core conflict throughout - Subplots: Connected and deepening themes 4. **Symbolic Evolution** - 1-2 core symbols - Meaning transforms across phases 5. **Social Context** - Individual problems connected to social structures - Specific contemporary realities **Absolutely Forbidden:** - Repetition of same events/situations - Characters stuck in same emotions - Plot resets or circular structure - Independent episodes **Length Planning:** - Total 8,000+ words - ~800 words per phase - Balanced progression Create a work with one powerful narrative from beginning to end.""" } return lang_prompts.get(language, lang_prompts["Korean"]) def create_critic_director_prompt(self, director_plan: str, user_query: str, language: str) -> str: """비평가의 감독자 기획 검토 - 서사 통합성 중심""" lang_prompts = { "Korean": f"""당신은 서사 구조 전문 비평가입니다. 이 기획이 진정한 '장편 소설'인지 엄격히 검토하세요. **원 주제:** {user_query} **감독자 기획:** {director_plan} **핵심 검토 사항:** 1. **서사의 통합성과 진행성** - 10개 단계가 하나의 이야기로 연결되는가? - 각 단계가 이전 단계의 필연적 결과인가? - 동일한 상황의 반복은 없는가? 2. **인물 변화의 궤적** - 주인공이 명확한 변화의 arc를 가지는가? - 변화가 구체적이고 신빙성 있는가? - 관계의 발전이 계획되어 있는가? 3. **플롯의 축적성** - 갈등이 점진적으로 심화되는가? - 새로운 요소가 추가되며 복잡성이 증가하는가? - 해결이 자연스럽고 필연적인가? 4. **분량과 밀도** - 8,000단어를 채울 충분한 내용인가? - 각 단계가 800단어의 밀도를 가질 수 있는가? **판정:** - 통과: 진정한 장편 서사 구조 - 재작성: 반복적/순환적 구조 구체적 개선 방향을 제시하세요.""", "English": f"""You are a narrative structure critic. Strictly review whether this plan is a true 'novel' rather than repeated episodes. **Original Theme:** {user_query} **Director's Plan:** {director_plan} **Key Review Points:** 1. **Narrative Integration and Progression** - Do 10 phases connect as one story? - Does each phase necessarily follow from previous? - No repetition of same situations? 2. **Character Transformation Arcs** - Clear protagonist transformation arc? - Concrete and credible changes? - Planned relationship development? 3. **Plot Accumulation** - Progressive conflict deepening? - Added complexity through new elements? - Natural and inevitable resolution? 4. **Length and Density** - Sufficient content for 8,000 words? - Can each phase sustain 800 words? **Verdict:** - Pass: True novel structure - Rewrite: Repetitive/circular structure Provide specific improvements.""" } return lang_prompts.get(language, lang_prompts["Korean"]) def create_writer_prompt(self, writer_number: int, director_plan: str, previous_content: str, phase_requirements: str, narrative_summary: str, language: str) -> str: """작가 프롬프트 - 서사 진행 강제""" phase_name = NARRATIVE_PHASES[writer_number-1] target_words = MIN_WORDS_PER_WRITER lang_prompts = { "Korean": f"""당신은 작가 {writer_number}번입니다. **현재 단계: {phase_name}** **전체 서사 구조:** {director_plan} **지금까지의 이야기 요약:** {narrative_summary} **이전 내용 (직전 부분):** {previous_content[-1500:] if previous_content else "시작"} **이번 단계 필수 요구사항:** {phase_requirements} **작성 지침:** 1. **분량**: {target_words}-900 단어 (필수) - 내면 묘사와 구체적 디테일로 분량 확보 - 장면을 충분히 전개하고 깊이 있게 묘사 2. **서사 진행 (가장 중요)** - 이전 단계에서 일어난 일의 직접적 결과로 시작 - 새로운 사건/인식/변화를 추가하여 이야기 전진 - 다음 단계로 자연스럽게 연결될 고리 마련 3. **인물의 변화** - 이 단계에서 인물이 겪는 구체적 변화 묘사 - 내면의 미묘한 변화도 포착 - 관계의 역학 변화 반영 4. **문체와 기법** - 한국 현대 문학의 섬세한 심리 묘사 - 일상 속 사회적 맥락 녹여내기 - 감각적 디테일과 내면 의식의 균형 5. **연속성 유지** - 인물의 목소리와 말투 일관성 - 공간과 시간의 연속성 - 상징과 모티프의 발전 **절대 금지:** - 이전과 동일한 상황 반복 - 서사의 정체나 후퇴 - 분량 미달 (최소 {target_words}단어) 이전의 흐름을 이어받아 새로운 국면으로 발전시키세요.""", "English": f"""You are Writer #{writer_number}. **Current Phase: {phase_name}** **Overall Narrative Structure:** {director_plan} **Story So Far:** {narrative_summary} **Previous Content (immediately before):** {previous_content[-1500:] if previous_content else "Beginning"} **Phase Requirements:** {phase_requirements} **Writing Guidelines:** 1. **Length**: {target_words}-900 words (mandatory) - Use interior description and concrete details - Fully develop scenes with depth 2. **Narrative Progression (Most Important)** - Start as direct result of previous phase - Add new events/awareness/changes to advance story - Create natural connection to next phase 3. **Character Change** - Concrete changes in this phase - Capture subtle interior shifts - Reflect relationship dynamics 4. **Style and Technique** - Delicate psychological portrayal - Social context in daily life - Balance sensory details with consciousness 5. **Continuity** - Consistent character voices - Spatial/temporal continuity - Symbol/motif development **Absolutely Forbidden:** - Repeating previous situations - Narrative stagnation/regression - Under word count (minimum {target_words}) Continue the flow and develop into new phase.""" } return lang_prompts.get(language, lang_prompts["Korean"]) def create_critic_consistency_prompt(self, all_content: str, narrative_tracker: ProgressiveNarrativeTracker, user_query: str, language: str) -> str: """비평가 중간 검토 - 서사 누적성 확인""" # 서사 진행 체크 phase_count = len(narrative_tracker.phase_summaries) progression_ok, issues = narrative_tracker.check_narrative_progression(phase_count) lang_prompts = { "Korean": f"""서사 진행 전문 비평가로서 작품을 검토하세요. **원 주제:** {user_query} **현재까지 진행된 서사 단계:** {phase_count}/10 **발견된 진행 문제:** {chr(10).join(issues) if issues else "없음"} **작품 내용 (최근 부분):** {all_content[-4000:]} **집중 검토 사항:** 1. **서사의 축적과 진행** - 이야기가 실제로 전진하고 있는가? - 각 단계가 이전의 결과로 연결되는가? - 동일한 갈등이나 상황이 반복되지 않는가? 2. **인물의 변화 궤적** - 주인공이 초기와 비교해 어떻게 변했는가? - 변화가 설득력 있고 점진적인가? - 관계가 역동적으로 발전하는가? 3. **주제의 심화** - 초기 주제가 어떻게 발전했는가? - 새로운 층위가 추가되었는가? - 복잡성이 증가했는가? 4. **분량과 밀도** - 현재까지 총 단어 수 확인 - 목표(8,000단어)에 도달 가능한가? **수정 지시:** 각 작가에게 구체적인 진행 방향 제시.""", "English": f"""As a narrative progression critic, review the work. **Original Theme:** {user_query} **Narrative Phases Completed:** {phase_count}/10 **Detected Progression Issues:** {chr(10).join(issues) if issues else "None"} **Work Content (recent):** {all_content[-4000:]} **Focus Review Areas:** 1. **Narrative Accumulation and Progress** - Is story actually moving forward? - Does each phase connect as result of previous? - No repetition of same conflicts/situations? 2. **Character Transformation Arcs** - How has protagonist changed from beginning? - Are changes credible and gradual? - Dynamic relationship development? 3. **Thematic Deepening** - How has initial theme developed? - New layers added? - Increased complexity? 4. **Length and Density** - Current total word count - Can reach 8,000 word target? **Revision Instructions:** Specific progression directions for each writer.""" } return lang_prompts.get(language, lang_prompts["Korean"]) def create_writer_revision_prompt(self, writer_number: int, initial_content: str, critic_feedback: str, language: str) -> str: """작가 수정 프롬프트""" target_words = MIN_WORDS_PER_WRITER return f"""작가 {writer_number}번, 비평을 반영하여 수정하세요. **초안:** {initial_content} **비평 피드백:** {critic_feedback} **수정 핵심:** 1. 서사 진행성 강화 - 반복 제거, 새로운 전개 추가 2. 인물 변화 구체화 - 이전과 달라진 모습 명확히 3. 분량 확보 - 최소 {target_words}단어 유지 4. 내면 묘사와 사회적 맥락 심화 전면 재작성이 필요하면 과감히 수정하세요. 수정본만 제시하세요.""" def create_critic_final_prompt(self, complete_novel: str, word_count: int, language: str) -> str: """최종 비평 - AGI 평가 기준""" return f"""완성된 소설을 AGI 튜링테스트 기준으로 평가하세요. **작품 정보:** - 총 분량: {word_count}단어 - 목표 분량: 8,000단어 이상 **작품 (마지막 부분):** {complete_novel[-3000:]} **평가 기준 (AGI 튜링테스트):** 1. **장편소설로서의 완성도 (40점)** - 통합된 서사 구조 (반복 없음) - 인물의 명확한 변화 arc - 플롯의 축적과 해결 - 8,000단어 이상 분량 2. **문학적 성취 (30점)** - 주제 의식의 깊이 - 인물 심리의 설득력 - 문체의 일관성과 아름다움 - 상징과 은유의 효과 3. **사회적 통찰 (20점)** - 현대 사회 문제 포착 - 개인과 구조의 연결 - 보편성과 특수성 균형 4. **독창성과 인간성 (10점)** - AI가 아닌 인간 작가의 느낌 - 독창적 표현과 통찰 - 감정적 진정성 **총점: /100점** 특히 '반복 구조' 문제가 있었는지 엄격히 평가하세요.""" # --- LLM 호출 함수들 --- def call_llm_sync(self, messages: List[Dict[str, str]], role: str, language: str) -> str: full_content = "" for chunk in self.call_llm_streaming(messages, role, language): full_content += chunk if full_content.startswith("❌"): raise Exception(f"LLM Call Failed: {full_content}") return full_content def call_llm_streaming(self, messages: List[Dict[str, str]], role: str, language: str) -> Generator[str, None, None]: try: system_prompts = self.get_system_prompts(language) full_messages = [{"role": "system", "content": system_prompts.get(role, "")}, *messages] # 작가 역할일 때는 더 많은 토큰 허용 max_tokens = 15000 if role.startswith("writer") else 10000 payload = { "model": self.model_id, "messages": full_messages, "max_tokens": max_tokens, "temperature": 0.8, "top_p": 0.95, "presence_penalty": 0.5, "frequency_penalty": 0.3, "stream": True } response = requests.post( self.api_url, headers=self.create_headers(), json=payload, stream=True, timeout=180 ) if response.status_code != 200: yield f"❌ API 오류 (상태 코드: {response.status_code})" return buffer = "" for line in response.iter_lines(): if not line: continue try: line_str = line.decode('utf-8').strip() if not line_str.startswith("data: "): continue data_str = line_str[6:] if data_str == "[DONE]": break data = json.loads(data_str) choices = data.get("choices", []) if choices and choices[0].get("delta", {}).get("content"): content = choices[0]["delta"]["content"] buffer += content if len(buffer) >= 50 or '\n' in buffer: yield buffer buffer = "" time.sleep(0.01) except Exception as e: logger.error(f"청크 처리 오류: {str(e)}") continue if buffer: yield buffer except Exception as e: logger.error(f"스트리밍 오류: {type(e).__name__}: {str(e)}") yield f"❌ 오류 발생: {str(e)}" def get_system_prompts(self, language: str) -> Dict[str, str]: """역할별 시스템 프롬프트""" base_prompts = { "Korean": { "director": """당신은 한국 현대 문학의 거장입니다. 반복이 아닌 진행, 순환이 아닌 발전을 통해 하나의 강력한 서사를 구축하세요. 개인의 문제를 사회 구조와 연결하며, 인물의 진정한 변화를 그려내세요.""", "critic": """당신은 엄격한 문학 비평가입니다. 특히 '반복 구조'와 '서사 정체'를 철저히 감시하세요. 작품이 진정한 장편소설인지, 아니면 반복되는 단편의 집합인지 구별하세요.""", "writer_base": """당신은 현대 한국 문학 작가입니다. 이전 단계의 결과를 받아 새로운 국면으로 발전시키세요. 최소 800단어를 작성하며, 내면과 사회를 동시에 포착하세요. 절대 이전과 같은 상황을 반복하지 마세요.""" }, "English": { "director": """You are a master of contemporary literary fiction. Build one powerful narrative through progression not repetition, development not cycles. Connect individual problems to social structures while depicting genuine character transformation.""", "critic": """You are a strict literary critic. Vigilantly monitor for 'repetitive structure' and 'narrative stagnation'. Distinguish whether this is a true novel or a collection of repeated episodes.""", "writer_base": """You are a contemporary literary writer. Take results from previous phase and develop into new territory. Write minimum 800 words, capturing both interior and society. Never repeat previous situations.""" } } prompts = base_prompts.get(language, base_prompts["Korean"]).copy() # 특수 작가 프롬프트 for i in range(1, 11): prompts[f"writer{i}"] = prompts["writer_base"] return prompts # --- 메인 프로세스 --- def process_novel_stream(self, query: str, language: str, session_id: Optional[str] = None) -> Generator[Tuple[str, List[Dict[str, Any]], str], None, None]: """소설 생성 프로세스""" try: resume_from_stage = 0 if session_id: self.current_session_id = session_id session = NovelDatabase.get_session(session_id) if session: query = session['user_query'] language = session['language'] resume_from_stage = session['current_stage'] + 1 # 서사 추적기 복원 saved_tracker = NovelDatabase.load_narrative_tracker(session_id) if saved_tracker: self.narrative_tracker = saved_tracker else: self.current_session_id = NovelDatabase.create_session(query, language) logger.info(f"Created new session: {self.current_session_id}") stages = [] if resume_from_stage > 0: stages = [{ "name": s['stage_name'], "status": s['status'], "content": s.get('content', ''), "word_count": s.get('word_count', 0), "progression_score": s.get('progression_score', 0.0) } for s in NovelDatabase.get_stages(self.current_session_id)] # 총 단어 수 추적 total_words = NovelDatabase.get_total_words(self.current_session_id) for stage_idx in range(resume_from_stage, len(PROGRESSIVE_STAGES)): role, stage_name = PROGRESSIVE_STAGES[stage_idx] if stage_idx >= len(stages): stages.append({ "name": stage_name, "status": "active", "content": "", "word_count": 0, "progression_score": 0.0 }) else: stages[stage_idx]["status"] = "active" yield f"🔄 진행 중... (현재 {total_words:,}단어)", stages, self.current_session_id prompt = self.get_stage_prompt(stage_idx, role, query, language, stages) stage_content = "" for chunk in self.call_llm_streaming([{"role": "user", "content": prompt}], role, language): stage_content += chunk stages[stage_idx]["content"] = stage_content stages[stage_idx]["word_count"] = len(stage_content.split()) yield f"🔄 {stage_name} 작성 중... ({total_words + stages[stage_idx]['word_count']:,}단어)", stages, self.current_session_id # 진행도 평가 if role.startswith("writer"): writer_num = int(re.search(r'\d+', role).group()) progression_score = self.evaluate_progression(stage_content, writer_num) stages[stage_idx]["progression_score"] = progression_score # 서사 추적기 업데이트 self.update_narrative_tracker(stage_content, writer_num) stages[stage_idx]["status"] = "complete" NovelDatabase.save_stage( self.current_session_id, stage_idx, stage_name, role, stage_content, "complete", stages[stage_idx].get("progression_score", 0.0) ) # 서사 추적기 저장 NovelDatabase.save_narrative_tracker(self.current_session_id, self.narrative_tracker) # 총 단어 수 업데이트 total_words = NovelDatabase.get_total_words(self.current_session_id) yield f"✅ {stage_name} 완료 (총 {total_words:,}단어)", stages, self.current_session_id # 최종 소설 정리 final_novel = NovelDatabase.get_writer_content(self.current_session_id) final_word_count = len(final_novel.split()) final_report = self.generate_literary_report(final_novel, final_word_count, language) NovelDatabase.update_final_novel(self.current_session_id, final_novel, final_report) yield f"✅ 소설 완성! 총 {final_word_count:,}단어 (목표: {TARGET_WORDS:,}단어)", stages, self.current_session_id except Exception as e: logger.error(f"소설 생성 프로세스 오류: {e}", exc_info=True) yield f"❌ 오류 발생: {e}", stages if 'stages' in locals() else [], self.current_session_id def get_stage_prompt(self, stage_idx: int, role: str, query: str, language: str, stages: List[Dict]) -> str: """단계별 프롬프트 생성""" if stage_idx == 0: return self.create_director_initial_prompt(query, language) if stage_idx == 1: return self.create_critic_director_prompt(stages[0]["content"], query, language) if stage_idx == 2: return self.create_director_revision_prompt(stages[0]["content"], stages[1]["content"], query, language) master_plan = stages[2]["content"] if 3 <= stage_idx <= 12: # 작가 초안 writer_num = stage_idx - 2 previous_content = self.get_previous_writer_content(stages, writer_num) phase_requirements = self.narrative_tracker.generate_phase_requirements(writer_num) narrative_summary = self.generate_narrative_summary(stages, writer_num) return self.create_writer_prompt( writer_num, master_plan, previous_content, phase_requirements, narrative_summary, language ) if stage_idx == 13: # 비평가 중간 검토 all_content = self.get_all_writer_content(stages, 12) return self.create_critic_consistency_prompt( all_content, self.narrative_tracker, query, language ) if 14 <= stage_idx <= 23: # 작가 수정 writer_num = stage_idx - 13 initial_content = stages[2 + writer_num]["content"] feedback = stages[13]["content"] return self.create_writer_revision_prompt(writer_num, initial_content, feedback, language) if stage_idx == 24: # 최종 검토 complete_novel = self.get_all_writer_content(stages, 23) word_count = len(complete_novel.split()) return self.create_critic_final_prompt(complete_novel, word_count, language) return "" def create_director_revision_prompt(self, initial_plan: str, critic_feedback: str, user_query: str, language: str) -> str: """감독자 수정 프롬프트""" return f"""비평을 반영하여 통합된 서사 구조를 완성하세요. **원 주제:** {user_query} **초기 기획:** {initial_plan} **비평:** {critic_feedback} **핵심 수정 사항:** 1. 반복 구조 완전 제거 2. 10단계가 하나의 이야기로 연결 3. 인물의 명확한 변화 궤적 4. 8,000단어 분량 계획 각 단계가 이전의 필연적 결과가 되도록 수정하세요.""" def get_previous_writer_content(self, stages: List[Dict], current_writer: int) -> str: """이전 작가의 내용 가져오기""" if current_writer == 1: return "" # 바로 이전 작가의 내용 prev_idx = current_writer + 1 # stages 인덱스는 writer_num + 2 if prev_idx < len(stages) and stages[prev_idx]["content"]: return stages[prev_idx]["content"] return "" def get_all_writer_content(self, stages: List[Dict], up_to_stage: int) -> str: """특정 단계까지의 모든 작가 내용""" contents = [] for i, s in enumerate(stages): if i <= up_to_stage and "writer" in s.get("name", "") and s["content"]: contents.append(s["content"]) return "\n\n".join(contents) def generate_narrative_summary(self, stages: List[Dict], up_to_writer: int) -> str: """현재까지의 서사 요약""" if up_to_writer == 1: return "첫 시작입니다." summary_parts = [] for i in range(1, up_to_writer): if i in self.narrative_tracker.phase_summaries: summary_parts.append(f"[{NARRATIVE_PHASES[i-1]}]: {self.narrative_tracker.phase_summaries[i]}") return "\n".join(summary_parts) if summary_parts else "이전 내용을 이어받아 진행하세요." def update_narrative_tracker(self, content: str, writer_num: int): """서사 추적기 업데이트""" # 간단한 요약 생성 (실제로는 더 정교한 분석 필요) lines = content.split('\n') key_events = [line.strip() for line in lines if len(line.strip()) > 50][:3] if key_events: summary = " ".join(key_events[:2])[:200] + "..." self.narrative_tracker.phase_summaries[writer_num] = summary def evaluate_progression(self, content: str, phase: int) -> float: """서사 진행도 평가""" score = 5.0 # 분량 체크 word_count = len(content.split()) if word_count >= MIN_WORDS_PER_WRITER: score += 2.0 # 새로운 요소 체크 if phase > 1: prev_summary = self.narrative_tracker.phase_summaries.get(phase-1, "") if prev_summary and len(set(content.split()) - set(prev_summary.split())) > 100: score += 1.5 # 변화 언급 체크 change_keywords = ['변했', '달라졌', '새로운', '이제는', '더 이상', 'changed', 'different', 'new', 'now', 'no longer'] if any(keyword in content for keyword in change_keywords): score += 1.5 return min(10.0, score) def generate_literary_report(self, complete_novel: str, word_count: int, language: str) -> str: """최종 문학적 평가""" prompt = self.create_critic_final_prompt(complete_novel, word_count, language) try: report = self.call_llm_sync([{"role": "user", "content": prompt}], "critic", language) return report except Exception as e: logger.error(f"최종 보고서 생성 실패: {e}") return "보고서 생성 중 오류 발생" # --- 유틸리티 함수들 --- def process_query(query: str, language: str, session_id: Optional[str] = None) -> Generator[Tuple[str, str, str, str], None, None]: """메인 쿼리 처리 함수""" if not query.strip(): yield "", "", "❌ 주제를 입력해주세요.", session_id return system = ProgressiveLiterarySystem() stages_markdown = "" novel_content = "" for status, stages, current_session_id in system.process_novel_stream(query, language, session_id): stages_markdown = format_stages_display(stages) # 최종 소설 내용 가져오기 if stages and all(s.get("status") == "complete" for s in stages[-10:]): novel_content = NovelDatabase.get_writer_content(current_session_id) novel_content = format_novel_display(novel_content) yield stages_markdown, novel_content, status or "🔄 처리 중...", current_session_id def get_active_sessions(language: str) -> List[str]: """활성 세션 목록""" sessions = NovelDatabase.get_active_sessions() return [f"{s['session_id'][:8]}... - {s['user_query'][:50]}... ({s['created_at']}) [{s['total_words']:,}단어]" for s in sessions] def auto_recover_session(language: str) -> Tuple[Optional[str], str]: """최근 세션 자동 복구""" sessions = NovelDatabase.get_active_sessions() if sessions: latest_session = sessions[0] return latest_session['session_id'], f"세션 {latest_session['session_id'][:8]}... 복구됨" return None, "복구할 세션이 없습니다." def resume_session(session_id: str, language: str) -> Generator[Tuple[str, str, str, str], None, None]: """세션 재개""" if not session_id: yield "", "", "❌ 세션 ID가 없습니다.", session_id return if "..." in session_id: session_id = session_id.split("...")[0] session = NovelDatabase.get_session(session_id) if not session: yield "", "", "❌ 세션을 찾을 수 없습니다.", None return yield from process_query(session['user_query'], session['language'], session_id) def download_novel(novel_text: str, format_type: str, language: str, session_id: str) -> Optional[str]: """소설 다운로드 파일 생성""" if not novel_text or not session_id: return None timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") filename = f"novel_{session_id[:8]}_{timestamp}" try: if format_type == "DOCX" and DOCX_AVAILABLE: return export_to_docx(novel_text, filename, language, session_id) else: return export_to_txt(novel_text, filename) except Exception as e: logger.error(f"파일 생성 실패: {e}") return None def format_stages_display(stages: List[Dict]) -> str: """단계별 진행 상황 표시""" markdown = "## 🎬 진행 상황\n\n" # 총 단어 수 계산 total_words = sum(s.get('word_count', 0) for s in stages if 'writer' in s.get('name', '')) markdown += f"**총 단어 수: {total_words:,} / {TARGET_WORDS:,}**\n\n" for i, stage in enumerate(stages): status_icon = "✅" if stage['status'] == 'complete' else "🔄" if stage['status'] == 'active' else "⏳" markdown += f"{status_icon} **{stage['name']}**" if stage.get('word_count', 0) > 0: markdown += f" ({stage['word_count']:,}단어)" if stage.get('progression_score', 0) > 0: markdown += f" [진행도: {stage['progression_score']:.1f}/10]" markdown += "\n" if stage['content']: preview = stage['content'][:200] + "..." if len(stage['content']) > 200 else stage['content'] markdown += f"> {preview}\n\n" return markdown def format_novel_display(novel_text: str) -> str: """소설 내용 표시""" if not novel_text: return "아직 완성된 내용이 없습니다." formatted = "# 📖 완성된 소설\n\n" # 단어 수 표시 word_count = len(novel_text.split()) formatted += f"**총 분량: {word_count:,}단어 (목표: {TARGET_WORDS:,}단어)**\n\n" formatted += "---\n\n" # 각 단계를 구분하여 표시 sections = novel_text.split('\n\n') for i, section in enumerate(sections): if section.strip(): formatted += f"{section}\n\n" return formatted def export_to_docx(content: str, filename: str, language: str, session_id: str) -> str: """DOCX 파일로 내보내기""" doc = Document() # 페이지 설정 section = doc.sections[0] section.page_height = Inches(11) section.page_width = Inches(8.5) section.top_margin = Inches(1) section.bottom_margin = Inches(1) section.left_margin = Inches(1.25) section.right_margin = Inches(1.25) # 세션 정보 session = NovelDatabase.get_session(session_id) # 제목 페이지 title_para = doc.add_paragraph() title_para.alignment = WD_ALIGN_PARAGRAPH.CENTER if session: title_run = title_para.add_run(session["user_query"]) title_run.font.size = Pt(24) title_run.bold = True # 메타 정보 doc.add_paragraph() meta_para = doc.add_paragraph() meta_para.alignment = WD_ALIGN_PARAGRAPH.CENTER meta_para.add_run(f"생성일: {datetime.now().strftime('%Y년 %m월 %d일')}\n") meta_para.add_run(f"총 단어 수: {len(content.split()):,}단어") # 페이지 나누기 doc.add_page_break() # 본문 스타일 설정 style = doc.styles['Normal'] style.font.name = 'Calibri' style.font.size = Pt(11) style.paragraph_format.line_spacing = 1.5 style.paragraph_format.space_after = Pt(6) # 본문 추가 paragraphs = content.split('\n\n') for para_text in paragraphs: if para_text.strip(): para = doc.add_paragraph(para_text.strip()) # 파일 저장 filepath = f"{filename}.docx" doc.save(filepath) return filepath def export_to_txt(content: str, filename: str) -> str: """TXT 파일로 내보내기""" filepath = f"{filename}.txt" with open(filepath, 'w', encoding='utf-8') as f: f.write(content) return filepath # CSS 스타일 custom_css = """ .gradio-container { background: linear-gradient(135deg, #1e3c72 0%, #2a5298 50%, #1e3c72 100%); min-height: 100vh; } .main-header { background-color: rgba(255, 255, 255, 0.1); backdrop-filter: blur(10px); padding: 30px; border-radius: 12px; margin-bottom: 30px; text-align: center; color: white; border: 1px solid rgba(255, 255, 255, 0.2); } .progress-note { background-color: rgba(255, 223, 0, 0.1); border-left: 3px solid #ffd700; padding: 15px; margin: 20px 0; border-radius: 8px; color: #fff; } .input-section { background-color: rgba(255, 255, 255, 0.1); backdrop-filter: blur(10px); padding: 20px; border-radius: 12px; margin-bottom: 20px; border: 1px solid rgba(255, 255, 255, 0.2); } .session-section { background-color: rgba(255, 255, 255, 0.1); backdrop-filter: blur(10px); padding: 15px; border-radius: 8px; margin-top: 20px; color: white; border: 1px solid rgba(255, 255, 255, 0.2); } #stages-display { background-color: rgba(255, 255, 255, 0.95); padding: 20px; border-radius: 12px; max-height: 600px; overflow-y: auto; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); } #novel-output { background-color: rgba(255, 255, 255, 0.95); padding: 30px; border-radius: 12px; max-height: 700px; overflow-y: auto; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); } .download-section { background-color: rgba(255, 255, 255, 0.9); padding: 15px; border-radius: 8px; margin-top: 20px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } /* 진행 표시기 스타일 */ .progress-bar { background-color: #e0e0e0; height: 20px; border-radius: 10px; overflow: hidden; margin: 10px 0; } .progress-fill { background-color: #4CAF50; height: 100%; transition: width 0.3s ease; } """ # Gradio 인터페이스 생성 def create_interface(): with gr.Blocks(css=custom_css, title="AI 진행형 장편소설 생성 시스템") as interface: gr.HTML("""

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

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

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

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