diff --git "a/app.py" "b/app.py"
--- "a/app.py"
+++ "b/app.py"
@@ -1,6 +1,3 @@
-# 2점대 평가에 불과하다.
-
-
import gradio as gr
import os
import json
@@ -26,7 +23,7 @@ logger = logging.getLogger(__name__)
# --- Document export imports ---
try:
from docx import Document
- from docx.shared import Inches, Pt, RGBColor
+ 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
@@ -41,11 +38,11 @@ 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_v7.db"
+DB_PATH = "novel_sessions_v6.db"
-# 목표 분량 설정 (기존 값으로 복원)
+# 목표 분량 설정
TARGET_WORDS = 8000 # 안전 마진을 위해 8000단어
-MIN_WORDS_PER_WRITER = 800 # 각 작가 최소 분량 800단어
+MIN_WORDS_PER_PART = 800 # 각 파트 최소 분량
# --- 환경 변수 검증 ---
if not FRIENDLI_TOKEN:
@@ -72,499 +69,142 @@ NARRATIVE_PHASES = [
"결말 2: 열린 질문"
]
-# 편집자 단계 사용 여부 (False로 설정하면 편집자 단계 건너뛰기)
-USE_EDITOR_STAGE = False # 편집자가 과도하게 삭제하는 문제로 비활성화
-
-# 단계별 구성 - 편집자 단계 조건부 포함
-PROGRESSIVE_STAGES = [
+# 새로운 단계별 구성 - 단일 작가 시스템
+UNIFIED_STAGES = [
("director", "🎬 감독자: 통합된 서사 구조 기획"),
- ("critic", "📝 비평가: 서사 진행성과 깊이 검토"),
- ("director", "🎬 감독자: 수정된 마스터플랜"),
-] + [
- (f"writer{i}", f"✍️ 작가 {i}: 초안 - {NARRATIVE_PHASES[i-1]}")
- for i in range(1, 11)
+ ("critic_director", "📝 비평가: 서사 구조 심층 검토"),
+ ("director", "🎬 감독자: 최종 마스터플랜"),
] + [
- ("critic", "📝 비평가: 중간 검토 (서사 누적성과 변화)"),
+ item for i in range(1, 11)
+ for item in [
+ ("writer", f"✍️ 작가: 파트 {i} - {NARRATIVE_PHASES[i-1]}"),
+ (f"critic_part{i}", f"📝 파트 {i} 비평가: 즉시 검토 및 수정 요청"),
+ ("writer", f"✍️ 작가: 파트 {i} 수정본")
+ ]
] + [
- (f"writer{i}", f"✍️ 작가 {i}: 수정본 - {NARRATIVE_PHASES[i-1]}")
- for i in range(1, 11)
+ ("critic_final", "📝 최종 비평가: 종합 평가 및 문학적 성취도"),
]
-# 편집자 단계 조건부 추가
-if USE_EDITOR_STAGE:
- PROGRESSIVE_STAGES.extend([
- ("editor", "✂️ 편집자: 반복 제거 및 서사 재구성"),
- ("critic", f"📝 비평가: 최종 검토 및 문학적 평가"),
- ])
-else:
- PROGRESSIVE_STAGES.append(
- ("critic", f"📝 비평가: 최종 검토 및 문학적 평가"),
- )
-
-# 절대 금지사항 - 언어 오류 방지 (간소화)
-STRICT_RULES = """
-금지사항:
-1. 일본어, 중국어 등 외국어 문자 사용 금지
-2. 이전 단계 사건의 단순 반복 금지
-3. 캐릭터 이름 변경 금지
-"""
-
# --- 데이터 클래스 ---
@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
+class StoryBible:
+ """전체 이야기의 일관성을 유지하는 스토리 바이블"""
+ 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 SymbolicEvolution:
- """상징의 의미 변화 추적"""
- symbol: str
- initial_meaning: str
- phase_meanings: Dict[int, str] = field(default_factory=dict)
- transformation_complete: bool = False
-
-@dataclass
-class CharacterConsistency:
- """캐릭터 일관성 관리"""
- primary_names: Dict[str, str] = field(default_factory=dict) # role -> canonical name
- aliases: Dict[str, List[str]] = field(default_factory=dict) # canonical -> aliases
- name_history: List[Tuple[int, str, str]] = field(default_factory=list) # (phase, role, used_name)
-
- def validate_name(self, phase: int, role: str, name: str) -> bool:
- """이름 일관성 검증"""
- if role in self.primary_names:
- canonical = self.primary_names[role]
- if name != canonical and name not in self.aliases.get(canonical, []):
- return False
- return True
-
- def register_name(self, phase: int, role: str, name: str):
- """이름 등록"""
- if role not in self.primary_names:
- self.primary_names[role] = name
- self.name_history.append((phase, role, name))
-
+class PartCritique:
+ """각 파트별 비평 내용"""
+ 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) # 문학성 평가 추가
# --- 핵심 로직 클래스 ---
-class LanguageFilter:
- """언어 혼입 및 특수기호 오류 방지"""
+class UnifiedNarrativeTracker:
+ """단일 작가 시스템을 위한 통합 서사 추적기"""
def __init__(self):
- self.forbidden_patterns = [
- r'[ぁ-ん]+', # 히라가나
- r'[ァ-ヶ]+', # 가타카나
- r'[\u4e00-\u9fff]+', # 한자
- r'\$\s*[.,]', # 특수기호 오류
- r'[는은이가을를에의와과도로부터까지만든한다]\s*\$' # 조사 뒤 특수기호
- ]
+ 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 clean_text(self, text: str) -> str:
- """생성된 텍스트 정제"""
- import re
- cleaned = text
-
- # 일본어 문자 제거
- for pattern in self.forbidden_patterns:
- cleaned = re.sub(pattern, '', cleaned)
+ def update_story_bible(self, element_type: str, key: str, value: Any):
+ """스토리 바이블 업데이트"""
+ 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)
- # 연속 공백 정리
- cleaned = re.sub(r'\s+', ' ', cleaned)
-
- # 문장 끝 정리
- cleaned = re.sub(r'([.!?])\s*\$', r'\1', cleaned)
-
- return cleaned.strip()
-
-
-class ContentDeduplicator:
- """중복 콘텐츠 감지 및 제거"""
- def __init__(self):
- self.seen_paragraphs = set()
- self.seen_key_phrases = set()
- self.similarity_threshold = 0.85
-
- def check_similarity(self, text1: str, text2: str) -> float:
- """두 텍스트의 유사도 측정"""
- # 간단한 Jaccard 유사도 구현
- words1 = set(text1.lower().split())
- words2 = set(text2.lower().split())
-
- intersection = words1.intersection(words2)
- union = words1.union(words2)
-
- return len(intersection) / len(union) if union else 0
-
- def extract_key_phrases(self, text: str) -> List[str]:
- """핵심 문구 추출"""
- # 20자 이상의 문장들을 핵심 문구로 간주
- sentences = [s.strip() for s in re.split(r'[.!?]', text) if len(s.strip()) > 20]
- return sentences[:5] # 상위 5개만
-
- def is_duplicate(self, paragraph: str) -> bool:
- """중복 문단 감지"""
- # 핵심 문구 체크
- key_phrases = self.extract_key_phrases(paragraph)
- for phrase in key_phrases:
- if phrase in self.seen_key_phrases:
- return True
-
- # 전체 문단 유사도 체크
- for seen in self.seen_paragraphs:
- if self.check_similarity(paragraph, seen) > self.similarity_threshold:
- return True
-
- # 중복이 아니면 저장
- self.seen_paragraphs.add(paragraph)
- self.seen_key_phrases.update(key_phrases)
- return False
-
- def get_used_elements(self) -> List[str]:
- """사용된 핵심 요소 반환"""
- return list(self.seen_key_phrases)[:10] # 최근 10개
-
- def count_repetitions(self, content: str) -> int:
- """텍스트 내의 반복 횟수 계산"""
- paragraphs = content.split('\n\n')
- repetitions = 0
-
- for i, para1 in enumerate(paragraphs):
- for para2 in paragraphs[i+1:]:
- if self.check_similarity(para1, para2) > 0.7:
- repetitions += 1
-
- return repetitions
-
-
-class RealTimeConsistencyChecker:
- """실시간 서사 일관성 검증"""
- def __init__(self):
- self.plot_graph = {} # 인과관계 그래프
- self.character_states = {} # 캐릭터 상태 추적
- self.event_timeline = [] # 시간순 이벤트
- self.resolved_conflicts = set() # 해결된 갈등
+ def add_part_critique(self, part_number: int, critique: PartCritique):
+ """파트별 비평 추가"""
+ self.part_critiques[part_number] = critique
- def validate_new_content(self, phase: int, content: str,
- previous_contents: List[str]) -> Tuple[bool, List[str]]:
- """새 콘텐츠의 일관성 검증"""
+ def check_continuity(self, current_part: int, new_content: str) -> List[str]:
+ """연속성 검사"""
issues = []
- # 1. 시간 역행 검사
- time_markers = self.extract_time_markers(content)
- if self.check_time_contradiction(time_markers):
- issues.append("시간 순서 모순 발견")
-
- # 2. 캐릭터 상태 모순 검사
- character_actions = self.extract_character_actions(content)
- for char, action in character_actions.items():
- if not self.is_action_possible(char, action, phase):
- issues.append(f"{char}의 행동이 이전 상태와 모순됨")
-
- # 3. 이미 해결된 갈등의 재등장 검사
- conflicts = self.extract_conflicts(content)
- for conflict in conflicts:
- if conflict in self.resolved_conflicts:
- issues.append(f"이미 해결된 갈등 '{conflict}'이 다시 등장")
-
- return len(issues) == 0, issues
-
- def extract_time_markers(self, content: str) -> List[str]:
- """시간 표지 추출"""
- markers = re.findall(r'(아침|점심|저녁|밤|새벽|오전|오후|다음날|며칠 후|일주일 후)', content)
- return markers
-
- def extract_character_actions(self, content: str) -> Dict[str, str]:
- """캐릭터 행동 추출"""
- actions = {}
- # 간단한 패턴 매칭 (실제로는 더 정교한 NLP 필요)
- patterns = re.findall(r'(\w+)(?:이|가|은|는)\s+(\w+(?:했다|한다|하다))', content)
- for char, action in patterns:
- actions[char] = action
- return actions
-
- def extract_conflicts(self, content: str) -> List[str]:
- """갈등 요소 추출"""
- conflict_keywords = ['갈등', '대립', '충돌', '문제', '위기']
- conflicts = []
- for keyword in conflict_keywords:
- if keyword in content:
- # 주변 문맥 추출
- idx = content.index(keyword)
- context = content[max(0, idx-20):min(len(content), idx+20)]
- conflicts.append(context)
- return conflicts
-
- def check_time_contradiction(self, markers: List[str]) -> bool:
- """시간 모순 검사"""
- # 간단한 시간 순서 체크
- time_order = ['새벽', '아침', '오전', '점심', '오후', '저녁', '밤']
- prev_idx = -1
- for marker in markers:
- if marker in time_order:
- curr_idx = time_order.index(marker)
- if curr_idx < prev_idx:
- return True
- prev_idx = curr_idx
- return False
-
- def is_action_possible(self, character: str, action: str, phase: int) -> bool:
- """캐릭터 행동 가능성 검사"""
- # 캐릭터 상태 기반 검증
- if character not in self.character_states:
- self.character_states[character] = {"phase": phase, "status": "active"}
- return True
+ # 캐릭터 일관성 체크
+ 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}의 버려진 특성 '{trait['name']}'이 다시 나타남")
+
+ # 시간선 일관성 체크
+ if len(self.story_bible.timeline) > 0:
+ last_event = self.story_bible.timeline[-1]
+ # 시간 역행 체크 등
- char_state = self.character_states[character]
- # 예: 죽은 캐릭터가 다시 등장하는 경우 등
- if char_state.get("status") == "dead" and "했다" in action:
- return False
-
- return True
-
-
-class ProgressionMonitor:
- """실시간 서사 진행 모니터링"""
- def __init__(self):
- self.phase_keywords = {}
- self.locations = set()
- self.characters = set()
-
- def count_new_characters(self, content: str, phase: int) -> int:
- """새로운 인물 등장 횟수"""
- # 간단한 고유명사 추출 (대문자로 시작하는 단어)
- potential_names = re.findall(r'\b[A-Z가-힣][a-z가-힣]+\b', content)
- new_chars = set(potential_names) - self.characters
- self.characters.update(new_chars)
- return len(new_chars)
-
- def count_new_locations(self, content: str, phase: int) -> int:
- """새로운 장소 등장 횟수"""
- # 장소 관련 키워드
- location_markers = ['에서', '으로', '에', '의', 'at', 'in', 'to']
- new_locs = 0
-
- for marker in location_markers:
- matches = re.findall(rf'(\S+)\s*{marker}', content)
- for match in matches:
- if match not in self.locations and len(match) > 2:
- self.locations.add(match)
- new_locs += 1
-
- return new_locs
-
- def calculate_content_difference(self, current_phase: int, content: str, previous_content: str) -> float:
- """이전 단계와의 내용 차이 비율"""
- if not previous_content:
- return 1.0
+ # 인과관계 체크
+ if current_part > 1 and not any(kw in new_content for kw in
+ ['때문에', '그래서', '결과', '이로 인해', 'because', 'therefore']):
+ issues.append("이전 파트와의 인과관계가 불명확함")
- dedup = ContentDeduplicator()
- return 1.0 - dedup.check_similarity(content, previous_content)
-
- def count_repetitions(self, content: str) -> int:
- """반복 횟수 계산"""
- paragraphs = content.split('\n\n')
- repetitions = 0
-
- for i, para1 in enumerate(paragraphs):
- for para2 in paragraphs[i+1:]:
- similarity = ContentDeduplicator().check_similarity(para1, para2)
- if similarity > 0.7:
- repetitions += 1
-
- return repetitions
+ return issues
- def calculate_progression_score(self, current_phase: int, content: str, previous_content: str = "") -> Dict[str, float]:
- """진행도 점수 계산"""
-
- scores = {
- "new_elements": 0.0, # 새로운 요소
- "character_growth": 0.0, # 인물 성장
- "plot_advancement": 0.0, # 플롯 진전
- "no_repetition": 0.0 # 반복 없음
- }
-
- # 새로운 요소 체크
- new_characters = self.count_new_characters(content, current_phase)
- new_locations = self.count_new_locations(content, current_phase)
- scores["new_elements"] = min(10, (new_characters * 3 + new_locations * 2))
-
- # 성장 관련 키워드
- growth_keywords = ["깨달았다", "이제는", "달라졌다", "새롭게", "비로소", "변했다", "더 이상"]
- growth_count = sum(1 for k in growth_keywords if k in content)
- scores["character_growth"] = min(10, growth_count * 2)
-
- # 플롯 진전 (이전 단계와의 차이)
- if current_phase > 1 and previous_content:
- diff_ratio = self.calculate_content_difference(current_phase, content, previous_content)
- scores["plot_advancement"] = min(10, diff_ratio * 10)
- else:
- scores["plot_advancement"] = 8.0 # 첫 단계는 기본 점수
-
- # 반복 체크 (역산)
- repetition_count = self.count_repetitions(content)
- scores["no_repetition"] = max(0, 10 - repetition_count * 2)
-
- return scores
-
-
-class ProgressiveNarrativeTracker:
- """서사 진행과 누적을 추적하는 시스템"""
- def __init__(self):
- 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] = []
- self.philosophical_insights: List[str] = [] # 철학적 통찰 추적
- self.literary_devices: Dict[int, List[str]] = {} # 문학적 기법 사용 추적
- self.character_consistency = CharacterConsistency() # 캐릭터 일관성 추가
- self.content_deduplicator = ContentDeduplicator() # 중복 감지기 추가
- self.progression_monitor = ProgressionMonitor() # 진행도 모니터 추가
- self.used_expressions: Set[str] = set() # 사용된 표현 추적
- self.consistency_checker = RealTimeConsistencyChecker() # 실시간 일관성 체커 추가
-
- def register_character_arc(self, name: str, initial_state: Dict[str, Any]):
- """캐릭터 아크 등록"""
- self.character_arcs[name] = CharacterArc(name=name, initial_state=initial_state)
- self.character_consistency.register_name(0, "protagonist", name)
- logger.info(f"Character arc registered: {name}")
+ def calculate_narrative_momentum(self, part_number: int, content: str) -> float:
+ """서사적 추진력 계산"""
+ momentum = 5.0
- 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}")
+ # 새로운 요소 도입
+ new_elements = len(set(content.split()) - set(' '.join(self.accumulated_content).split()))
+ if new_elements > 100:
+ momentum += 2.0
- 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)
+ # 갈등의 고조
+ tension_words = ['위기', '갈등', '충돌', '대립', 'crisis', 'conflict']
+ if any(word in content for word in tension_words):
+ momentum += 1.5
- def check_narrative_progression(self, current_phase: int) -> Tuple[bool, List[str]]:
- """서사가 실제로 진행되고 있는지 확인"""
- issues = []
+ # 인과관계 명확성
+ causal_words = ['때문에', '그래서', '따라서', 'because', 'therefore']
+ causal_count = sum(1 for word in causal_words if word in content)
+ momentum += min(causal_count * 0.5, 2.0)
- # 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 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
- if static_symbols:
- issues.append(f"의미가 발전하지 않은 상징: {', '.join(static_symbols)}")
-
- # 4. 철학적 깊이 확인
- if len(self.philosophical_insights) < current_phase // 2:
- issues.append("철학적 성찰과 인간에 대한 통찰이 부족합니다")
-
- # 5. 문학적 기법 다양성
- unique_devices = set()
- for devices in self.literary_devices.values():
- unique_devices.update(devices)
- if len(unique_devices) < 5:
- issues.append("문학적 기법이 단조롭습니다. 더 다양한 표현 기법이 필요합니다")
-
- # 6. 캐릭터 이름 일관성
- name_issues = []
- for phase, role, name in self.character_consistency.name_history:
- if not self.character_consistency.validate_name(phase, role, name):
- name_issues.append(f"Phase {phase}: {role} 이름 불일치 ({name})")
- if name_issues:
- issues.extend(name_issues)
-
- # 7. 인과관계 체크 (추가)
- if current_phase > 3:
- # 사건의 연쇄성 확인
- if len(self.accumulated_events) < current_phase - 1:
- issues.append("사건 간 인과관계가 불명확합니다. 각 사건이 다음 사건의 원인이 되어야 합니다")
-
- 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][:200]}...")
-
- # 사용된 표현 목록 (5개만)
- if self.used_expressions:
- requirements.append("\n❌ 다음 표현은 이미 사용됨:")
- for expr in list(self.used_expressions)[-5:]: # 최근 5개만
- requirements.append(f"- {expr[:50]}...")
-
- # 단계별 특수 요구사항
- phase_name = NARRATIVE_PHASES[phase-1] if phase <= 10 else "수정"
-
- requirements.append(f"\n✅ {phase_name} 필수 포함:")
- requirements.append(f"- 최소 {MIN_WORDS_PER_WRITER}단어 작성")
- requirements.append("- 구체적인 장면 묘사와 대화")
- requirements.append("- 인물의 내면 탐구")
- requirements.append("- 이전 단계의 결과로 시작")
-
- return "\n".join(requirements)
-
- def extract_used_elements(self, content: str):
- """사용된 핵심 표현 추출 및 저장"""
- # 20자 이상의 특징적인 문장들 추출
- sentences = re.findall(r'[^.!?]+[.!?]', content)
- for sent in sentences:
- if len(sent) > 20 and len(sent) < 100:
- self.used_expressions.add(sent.strip())
-
+ return max(0.0, min(10.0, momentum))
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,
@@ -577,10 +217,13 @@ class NovelDatabase:
final_novel TEXT,
literary_report TEXT,
total_words INTEGER DEFAULT 0,
- narrative_tracker TEXT
+ story_bible TEXT,
+ narrative_tracker TEXT,
+ opening_sentence TEXT
)
''')
+ # 스테이지 테이블
cursor.execute('''
CREATE TABLE IF NOT EXISTS stages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -591,9 +234,7 @@ class NovelDatabase:
content TEXT,
word_count INTEGER DEFAULT 0,
status TEXT DEFAULT 'pending',
- progression_score REAL DEFAULT 0.0,
- repetition_score REAL DEFAULT 0.0,
- consistency_check TEXT,
+ 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),
@@ -601,41 +242,13 @@ class NovelDatabase:
)
''')
+ # 비평 테이블
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)
- )
- ''')
-
- # 새로운 테이블: 중복 감지 기록
- cursor.execute('''
- CREATE TABLE IF NOT EXISTS duplicate_detection (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- session_id TEXT NOT NULL,
- phase INTEGER NOT NULL,
- duplicate_content TEXT,
- original_phase INTEGER,
- similarity_score REAL,
- created_at TEXT DEFAULT (datetime('now')),
- FOREIGN KEY (session_id) REFERENCES sessions(session_id)
- )
- ''')
-
- # 새로운 테이블: 품질 평가 기록
- cursor.execute('''
- CREATE TABLE IF NOT EXISTS quality_evaluations (
+ CREATE TABLE IF NOT EXISTS critiques (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL,
- evaluation_type TEXT NOT NULL,
- score REAL,
- details TEXT,
+ part_number INTEGER NOT NULL,
+ critique_data TEXT,
created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (session_id) REFERENCES sessions(session_id)
)
@@ -643,7 +256,6 @@ class NovelDatabase:
conn.commit()
- # 기존 메서드들 유지
@staticmethod
@contextmanager
def get_db():
@@ -669,18 +281,20 @@ class NovelDatabase:
@staticmethod
def save_stage(session_id: str, stage_number: int, stage_name: str,
role: str, content: str, status: str = 'complete',
- progression_score: float = 0.0, repetition_score: float = 0.0,
- consistency_check: str = ""):
+ 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, progression_score, repetition_score, consistency_check)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ 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=?, progression_score=?, repetition_score=?, consistency_check=?, updated_at=datetime('now')
- ''', (session_id, stage_number, stage_name, role, content, word_count, status, progression_score, repetition_score, consistency_check,
- content, word_count, status, stage_name, progression_score, repetition_score, consistency_check))
+ 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))
# 총 단어 수 업데이트
cursor.execute('''
@@ -688,7 +302,7 @@ class NovelDatabase:
SET total_words = (
SELECT SUM(word_count)
FROM stages
- WHERE session_id = ? AND role LIKE 'writer%' AND content IS NOT NULL
+ WHERE session_id = ? AND role = 'writer' AND content IS NOT NULL
),
updated_at = datetime('now'),
current_stage = ?
@@ -698,56 +312,59 @@ class NovelDatabase:
conn.commit()
@staticmethod
- def get_writer_content(session_id: str) -> str:
- """작가 콘텐츠 가져오기 (수정본 우선)"""
+ def save_critique(session_id: str, part_number: int, critique: PartCritique):
+ """비평 저장"""
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)
+ 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 get_total_words(session_id: str) -> int:
- """총 단어 수 가져오기"""
+ def save_opening_sentence(session_id: str, opening_sentence: str):
+ """첫문장 저장"""
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
+ conn.cursor().execute(
+ 'UPDATE sessions SET opening_sentence = ? WHERE session_id = ?',
+ (opening_sentence, session_id)
+ )
+ conn.commit()
@staticmethod
- def save_narrative_tracker(session_id: str, tracker: ProgressiveNarrativeTracker):
- """서사 추적기 저장"""
+ def get_writer_content(session_id: str) -> str:
+ """작가 콘텐츠 가져오기 - 모든 수정본 통합"""
+ with NovelDatabase.get_db() as conn:
+ rows = conn.cursor().execute('''
+ SELECT content FROM stages
+ WHERE session_id = ? AND role = 'writer'
+ AND stage_name LIKE '%수정본%'
+ ORDER BY stage_number
+ ''', (session_id,)).fetchall()
+
+ if rows:
+ return '\n\n'.join(row['content'] for row in rows if row['content'])
+ else:
+ # 수정본이 없으면 초안들을 사용
+ rows = conn.cursor().execute('''
+ SELECT content FROM stages
+ WHERE session_id = ? AND role = 'writer'
+ AND stage_name NOT LIKE '%수정본%'
+ 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):
+ """통합 서사 추적기 저장"""
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,
- 'philosophical_insights': tracker.philosophical_insights,
- 'literary_devices': tracker.literary_devices,
- 'character_consistency': asdict(tracker.character_consistency),
- 'used_expressions': list(tracker.used_expressions)
+ '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 = ?',
@@ -756,8 +373,8 @@ class NovelDatabase:
conn.commit()
@staticmethod
- def load_narrative_tracker(session_id: str) -> Optional[ProgressiveNarrativeTracker]:
- """서사 추적기 로드"""
+ def load_narrative_tracker(session_id: str) -> Optional[UnifiedNarrativeTracker]:
+ """통합 서사 추적기 로드"""
with NovelDatabase.get_db() as conn:
row = conn.cursor().execute(
'SELECT narrative_tracker FROM sessions WHERE session_id = ?',
@@ -766,69 +383,46 @@ class NovelDatabase:
if row and row['narrative_tracker']:
data = json.loads(row['narrative_tracker'])
- tracker = ProgressiveNarrativeTracker()
+ tracker = UnifiedNarrativeTracker()
- # 데이터 복원
- 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', [])
- tracker.philosophical_insights = data.get('philosophical_insights', [])
- tracker.literary_devices = data.get('literary_devices', {})
+ # 스토리 바이블 복원
+ bible_data = data.get('story_bible', {})
+ tracker.story_bible = StoryBible(**bible_data)
- # 캐릭터 일관성 복원
- if 'character_consistency' in data:
- tracker.character_consistency = CharacterConsistency(**data['character_consistency'])
-
- # 사용된 표현 복원
- if 'used_expressions' in data:
- tracker.used_expressions = set(data['used_expressions'])
+ # 비평 복원
+ 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
- @staticmethod
- def save_duplicate_detection(session_id: str, phase: int, duplicate_content: str,
- original_phase: int, similarity_score: float):
- """중복 감지 기록 저장"""
- with NovelDatabase.get_db() as conn:
- conn.cursor().execute('''
- INSERT INTO duplicate_detection
- (session_id, phase, duplicate_content, original_phase, similarity_score)
- VALUES (?, ?, ?, ?, ?)
- ''', (session_id, phase, duplicate_content, original_phase, similarity_score))
- conn.commit()
-
- @staticmethod
- def save_quality_evaluation(session_id: str, evaluation_type: str, score: float, details: str):
- """품질 평가 저장"""
- with NovelDatabase.get_db() as conn:
- conn.cursor().execute('''
- INSERT INTO quality_evaluations
- (session_id, evaluation_type, score, details)
- VALUES (?, ?, ?, ?)
- ''', (session_id, evaluation_type, score, details))
- conn.commit()
-
+ # 기존 메서드들 유지
@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()
+ 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()
+ 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 = ?",
+ '''UPDATE sessions SET final_novel = ?, status = 'complete',
+ updated_at = datetime('now'), literary_report = ? WHERE session_id = ?''',
(final_novel, literary_report, session_id)
)
conn.commit()
@@ -837,15 +431,20 @@ class NovelDatabase:
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,
- COALESCE(total_words, 0) as total_words
- FROM sessions
- WHERE status = 'active'
- ORDER BY updated_at DESC
- LIMIT 10"""
+ '''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:
+ """총 단어 수 가져오기"""
+ 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:
"""웹 검색 기능"""
@@ -894,1371 +493,1072 @@ class WebSearchIntegration:
return "\n".join(extracted)
-class ProgressiveLiterarySystem:
- """진행형 문학 소설 생성 시스템"""
+class UnifiedLiterarySystem:
+ """단일 작가 진행형 문학 소설 생성 시스템"""
def __init__(self):
self.token = FRIENDLI_TOKEN
self.api_url = API_URL
self.model_id = MODEL_ID
- self.narrative_tracker = ProgressiveNarrativeTracker()
+ self.narrative_tracker = UnifiedNarrativeTracker()
self.web_search = WebSearchIntegration()
- self.language_filter = LanguageFilter() # 언어 필터 추가
self.current_session_id = None
NovelDatabase.init_db()
- def augment_prompt_if_needed(self, user_query: str, language: str) -> str:
- """프롬프트 증강 - 특별한 지시가 없을 때 예시 형식으로 증강"""
- # 특별한 지시 키워드 체크
- special_keywords = ['인과관계', '캐릭터 일관성', '설정 일관성', '문체 일관성',
- 'causality', 'character consistency', 'setting consistency', 'style consistency']
-
- has_special_instruction = any(keyword in user_query.lower() for keyword in special_keywords)
-
- if has_special_instruction:
- return user_query
-
- # 특별한 지시가 없으면 예시 형식으로 증강
- example_augmentation = {
- "Korean": f"""인과관계 치밀함: {user_query}. 모든 사건은 필연적 인과관계로 연결되며, 외부 충격이 내적 갈등을 심화시키고, 각 선택이 다음 국면을 결정한다. 우연은 없고 모든 장면이 결말을 향한 필수 요소가 된다.
-캐릭터 일관성: 주인공은 처음부터 끝까지 핵심 가치관을 유지하되, 사건을 통해 그 가치관의 새로운 면을 발견한다. 각 인물은 고유한 말투와 행동 패턴을 가지며, 갈등 상황에서도 캐릭터의 본질에서 벗어나지 않는다.
-설정 일관성: 핵심 상징물은 이야기 전체를 관통하며 점진적으로 의미가 확장된다. 공간과 시간 설정은 단순한 배경이 아니라 서사의 필수 요소로 기능한다.
-문체 일관성: 선택한 서술 시점과 문체 톤을 끝까지 유지하되, 서사의 흐름에 따라 리듬과 호흡을 조절한다. 장르적 특성을 살린 문체로 독자를 몰입시킨다.""",
-
- "English": f"""Causal Tightness: {user_query}. All events connect through necessary causality, external shocks deepen internal conflicts, each choice determines the next phase. No coincidences - every scene is essential to the conclusion.
-Character Consistency: Protagonist maintains core values throughout while discovering new facets through events. Each character has unique speech patterns and behaviors, staying true to essence even in conflict.
-Setting Consistency: Core symbols pervade the entire story with gradually expanding meanings. Space and time settings function as narrative necessities, not mere backdrops.
-Style Consistency: Maintain chosen POV and tone throughout while adjusting rhythm and pacing to narrative flow. Genre-appropriate style immerses readers."""
- }
-
- return example_augmentation.get(language, example_augmentation["Korean"])
-
def create_headers(self):
return {"Authorization": f"Bearer {self.token}", "Content-Type": "application/json"}
# --- 프롬프트 생성 함수들 ---
+ def augment_query(self, user_query: str, language: str) -> str:
+ """프롬프트 증강"""
+ 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:
+ """주제에 맞는 강렬한 첫문장 생성"""
+
+ 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:
- """감독자 초기 기획 - 통합된 서사 구조"""
- # 프롬프트 증강 - 특별한 지시가 없을 때 예시 형식으로 증강
- augmented_query = self.augment_prompt_if_needed(user_query, language)
+ """감독자 초기 기획 - 강화된 버전"""
+ augmented_query = self.augment_query(user_query, language)
+
+ # 첫문장 생성
+ 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} 철학적 의미", # 철학적 관점
- f"인간 존재 의미 {short_query}", # 실존적 주제
- f"{short_query} 문학 작품",
+ f"{short_query} 철학적 의미",
+ f"인간 존재 의미 {short_query}",
+ f"{short_query} 문학 작품"
]
- for q in queries[:2]: # 2개까지만 검색
+ 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)[:500] + "\n"
+ search_results_str += self.web_search.extract_relevant_info(results) + "\n"
except Exception as e:
- logger.warning(f"검색 쿼리 실패: {q[:50]}... - {str(e)}")
- continue
+ logger.warning(f"검색 실패: {str(e)}")
lang_prompts = {
- "Korean": f"""당신은 노벨문학상 수상작가 수준의 한국 문학 거장입니다.
-중편 소설(8,000단어)을 위한 통합된 서사 구조를 기획하세요.
+ "Korean": f"""노벨문학상 수준의 철학적 깊이를 지닌 중편소설(8,000단어)을 기획하세요.
**주제:** {augmented_query}
-**참고 자료:**
-{search_results_str[:500] if search_results_str else "N/A"}
-
-**필수 요구사항:**
+**필수 첫문장:** {opening_sentence}
-1. **인과관계가 치밀한 통합 서사**
- - 10개 단계가 필연적으로 연결된 단일 서사
- - 각 단계는 이전 단계의 직접적 결과로 이어짐
- - 외부 충격과 내적 갈등의 인과관계 명확히
- - 주인공과 조연들의 이름/설정 처음부터 고정
-
- 단계별 진행:
- 1) 도입: 일상과 균열
- 2) 발전 1: 불안의 고조
- 3) 발전 2: 외부 충격
- 4) 발전 3: 내적 갈등 심화
- 5) 절정 1: 위기의 정점
- 6) 절정 2: 선택의 순간
- 7) 하강 1: 결과와 여파
- 8) 하강 2: 새로운 인식
- 9) 결말 1: 변화된 일상
- 10) 결말 2: 열린 질문
-
-2. **캐릭터 일관성과 입체성**
- - 각 인물의 핵심 가치관/성격 명시
- - 주인공: 초기 → 중간 → 최종 상태의 변화 궤적
- - 조연들: 고유한 말투와 행동 패턴 설정
- - 갈등이 인물의 본질을 드러내도록 설계
-
-3. **설정과 상징의 일관된 활용**
- - 핵심 상징물(1-2개)의 의미 변화 추적
- - 공간/시간 설정의 서사적 기능 명시
- - 반복되는 모티프의 점진적 의미 확장
-
-4. **문체 일관성 계획**
- - 서술 시점과 문체 톤 결정
- - 장르적 특성을 살린 문체 전략
- - 리듬과 호흡의 변주 계획
-
-5. **분량 계획**
- - 총 8,000단어
- - 각 단계 800단어
-
-치밀하고 유기적인 계획을 제시하세요.""",
-
- "English": f"""You are a Nobel Prize-winning literary master.
-Plan an integrated narrative structure for a novella (8,000 words).
+**참고 자료:**
+{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}
-**Reference:**
-{search_results_str[:500] if search_results_str else "N/A"}
-
-**Requirements:**
-
-1. **Causally Tight Integrated Structure**
- - 10 phases connected by necessity
- - Each phase as direct result of previous
- - Clear external conflicts and internal struggles
- - Fixed protagonist/supporting cast names
-
-2. **Character Consistency & Depth**
- - Core values/personality for each character
- - Protagonist's transformation trajectory
- - Unique speech patterns and behaviors
- - Conflicts reveal character essence
-
-3. **Consistent Settings & Symbols**
- - 1-2 core symbols with evolving meanings
- - Narrative function of space/time settings
- - Progressive expansion of recurring motifs
+**Required Opening:** {opening_sentence}
-4. **Stylistic Consistency Plan**
- - Narrative POV and tone decisions
- - Genre-appropriate style strategy
- - Rhythm and pacing variations
-
-5. **Length Plan**
- - Total 8,000 words
- - 800 words per phase
-
-Present precise, organic plan."""
+**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**
+ [Details as above]
+
+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_writer_prompt_enhanced(self, writer_number: int, director_plan: str,
- previous_content: str, phase_requirements: str,
- narrative_summary: str, language: str,
- used_elements: List[str]) -> str:
- """강화된 작가 프롬프트 - 분량 문제 해결"""
-
- phase_name = NARRATIVE_PHASES[writer_number-1]
- target_words = MIN_WORDS_PER_WRITER
-
- # 현재까지 총 단어 수
- total_words = NovelDatabase.get_total_words(self.current_session_id) if self.current_session_id else 0
- remaining_words = TARGET_WORDS - total_words
-
+ def create_critic_director_prompt(self, director_plan: str, user_query: str, language: str) -> str:
+ """감독자 기획 심층 검토 - 강화된 버전"""
lang_prompts = {
- "Korean": f"""당신은 작가 {writer_number}번입니다.
-**현재 단계: {phase_name}**
-
-**전체 계획 (요약):**
-{director_plan[:800]}
-
-**이전 내용:**
-{previous_content[-800:] if previous_content else "시작"}
+ "Korean": f"""서사 구조 전문가로서 이 기획을 심층 분석하세요.
-**현재 진행 상황:**
-- 현재까지 총 단어: {total_words}
-- 목표까지 남은 단어: {remaining_words}
-- 이번 단계 최소 단어: {target_words}
-
-**작성 지침:**
+**원 주제:** {user_query}
-1. **필수 분량: {target_words}-1000 단어**
-
-2. **서사의 필연성 확보:**
- - 이전 단계의 직접적 결과로 시작
- - 새로운 사건은 기존 갈등의 논리적 귀결
- - 인물의 선택이 다음 국면을 결정
- - 우연이 아닌 필연으로 전개
+**감독자 기획:**
+{director_plan}
-3. **캐릭터 일관성 유지:**
- - 설정된 인물명과 관계 엄수
- - 각 인물의 고유 말투 유지
- - 성격의 일관성 속에서 변화 표현
- - 행동이 캐릭터의 본질에서 출발
+**심층 검토 항목:**
-4. **구체적 장면 구성:**
- - 감각적 묘사 (시각, 청각, 촉각 등)
- - 생생한 대화 (인물별 어투 차별화)
- - 내면 심리의 구체적 표현
- - 공간과 시간의 명확한 설정
+1. **인과관계 검증**
+ 각 파트 간 연결을 검토하고 논리적 비약을 찾으세요:
+ - 파트 1→2: [연결성 평가]
+ - 파트 2→3: [연결성 평가]
+ (모든 연결 지점 검토)
-5. **금지사항:**
- - 이전 상황 단순 반복
- - 캐릭터 이름/설정 변경
- - 갑작스러운 설정 변화
- - 분량 미달
+2. **철학적 깊이 평가**
+ - 제시된 철학적 주제가 충분히 깊은가?
+ - 현대적 관련성이 있는가?
+ - 독창적 통찰이 있는가?
-**중요: 반드시 {target_words}단어 이상, 인과관계가 명확한 서사를 작성하세요!**
+3. **문학적 장치의 효과성**
+ - 은유와 상징이 유기적으로 작동하는가?
+ - 과도하거나 부족하지 않은가?
+ - 주제와 긴밀히 연결되는가?
-이제 시작하세요:""",
+4. **캐릭터 아크 실현 가능성**
+ - 변화가 충분히 점진적인가?
+ - 각 단계의 동기가 명확한가?
+ - 심리적 신뢰성이 있는가?
- "English": f"""You are Writer #{writer_number}.
-**Current Phase: {phase_name}**
+5. **8,000단어 실현 가능성**
+ - 각 파트가 800단어를 유지할 수 있는가?
+ - 늘어지거나 압축되는 부분은 없는가?
-**Overall Plan (Summary):**
-{director_plan[:800]}
+**필수 개선사항을 구체적으로 제시하세요.**""",
-**Previous Content:**
-{previous_content[-800:] if previous_content else "Beginning"}
+ "English": f"""As narrative structure expert, deeply analyze this plan.
-**Progress Status:**
-- Total words so far: {total_words}
-- Words remaining to target: {remaining_words}
-- Minimum words this phase: {target_words}
+**Original Theme:** {user_query}
-**Writing Guidelines:**
+**Director's Plan:**
+{director_plan}
-1. **Required Length: {target_words}-1000 words**
+**Deep Review Items:**
-2. **Narrative Necessity:**
- - Start from direct results of previous
- - New events as logical consequences
- - Character choices determine next phase
- - Necessity, not coincidence
+1. **Causality Verification**
+ Review connections between parts, find logical leaps:
+ - Part 1→2: [Connection assessment]
+ - Part 2→3: [Connection assessment]
+ (Review all connection points)
-3. **Character Consistency:**
- - Maintain established names/relationships
- - Keep unique speech patterns
- - Express change within consistent personality
- - Actions stem from character essence
+2. **Philosophical Depth Assessment**
+ - Is philosophical theme deep enough?
+ - Contemporary relevance?
+ - Original insights?
-4. **Concrete Scene Construction:**
- - Sensory descriptions (visual, auditory, tactile)
- - Vivid dialogue (differentiated speech)
- - Specific psychological expression
- - Clear spatial/temporal settings
+3. **Literary Device Effectiveness**
+ - Do metaphors and symbols work organically?
+ - Not excessive or insufficient?
+ - Tightly connected to theme?
-5. **Forbidden:**
- - Simple repetition
- - Character name/setting changes
- - Sudden setting shifts
- - Under word count
+4. **Character Arc Feasibility**
+ - Is change sufficiently gradual?
+ - Are motivations clear at each stage?
+ - Psychological credibility?
-**IMPORTANT: Must write {target_words}+ words with clear causality!**
+5. **8,000-word Feasibility**
+ - Can each part sustain 800 words?
+ - Any dragging or compressed sections?
-Begin now:"""
+**Provide specific required improvements.**"""
}
return lang_prompts.get(language, lang_prompts["Korean"])
- def create_critic_consistency_prompt_enhanced(self, all_content: str,
- narrative_tracker: ProgressiveNarrativeTracker,
- user_query: str, language: str) -> str:
- """강화된 비평가 중간 검토"""
-
- # 서사 진행 체크
- phase_count = len(narrative_tracker.phase_summaries)
- progression_ok, issues = narrative_tracker.check_narrative_progression(phase_count)
-
- # 중복 감지
- duplicates = []
- paragraphs = all_content.split('\n\n')
- for i, para1 in enumerate(paragraphs[:10]): # 최근 10개만
- for j, para2 in enumerate(paragraphs[i+1:i+11]):
- if narrative_tracker.content_deduplicator.check_similarity(para1, para2) > 0.7:
- duplicates.append(f"문단 {i+1}과 문단 {i+j+2} 유사")
+ def create_writer_prompt(self, part_number: int, master_plan: str,
+ accumulated_content: str, story_bible: StoryBible,
+ language: str) -> str:
+ """단일 작가 프롬프트 - 강화된 버전"""
+
+ phase_name = NARRATIVE_PHASES[part_number-1]
+ target_words = MIN_WORDS_PER_PART
- # 인과관계 체크 (추가)
- causality_issues = []
- if phase_count > 3:
- # 각 단계가 이전 단계의 결과인지 확인
- if "그러나 갑자기" in all_content or "우연히" in all_content or "뜻밖에" in all_content:
- causality_issues.append("우연적 사건 발생 - 필연적 전개 필요")
+ # 파트별 특별 지침
+ philosophical_focus = {
+ 1: "일상의 균열을 통해 실존적 불안 도입",
+ 2: "개인과 사회의 첫 충돌",
+ 3: "타자와의 만남을 통한 자아 인식",
+ 4: "신념의 흔들림과 가치관의 충돌",
+ 5: "선택의 무게와 자유의 역설",
+ 6: "극한 상황에서의 인간성 시험",
+ 7: "행동의 결과와 책임의 무게",
+ 8: "타자의 시선을 통한 자기 재발견",
+ 9: "화해 불가능한 것과의 화해",
+ 10: "새로운 삶의 가능성과 미해결 질문"
+ }
+
+ literary_techniques = {
+ 1: "객관적 상관물 도입",
+ 2: "대위법적 서술",
+ 3: "의식의 흐름",
+ 4: "시점의 미묘한 전환",
+ 5: "침묵과 생략의 미학",
+ 6: "시간의 주관적 변형",
+ 7: "복수 시점의 교차",
+ 8: "메타포의 전복",
+ 9: "원형적 이미지의 재해석",
+ 10: "열린 결말의 다층성"
+ }
+
+ # 스토리 바이블 요약
+ bible_summary = f"""
+**등장인물:** {', '.join(story_bible.characters.keys())}
+**핵심 상징:** {', '.join(story_bible.symbols.keys())}
+**주제:** {', '.join(story_bible.themes[:3])}
+**문체:** {story_bible.style_guide.get('voice', 'N/A')}
+"""
+
+ # 직전 내용 요약 (더 많은 컨텍스트 제공)
+ prev_content = ""
+ if accumulated_content:
+ prev_parts = accumulated_content.split('\n\n')
+ if len(prev_parts) >= 1:
+ prev_content = prev_parts[-1][-2000:] # 마지막 파트의 끝부분 2000자
lang_prompts = {
- "Korean": f"""서사 진행을 검토하세요.
+ "Korean": f"""당신은 현대 문학의 최전선에 선 작가입니다.
+**현재: 파트 {part_number} - {phase_name}**
-**원 주제:** {user_query}
+{"**필수 첫문장:** " + story_bible.opening_sentence if part_number == 1 and story_bible.opening_sentence else ""}
-**현재 진행:** {phase_count}/10 단계
+**이번 파트의 철학적 초점:** {philosophical_focus[part_number]}
+**핵심 문학 기법:** {literary_techniques[part_number]}
-**발견된 문제:**
-{chr(10).join(issues[:5]) if issues else "없음"}
+**전체 계획:**
+{master_plan}
-**중복 발견:**
-{chr(10).join(duplicates[:3]) if duplicates else "없음"}
+**스토리 바이블:**
+{bible_summary}
-**인과관계 문제:**
-{chr(10).join(causality_issues) if causality_issues else "없음"}
+**직전 내용:**
+{prev_content if prev_content else "첫 파트입니다"}
-**검토 항목:**
-1. 서사가 필연적으로 진행되는가?
-2. 인물이 일관되게 변화하는가?
-3. 반복이나 자기복제가 있는가?
-4. 각 단계가 이전의 결과인가?
-5. 분량이 충분한가?
+**파트 {part_number} 작성 지침:**
-**판정:** 통과/재작성 필요
+1. **분량:** {target_words}-900 단어 (필수)
-치밀한 인과관계와 캐릭터 일관성을 중심으로 평가하세요.""",
+2. **문학적 수사 요구사항:**
+ - 최소 3개의 독창적 은유/직유
+ - 1개 이상의 상징적 이미지 심화
+ - 감각적 묘사와 추상적 사유의 융합
+ - 리듬감 있는 문장 구성 (장단의 변주)
- "English": f"""Review narrative progression.
+3. **현대적 고뇌 표현:**
+ - 디지털 시대의 소외감
+ - 자본주의적 삶의 부조리
+ - 관계의 표면성과 진정성 갈망
+ - 의미 추구와 무의미의 직면
-**Theme:** {user_query}
+4. **사회적 메시지 내재화:**
+ - 직접적 주장이 아닌 상황과 인물을 통한 암시
+ - 개인의 고통과 사회 구조의 연결
+ - 미시적 일상과 거시적 문제의 교차
-**Progress:** {phase_count}/10 phases
+5. **서사적 추진력:**
+ - 이전 파트의 필연적 결과로 시작
+ - 새로운 갈등 층위 추가
+ - 다음 파트를 향한 긴장감 조성
-**Issues Found:**
-{chr(10).join(issues[:5]) if issues else "None"}
+**문학적 금기:**
+- 진부한 표현이나 상투적 은유
+- 감정의 직접적 설명
+- 도덕적 판단이나 교훈
+- 인위적인 해결이나 위안
-**Duplications:**
-{chr(10).join(duplicates[:3]) if duplicates else "None"}
+파트 {part_number}를 깊이 있는 문학적 성취로 만드세요.""",
-**Causality Issues:**
-{chr(10).join(causality_issues) if causality_issues else "None"}
+ "English": f"""You are a writer at the forefront of contemporary literature.
+**Current: Part {part_number} - {phase_name}**
-**Review Items:**
-1. Is narrative progressing necessarily?
-2. Are characters changing consistently?
-3. Any repetitions or self-copying?
-4. Does each phase result from previous?
-5. Sufficient length?
+{"**Required Opening:** " + story_bible.opening_sentence if part_number == 1 and story_bible.opening_sentence else ""}
-**Verdict:** Pass/Rewrite needed
+**Philosophical Focus:** {philosophical_focus[part_number]}
+**Core Literary Technique:** {literary_techniques[part_number]}
-Evaluate focusing on tight causality and character consistency."""
- }
-
- return lang_prompts.get(language, lang_prompts["Korean"])
+**Master Plan:**
+{master_plan}
-
- def create_writer_revision_prompt(self, writer_number: int, initial_content: str,
- critic_feedback: str, language: str) -> str:
- """작가 수정 프롬프트 (간소화)"""
- target_words = MIN_WORDS_PER_WRITER
-
- lang_prompts = {
- "Korean": f"""작가 {writer_number}번, 수정하세요.
+**Story Bible:**
+{bible_summary}
+
+**Previous Content:**
+{prev_content if prev_content else "This is the first part"}
+
+**Part {part_number} Guidelines:**
-**비평 요점:**
-{critic_feedback[:500]}
+1. **Length:** {target_words}-900 words (mandatory)
-**수정 방향:**
-1. 반복 제거, 새로운 전개 추가
-2. 최소 {target_words}단어 유지
-3. 인물 변화 구체화
-4. 대화와 묘사 추가
+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
- "English": f"""Writer #{writer_number}, revise.
+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
-**Critique Points:**
-{critic_feedback[:500]}
+5. **Narrative Momentum:**
+ - Start as inevitable result of previous part
+ - Add new conflict layers
+ - Create tension toward next part
-**Revision Direction:**
-1. Remove repetition, add new development
-2. Maintain {target_words} words minimum
-3. Specify character changes
-4. Add dialogue and description
+**Literary Taboos:**
+- Clichéd expressions or trite metaphors
+- Direct emotion explanation
+- Moral judgment or preaching
+- Artificial resolution or comfort
-Boldly rewrite if needed.
-Present only revised version."""
+Make Part {part_number} a profound literary achievement."""
}
return lang_prompts.get(language, lang_prompts["Korean"])
- def create_editor_prompt(self, complete_novel: str, issues: List[str], language: str) -> str:
- """편집자 프롬프트 - 보존 중심 편집"""
-
- current_word_count = len(complete_novel.split())
- min_words = int(current_word_count * 0.95) # 95% 이상 유지
-
- lang_prompts = {
- "Korean": f"""당신은 보존을 중시하는 편집자입니다.
-
-**원고 분량: {current_word_count}단어**
-**편집 후 최소 분량: {min_words}단어 (필수!)**
-
-**편집 규칙:**
-
-1. **원고 보존이 최우선**
- - 원고의 95% 이상을 반드시 유지
- - 삭제보다는 수정을 우선
- - 전체 문단 삭제는 절대 금지
-
-2. **허용되는 편집:**
- - 완전히 동일한 문장이 연속으로 나올 때만 하나 제거
- - 문법 오류나 오타 수정
- - 연결이 어색한 부분에 접속사 추가 (1-2단어)
-
-3. **절대 금지사항:**
- - 문단 통째로 삭제 ❌
- - 내용 요약이나 축약 ❌
- - 작가의 문체 변경 ❌
- - 줄거리 재구성 ❌
+ def create_part_critic_prompt(self, part_number: int, part_content: str,
+ master_plan: str, accumulated_content: str,
+ story_bible: StoryBible, language: str) -> str:
+ """파트별 즉시 비평 - 강화된 버전"""
+
+ lang_prompts = {
+ "Korean": f"""파트 {part_number}의 문학적 성취도를 엄격히 평가하세요.
-4. **편집 방법:**
- - 중복 문장: 두 번째 것만 제거
- - 어색한 연결: 접속사로 연결
- - 오타: 최소한의 수정
+**마스터플랜 파트 {part_number} 요구사항:**
+{self._extract_part_plan(master_plan, part_number)}
-**중요: 거의 모든 내용을 그대로 유지하면서 아주 작은 문제만 수정하세요.**
+**작성된 내용:**
+{part_content}
-원고 전체를 다시 작성하지 말고, 원본을 복사한 후 최소한의 수정만 가하세요.
+**스토리 바이블 체크:**
+- 캐릭터: {', '.join(story_bible.characters.keys())}
+- 설정: {', '.join(story_bible.settings.keys())}
-편집된 전체 원고를 제시하세요. ({min_words}단어 이상 필수!)""",
+**평가 기준:**
- "English": f"""You are a preservation-focused editor.
+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())}
+- Settings: {', '.join(story_bible.settings.keys())}
+
+**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"])
-**Manuscript length: {current_word_count} words**
-**Minimum after editing: {min_words} words (REQUIRED!)**
+ def create_writer_revision_prompt(self, part_number: int, original_content: str,
+ critic_feedback: str, language: str) -> str:
+ """작가 수정 프롬프트"""
+
+ lang_prompts = {
+ "Korean": f"""파트 {part_number}를 비평에 따라 수정하세요.
-**Editing Rules:**
+**원본:**
+{original_content}
-1. **Preservation is Priority**
- - Must keep 95%+ of manuscript
- - Prefer correction over deletion
- - Never delete whole paragraphs
+**비평 피드백:**
+{critic_feedback}
-2. **Allowed Edits:**
- - Remove only when exact same sentence appears consecutively
- - Fix grammar errors or typos
- - Add conjunctions for awkward connections (1-2 words)
+**수정 지침:**
+1. 모든 '필수 수정' 사항을 반영
+2. 가능한 '권장 개선' 사항도 포함
+3. 원본의 강점은 유지
+4. 분량 {MIN_WORDS_PER_PART}단어 이상 유지
+5. 작가로서의 일관된 목소리 유지
+6. 문학적 수준을 한 단계 높이기
-3. **Absolutely Forbidden:**
- - Deleting whole paragraphs ❌
- - Summarizing or abbreviating ❌
- - Changing author's style ❌
- - Restructuring plot ❌
+수정본만 제시하세요. 설명은 불필요합니다.""",
-4. **Editing Method:**
- - Duplicates: Remove only second occurrence
- - Awkward connections: Connect with conjunctions
- - Typos: Minimal fixes
+ "English": f"""Revise Part {part_number} according to critique.
-**IMPORTANT: Keep almost everything while fixing only tiny issues.**
+**Original:**
+{original_content}
-Don't rewrite the manuscript, copy the original and apply minimal edits.
+**Critique Feedback:**
+{critic_feedback}
-Present the full edited manuscript. ({min_words}+ words required!)"""
- }
-
- return lang_prompts.get(language, lang_prompts["Korean"])
+**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
- def create_critic_final_prompt(self, content: str, query: str, language: str) -> str:
- """최종 비평"""
-
- word_count = len(content.split())
+Present only the revision. No explanation needed."""
+ }
- # 서사 구조 분석 (추가)
- has_single_plot = self.check_single_narrative(content)
- character_consistency = self.check_character_consistency_in_final(content)
+ 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:
+ """최종 종합 평가"""
lang_prompts = {
- "Korean": f"""완성된 소설을 평가하세요.
+ "Korean": f"""완성된 소설을 종합 평가하세요.
-**주제:** {query}
-**분량:** {word_count}단어 (목표: 8,000)
+**작품 정보:**
+- 총 분량: {word_count}단어
+- 목표: 8,000단어
**평가 기준:**
-1. 통합된 서사 구조 (30점)
- - 단일한 플롯 존재 여부
- - 인과관계의 치밀함
- - 각 단계의 필연성
-
-2. 캐릭터 일관성 (25점)
- - 이름과 설정의 통일성
- - 성격과 행동의 일관성
- - 변화의 설득력
-
-3. 문학적 완성도 (25점)
- - 문체의 일관성
- - 상징과 모티프 활용
- - 문학적 기법의 다양성
-
-4. 주제 의식과 통찰 (20점)
- - 철학적 깊이
- - 인간에 대한 통찰
- - 독창적 해석
-**감점 요인:**
-- 플롯 자기복제 (-10점)
-- 캐릭터 설정 충돌 (-10점)
-- 단순 반복 (-5점)
-- 분량 미달 (-5점)
+1. **서사적 통합성 (30점)**
+ - 10개 파트가 하나의 이야기로 통합되었는가?
+ - 인과관계가 명확하고 필연적인가?
+ - 반복이나 순환 없이 진행되는가?
-**종합 평가:**
-작품이 하나의 완결된 장편소설로 기능하는가?
+2. **캐릭터 아크 (25점)**
+ - 주인공의 변화가 설득력 있는가?
+ - 변화가 점진적이고 자연스러운가?
+ - 최종 상태가 초기와 명확히 다른가?
-총점: /100점
+3. **문학적 성취 (25점)**
+ - 주제가 깊이 있게 탐구되었는가?
+ - 상징이 효과적으로 활용되었는가?
+ - 문체가 일관되고 아름다운가?
+ - 현대적 철학과 사회적 메시지가 녹아있는가?
-작품의 문학적 가치와 구조적 완성도를 종합적으로 평가하세요.""",
+4. **기술적 완성도 (20점)**
+ - 목표 분량을 달성했는가?
+ - 각 파트가 균형 있게 전개되었는가?
+ - 문법과 표현이 정확한가?
- "English": f"""Evaluate the completed novel.
+**총점: /100점**
-**Theme:** {query}
-**Length:** {word_count} words (target: 8,000)
+구체적인 강점과 약점을 제시하세요.""",
-**Criteria:**
-1. Integrated Narrative Structure (30 pts)
- - Single plot existence
- - Causal tightness
- - Necessity of each phase
-
-2. Character Consistency (25 pts)
- - Name/setting unity
- - Personality/behavior consistency
- - Convincing transformation
-
-3. Literary Quality (25 pts)
- - Stylistic consistency
- - Symbol/motif usage
- - Literary technique variety
-
-4. Thematic Insight (20 pts)
- - Philosophical depth
- - Human insight
- - Original interpretation
+ "English": f"""Comprehensively evaluate the completed novel.
-**Deductions:**
-- Plot self-replication (-10 pts)
-- Character setting conflicts (-10 pts)
-- Simple repetition (-5 pts)
-- Under length (-5 pts)
+**Work Info:**
+- Total length: {word_count} words
+- Target: 8,000 words
-**Overall Assessment:**
-Does the work function as a complete novel?
+**Evaluation Criteria:**
-Total: /100 points
+1. **Narrative Integration (30 points)**
+ - Are 10 parts integrated into one story?
+ - Clear and inevitable causality?
+ - Progress without repetition or cycles?
-Comprehensively evaluate literary value and structural completeness."""
- }
-
- return lang_prompts.get(language, lang_prompts["Korean"])
+2. **Character Arc (25 points)**
+ - Convincing protagonist transformation?
+ - Gradual and natural changes?
+ - Final state clearly different from initial?
- # --- 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}")
-
- # 언어 필터 적용
- filtered_content = self.language_filter.clean_text(full_content)
- return filtered_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]
-
- # 작가와 편집자 역할일 때는 더 많은 토큰 허용
- if role.startswith("writer"):
- max_tokens = 25000
- elif role == "editor":
- # 편집자는 원본 길이 + 여유분
- max_tokens = 30000 # 편집자에게 더 많은 토큰 할당
- else:
- max_tokens = 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:
- # 언어 필터 적용
- clean_buffer = self.language_filter.clean_text(buffer)
- yield clean_buffer
- buffer = ""
- time.sleep(0.01)
-
- except Exception as e:
- logger.error(f"청크 처리 오류: {str(e)}")
- continue
-
- if buffer:
- clean_buffer = self.language_filter.clean_text(buffer)
- yield clean_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": f"""당신은 한국 문학 작가입니다.
-반드시 {MIN_WORDS_PER_WRITER}단어 이상 작성하세요.
-이전 단계의 필연적 결과로 시작하세요.
-캐릭터 이름과 설정을 절대 변경하지 마세요.
-구체적 장면과 생생한 대화로 서사를 전개하세요.""",
-
- "editor": """당신은 원고 보존을 최우선으로 하는 편집자입니다.
-원고의 95% 이상을 반드시 유지하세요.
-전체 문단을 삭제하지 마세요.
-원본을 복사한 후 최소한의 수정만 하세요.
-편집 후에도 거의 모든 내용이 남아있어야 합니다."""
- },
- "English": {
- "director": """You are a Nobel Prize-winning author.
-Design a single narrative with tight causality.
-Fix character names and settings clearly from start.
-Build necessary development without plot self-replication.""",
-
- "critic": """You are a literary critic.
-Strictly review narrative causality and character consistency.
-Find plot self-replication and setting conflicts.
-Evaluate if work is one complete novel.""",
-
- "writer_base": f"""You are a literary writer.
-Must write at least {MIN_WORDS_PER_WRITER} words.
-Start as necessary result of previous phase.
-Never change character names or settings.
-Develop narrative with concrete scenes and vivid dialogue.""",
-
- "editor": """You are a preservation-focused editor.
-Must maintain 95%+ of manuscript.
-Never delete whole paragraphs.
-Copy original and apply minimal edits only.
-Almost all content must remain after editing."""
- }
- }
-
- 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),
- "repetition_score": s.get('repetition_score', 0.0),
- "consistency_check": s.get('consistency_check', '')
- } for s in NovelDatabase.get_stages(self.current_session_id)]
-
- # 총 단어 수 추적
- total_words = NovelDatabase.get_total_words(self.current_session_id)
-
- for stage_idx in range(resume_from_stage, len(PROGRESSIVE_STAGES)):
- role, stage_name = PROGRESSIVE_STAGES[stage_idx]
- if stage_idx >= len(stages):
- stages.append({
- "name": stage_name,
- "status": "active",
- "content": "",
- "word_count": 0,
- "progression_score": 0.0,
- "repetition_score": 0.0,
- "consistency_check": ""
- })
- 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"):
- word_count = len(stage_content.split())
- writer_num = int(re.search(r'\d+', role).group())
-
- # 단어 수가 부족하면 추가 생성 요청
- if word_count < MIN_WORDS_PER_WRITER * 0.9: # 90% 미만이면
- retry_prompt = f"""
-현재 {word_count}단어만 작성되었습니다.
-최소 {MIN_WORDS_PER_WRITER}단어가 필요합니다.
-
-다음을 추가하여 {MIN_WORDS_PER_WRITER - word_count}단어 이상 더 작성하세요:
-- 더 자세한 인물 묘사
-- 추가 대화 장면
-- 배경과 분위기 묘사
-- 인물의 과거 회상
-- 내적 독백 확대
-
-기존 내용에 자연스럽게 이어서 작성하세요:
-"""
-
- additional_content = self.call_llm_sync(
- [{"role": "user", "content": retry_prompt}],
- role,
- language
- )
- stage_content += "\n\n" + additional_content
- stages[stage_idx]["content"] = stage_content
- stages[stage_idx]["word_count"] = len(stage_content.split())
-
- # 진행도 평가
- previous_content = self.get_previous_writer_content(stages, writer_num)
-
- # 진행도 점수 계산
- progression_scores = self.narrative_tracker.progression_monitor.calculate_progression_score(
- writer_num, stage_content, previous_content
- )
- progression_score = sum(progression_scores.values()) / len(progression_scores)
- stages[stage_idx]["progression_score"] = progression_score
-
- # 반복도 점수 계산
- repetition_score = 10.0 - self.narrative_tracker.progression_monitor.count_repetitions(stage_content)
- stages[stage_idx]["repetition_score"] = max(0, repetition_score)
-
- # 일관성 체크
- consistency_ok, consistency_issues = self.narrative_tracker.consistency_checker.validate_new_content(
- writer_num, stage_content, [s["content"] for s in stages[:stage_idx] if s["content"]]
- )
- stages[stage_idx]["consistency_check"] = "통과" if consistency_ok else "; ".join(consistency_issues)
-
- # 서사 추적기 업데이트
- self.update_narrative_tracker(stage_content, writer_num)
- self.narrative_tracker.extract_used_elements(stage_content)
-
- # 편집 단계 특별 처리
- if role == "editor" and stage_content:
- # 편집 전후 단어 수 비교
- original_novel = ""
- for i in range(1, 11):
- for s in stages:
- if f"writer{i}" in s.get("name", "") and "수정본" in s.get("name", "") and s["content"]:
- original_novel += s["content"] + "\n\n"
-
- original_words = len(original_novel.split())
- edited_words = len(stage_content.split())
-
- logger.info(f"편집 결과: {original_words}단어 → {edited_words}단어")
-
- # 편집으로 20% 이상 삭제되었다면 원본 사용
- if edited_words < original_words * 0.8:
- logger.warning(f"편집자가 과도하게 삭제함 ({100 - (edited_words/original_words*100):.1f}% 삭제). 원본 유지.")
- stage_content = original_novel
- stages[stage_idx]["content"] = stage_content
- stages[stage_idx]["word_count"] = original_words
- stages[stage_idx]["note"] = "편집 과도로 원본 유지"
-
- stages[stage_idx]["status"] = "complete"
- NovelDatabase.save_stage(
- self.current_session_id, stage_idx, stage_name, role,
- stage_content, "complete",
- stages[stage_idx].get("progression_score", 0.0),
- stages[stage_idx].get("repetition_score", 0.0),
- stages[stage_idx].get("consistency_check", "")
- )
-
- # 서사 추적기 저장
- NovelDatabase.save_narrative_tracker(self.current_session_id, self.narrative_tracker)
-
- # 총 단어 수 업데이트
- total_words = NovelDatabase.get_total_words(self.current_session_id)
-
- # 단어 수 경고
- if role.startswith("writer") and stages[stage_idx]["word_count"] < MIN_WORDS_PER_WRITER:
- logger.warning(f"단어 수 부족: {stage_name} - {stages[stage_idx]['word_count']}단어")
-
- yield f"✅ {stage_name} 완료 (총 {total_words:,}단어)", stages, self.current_session_id
+3. **Literary Achievement (25 points)**
+ - Theme explored with depth?
+ - Symbols used effectively?
+ - Consistent and beautiful style?
+ - Contemporary philosophy and social message integrated?
- # 최종 소설 정리
- final_novel = NovelDatabase.get_writer_content(self.current_session_id)
-
- # 편집자가 처리한 내용이 있으면 그것을 사용
- edited_content = self.get_edited_content(stages)
- if edited_content:
- final_novel = edited_content
-
- final_word_count = len(final_novel.split())
- final_report = self.generate_literary_report(final_novel, query, language)
-
- # 품질 평가 저장
- self.save_quality_evaluation(final_report)
-
- NovelDatabase.update_final_novel(self.current_session_id, final_novel, final_report)
-
- # 최종 단어 수 확인
- if final_word_count < TARGET_WORDS:
- yield f"⚠️ 소설 완성! 총 {final_word_count:,}단어 (목표 미달: {TARGET_WORDS - final_word_count:,}단어 부족)", stages, self.current_session_id
- else:
- yield f"✅ 소설 완성! 총 {final_word_count:,}단어 (목표: {TARGET_WORDS:,}단어 달성!)", stages, self.current_session_id
+4. **Technical Completion (20 points)**
+ - Target length achieved?
+ - Each part balanced in development?
+ - Grammar and expression accurate?
- except Exception as e:
- logger.error(f"소설 생성 프로세스 오류: {e}", exc_info=True)
- yield f"❌ 오류 발생: {e}", stages if 'stages' in locals() else [], self.current_session_id
+**Total Score: /100 points**
- def get_stage_prompt(self, stage_idx, role, query, language, stages):
- """단계별 프롬프트 생성"""
- if stage_idx == 0:
- # 첫 번째 단계 - 감독자 초기 기획
- return self.create_director_initial_prompt(query, language)
-
- elif stage_idx == 1:
- # 두 번째 단계 - 비평가가 감독자 기획 검토
- if role == "critic":
- return self.create_critic_consistency_prompt_enhanced(
- stages[0]["content"],
- self.narrative_tracker,
- query,
- language
- )
+Present specific strengths and weaknesses."""
+ }
- elif stage_idx == 2:
- # 세 번째 단계 - 감독자 수정
- if role == "director":
- return self.create_director_revision_prompt(
- stages[0]["content"], # 초기 기획
- stages[1]["content"], # 비평
- query,
- language
- )
+ return lang_prompts.get(language, lang_prompts["Korean"])
+
+ def _extract_part_plan(self, master_plan: str, part_number: int) -> str:
+ """마스터플랜에서 특정 파트 계획 추출"""
+ lines = master_plan.split('\n')
+ part_section = []
+ capturing = False
- else:
- # 작가 단계들
- if role.startswith("writer"):
- writer_num = int(re.search(r'\d+', role).group())
-
- # 이전 작가들의 내용 가져오기
- previous_content = ""
- if writer_num > 1:
- # writer_num - 1까지의 모든 작가 내용
- for i in range(1, writer_num):
- for stage in stages:
- if f"writer{i}" in stage.get("name", "") and stage["content"]:
- previous_content += stage["content"] + "\n\n"
-
- # 감독자 플랜 가져오기
- director_plan = ""
- for stage in stages[:3]: # 처음 3개 단계에서 감독자 플랜 찾기
- if "감독자" in stage.get("name", "") and "수정" in stage.get("name", "") and stage["content"]:
- director_plan = stage["content"]
- break
- if not director_plan: # 수정된 플랜이 없으면 초기 플랜 사용
- director_plan = stages[0]["content"] if stages and stages[0]["content"] else ""
-
- # 서사 요약 생성
- narrative_summary = self.generate_narrative_summary(stages, writer_num)
-
- # 단계별 요구사항 생성
- phase_requirements = self.narrative_tracker.generate_phase_requirements(writer_num)
-
- # 사용된 요소들
- used_elements = list(self.narrative_tracker.used_expressions)
+ for line in lines:
+ if f"파트 {part_number}:" in line or f"Part {part_number}:" in line:
+ capturing = True
+ elif capturing and (f"파트 {part_number+1}:" in line or f"Part {part_number+1}:" in line):
+ break
+ elif capturing:
+ part_section.append(line)
- # 초안인지 수정본인지 확인
- stage_name = stages[stage_idx]["name"] if stage_idx < len(stages) else ""
- if "초안" in stage_name:
- return self.create_writer_prompt_enhanced(
- writer_num,
- director_plan,
- previous_content,
- phase_requirements,
- narrative_summary,
- language,
- used_elements
- )
- else: # 수정본
- # 해당 작가의 초안 찾기
- initial_content = ""
- critic_feedback = ""
-
- # 초안 찾기
- for i, stage in enumerate(stages):
- if f"writer{writer_num}" in stage.get("name", "") and "초안" in stage.get("name", ""):
- initial_content = stage["content"]
- break
-
- # 중간 비평 찾기
- for i, stage in enumerate(stages):
- if "critic" in stage.get("name", "") and "중간 검토" in stage.get("name", ""):
- critic_feedback = stage["content"]
- break
-
- return self.create_writer_revision_prompt(
- writer_num,
- initial_content,
- critic_feedback,
- language
- )
+ return '\n'.join(part_section) if part_section else "해당 파트 계획을 찾을 수 없습니다."
+
+ # --- 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 == "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
- elif role == "critic":
- # 중간 비평가 검토
- if "중간 검토" in stages[stage_idx]["name"]:
- # 모든 작가 콘텐츠 수집
- all_content = ""
- for stage in stages:
- if "writer" in stage.get("name", "") and "초안" in stage.get("name", "") and stage["content"]:
- all_content += stage["content"] + "\n\n"
+ 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
- return self.create_critic_consistency_prompt_enhanced(
- all_content,
- self.narrative_tracker,
- query,
- language
- )
- # 최종 비평
- else:
- # 편집된 내용이 있으면 그것을, 없으면 모든 작가 수정본
- final_content = ""
- for stage in stages:
- if "편집자" in stage.get("name", "") and stage["content"]:
- final_content = stage["content"]
- break
+ data_str = line_str[6:]
+ if data_str == "[DONE]":
+ break
- if not final_content:
- # 편집된 내용이 없으면 모든 작가 수정본 수집
- for i in range(1, 11):
- for stage in stages:
- if f"writer{i}" in stage.get("name", "") and "수정본" in stage.get("name", "") and stage["content"]:
- final_content += stage["content"] + "\n\n"
+ 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)
- return self.create_critic_final_prompt(final_content, query, language)
+ except Exception as e:
+ logger.error(f"청크 처리 오류: {str(e)}")
+ continue
- elif role == "editor":
- # 편집자 - 모든 작가 수정본 수집
- complete_novel = ""
- writer_contents = []
- for i in range(1, 11):
- for stage in stages:
- if f"writer{i}" in stage.get("name", "") and "수정본" in stage.get("name", "") and stage["content"]:
- writer_contents.append(stage["content"])
- complete_novel += stage["content"] + "\n\n"
+ 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": """당신은 현대 세계문학의 정점을 지향하는 작품을 설계합니다.
+깊은 철학적 통찰과 날카로운 사회 비판을 결합하세요.
+인간 조건의 복잡성을 10개의 유기적 파트로 구현하세요.
+독자의 영혼을 뒤흔들 강렬한 첫문장부터 시작하세요.""",
+
+ "critic_director": """서사 구조의 논리성과 실현 가능성을 검증하는 전문가입니다.
+인과관계의 허점을 찾아내세요.
+캐릭터 발전의 신빙성을 평가하세요.
+철학적 깊이와 문학적 가치를 판단하세요.
+8,000단어 분량의 적절성을 판단하세요.""",
- # 문제점 감지 - 매우 심각한 문제만
- issues = []
+ "writer": """당신은 언어의 연금술사입니다.
+일상어를 시로, 구체를 추상으로, 개인을 보편으로 변환하세요.
+현대인의 영혼의 어둠과 빛을 동시에 포착하세요.
+독자가 자신을 재발견하게 만드는 거울이 되세요.""",
- # 완전히 동일한 문단 체크
- paragraphs = complete_novel.split('\n\n')
- seen_paragraphs = set()
- exact_duplicates = 0
- for para in paragraphs:
- if para.strip() in seen_paragraphs and len(para.strip()) > 100:
- exact_duplicates += 1
- seen_paragraphs.add(para.strip())
+ "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.""",
- if exact_duplicates > 5:
- issues.append(f"{exact_duplicates}개의 완전히 동일한 문단 발견")
+ "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.""",
- # 문제가 거의 없으면 편집 최소화
- if len(issues) == 0:
- issues = ["원고 상태가 양호합니다. 편집을 최소화하세요."]
+ "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.""",
- return self.create_editor_prompt(complete_novel, issues, language)
+ "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."""
+ }
+ }
- # 기본값 (에러 방지)
- return f"역할 {role}에 대한 프롬프트가 정의되지 않았습니다."
-
-
- def create_director_revision_prompt(self, initial_plan: str, critic_feedback: str, user_query: str, language: str) -> str:
- """감독자 수정 프롬프트"""
- lang_prompts = {
- "Korean": f"""비평을 반영하여 수정하세요.
-
-**원 주제:** {user_query}
+ prompts = base_prompts.get(language, base_prompts["Korean"]).copy()
+
+ # 파트별 비평가 프롬프트 추가
+ for i in range(1, 11):
+ prompts[f"critic_part{i}"] = f"""파트 {i} 전담 비평가입니다.
+이전 파트와의 인과관계를 최우선으로 검토하세요.
+캐릭터 일관성과 발전을 확인하세요.
+마스터플랜과의 일치도를 평가하세요.
+문학적 수준과 철학적 깊이를 평가하세요.
+구체적이고 실행 가능한 수정 지시를 제공하세요."""
+
+ return prompts
-**비평 요점:**
-{critic_feedback[:500]}
+ # --- 메인 프로세스 ---
+ 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),
+ "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"
-**수정 핵심:**
-1. 10단계가 인과관계로 긴밀히 연결
-2. 캐릭터 이름과 설정 처음부터 고정
-3. 각 단계가 이전의 필연적 결과
-4. 플롯의 자기복제 방지
-5. 8,000단어 달성 전략
+ 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 == "writer":
+ # 파트 번호 계산
+ 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())
+
+ # 서사 추진력 계산
+ momentum = self.narrative_tracker.calculate_narrative_momentum(part_num, stage_content)
+ stages[stage_idx]["momentum"] = momentum
+
+ # 스토리 바이블 업데이트
+ 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} 완료 (총 {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:,}단어", 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
-치밀한 인과관계의 수정 계획을 제시하세요.""",
-
- "English": f"""Revise based on critique.
+ 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_final_prompt(stages[0]["content"], stages[1]["content"], query, language)
-**Original Theme:** {user_query}
+ master_plan = stages[2]["content"]
+
+ # 작가 파트 작성
+ if role == "writer" and "수정본" 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)
+
+ # 파트별 비평
+ if role.startswith("critic_part"):
+ part_num = int(role.replace("critic_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)
+
+ # 작가 수정본
+ if role == "writer" and "수정본" in stages[stage_idx]["name"]:
+ part_num = self._get_part_number(stage_idx)
+ original_content = stages[stage_idx-2]["content"] # 원본
+ critic_feedback = stages[stage_idx-1]["content"] # 비평
+ return self.create_writer_revision_prompt(part_num, original_content,
+ critic_feedback, language)
+
+ # 최종 비평
+ 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 ""
-**Critique Points:**
-{critic_feedback[:500]}
+ def create_director_final_prompt(self, initial_plan: str, critic_feedback: str,
+ user_query: str, language: str) -> str:
+ """감독자 최종 마스터플랜"""
+ return f"""비평을 반영하여 최종 마스터플랜을 완성하세요.
-**Revision Focus:**
-1. 10 phases tightly connected by causality
-2. Character names/settings fixed from start
-3. Each phase as necessary result of previous
-4. Prevent plot self-replication
-5. Strategy to achieve 8,000 words
+**원 주제:** {user_query}
-**Structure Enhancement:**
-- External events trigger internal changes
-- Choices determine next phase
-- Necessary, not coincidental development
+**초기 기획:**
+{initial_plan}
-Present revision plan with tight causality."""
- }
-
- return lang_prompts.get(language, lang_prompts["Korean"])
+**비평 피드백:**
+{critic_feedback}
- def get_previous_writer_content(self, stages: List[Dict], current_writer: int) -> str:
- """이전 작가의 내용 가져오기"""
- if current_writer == 1:
- return ""
-
- # 이전 작가들의 내용 수집
- previous_content = []
- for i in range(1, current_writer):
- for stage in stages:
- if f"writer{i}" in stage.get("name", "") and stage["content"]:
- previous_content.append(stage["content"])
- break
-
- return "\n\n".join(previous_content)
-
- 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 get_edited_content(self, stages: List[Dict]) -> str:
- """편집된 내용 가져오기"""
- for s in stages:
- if "편집자" in s.get("name", "") and s["content"]:
- return s["content"]
- return ""
-
- def generate_narrative_summary(self, stages: List[Dict], up_to_writer: int) -> str:
- """현재까지의 서사 요약"""
- if up_to_writer == 1:
- return "첫 시작입니다."
-
- summary_parts = []
- for i in range(1, up_to_writer):
- if i in self.narrative_tracker.phase_summaries:
- summary_parts.append(f"[{NARRATIVE_PHASES[i-1]}]: {self.narrative_tracker.phase_summaries[i][:100]}...")
-
- return "\n".join(summary_parts) if summary_parts else "이전 내용을 이어받아 진행하세요."
+**최종 마스터플랜 요구사항:**
+1. 모든 비평 지적사항 반영
+2. 10개 파트의 구체적 내용과 인과관계
+3. 주인공의 명확한 변화 단계
+4. 중심 상징의 의미 변화 과정
+5. 각 파트 800단어 실현 가능성
+6. 철학적 깊이와 사회적 메시지 구현 방안
- 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
-
- # 철학적 통찰 추출 (간단한 키워드 기반)
- philosophical_keywords = ['존재', '의미', '삶', '죽음', '인간', '고통', '희망', '사랑',
- 'existence', 'meaning', 'life', 'death', 'human', 'suffering', 'hope', 'love']
- for keyword in philosophical_keywords:
- if keyword in content:
- self.narrative_tracker.philosophical_insights.append(f"Phase {writer_num}: {keyword} 탐구")
- break
-
- # 문학적 기법 감지
- literary_devices = []
- if '처럼' in content or 'like' in content or 'as if' in content:
- literary_devices.append('비유')
- if '...' in content or '—' in content:
- literary_devices.append('의식의 흐름')
- if content.count('"') > 4:
- literary_devices.append('대화')
-
- if literary_devices:
- self.narrative_tracker.literary_devices[writer_num] = literary_devices
+구체적이고 실행 가능한 최종 계획을 제시하세요."""
- def detect_issues(self, content: str) -> List[str]:
- """문제점 감지 - 심각한 문제만"""
- issues = []
-
- # 완전히 동일한 문단만 감지
- paragraphs = content.split('\n\n')
- exact_duplicates = 0
- for i, para1 in enumerate(paragraphs):
- for j, para2 in enumerate(paragraphs[i+1:]):
- if para1.strip() == para2.strip() and len(para1.strip()) > 50:
- exact_duplicates += 1
-
- if exact_duplicates > 0:
- issues.append(f"{exact_duplicates}개의 완전히 동일한 문단 발견")
-
- # 5회 이상 반복되는 표현만 감지
- repetitive_phrases = ["습기가 찬 아침", "나라미 어플", "43만원", "개구리알을 바라보았다"]
- for phrase in repetitive_phrases:
- count = content.count(phrase)
- if count > 5: # 임계값을 3에서 5로 상향
- issues.append(f"'{phrase}' 표현이 {count}회 과도하게 반복됨")
-
- # 심각한 캐릭터 이름 불일치만
- name_variations = ["나라미", "안정", "나"]
- found_names = [name for name in name_variations if content.count(name) > 10]
- if len(found_names) > 2:
- issues.append(f"주인공 이름 심각한 불일치: {', '.join(found_names)}")
-
- # 언어 오류 감지
- if re.search(r'[ぁ-んァ-ヶ一-龯]', content):
- issues.append("일본어/중국어 문자 발견")
-
- return issues[:3] # 가장 심각한 3개만 반환
+ def _get_part_number(self, stage_idx: int) -> Optional[int]:
+ """스테이지 인덱스에서 파트 번호 추출"""
+ stage_name = UNIFIED_STAGES[stage_idx][1]
+ match = re.search(r'파트 (\d+)', stage_name)
+ if match:
+ return int(match.group(1))
+ return None
- 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
-
- # 철학적 깊이 체크
- philosophical_keywords = ['존재', '의미', '삶의', '인간의', '왜', 'existence', 'meaning', 'life', 'human', 'why']
- if any(keyword in content for keyword in philosophical_keywords):
- score += 0.5
-
- # 문학적 기법 체크
- if not any(phrase in content for phrase in ['느꼈다', '였다', 'felt', 'was']):
- score += 0.5 # 보여주기 기법 사용
-
- return min(10.0, score)
-
- def generate_literary_report(self, complete_novel: str, query: str, language: str) -> str:
- """최종 문학적 평가"""
- prompt = self.create_critic_final_prompt(complete_novel, query, 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 check_single_narrative(self, content: str) -> bool:
- """단일 서사 구조 확인"""
- # 주인공 이름의 일관성 체크
- names = re.findall(r'[가-힣]{2,4}(?=이|가|은|는|을|를)', content)
- name_counts = {}
- for name in names:
- name_counts[name] = name_counts.get(name, 0) + 1
-
- # 가장 많이 등장하는 이름이 전체의 50% 이상인지
- if name_counts:
- max_count = max(name_counts.values())
- total_count = sum(name_counts.values())
- return max_count / total_count > 0.5
- return False
-
- def check_character_consistency_in_final(self, content: str) -> float:
- """최종 작품의 캐릭터 일관성 점수"""
- # 캐릭터 이름과 직업/설정의 일관성 체크
- consistency_score = 1.0
-
- # 같은 이름이 다른 설정으로 등장하는지 체크
- name_professions = {}
- patterns = [
- r'([가-힣]{2,4})(?:은|는|이|가)\s+(\w+(?:작가|기자|교사|의사|변호사))',
- r'(\w+(?:작가|기자|교사|의사|변호사))\s+([가-힣]{2,4})'
- ]
-
- for pattern in patterns:
- matches = re.findall(pattern, content)
- for match in matches:
- name = match[0] if '가' <= match[0][0] <= '힣' else match[1]
- profession = match[1] if '가' <= match[0][0] <= '힣' else match[0]
-
- if name in name_professions and name_professions[name] != profession:
- consistency_score -= 0.2 # 불일치마다 감점
- else:
- name_professions[name] = profession
+ def _update_story_bible_from_content(self, content: str, part_num: int):
+ """컨텐츠에서 스토리 바이블 자동 업데이트"""
+ # 간단한 키워드 기반 추출 (실제로는 더 정교한 NLP 필요)
+ lines = content.split('\n')
- return max(0, consistency_score)
-
- def save_quality_evaluation(self, report: str):
- """품질 평가 저장"""
- try:
- # 점수 추출 (간단한 패턴 매칭)
- score_match = re.search(r'총점:\s*(\d+(?:\.\d+)?)/100', report)
- score = float(score_match.group(1)) if score_match else 0.0
-
- NovelDatabase.save_quality_evaluation(
- self.current_session_id,
- "final_evaluation",
- score,
- report
- )
- except Exception as e:
- logger.error(f"품질 평가 저장 ��패: {e}")
+ # 캐릭터 이름 추출 (대문자로 시작하는 단어들)
+ 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:
+ """최종 문학적 평가 보고서 생성"""
+ 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"최종 보고서 생성 실패: {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)
- # 편집된 내용이 있으면 그것을 사용
- edited = system.get_edited_content(stages)
- if edited:
- novel_content = edited
- novel_content = format_novel_display(novel_content)
-
- yield stages_markdown, novel_content, status or "🔄 처리 중...", current_session_id
+ """메인 쿼리 처리 함수"""
+ if not query.strip():
+ yield "", "", "❌ 주제를 입력해주세요.", 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)
+
+ # 최종 소설 내용 가져오기
+ 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()
- result = []
- for s in sessions:
- # None 값 체크 추가
- session_id = s.get('session_id', '')
- user_query = s.get('user_query', '')
- created_at = s.get('created_at', '')
- total_words = s.get('total_words', 0) or 0 # None일 경우 0으로 처리
-
- if session_id and user_query: # 필수 값이 있는 경우만 추가
- result.append(
- f"{session_id[:8]}... - {user_query[:50]}... ({created_at}) [{total_words:,}단어]"
- )
- return result
+ return [f"{s['session_id'][:8]}... - {s['user_query'][:50]}... ({s['created_at']}) [{s['total_words']:,}단어]"
+ for s in sessions]
def auto_recover_session(language: str) -> Tuple[Optional[str], str]:
- """최근 세션 자동 복구"""
- sessions = NovelDatabase.get_active_sessions()
- if sessions:
- latest_session = sessions[0]
- return latest_session['session_id'], f"세션 {latest_session['session_id'][:8]}... 복구됨"
- return None, "복구할 세션이 없습니다."
+ """최근 세션 자동 복구"""
+ sessions = NovelDatabase.get_active_sessions()
+ if sessions:
+ latest_session = sessions[0]
+ return latest_session['session_id'], f"세션 {latest_session['session_id'][:8]}... 복구됨"
+ return None, "복구할 세션이 없습니다."
def resume_session(session_id: str, language: str) -> Generator[Tuple[str, str, str, str], None, None]:
- """세션 재개"""
- if not session_id:
- yield "", "", "❌ 세션 ID가 없습니다.", session_id
- return
-
- if "..." in session_id:
- session_id = session_id.split("...")[0]
-
- session = NovelDatabase.get_session(session_id)
- if not session:
- yield "", "", "❌ 세션을 찾을 수 없습니다.", None
- return
-
- yield from process_query(session['user_query'], session['language'], session_id)
+ """세션 재개"""
+ if not session_id:
+ yield "", "", "❌ 세션 ID가 없습니다.", session_id
+ return
+
+ if "..." in session_id:
+ session_id = session_id.split("...")[0]
+
+ session = NovelDatabase.get_session(session_id)
+ if not session:
+ yield "", "", "❌ 세션을 찾을 수 없습니다.", None
+ return
+
+ yield from process_query(session['user_query'], session['language'], session_id)
def download_novel(novel_text: str, format_type: str, language: str, session_id: str) -> Optional[str]:
"""소설 다운로드 파일 생성"""
@@ -2278,51 +1578,63 @@ def download_novel(novel_text: str, format_type: str, language: str, session_id:
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', ''))
- progress_percent = (total_words / TARGET_WORDS) * 100 if TARGET_WORDS > 0 else 0
+ # 총 단어 수 계산 (작가 스테이지만)
+ total_words = sum(s.get('word_count', 0) for s in stages
+ if s.get('name', '').startswith('✍️ 작가:') and '수정본' in s.get('name', ''))
+ markdown += f"**총 단어 수: {total_words:,} / {TARGET_WORDS:,}**\n\n"
+
+ # 진행 상황 요약
+ completed_parts = sum(1 for s in stages
+ if '수정본' in s.get('name', '') and s.get('status') == 'complete')
+ markdown += f"**완성된 파트: {completed_parts} / 10**\n\n"
- markdown += f"**총 단어 수: {total_words:,} / {TARGET_WORDS:,}**\n"
- markdown += f"**진행률: {progress_percent:.1f}%**\n"
- markdown += f"{'█' * int(progress_percent // 5)}{'░' * (20 - int(progress_percent // 5))}\n\n"
+ # 서사 추진력 평균
+ 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"**평균 서사 추진력: {avg_momentum:.1f} / 10**\n\n"
+ markdown += "---\n\n"
+
+ # 각 스테이지 표시
+ current_part = 0
for i, stage in enumerate(stages):
status_icon = "✅" if stage['status'] == 'complete' else "🔄" if stage['status'] == 'active' else "⏳"
+
+ # 파트 구분선 추가
+ if '파트' in stage.get('name', '') and '비평가' not in stage.get('name', ''):
+ part_match = re.search(r'파트 (\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### 📚 파트 {current_part}\n\n"
+
markdown += f"{status_icon} **{stage['name']}**"
if stage.get('word_count', 0) > 0:
markdown += f" ({stage['word_count']:,}단어)"
-
- # 작가 단계에서 단어 수 부족 경고
- if 'writer' in stage.get('name', '') and stage['word_count'] < MIN_WORDS_PER_WRITER:
- markdown += f" ⚠️ **분량 부족!**"
- # 진행도와 반복도 점수 표시
- if stage.get('progression_score', 0) > 0:
- markdown += f" [진행도: {stage['progression_score']:.1f}/10]"
- if stage.get('repetition_score', 0) > 0:
- markdown += f" [반복도: {stage['repetition_score']:.1f}/10]"
-
- # 일관성 체크 표시
- if stage.get('consistency_check'):
- if stage['consistency_check'] == "통과":
- markdown += " ✔️"
- else:
- markdown += f" ⚠️ {stage['consistency_check']}"
+ if stage.get('momentum', 0) > 0:
+ markdown += f" [추진력: {stage['momentum']:.1f}/10]"
markdown += "\n"
- if stage['content']:
- preview = stage['content'][:200] + "..." if len(stage['content']) > 200 else stage['content']
+ if stage['content'] and stage['status'] == 'complete':
+ # 미리보기 길이를 역할에 따라 조정
+ 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 += "> *작성 중...*\n\n"
return markdown
def format_novel_display(novel_text: str) -> str:
- """소설 내용 표시"""
+ """소설 내용 표시 - 파트별 구분 강화"""
if not novel_text:
return "아직 완성된 내용이 없습니다."
@@ -2332,277 +1644,316 @@ def format_novel_display(novel_text: str) -> str:
word_count = len(novel_text.split())
formatted += f"**총 분량: {word_count:,}단어 (목표: {TARGET_WORDS:,}단어)**\n\n"
- if word_count < TARGET_WORDS:
- shortage = TARGET_WORDS - word_count
- formatted += f"⚠️ **주의: 목표 분량에 {shortage:,}단어 부족합니다.**\n\n"
- else:
- formatted += f"✅ **목표 분량 달성!**\n\n"
-
+ # 달성률
+ achievement = (word_count / TARGET_WORDS) * 100
+ formatted += f"**달성률: {achievement:.1f}%**\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"
+ # 각 파트를 구분하여 표시
+ parts = novel_text.split('\n\n')
+
+ for i, part in enumerate(parts):
+ if part.strip():
+ # 파트 제목 추가
+ if i < len(NARRATIVE_PHASES):
+ formatted += f"## {NARRATIVE_PHASES[i]}\n\n"
+
+ formatted += f"{part}\n\n"
+
+ # 파트 사이 구분선
+ if i < len(parts) - 1:
+ formatted += "---\n\n"
return formatted
def export_to_docx(content: str, filename: str, language: str, session_id: str) -> str:
- """DOCX 파일로 내보내기"""
- doc = Document()
-
- # 페이지 설정
- section = doc.sections[0]
- section.page_height = Inches(11)
- section.page_width = Inches(8.5)
- section.top_margin = Inches(1)
- section.bottom_margin = Inches(1)
- section.left_margin = Inches(1.25)
- section.right_margin = Inches(1.25)
-
- # 세션 정보
- session = NovelDatabase.get_session(session_id)
-
- # 제목 페이지
- title_para = doc.add_paragraph()
- title_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
-
- if session:
- title_run = title_para.add_run(session["user_query"])
- title_run.font.size = Pt(24)
- title_run.bold = True
-
- # 메타 정보
- doc.add_paragraph()
- meta_para = doc.add_paragraph()
- meta_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
- meta_para.add_run(f"생성일: {datetime.now().strftime('%Y년 %m월 %d일')}\n")
- meta_para.add_run(f"총 단어 수: {len(content.split()):,}단어")
-
- # 페이지 나누기
- doc.add_page_break()
-
- # 본문 스타일 설정
- style = doc.styles['Normal']
- style.font.name = 'Calibri'
- style.font.size = Pt(11)
- style.paragraph_format.line_spacing = 1.5
- style.paragraph_format.space_after = Pt(6)
-
- # 본문 추가
- paragraphs = content.split('\n\n')
- for para_text in paragraphs:
- if para_text.strip():
- para = doc.add_paragraph(para_text.strip())
-
- # 파일 저장
- filepath = f"{filename}.docx"
- doc.save(filepath)
- return filepath
+ """DOCX 파일로 내보내기 - 한국 신국판 규격"""
+ doc = Document()
+
+ # 한국 신국판 규격 설정 (152mm x 225mm)
+ section = doc.sections[0]
+ section.page_height = Mm(225) # 225mm
+ section.page_width = Mm(152) # 152mm
+ section.top_margin = Mm(20) # 상단 여백 20mm
+ section.bottom_margin = Mm(20) # 하단 여백 20mm
+ section.left_margin = Mm(20) # 좌측 여백 20mm
+ section.right_margin = Mm(20) # 우측 여백 20mm
+
+ # 세션 정보로부터 제목 생성
+ session = NovelDatabase.get_session(session_id)
+
+ # 제목 생성 함수
+ def generate_title(user_query: str, content_preview: str) -> str:
+ """주제와 내용을 기반으로 제목 생성"""
+ # 간단한 규칙 기반 제목 생성 (실제로는 LLM 호출 가능)
+ if len(user_query) < 20:
+ return user_query
+ else:
+ # 주제에서 핵심 키워드 추출
+ keywords = user_query.split()[:5]
+ return " ".join(keywords)
+
+ # 제목 페이지
+ title = generate_title(session["user_query"], content[:500]) if session else "무제"
+
+ # 제목 스타일 설정
+ 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)
+ title_run.font.name = '바탕'
+ title_run._element.rPr.rFonts.set(qn('w:eastAsia'), '바탕')
+ title_run.font.size = Pt(20)
+ title_run.bold = True
+
+ # 페이지 구분
+ doc.add_page_break()
+
+ # 본문 스타일 설정
+ style = doc.styles['Normal']
+ style.font.name = '바탕'
+ style._element.rPr.rFonts.set(qn('w:eastAsia'), '바탕')
+ style.font.size = Pt(10.5) # 한국 소설 표준 크기
+ style.paragraph_format.line_spacing = 1.8 # 행간 180%
+ style.paragraph_format.space_after = Pt(0)
+ style.paragraph_format.first_line_indent = Mm(10) # 들여쓰기 10mm
+
+ # 본문 내용 정제 - 순수 텍스트만 추출
+ def clean_content(text: str) -> str:
+ """불필요한 마크다운, 파트 번호 등 제거"""
+ # 파트 제목/번호 패턴 제거
+ patterns_to_remove = [
+ r'^#{1,6}\s+.*$', # 마크다운 헤더
+ r'^\*\*.*\*\*$', # 굵은 글씨 라인
+ r'^파트\s*\d+.*$', # 파트 번호
+ r'^Part\s*\d+.*$', # Part 번호
+ r'^\d+\.\s+.*:.*$', # 번호가 있는 제목
+ r'^---+$', # 구분선
+ r'^\s*\[.*\]\s*$', # 대괄호로 둘러싸인 라벨
+ ]
+
+ lines = text.split('\n')
+ cleaned_lines = []
+
+ for line in lines:
+ # 빈 줄은 유지
+ if not line.strip():
+ cleaned_lines.append('')
+ continue
+
+ # 패턴 매칭으로 불필요한 라인 제거
+ 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:
+ # 마크다운 강조 표시 제거
+ 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())
+
+ # 연속된 빈 줄 제거 (최대 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)
+
+ # 내용 정제
+ cleaned_content = clean_content(content)
+
+ # 본문 추가
+ paragraphs = cleaned_content.split('\n')
+ for para_text in paragraphs:
+ if para_text.strip():
+ para = doc.add_paragraph(para_text.strip())
+ # 스타일 재확인 (한글 폰트 적용)
+ for run in para.runs:
+ run.font.name = '바탕'
+ run._element.rPr.rFonts.set(qn('w:eastAsia'), '바탕')
+ else:
+ # 문단 구분을 위한 빈 줄
+ doc.add_paragraph()
+
+ # 파일 저장
+ 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("=" * 80 + "\n")
+ f.write(f"생성일: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
+ f.write(f"총 단어 수: {len(content.split()):,}단어\n")
+ f.write("=" * 80 + "\n\n")
+
+ # 본문
f.write(content)
+
+ # 푸터
+ f.write("\n\n" + "=" * 80 + "\n")
+ f.write("AI 문학 창작 시스템 v2.0\n")
+ f.write("=" * 80 + "\n")
+
return filepath
-
# CSS 스타일
custom_css = """
.gradio-container {
- background: linear-gradient(135deg, #1e3c72 0%, #2a5298 50%, #1e3c72 100%);
+ background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
min-height: 100vh;
}
.main-header {
- background-color: rgba(255, 255, 255, 0.1);
- backdrop-filter: blur(10px);
- padding: 30px;
- border-radius: 12px;
+ 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: 1px solid rgba(255, 255, 255, 0.2);
+ border: 2px solid rgba(255, 255, 255, 0.1);
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
}
.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;
-}
-
-.improvement-note {
- background-color: rgba(0, 255, 127, 0.1);
- border-left: 3px solid #00ff7f;
- padding: 15px;
- margin: 20px 0;
- border-radius: 8px;
- color: #fff;
-}
-
-.warning-note {
- background-color: rgba(255, 69, 0, 0.1);
- border-left: 3px solid #ff4500;
- padding: 15px;
- margin: 20px 0;
- border-radius: 8px;
+ 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;
}
.input-section {
- background-color: rgba(255, 255, 255, 0.1);
- backdrop-filter: blur(10px);
- padding: 20px;
- border-radius: 12px;
- margin-bottom: 20px;
- border: 1px solid rgba(255, 255, 255, 0.2);
+ background-color: rgba(255, 255, 255, 0.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.1);
+ background-color: rgba(255, 255, 255, 0.06);
backdrop-filter: blur(10px);
- padding: 15px;
- border-radius: 8px;
- margin-top: 20px;
+ padding: 20px;
+ border-radius: 12px;
+ margin-top: 25px;
color: white;
- border: 1px solid rgba(255, 255, 255, 0.2);
+ border: 1px solid rgba(255, 255, 255, 0.08);
}
#stages-display {
- background-color: rgba(255, 255, 255, 0.95);
- padding: 20px;
- border-radius: 12px;
- max-height: 600px;
+ background-color: rgba(255, 255, 255, 0.97);
+ padding: 25px;
+ border-radius: 15px;
+ max-height: 650px;
overflow-y: auto;
- box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
+ color: #2c3e50;
}
#novel-output {
- background-color: rgba(255, 255, 255, 0.95);
- padding: 30px;
- border-radius: 12px;
- max-height: 700px;
+ background-color: rgba(255, 255, 255, 0.97);
+ padding: 35px;
+ border-radius: 15px;
+ max-height: 750px;
overflow-y: auto;
- box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+ 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.9);
- padding: 15px;
- border-radius: 8px;
- margin-top: 20px;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ background-color: rgba(255, 255, 255, 0.92);
+ padding: 20px;
+ border-radius: 12px;
+ margin-top: 25px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
-/* 진행 표시기 스타일 */
+/* 진행 표시기 개선 */
.progress-bar {
background-color: #e0e0e0;
- height: 20px;
- border-radius: 10px;
+ height: 25px;
+ border-radius: 12px;
overflow: hidden;
- margin: 10px 0;
+ margin: 15px 0;
+ box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1);
}
.progress-fill {
- background-color: #4CAF50;
+ background: linear-gradient(90deg, #4CAF50, #8BC34A);
height: 100%;
- transition: width 0.3s ease;
+ transition: width 0.5s ease;
+ box-shadow: 0 2px 8px rgba(76, 175, 80, 0.3);
}
-/* 점수 표시 스타일 */
-.score-badge {
- display: inline-block;
- padding: 2px 8px;
- border-radius: 12px;
- font-size: 0.9em;
- font-weight: bold;
- margin-left: 5px;
-}
-
-.score-high {
- background-color: #4CAF50;
- color: white;
+/* 스크롤바 스타일 */
+::-webkit-scrollbar {
+ width: 10px;
}
-.score-medium {
- background-color: #FF9800;
- color: white;
+::-webkit-scrollbar-track {
+ background: rgba(0, 0, 0, 0.1);
+ border-radius: 5px;
}
-.score-low {
- background-color: #F44336;
- color: white;
+::-webkit-scrollbar-thumb {
+ background: rgba(0, 0, 0, 0.3);
+ border-radius: 5px;
}
-/* 일관성 체크 표시 */
-.consistency-pass {
- color: #4CAF50;
- font-weight: bold;
+::-webkit-scrollbar-thumb:hover {
+ background: rgba(0, 0, 0, 0.5);
}
-.consistency-fail {
- color: #F44336;
- font-weight: bold;
-}
-
-/* 보고서 표시 스타일 */
-#report-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);
-}
-
-/* 분량 경고 스타일 */
-.word-count-warning {
- background-color: #FFF3CD;
- border-left: 4px solid #FFC107;
- padding: 10px 15px;
- margin: 10px 0;
- border-radius: 4px;
+/* 버튼 호버 효과 */
+.gr-button:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
+ transition: all 0.3s ease;
}
"""
# Gradio 인터페이스 생성
def create_interface():
- with gr.Blocks(css=custom_css, title="AI 진행형 장편소설 생성 시스템 v3.1") as interface:
+ with gr.Blocks(css=custom_css, title="AI 단일 작가 장편소설 시스템 v2.0") as interface:
gr.HTML("""
-
- 📚 AI 진행형 장편소설 생성 시스템 v3.1
+
+ 📚 AI 단일 작가 장편소설 시스템 v2.0
-
- 목표 분량 달성을 위한 최적화 버전
+
+ 하나의 일관된 목소리로 만드는 8,000단어 통합 서사
-
- 10개의 유기적으로 연결된 단계를 통해 하나의 완전한 이야기를 만들어냅니다.
+
+ 단일 작가가 10개 파트를 순차적으로 집필하며, 각 파트는 전담 비평가의 즉각적 피드백을 받아 수정됩��다.
- 각 단계는 이전 단계의 필연적 결과로 이어지며, 인물의 변화와 성장을 추적합니다.
+ 인과관계의 명확성과 서사의 유기적 진행을 최우선으로 추구합니다.
- ⚡ 반복이 아닌 축적, 순환이 아닌 진행을 통한 진정한 장편 서사
-
-
- 🆕 v3.1 핵심 개선사항:
-
- - 목표 단어 수 8,000단어로 조정
- - 프롬프트 간소화로 생성 공간 확보
- - 단어 수 부족 시 자동 재생성
- - 실시간 진행률 표시
- - 분량 미달 경고 시스템
- - 프롬프트 자동 증강 기능
- - 인과관계와 캐릭터 일관성 강화
-
-
-
- ⚠️ 분량 목표: 각 작가당 최소 800단어, 전체 8,000단어 이상
+ 🎯 핵심 혁신: 여러 작가의 파편화된 텍스트가 아닌,
+ 한 명의 작가가 처음부터 끝까지 일관되게 집필하는 진정한 장편소설
""")
@@ -2615,8 +1966,11 @@ def create_interface():
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
+ placeholder="""중편소설의 주제를 입력하세요.
+예: 인물의 내적 변화, 관계의 발전, 사회적 갈등과 개인의 선택...
+Enter your novella theme.
+Ex: Character transformation, relationship evolution, social conflict and personal choice...""",
+ lines=5
)
language_select = gr.Radio(
@@ -2626,61 +1980,55 @@ def create_interface():
)
with gr.Row():
- submit_btn = gr.Button("🚀 소설 생성 시작", variant="primary", scale=2)
+ submit_btn = gr.Button("🚀 집필 시작", variant="primary", scale=2)
clear_btn = gr.Button("🗑️ 초기화", scale=1)
status_text = gr.Textbox(
- label="상태",
+ label="진행 상태",
interactive=False,
value="🔄 준비 완료"
)
# 세션 관리
with gr.Group(elem_classes=["session-section"]):
- gr.Markdown("### 💾 진행 중인 세션")
+ gr.Markdown("### 💾 진행 중인 작품")
session_dropdown = gr.Dropdown(
- label="세션 선택",
+ 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)
+ 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("📝 창작 진행"):
+ with gr.Tab("📝 집필 과정"):
stages_display = gr.Markdown(
- value="창작 과정이 여기에 표시됩니다...",
+ value="집필 과정이 실시간으로 표시됩니다...",
elem_id="stages-display"
)
- with gr.Tab("📖 완성된 소설"):
+ with gr.Tab("📖 완성 작품"):
novel_output = gr.Markdown(
value="완성된 소설이 여기에 표시됩니다...",
elem_id="novel-output"
)
with gr.Group(elem_classes=["download-section"]):
- gr.Markdown("### 📥 소설 다운로드")
+ gr.Markdown("### 📥 작품 다운로드")
with gr.Row():
format_select = gr.Radio(
choices=["DOCX", "TXT"],
value="DOCX" if DOCX_AVAILABLE else "TXT",
- label="형식"
+ label="파일 형식"
)
download_btn = gr.Button("⬇️ 다운로드", variant="secondary")
download_file = gr.File(
- label="다운로드된 파일",
+ label="다운로드 파일",
visible=False
)
-
- with gr.Tab("📊 평가 보고서"):
- report_display = gr.Markdown(
- value="평가 보고서가 여기에 표시됩니다...",
- elem_id="report-display"
- )
# 숨겨진 상태
novel_text_state = gr.State("")
@@ -2689,15 +2037,13 @@ def create_interface():
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 시대에 일자리를 잃은 번역가의 새로운 도전"],
- ["재개발로 사라져가는 동네에서의 마지막 계절"]
+ ["실직한 중년 남성이 도시를 떠나 시골에서 새로운 삶의 의미를 찾아가는 과정"],
+ ["전쟁 트라우마를 가진 의사가 국경없는의사회 활동을 통해 치유되는 이야기"],
+ ["AI에게 일자리를 빼앗긴 번역가가 고전 문학 필사를 통해 언어의 본질을 재발견하는 여정"],
+ ["A daughter discovering her mother's hidden past through old letters"],
+ ["An architect losing sight who learns to design through touch and sound"],
+ ["재개발로 사라질 동네 서점을 지키려는 주민들의 연대"],
+ ["기억을 잃어가는 노교수와 그의 마지막 제자의 일 년"]
],
inputs=query_input,
label="💡 주제 예시"
@@ -2709,34 +2055,18 @@ def create_interface():
sessions = get_active_sessions("Korean")
return gr.update(choices=sessions)
except Exception as e:
- logger.error(f"Error refreshing sessions: {str(e)}")
- logger.error(f"Full error: {e}", exc_info=True) # 전체 스택 트레이스 로깅
+ logger.error(f"세션 새로고침 오류: {str(e)}")
return gr.update(choices=[])
def handle_auto_recover(language):
session_id, message = auto_recover_session(language)
return session_id, message
- def update_displays(stages_md, novel_md, status, session_id):
- """모든 디스플레이 업데이트"""
- # 평가 보고서 가져오기
- report = ""
- if session_id:
- session = NovelDatabase.get_session(session_id)
- if session and session.get('literary_report'):
- report = session['literary_report']
-
- return stages_md, novel_md, status, session_id, report
-
# 이벤트 연결
submit_btn.click(
fn=process_query,
inputs=[query_input, language_select, current_session_id],
outputs=[stages_display, novel_output, status_text, current_session_id]
- ).then(
- fn=lambda s, n, st, sid: (s, n, st, sid, NovelDatabase.get_session(sid).get('literary_report', '') if sid and NovelDatabase.get_session(sid) else ''),
- inputs=[stages_display, novel_output, status_text, current_session_id],
- outputs=[stages_display, novel_output, status_text, current_session_id, report_display]
)
novel_output.change(
@@ -2753,10 +2083,6 @@ def create_interface():
fn=resume_session,
inputs=[current_session_id, language_select],
outputs=[stages_display, novel_output, status_text, current_session_id]
- ).then(
- fn=lambda s, n, st, sid: (s, n, st, sid, NovelDatabase.get_session(sid).get('literary_report', '') if sid and NovelDatabase.get_session(sid) else ''),
- inputs=[stages_display, novel_output, status_text, current_session_id],
- outputs=[stages_display, novel_output, status_text, current_session_id, report_display]
)
auto_recover_btn.click(
@@ -2767,10 +2093,6 @@ def create_interface():
fn=resume_session,
inputs=[current_session_id, language_select],
outputs=[stages_display, novel_output, status_text, current_session_id]
- ).then(
- fn=lambda s, n, st, sid: (s, n, st, sid, NovelDatabase.get_session(sid).get('literary_report', '') if sid and NovelDatabase.get_session(sid) else ''),
- inputs=[stages_display, novel_output, status_text, current_session_id],
- outputs=[stages_display, novel_output, status_text, current_session_id, report_display]
)
refresh_btn.click(
@@ -2779,8 +2101,8 @@ def create_interface():
)
clear_btn.click(
- fn=lambda: ("", "", "🔄 준비 완료", "", None, ""),
- outputs=[stages_display, novel_output, status_text, novel_text_state, current_session_id, report_display]
+ 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):
@@ -2807,23 +2129,16 @@ def create_interface():
return interface
-
# 메인 실행
if __name__ == "__main__":
- logger.info("AI 진행형 장편소설 생성 시스템 v3.1 시작...")
+ logger.info("AI 단일 작가 장편소설 시스템 v2.0 시작...")
logger.info("=" * 60)
# 환경 확인
logger.info(f"API 엔드포인트: {API_URL}")
logger.info(f"목표 분량: {TARGET_WORDS:,}단어")
- logger.info(f"작가당 최소 분량: {MIN_WORDS_PER_WRITER:,}단어")
- logger.info("주요 개선사항:")
- logger.info("- 분량 목표 8,000단어로 조정")
- logger.info("- 프롬프트 간소화")
- logger.info("- 단어 수 부족 시 자동 재생성")
- logger.info("- 실시간 진행률 표시")
- logger.info("- 프롬프트 자동 증강 기능")
- logger.info("- 인과관계와 캐릭터 일관성 강화")
+ logger.info(f"파트당 최소 분량: {MIN_WORDS_PER_PART:,}단어")
+ logger.info("시스템 특징: 단일 작가 + 파트별 즉시 비평")
if BRAVE_SEARCH_API_KEY:
logger.info("웹 검색이 활성화되었습니다.")
@@ -2850,4 +2165,4 @@ if __name__ == "__main__":
server_port=7860,
share=False,
debug=True
- )
\ No newline at end of file
+ )
\ No newline at end of file