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