Spaces:
Running
Running
import gradio as gr | |
import os | |
import json | |
import requests | |
from datetime import datetime | |
import time | |
from typing import List, Dict, Any, Generator, Tuple, Optional, Set | |
import logging | |
import re | |
import tempfile | |
from pathlib import Path | |
import sqlite3 | |
import hashlib | |
import threading | |
from contextlib import contextmanager | |
from dataclasses import dataclass, field, asdict | |
from collections import defaultdict | |
# --- λ‘κΉ μ€μ --- | |
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') | |
logger = logging.getLogger(__name__) | |
# --- Document export imports --- | |
try: | |
from docx import Document | |
from docx.shared import Inches, Pt, RGBColor | |
from docx.enum.text import WD_ALIGN_PARAGRAPH | |
from docx.enum.style import WD_STYLE_TYPE | |
from docx.oxml.ns import qn | |
from docx.oxml import OxmlElement | |
DOCX_AVAILABLE = True | |
except ImportError: | |
DOCX_AVAILABLE = False | |
logger.warning("python-docx not installed. DOCX export will be disabled.") | |
# --- νκ²½ λ³μ λ° μμ --- | |
FRIENDLI_TOKEN = os.getenv("FRIENDLI_TOKEN", "") | |
BRAVE_SEARCH_API_KEY = os.getenv("BRAVE_SEARCH_API_KEY", "") | |
API_URL = "https://api.friendli.ai/dedicated/v1/chat/completions" | |
MODEL_ID = "dep89a2fld32mcm" | |
DB_PATH = "novel_sessions_v7.db" | |
# λͺ©ν λΆλ μ€μ (κΈ°μ‘΄ κ°μΌλ‘ 볡μ) | |
TARGET_WORDS = 8000 # μμ λ§μ§μ μν΄ 8000λ¨μ΄ | |
MIN_WORDS_PER_WRITER = 800 # κ° μκ° μ΅μ λΆλ 800λ¨μ΄ | |
# --- νκ²½ λ³μ κ²μ¦ --- | |
if not FRIENDLI_TOKEN: | |
logger.error("FRIENDLI_TOKEN not set. Application will not work properly.") | |
FRIENDLI_TOKEN = "dummy_token_for_testing" | |
if not BRAVE_SEARCH_API_KEY: | |
logger.warning("BRAVE_SEARCH_API_KEY not set. Web search features will be disabled.") | |
# --- μ μ λ³μ --- | |
db_lock = threading.Lock() | |
# μμ¬ μ§ν λ¨κ³ μ μ | |
NARRATIVE_PHASES = [ | |
"λμ : μΌμκ³Ό κ· μ΄", | |
"λ°μ 1: λΆμμ κ³ μ‘°", | |
"λ°μ 2: μΈλΆ 좩격", | |
"λ°μ 3: λ΄μ κ°λ± μ¬ν", | |
"μ μ 1: μκΈ°μ μ μ ", | |
"μ μ 2: μ νμ μκ°", | |
"νκ° 1: κ²°κ³Όμ μ¬ν", | |
"νκ° 2: μλ‘μ΄ μΈμ", | |
"κ²°λ§ 1: λ³νλ μΌμ", | |
"κ²°λ§ 2: μ΄λ¦° μ§λ¬Έ" | |
] | |
# νΈμ§μ λ¨κ³ μ¬μ© μ¬λΆ (Falseλ‘ μ€μ νλ©΄ νΈμ§μ λ¨κ³ 건λλ°κΈ°) | |
USE_EDITOR_STAGE = False # νΈμ§μκ° κ³Όλνκ² μμ νλ λ¬Έμ λ‘ λΉνμ±ν | |
# λ¨κ³λ³ κ΅¬μ± - νΈμ§μ λ¨κ³ μ‘°κ±΄λΆ ν¬ν¨ | |
PROGRESSIVE_STAGES = [ | |
("director", "π¬ κ°λ μ: ν΅ν©λ μμ¬ κ΅¬μ‘° κΈ°ν"), | |
("critic", "π λΉνκ°: μμ¬ μ§νμ±κ³Ό κΉμ΄ κ²ν "), | |
("director", "π¬ κ°λ μ: μμ λ λ§μ€ν°νλ"), | |
] + [ | |
(f"writer{i}", f"βοΈ μκ° {i}: μ΄μ - {NARRATIVE_PHASES[i-1]}") | |
for i in range(1, 11) | |
] + [ | |
("critic", "π λΉνκ°: μ€κ° κ²ν (μμ¬ λμ μ±κ³Ό λ³ν)"), | |
] + [ | |
(f"writer{i}", f"βοΈ μκ° {i}: μμ λ³Έ - {NARRATIVE_PHASES[i-1]}") | |
for i in range(1, 11) | |
] | |
# νΈμ§μ λ¨κ³ μ‘°κ±΄λΆ μΆκ° | |
if USE_EDITOR_STAGE: | |
PROGRESSIVE_STAGES.extend([ | |
("editor", "βοΈ νΈμ§μ: λ°λ³΅ μ κ±° λ° μμ¬ μ¬κ΅¬μ±"), | |
("critic", f"π λΉνκ°: μ΅μ’ κ²ν λ° λ¬Ένμ νκ°"), | |
]) | |
else: | |
PROGRESSIVE_STAGES.append( | |
("critic", f"π λΉνκ°: μ΅μ’ κ²ν λ° λ¬Ένμ νκ°"), | |
) | |
# μ λ κΈμ§μ¬ν - μΈμ΄ μ€λ₯ λ°©μ§ (κ°μν) | |
STRICT_RULES = """ | |
κΈμ§μ¬ν: | |
1. μΌλ³Έμ΄, μ€κ΅μ΄ λ± μΈκ΅μ΄ λ¬Έμ μ¬μ© κΈμ§ | |
2. μ΄μ λ¨κ³ μ¬κ±΄μ λ¨μ λ°λ³΅ κΈμ§ | |
3. μΊλ¦ν° μ΄λ¦ λ³κ²½ κΈμ§ | |
""" | |
# --- λ°μ΄ν° ν΄λμ€ --- | |
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) # κ΄κ³ λ³ν | |
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 SymbolicEvolution: | |
"""μμ§μ μλ―Έ λ³ν μΆμ """ | |
symbol: str | |
initial_meaning: str | |
phase_meanings: Dict[int, str] = field(default_factory=dict) | |
transformation_complete: bool = False | |
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 LanguageFilter: | |
"""μΈμ΄ νΌμ λ° νΉμκΈ°νΈ μ€λ₯ λ°©μ§""" | |
def __init__(self): | |
self.forbidden_patterns = [ | |
r'[γ-γ]+', # νλΌκ°λ | |
r'[γ‘-γΆ]+', # κ°νμΉ΄λ | |
r'[\u4e00-\u9fff]+', # νμ | |
r'\$\s*[.,]', # νΉμκΈ°νΈ μ€λ₯ | |
r'[λμμ΄κ°μλ₯Όμμμκ³Όλλ‘λΆν°κΉμ§λ§λ νλ€]\s*\$' # μ‘°μ¬ λ€ νΉμκΈ°νΈ | |
] | |
def clean_text(self, text: str) -> str: | |
"""μμ±λ ν μ€νΈ μ μ """ | |
import re | |
cleaned = text | |
# μΌλ³Έμ΄ λ¬Έμ μ κ±° | |
for pattern in self.forbidden_patterns: | |
cleaned = re.sub(pattern, '', cleaned) | |
# μ°μ 곡백 μ 리 | |
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 validate_new_content(self, phase: int, content: str, | |
previous_contents: List[str]) -> Tuple[bool, 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 | |
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 | |
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 | |
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 update_character_state(self, name: str, phase: int, new_state: Dict[str, Any], transformation: str): | |
"""μΊλ¦ν° μν μ λ°μ΄νΈ λ° λ³ν κΈ°λ‘""" | |
if name in self.character_arcs: | |
arc = self.character_arcs[name] | |
arc.phase_states[phase] = new_state | |
arc.transformations.append(f"Phase {phase}: {transformation}") | |
logger.info(f"Character {name} transformed in phase {phase}: {transformation}") | |
def add_plot_thread(self, thread_id: str, description: str, intro_phase: int): | |
"""μλ‘μ΄ νλ‘― λΌμΈ μΆκ°""" | |
self.plot_threads[thread_id] = PlotThread( | |
thread_id=thread_id, | |
description=description, | |
introduction_phase=intro_phase, | |
development_phases=[] | |
) | |
def develop_plot_thread(self, thread_id: str, phase: int): | |
"""νλ‘― λΌμΈ λ°μ """ | |
if thread_id in self.plot_threads: | |
self.plot_threads[thread_id].development_phases.append(phase) | |
def check_narrative_progression(self, current_phase: int) -> Tuple[bool, List[str]]: | |
"""μμ¬κ° μ€μ λ‘ μ§νλκ³ μλμ§ νμΈ""" | |
issues = [] | |
# 1. μΊλ¦ν° λ³ν νμΈ | |
static_characters = [] | |
for name, arc in self.character_arcs.items(): | |
if len(arc.transformations) < current_phase // 3: # μ΅μ 3λ¨κ³λ§λ€ λ³ν νμ | |
static_characters.append(name) | |
if static_characters: | |
issues.append(f"λ€μ μΈλ¬Όλ€μ λ³νκ° λΆμ‘±ν©λλ€: {', '.join(static_characters)}") | |
# 2. νλ‘― μ§ν νμΈ | |
unresolved_threads = [] | |
for thread_id, thread in self.plot_threads.items(): | |
if thread.status == "active" and len(thread.development_phases) < 2: | |
unresolved_threads.append(thread.description) | |
if unresolved_threads: | |
issues.append(f"μ§μ λμ§ μμ νλ‘―: {', '.join(unresolved_threads)}") | |
# 3. μμ§ λ°μ νμΈ | |
static_symbols = [] | |
for symbol, evolution in self.symbolic_evolutions.items(): | |
if len(evolution.phase_meanings) < current_phase // 4: | |
static_symbols.append(symbol) | |
if static_symbols: | |
issues.append(f"μλ―Έκ° λ°μ νμ§ μμ μμ§: {', '.join(static_symbols)}") | |
# 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()) | |
class NovelDatabase: | |
"""λ°μ΄ν°λ² μ΄μ€ κ΄λ¦¬""" | |
def init_db(): | |
with sqlite3.connect(DB_PATH) as conn: | |
conn.execute("PRAGMA journal_mode=WAL") | |
cursor = conn.cursor() | |
# κΈ°μ‘΄ ν μ΄λΈλ€ | |
cursor.execute(''' | |
CREATE TABLE IF NOT EXISTS sessions ( | |
session_id TEXT PRIMARY KEY, | |
user_query TEXT NOT NULL, | |
language TEXT NOT NULL, | |
created_at TEXT DEFAULT (datetime('now')), | |
updated_at TEXT DEFAULT (datetime('now')), | |
status TEXT DEFAULT 'active', | |
current_stage INTEGER DEFAULT 0, | |
final_novel TEXT, | |
literary_report TEXT, | |
total_words INTEGER DEFAULT 0, | |
narrative_tracker TEXT | |
) | |
''') | |
cursor.execute(''' | |
CREATE TABLE IF NOT EXISTS stages ( | |
id INTEGER PRIMARY KEY AUTOINCREMENT, | |
session_id TEXT NOT NULL, | |
stage_number INTEGER NOT NULL, | |
stage_name TEXT NOT NULL, | |
role TEXT NOT NULL, | |
content TEXT, | |
word_count INTEGER DEFAULT 0, | |
status TEXT DEFAULT 'pending', | |
progression_score REAL DEFAULT 0.0, | |
repetition_score REAL DEFAULT 0.0, | |
consistency_check TEXT, | |
created_at TEXT DEFAULT (datetime('now')), | |
updated_at TEXT DEFAULT (datetime('now')), | |
FOREIGN KEY (session_id) REFERENCES sessions(session_id), | |
UNIQUE(session_id, stage_number) | |
) | |
''') | |
cursor.execute(''' | |
CREATE TABLE IF NOT EXISTS plot_threads ( | |
id INTEGER PRIMARY KEY AUTOINCREMENT, | |
session_id TEXT NOT NULL, | |
thread_id TEXT NOT NULL, | |
description TEXT, | |
introduction_phase INTEGER, | |
status TEXT DEFAULT 'active', | |
created_at TEXT DEFAULT (datetime('now')), | |
FOREIGN KEY (session_id) REFERENCES sessions(session_id) | |
) | |
''') | |
# μλ‘μ΄ ν μ΄λΈ: μ€λ³΅ κ°μ§ κΈ°λ‘ | |
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 ( | |
id INTEGER PRIMARY KEY AUTOINCREMENT, | |
session_id TEXT NOT NULL, | |
evaluation_type TEXT NOT NULL, | |
score REAL, | |
details TEXT, | |
created_at TEXT DEFAULT (datetime('now')), | |
FOREIGN KEY (session_id) REFERENCES sessions(session_id) | |
) | |
''') | |
conn.commit() | |
# κΈ°μ‘΄ λ©μλλ€ μ μ§ | |
def get_db(): | |
with db_lock: | |
conn = sqlite3.connect(DB_PATH, timeout=30.0) | |
conn.row_factory = sqlite3.Row | |
try: | |
yield conn | |
finally: | |
conn.close() | |
def create_session(user_query: str, language: str) -> str: | |
session_id = hashlib.md5(f"{user_query}{datetime.now()}".encode()).hexdigest() | |
with NovelDatabase.get_db() as conn: | |
conn.cursor().execute( | |
'INSERT INTO sessions (session_id, user_query, language) VALUES (?, ?, ?)', | |
(session_id, user_query, language) | |
) | |
conn.commit() | |
return session_id | |
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 = ""): | |
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) | |
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)) | |
# μ΄ λ¨μ΄ μ μ λ°μ΄νΈ | |
cursor.execute(''' | |
UPDATE sessions | |
SET total_words = ( | |
SELECT SUM(word_count) | |
FROM stages | |
WHERE session_id = ? AND role LIKE 'writer%' AND content IS NOT NULL | |
), | |
updated_at = datetime('now'), | |
current_stage = ? | |
WHERE session_id = ? | |
''', (session_id, stage_number, session_id)) | |
conn.commit() | |
def get_writer_content(session_id: str) -> str: | |
"""μκ° μ½ν μΈ κ°μ Έμ€κΈ° (μμ λ³Έ μ°μ )""" | |
with NovelDatabase.get_db() as conn: | |
all_content = [] | |
for writer_num in range(1, 11): | |
# μμ λ³Έμ΄ μμΌλ©΄ μμ λ³Έμ, μμΌλ©΄ μ΄μμ | |
row = conn.cursor().execute(''' | |
SELECT content FROM stages | |
WHERE session_id = ? AND role = ? | |
AND stage_name LIKE '%μμ λ³Έ%' | |
ORDER BY stage_number DESC LIMIT 1 | |
''', (session_id, f'writer{writer_num}')).fetchone() | |
if not row or not row['content']: | |
# μμ λ³Έμ΄ μμΌλ©΄ μ΄μ μ¬μ© | |
row = conn.cursor().execute(''' | |
SELECT content FROM stages | |
WHERE session_id = ? AND role = ? | |
AND stage_name LIKE '%μ΄μ%' | |
ORDER BY stage_number DESC LIMIT 1 | |
''', (session_id, f'writer{writer_num}')).fetchone() | |
if row and row['content']: | |
all_content.append(row['content'].strip()) | |
return '\n\n'.join(all_content) | |
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 | |
def save_narrative_tracker(session_id: str, tracker: ProgressiveNarrativeTracker): | |
"""μμ¬ μΆμ κΈ° μ μ₯""" | |
with NovelDatabase.get_db() as conn: | |
tracker_data = json.dumps({ | |
'character_arcs': {k: asdict(v) for k, v in tracker.character_arcs.items()}, | |
'plot_threads': {k: asdict(v) for k, v in tracker.plot_threads.items()}, | |
'phase_summaries': tracker.phase_summaries, | |
'thematic_deepening': tracker.thematic_deepening, | |
'philosophical_insights': tracker.philosophical_insights, | |
'literary_devices': tracker.literary_devices, | |
'character_consistency': asdict(tracker.character_consistency), | |
'used_expressions': list(tracker.used_expressions) | |
}) | |
conn.cursor().execute( | |
'UPDATE sessions SET narrative_tracker = ? WHERE session_id = ?', | |
(tracker_data, session_id) | |
) | |
conn.commit() | |
def load_narrative_tracker(session_id: str) -> Optional[ProgressiveNarrativeTracker]: | |
"""μμ¬ μΆμ κΈ° λ‘λ""" | |
with NovelDatabase.get_db() as conn: | |
row = conn.cursor().execute( | |
'SELECT narrative_tracker FROM sessions WHERE session_id = ?', | |
(session_id,) | |
).fetchone() | |
if row and row['narrative_tracker']: | |
data = json.loads(row['narrative_tracker']) | |
tracker = ProgressiveNarrativeTracker() | |
# λ°μ΄ν° 볡μ | |
for name, arc_data in data.get('character_arcs', {}).items(): | |
tracker.character_arcs[name] = CharacterArc(**arc_data) | |
for thread_id, thread_data in data.get('plot_threads', {}).items(): | |
tracker.plot_threads[thread_id] = PlotThread(**thread_data) | |
tracker.phase_summaries = data.get('phase_summaries', {}) | |
tracker.thematic_deepening = data.get('thematic_deepening', []) | |
tracker.philosophical_insights = data.get('philosophical_insights', []) | |
tracker.literary_devices = data.get('literary_devices', {}) | |
# μΊλ¦ν° μΌκ΄μ± 볡μ | |
if 'character_consistency' in data: | |
tracker.character_consistency = CharacterConsistency(**data['character_consistency']) | |
# μ¬μ©λ νν 볡μ | |
if 'used_expressions' in data: | |
tracker.used_expressions = set(data['used_expressions']) | |
return tracker | |
return None | |
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() | |
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() | |
def get_session(session_id: str) -> Optional[Dict]: | |
with NovelDatabase.get_db() as conn: | |
row = conn.cursor().execute('SELECT * FROM sessions WHERE session_id = ?', (session_id,)).fetchone() | |
return dict(row) if row else None | |
def get_stages(session_id: str) -> List[Dict]: | |
with NovelDatabase.get_db() as conn: | |
rows = conn.cursor().execute('SELECT * FROM stages WHERE session_id = ? ORDER BY stage_number', (session_id,)).fetchall() | |
return [dict(row) for row in rows] | |
def update_final_novel(session_id: str, final_novel: str, literary_report: str = ""): | |
with NovelDatabase.get_db() as conn: | |
conn.cursor().execute( | |
"UPDATE sessions SET final_novel = ?, status = 'complete', updated_at = datetime('now'), literary_report = ? WHERE session_id = ?", | |
(final_novel, literary_report, session_id) | |
) | |
conn.commit() | |
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""" | |
).fetchall() | |
return [dict(row) for row in rows] | |
class WebSearchIntegration: | |
"""μΉ κ²μ κΈ°λ₯""" | |
def __init__(self): | |
self.brave_api_key = BRAVE_SEARCH_API_KEY | |
self.search_url = "https://api.search.brave.com/res/v1/web/search" | |
self.enabled = bool(self.brave_api_key) | |
def search(self, query: str, count: int = 3, language: str = "en") -> List[Dict]: | |
if not self.enabled: | |
return [] | |
headers = { | |
"Accept": "application/json", | |
"X-Subscription-Token": self.brave_api_key | |
} | |
params = { | |
"q": query, | |
"count": count, | |
"search_lang": "ko" if language == "Korean" else "en", | |
"text_decorations": False, | |
"safesearch": "moderate" | |
} | |
try: | |
response = requests.get(self.search_url, headers=headers, params=params, timeout=10) | |
response.raise_for_status() | |
results = response.json().get("web", {}).get("results", []) | |
return results | |
except requests.exceptions.RequestException as e: | |
logger.error(f"μΉ κ²μ API μ€λ₯: {e}") | |
return [] | |
def extract_relevant_info(self, results: List[Dict], max_chars: int = 1500) -> str: | |
if not results: | |
return "" | |
extracted = [] | |
total_chars = 0 | |
for i, result in enumerate(results[:3], 1): | |
title = result.get("title", "") | |
description = result.get("description", "") | |
info = f"[{i}] {title}: {description}" | |
if total_chars + len(info) < max_chars: | |
extracted.append(info) | |
total_chars += len(info) | |
else: | |
break | |
return "\n".join(extracted) | |
class ProgressiveLiterarySystem: | |
"""μ§νν λ¬Έν μμ€ μμ± μμ€ν """ | |
def __init__(self): | |
self.token = FRIENDLI_TOKEN | |
self.api_url = API_URL | |
self.model_id = MODEL_ID | |
self.narrative_tracker = ProgressiveNarrativeTracker() | |
self.web_search = WebSearchIntegration() | |
self.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 create_director_initial_prompt(self, user_query: str, language: str) -> str: | |
"""κ°λ μ μ΄κΈ° κΈ°ν - ν΅ν©λ μμ¬ κ΅¬μ‘°""" | |
# ν둬ννΈ μ¦κ° - νΉλ³ν μ§μκ° μμ λ μμ νμμΌλ‘ μ¦κ° | |
augmented_query = self.augment_prompt_if_needed(user_query, language) | |
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} λ¬Έν μν", | |
] | |
for q in queries[:2]: # 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" | |
except Exception as e: | |
logger.warning(f"κ²μ 쿼리 μ€ν¨: {q[:50]}... - {str(e)}") | |
continue | |
lang_prompts = { | |
"Korean": f"""λΉμ μ λ Έλ²¨λ¬Ένμ μμμκ° μμ€μ νκ΅ λ¬Έν κ±°μ₯μ λλ€. | |
μ€νΈ μμ€(8,000λ¨μ΄)μ μν ν΅ν©λ μμ¬ κ΅¬μ‘°λ₯Ό κΈ°ννμΈμ. | |
**μ£Όμ :** {augmented_query} | |
**μ°Έκ³ μλ£:** | |
{search_results_str[:500] if search_results_str else "N/A"} | |
**νμ μꡬμ¬ν:** | |
1. **μΈκ³Όκ΄κ³κ° μΉλ°ν ν΅ν© μμ¬** | |
- 10κ° λ¨κ³κ° νμ°μ μΌλ‘ μ°κ²°λ λ¨μΌ μμ¬ | |
- κ° λ¨κ³λ μ΄μ λ¨κ³μ μ§μ μ κ²°κ³Όλ‘ μ΄μ΄μ§ | |
- μΈλΆ 좩격과 λ΄μ κ°λ±μ μΈκ³Όκ΄κ³ λͺ νν | |
- μ£ΌμΈκ³΅κ³Ό μ‘°μ°λ€μ μ΄λ¦/μ€μ μ²μλΆν° κ³ μ | |
λ¨κ³λ³ μ§ν: | |
1) λμ : μΌμκ³Ό κ· μ΄ | |
2) λ°μ 1: λΆμμ κ³ μ‘° | |
3) λ°μ 2: μΈλΆ 좩격 | |
4) λ°μ 3: λ΄μ κ°λ± μ¬ν | |
5) μ μ 1: μκΈ°μ μ μ | |
6) μ μ 2: μ νμ μκ° | |
7) νκ° 1: κ²°κ³Όμ μ¬ν | |
8) νκ° 2: μλ‘μ΄ μΈμ | |
9) κ²°λ§ 1: λ³νλ μΌμ | |
10) κ²°λ§ 2: μ΄λ¦° μ§λ¬Έ | |
2. **μΊλ¦ν° μΌκ΄μ±κ³Ό μ 체μ±** | |
- κ° μΈλ¬Όμ ν΅μ¬ κ°μΉκ΄/μ±κ²© λͺ μ | |
- μ£ΌμΈκ³΅: μ΄κΈ° β μ€κ° β μ΅μ’ μνμ λ³ν κΆ€μ | |
- μ‘°μ°λ€: κ³ μ ν λ§ν¬μ νλ ν¨ν΄ μ€μ | |
- κ°λ±μ΄ μΈλ¬Όμ λ³Έμ§μ λλ¬λ΄λλ‘ μ€κ³ | |
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). | |
**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 | |
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.""" | |
} | |
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 | |
lang_prompts = { | |
"Korean": f"""λΉμ μ μκ° {writer_number}λ²μ λλ€. | |
**νμ¬ λ¨κ³: {phase_name}** | |
**μ 체 κ³ν (μμ½):** | |
{director_plan[:800]} | |
**μ΄μ λ΄μ©:** | |
{previous_content[-800:] if previous_content else "μμ"} | |
**νμ¬ μ§ν μν©:** | |
- νμ¬κΉμ§ μ΄ λ¨μ΄: {total_words} | |
- λͺ©νκΉμ§ λ¨μ λ¨μ΄: {remaining_words} | |
- μ΄λ² λ¨κ³ μ΅μ λ¨μ΄: {target_words} | |
**μμ± μ§μΉ¨:** | |
1. **νμ λΆλ: {target_words}-1000 λ¨μ΄** | |
2. **μμ¬μ νμ°μ± ν보:** | |
- μ΄μ λ¨κ³μ μ§μ μ κ²°κ³Όλ‘ μμ | |
- μλ‘μ΄ μ¬κ±΄μ κΈ°μ‘΄ κ°λ±μ λ Όλ¦¬μ κ·κ²° | |
- μΈλ¬Όμ μ νμ΄ λ€μ κ΅λ©΄μ κ²°μ | |
- μ°μ°μ΄ μλ νμ°μΌλ‘ μ κ° | |
3. **μΊλ¦ν° μΌκ΄μ± μ μ§:** | |
- μ€μ λ μΈλ¬Όλͺ κ³Ό κ΄κ³ μμ | |
- κ° μΈλ¬Όμ κ³ μ λ§ν¬ μ μ§ | |
- μ±κ²©μ μΌκ΄μ± μμμ λ³ν νν | |
- νλμ΄ μΊλ¦ν°μ λ³Έμ§μμ μΆλ° | |
4. **ꡬ체μ μ₯λ©΄ ꡬμ±:** | |
- κ°κ°μ λ¬μ¬ (μκ°, μ²κ°, μ΄κ° λ±) | |
- μμν λν (μΈλ¬Όλ³ μ΄ν¬ μ°¨λ³ν) | |
- λ΄λ©΄ μ¬λ¦¬μ ꡬ체μ νν | |
- 곡κ°κ³Ό μκ°μ λͺ νν μ€μ | |
5. **κΈμ§μ¬ν:** | |
- μ΄μ μν© λ¨μ λ°λ³΅ | |
- μΊλ¦ν° μ΄λ¦/μ€μ λ³κ²½ | |
- κ°μμ€λ¬μ΄ μ€μ λ³ν | |
- λΆλ λ―Έλ¬ | |
**μ€μ: λ°λμ {target_words}λ¨μ΄ μ΄μ, μΈκ³Όκ΄κ³κ° λͺ νν μμ¬λ₯Ό μμ±νμΈμ!** | |
μ΄μ μμνμΈμ:""", | |
"English": f"""You are Writer #{writer_number}. | |
**Current Phase: {phase_name}** | |
**Overall Plan (Summary):** | |
{director_plan[:800]} | |
**Previous Content:** | |
{previous_content[-800:] if previous_content else "Beginning"} | |
**Progress Status:** | |
- Total words so far: {total_words} | |
- Words remaining to target: {remaining_words} | |
- Minimum words this phase: {target_words} | |
**Writing Guidelines:** | |
1. **Required Length: {target_words}-1000 words** | |
2. **Narrative Necessity:** | |
- Start from direct results of previous | |
- New events as logical consequences | |
- Character choices determine next phase | |
- Necessity, not coincidence | |
3. **Character Consistency:** | |
- Maintain established names/relationships | |
- Keep unique speech patterns | |
- Express change within consistent personality | |
- Actions stem from character essence | |
4. **Concrete Scene Construction:** | |
- Sensory descriptions (visual, auditory, tactile) | |
- Vivid dialogue (differentiated speech) | |
- Specific psychological expression | |
- Clear spatial/temporal settings | |
5. **Forbidden:** | |
- Simple repetition | |
- Character name/setting changes | |
- Sudden setting shifts | |
- Under word count | |
**IMPORTANT: Must write {target_words}+ words with clear causality!** | |
Begin now:""" | |
} | |
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} μ μ¬") | |
# μΈκ³Όκ΄κ³ μ²΄ν¬ (μΆκ°) | |
causality_issues = [] | |
if phase_count > 3: | |
# κ° λ¨κ³κ° μ΄μ λ¨κ³μ κ²°κ³ΌμΈμ§ νμΈ | |
if "κ·Έλ¬λ κ°μκΈ°" in all_content or "μ°μ°ν" in all_content or "λ»λ°μ" in all_content: | |
causality_issues.append("μ°μ°μ μ¬κ±΄ λ°μ - νμ°μ μ κ° νμ") | |
lang_prompts = { | |
"Korean": f"""μμ¬ μ§νμ κ²ν νμΈμ. | |
**μ μ£Όμ :** {user_query} | |
**νμ¬ μ§ν:** {phase_count}/10 λ¨κ³ | |
**λ°κ²¬λ λ¬Έμ :** | |
{chr(10).join(issues[:5]) if issues else "μμ"} | |
**μ€λ³΅ λ°κ²¬:** | |
{chr(10).join(duplicates[:3]) if duplicates else "μμ"} | |
**μΈκ³Όκ΄κ³ λ¬Έμ :** | |
{chr(10).join(causality_issues) if causality_issues else "μμ"} | |
**κ²ν νλͺ©:** | |
1. μμ¬κ° νμ°μ μΌλ‘ μ§νλλκ°? | |
2. μΈλ¬Όμ΄ μΌκ΄λκ² λ³ννλκ°? | |
3. λ°λ³΅μ΄λ μ기볡μ κ° μλκ°? | |
4. κ° λ¨κ³κ° μ΄μ μ κ²°κ³ΌμΈκ°? | |
5. λΆλμ΄ μΆ©λΆνκ°? | |
**νμ :** ν΅κ³Ό/μ¬μμ± νμ | |
μΉλ°ν μΈκ³Όκ΄κ³μ μΊλ¦ν° μΌκ΄μ±μ μ€μ¬μΌλ‘ νκ°νμΈμ.""", | |
"English": f"""Review narrative progression. | |
**Theme:** {user_query} | |
**Progress:** {phase_count}/10 phases | |
**Issues Found:** | |
{chr(10).join(issues[:5]) if issues else "None"} | |
**Duplications:** | |
{chr(10).join(duplicates[:3]) if duplicates else "None"} | |
**Causality Issues:** | |
{chr(10).join(causality_issues) if causality_issues else "None"} | |
**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? | |
**Verdict:** Pass/Rewrite needed | |
Evaluate focusing on tight causality and character consistency.""" | |
} | |
return lang_prompts.get(language, lang_prompts["Korean"]) | |
def create_writer_revision_prompt(self, writer_number: int, initial_content: str, | |
critic_feedback: str, language: str) -> str: | |
"""μκ° μμ ν둬ννΈ (κ°μν)""" | |
target_words = MIN_WORDS_PER_WRITER | |
lang_prompts = { | |
"Korean": f"""μκ° {writer_number}λ², μμ νμΈμ. | |
**λΉν μμ :** | |
{critic_feedback[:500]} | |
**μμ λ°©ν₯:** | |
1. λ°λ³΅ μ κ±°, μλ‘μ΄ μ κ° μΆκ° | |
2. μ΅μ {target_words}λ¨μ΄ μ μ§ | |
3. μΈλ¬Ό λ³ν ꡬ체ν | |
4. λνμ λ¬μ¬ μΆκ° | |
μ λ©΄ μ¬μμ±μ΄ νμνλ©΄ κ³Όκ°ν μμ νμΈμ. | |
μμ λ³Έλ§ μ μνμΈμ.""", | |
"English": f"""Writer #{writer_number}, revise. | |
**Critique Points:** | |
{critic_feedback[:500]} | |
**Revision Direction:** | |
1. Remove repetition, add new development | |
2. Maintain {target_words} words minimum | |
3. Specify character changes | |
4. Add dialogue and description | |
Boldly rewrite if needed. | |
Present only revised version.""" | |
} | |
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. **μ λ κΈμ§μ¬ν:** | |
- λ¬Έλ¨ ν΅μ§Έλ‘ μμ β | |
- λ΄μ© μμ½μ΄λ μΆμ½ β | |
- μκ°μ 문체 λ³κ²½ β | |
- μ€κ±°λ¦¬ μ¬κ΅¬μ± β | |
4. **νΈμ§ λ°©λ²:** | |
- μ€λ³΅ λ¬Έμ₯: λ λ²μ§Έ κ²λ§ μ κ±° | |
- μ΄μν μ°κ²°: μ μμ¬λ‘ μ°κ²° | |
- μ€ν: μ΅μνμ μμ | |
**μ€μ: κ±°μ λͺ¨λ λ΄μ©μ κ·Έλλ‘ μ μ§νλ©΄μ μμ£Ό μμ λ¬Έμ λ§ μμ νμΈμ.** | |
μκ³ μ 체λ₯Ό λ€μ μμ±νμ§ λ§κ³ , μλ³Έμ 볡μ¬ν ν μ΅μνμ μμ λ§ κ°νμΈμ. | |
νΈμ§λ μ 체 μκ³ λ₯Ό μ μνμΈμ. ({min_words}λ¨μ΄ μ΄μ νμ!)""", | |
"English": f"""You are a preservation-focused editor. | |
**Manuscript length: {current_word_count} words** | |
**Minimum after editing: {min_words} words (REQUIRED!)** | |
**Editing Rules:** | |
1. **Preservation is Priority** | |
- Must keep 95%+ of manuscript | |
- Prefer correction over deletion | |
- Never delete whole paragraphs | |
2. **Allowed Edits:** | |
- Remove only when exact same sentence appears consecutively | |
- Fix grammar errors or typos | |
- Add conjunctions for awkward connections (1-2 words) | |
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 | |
**IMPORTANT: Keep almost everything while fixing only tiny issues.** | |
Don't rewrite the manuscript, copy the original and apply minimal edits. | |
Present the full edited manuscript. ({min_words}+ words required!)""" | |
} | |
return lang_prompts.get(language, lang_prompts["Korean"]) | |
def create_critic_final_prompt(self, content: str, query: str, language: str) -> str: | |
"""μ΅μ’ λΉν""" | |
word_count = len(content.split()) | |
# μμ¬ κ΅¬μ‘° λΆμ (μΆκ°) | |
has_single_plot = self.check_single_narrative(content) | |
character_consistency = self.check_character_consistency_in_final(content) | |
lang_prompts = { | |
"Korean": f"""μμ±λ μμ€μ νκ°νμΈμ. | |
**μ£Όμ :** {query} | |
**λΆλ:** {word_count}λ¨μ΄ (λͺ©ν: 8,000) | |
**νκ° κΈ°μ€:** | |
1. ν΅ν©λ μμ¬ κ΅¬μ‘° (30μ ) | |
- λ¨μΌν νλ‘― μ‘΄μ¬ μ¬λΆ | |
- μΈκ³Όκ΄κ³μ μΉλ°ν¨ | |
- κ° λ¨κ³μ νμ°μ± | |
2. μΊλ¦ν° μΌκ΄μ± (25μ ) | |
- μ΄λ¦κ³Ό μ€μ μ ν΅μΌμ± | |
- μ±κ²©κ³Ό νλμ μΌκ΄μ± | |
- λ³νμ μ€λλ ₯ | |
3. λ¬Ένμ μμ±λ (25μ ) | |
- 문체μ μΌκ΄μ± | |
- μμ§κ³Ό λͺ¨ν°ν νμ© | |
- λ¬Ένμ κΈ°λ²μ λ€μμ± | |
4. μ£Όμ μμκ³Ό ν΅μ°° (20μ ) | |
- μ² νμ κΉμ΄ | |
- μΈκ°μ λν ν΅μ°° | |
- λ μ°½μ ν΄μ | |
**κ°μ μμΈ:** | |
- νλ‘― μ기볡μ (-10μ ) | |
- μΊλ¦ν° μ€μ μΆ©λ (-10μ ) | |
- λ¨μ λ°λ³΅ (-5μ ) | |
- λΆλ λ―Έλ¬ (-5μ ) | |
**μ’ ν© νκ°:** | |
μνμ΄ νλμ μκ²°λ μ₯νΈμμ€λ‘ κΈ°λ₯νλκ°? | |
μ΄μ : /100μ | |
μνμ λ¬Ένμ κ°μΉμ ꡬ쑰μ μμ±λλ₯Ό μ’ ν©μ μΌλ‘ νκ°νμΈμ.""", | |
"English": f"""Evaluate the completed novel. | |
**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 | |
**Deductions:** | |
- Plot self-replication (-10 pts) | |
- Character setting conflicts (-10 pts) | |
- Simple repetition (-5 pts) | |
- Under length (-5 pts) | |
**Overall Assessment:** | |
Does the work function as a complete novel? | |
Total: /100 points | |
Comprehensively evaluate literary value and structural completeness.""" | |
} | |
return lang_prompts.get(language, lang_prompts["Korean"]) | |
# --- 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 | |
# μ΅μ’ μμ€ μ 리 | |
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 | |
except Exception as e: | |
logger.error(f"μμ€ μμ± νλ‘μΈμ€ μ€λ₯: {e}", exc_info=True) | |
yield f"β μ€λ₯ λ°μ: {e}", stages if 'stages' in locals() else [], self.current_session_id | |
def get_stage_prompt(self, stage_idx, 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 | |
) | |
elif stage_idx == 2: | |
# μΈ λ²μ§Έ λ¨κ³ - κ°λ μ μμ | |
if role == "director": | |
return self.create_director_revision_prompt( | |
stages[0]["content"], # μ΄κΈ° κΈ°ν | |
stages[1]["content"], # λΉν | |
query, | |
language | |
) | |
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) | |
# μ΄μμΈμ§ μμ λ³ΈμΈμ§ νμΈ | |
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 | |
) | |
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" | |
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 | |
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" | |
return self.create_critic_final_prompt(final_content, query, language) | |
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" | |
# λ¬Έμ μ κ°μ§ - λ§€μ° μ¬κ°ν λ¬Έμ λ§ | |
issues = [] | |
# μμ ν λμΌν λ¬Έλ¨ μ²΄ν¬ | |
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()) | |
if exact_duplicates > 5: | |
issues.append(f"{exact_duplicates}κ°μ μμ ν λμΌν λ¬Έλ¨ λ°κ²¬") | |
# λ¬Έμ κ° κ±°μ μμΌλ©΄ νΈμ§ μ΅μν | |
if len(issues) == 0: | |
issues = ["μκ³ μνκ° μνΈν©λλ€. νΈμ§μ μ΅μννμΈμ."] | |
return self.create_editor_prompt(complete_novel, issues, language) | |
# κΈ°λ³Έκ° (μλ¬ λ°©μ§) | |
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} | |
**λΉν μμ :** | |
{critic_feedback[:500]} | |
**μμ ν΅μ¬:** | |
1. 10λ¨κ³κ° μΈκ³Όκ΄κ³λ‘ κΈ΄λ°ν μ°κ²° | |
2. μΊλ¦ν° μ΄λ¦κ³Ό μ€μ μ²μλΆν° κ³ μ | |
3. κ° λ¨κ³κ° μ΄μ μ νμ°μ κ²°κ³Ό | |
4. νλ‘―μ μ기볡μ λ°©μ§ | |
5. 8,000λ¨μ΄ λ¬μ± μ λ΅ | |
**ꡬ쑰 κ°ν:** | |
- μΈλΆ μ¬κ±΄μ΄ λ΄μ λ³νλ₯Ό μ΄λ° | |
- μ νμ κ²°κ³Όκ° λ€μ κ΅λ©΄ κ²°μ | |
- μ°μ°μ΄ μλ νμ°μ μ κ° | |
μΉλ°ν μΈκ³Όκ΄κ³μ μμ κ³νμ μ μνμΈμ.""", | |
"English": f"""Revise based on critique. | |
**Original Theme:** {user_query} | |
**Critique Points:** | |
{critic_feedback[:500]} | |
**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 | |
**Structure Enhancement:** | |
- External events trigger internal changes | |
- Choices determine next phase | |
- Necessary, not coincidental development | |
Present revision plan with tight causality.""" | |
} | |
return lang_prompts.get(language, lang_prompts["Korean"]) | |
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 "μ΄μ λ΄μ©μ μ΄μ΄λ°μ μ§ννμΈμ." | |
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 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 | |
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}") | |
# --- μ νΈλ¦¬ν° ν¨μλ€ --- | |
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 | |
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 | |
def auto_recover_session(language: str) -> Tuple[Optional[str], str]: | |
"""μ΅κ·Ό μΈμ μλ 볡ꡬ""" | |
sessions = NovelDatabase.get_active_sessions() | |
if sessions: | |
latest_session = sessions[0] | |
return latest_session['session_id'], f"μΈμ {latest_session['session_id'][:8]}... 볡ꡬλ¨" | |
return None, "볡ꡬν μΈμ μ΄ μμ΅λλ€." | |
def resume_session(session_id: str, language: str) -> Generator[Tuple[str, str, str, str], None, None]: | |
"""μΈμ μ¬κ°""" | |
if not session_id: | |
yield "", "", "β μΈμ IDκ° μμ΅λλ€.", session_id | |
return | |
if "..." in session_id: | |
session_id = session_id.split("...")[0] | |
session = NovelDatabase.get_session(session_id) | |
if not session: | |
yield "", "", "β μΈμ μ μ°Ύμ μ μμ΅λλ€.", None | |
return | |
yield from process_query(session['user_query'], session['language'], session_id) | |
def download_novel(novel_text: str, format_type: str, language: str, session_id: str) -> Optional[str]: | |
"""μμ€ λ€μ΄λ‘λ νμΌ μμ±""" | |
if not novel_text or not session_id: | |
return None | |
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") | |
filename = f"novel_{session_id[:8]}_{timestamp}" | |
try: | |
if format_type == "DOCX" and DOCX_AVAILABLE: | |
return export_to_docx(novel_text, filename, language, session_id) | |
else: | |
return export_to_txt(novel_text, filename) | |
except Exception as e: | |
logger.error(f"νμΌ μμ± μ€ν¨: {e}") | |
return None | |
def format_stages_display(stages: List[Dict]) -> str: | |
"""λ¨κ³λ³ μ§ν μν© νμ""" | |
markdown = "## π¬ μ§ν μν©\n\n" | |
# μ΄ λ¨μ΄ μ κ³μ° | |
total_words = sum(s.get('word_count', 0) for s in stages if 'writer' in s.get('name', '')) | |
progress_percent = (total_words / TARGET_WORDS) * 100 if TARGET_WORDS > 0 else 0 | |
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" | |
for i, stage in enumerate(stages): | |
status_icon = "β " if stage['status'] == 'complete' else "π" if stage['status'] == 'active' else "β³" | |
markdown += f"{status_icon} **{stage['name']}**" | |
if stage.get('word_count', 0) > 0: | |
markdown += f" ({stage['word_count']:,}λ¨μ΄)" | |
# μκ° λ¨κ³μμ λ¨μ΄ μ λΆμ‘± κ²½κ³ | |
if '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']}" | |
markdown += "\n" | |
if stage['content']: | |
preview = stage['content'][:200] + "..." if len(stage['content']) > 200 else stage['content'] | |
markdown += f"> {preview}\n\n" | |
return markdown | |
def format_novel_display(novel_text: str) -> str: | |
"""μμ€ λ΄μ© νμ""" | |
if not novel_text: | |
return "μμ§ μμ±λ λ΄μ©μ΄ μμ΅λλ€." | |
formatted = "# π μμ±λ μμ€\n\n" | |
# λ¨μ΄ μ νμ | |
word_count = len(novel_text.split()) | |
formatted += f"**μ΄ λΆλ: {word_count:,}λ¨μ΄ (λͺ©ν: {TARGET_WORDS:,}λ¨μ΄)**\n\n" | |
if word_count < TARGET_WORDS: | |
shortage = TARGET_WORDS - word_count | |
formatted += f"β οΈ **μ£Όμ: λͺ©ν λΆλμ {shortage:,}λ¨μ΄ λΆμ‘±ν©λλ€.**\n\n" | |
else: | |
formatted += f"β **λͺ©ν λΆλ λ¬μ±!**\n\n" | |
formatted += "---\n\n" | |
# κ° λ¨κ³λ₯Ό ꡬλΆνμ¬ νμ | |
sections = novel_text.split('\n\n') | |
for i, section in enumerate(sections): | |
if section.strip(): | |
formatted += f"{section}\n\n" | |
return formatted | |
def export_to_docx(content: str, filename: str, language: str, session_id: str) -> str: | |
"""DOCX νμΌλ‘ λ΄λ³΄λ΄κΈ°""" | |
doc = Document() | |
# νμ΄μ§ μ€μ | |
section = doc.sections[0] | |
section.page_height = Inches(11) | |
section.page_width = Inches(8.5) | |
section.top_margin = Inches(1) | |
section.bottom_margin = Inches(1) | |
section.left_margin = Inches(1.25) | |
section.right_margin = Inches(1.25) | |
# μΈμ μ 보 | |
session = NovelDatabase.get_session(session_id) | |
# μ λͺ© νμ΄μ§ | |
title_para = doc.add_paragraph() | |
title_para.alignment = WD_ALIGN_PARAGRAPH.CENTER | |
if session: | |
title_run = title_para.add_run(session["user_query"]) | |
title_run.font.size = Pt(24) | |
title_run.bold = True | |
# λ©ν μ 보 | |
doc.add_paragraph() | |
meta_para = doc.add_paragraph() | |
meta_para.alignment = WD_ALIGN_PARAGRAPH.CENTER | |
meta_para.add_run(f"μμ±μΌ: {datetime.now().strftime('%Yλ %mμ %dμΌ')}\n") | |
meta_para.add_run(f"μ΄ λ¨μ΄ μ: {len(content.split()):,}λ¨μ΄") | |
# νμ΄μ§ λλκΈ° | |
doc.add_page_break() | |
# λ³Έλ¬Έ μ€νμΌ μ€μ | |
style = doc.styles['Normal'] | |
style.font.name = 'Calibri' | |
style.font.size = Pt(11) | |
style.paragraph_format.line_spacing = 1.5 | |
style.paragraph_format.space_after = Pt(6) | |
# λ³Έλ¬Έ μΆκ° | |
paragraphs = content.split('\n\n') | |
for para_text in paragraphs: | |
if para_text.strip(): | |
para = doc.add_paragraph(para_text.strip()) | |
# νμΌ μ μ₯ | |
filepath = f"{filename}.docx" | |
doc.save(filepath) | |
return filepath | |
def export_to_txt(content: str, filename: str) -> str: | |
"""TXT νμΌλ‘ λ΄λ³΄λ΄κΈ°""" | |
filepath = f"{filename}.txt" | |
with open(filepath, 'w', encoding='utf-8') as f: | |
f.write(content) | |
return filepath | |
# CSS μ€νμΌ | |
custom_css = """ | |
.gradio-container { | |
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 50%, #1e3c72 100%); | |
min-height: 100vh; | |
} | |
.main-header { | |
background-color: rgba(255, 255, 255, 0.1); | |
backdrop-filter: blur(10px); | |
padding: 30px; | |
border-radius: 12px; | |
margin-bottom: 30px; | |
text-align: center; | |
color: white; | |
border: 1px solid rgba(255, 255, 255, 0.2); | |
} | |
.progress-note { | |
background-color: rgba(255, 223, 0, 0.1); | |
border-left: 3px solid #ffd700; | |
padding: 15px; | |
margin: 20px 0; | |
border-radius: 8px; | |
color: #fff; | |
} | |
.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; | |
color: #fff; | |
} | |
.input-section { | |
background-color: rgba(255, 255, 255, 0.1); | |
backdrop-filter: blur(10px); | |
padding: 20px; | |
border-radius: 12px; | |
margin-bottom: 20px; | |
border: 1px solid rgba(255, 255, 255, 0.2); | |
} | |
.session-section { | |
background-color: rgba(255, 255, 255, 0.1); | |
backdrop-filter: blur(10px); | |
padding: 15px; | |
border-radius: 8px; | |
margin-top: 20px; | |
color: white; | |
border: 1px solid rgba(255, 255, 255, 0.2); | |
} | |
#stages-display { | |
background-color: rgba(255, 255, 255, 0.95); | |
padding: 20px; | |
border-radius: 12px; | |
max-height: 600px; | |
overflow-y: auto; | |
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); | |
} | |
#novel-output { | |
background-color: rgba(255, 255, 255, 0.95); | |
padding: 30px; | |
border-radius: 12px; | |
max-height: 700px; | |
overflow-y: auto; | |
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); | |
} | |
.download-section { | |
background-color: rgba(255, 255, 255, 0.9); | |
padding: 15px; | |
border-radius: 8px; | |
margin-top: 20px; | |
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); | |
} | |
/* μ§ν νμκΈ° μ€νμΌ */ | |
.progress-bar { | |
background-color: #e0e0e0; | |
height: 20px; | |
border-radius: 10px; | |
overflow: hidden; | |
margin: 10px 0; | |
} | |
.progress-fill { | |
background-color: #4CAF50; | |
height: 100%; | |
transition: width 0.3s ease; | |
} | |
/* μ μ νμ μ€νμΌ */ | |
.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; | |
} | |
.score-medium { | |
background-color: #FF9800; | |
color: white; | |
} | |
.score-low { | |
background-color: #F44336; | |
color: white; | |
} | |
/* μΌκ΄μ± μ²΄ν¬ νμ */ | |
.consistency-pass { | |
color: #4CAF50; | |
font-weight: bold; | |
} | |
.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; | |
} | |
""" | |
# Gradio μΈν°νμ΄μ€ μμ± | |
def create_interface(): | |
with gr.Blocks(css=custom_css, title="AI μ§νν μ₯νΈμμ€ μμ± μμ€ν v3.1") as interface: | |
gr.HTML(""" | |
<div class="main-header"> | |
<h1 style="font-size: 2.5em; margin-bottom: 10px;"> | |
π AI μ§νν μ₯νΈμμ€ μμ± μμ€ν v3.1 | |
</h1> | |
<h3 style="color: #ddd; margin-bottom: 20px;"> | |
λͺ©ν λΆλ λ¬μ±μ μν μ΅μ ν λ²μ | |
</h3> | |
<p style="font-size: 1.1em; color: #eee; max-width: 800px; margin: 0 auto;"> | |
10κ°μ μ κΈ°μ μΌλ‘ μ°κ²°λ λ¨κ³λ₯Ό ν΅ν΄ νλμ μμ ν μ΄μΌκΈ°λ₯Ό λ§λ€μ΄λ λλ€. | |
<br> | |
κ° λ¨κ³λ μ΄μ λ¨κ³μ νμ°μ κ²°κ³Όλ‘ μ΄μ΄μ§λ©°, μΈλ¬Όμ λ³νμ μ±μ₯μ μΆμ ν©λλ€. | |
</p> | |
<div class="progress-note"> | |
β‘ λ°λ³΅μ΄ μλ μΆμ , μνμ΄ μλ μ§νμ ν΅ν μ§μ ν μ₯νΈ μμ¬ | |
</div> | |
<div class="improvement-note"> | |
π v3.1 ν΅μ¬ κ°μ μ¬ν: | |
<ul style="text-align: left; margin: 10px auto; max-width: 600px;"> | |
<li>λͺ©ν λ¨μ΄ μ 8,000λ¨μ΄λ‘ μ‘°μ </li> | |
<li>ν둬ννΈ κ°μνλ‘ μμ± κ³΅κ° ν보</li> | |
<li>λ¨μ΄ μ λΆμ‘± μ μλ μ¬μμ±</li> | |
<li>μ€μκ° μ§νλ₯ νμ</li> | |
<li>λΆλ λ―Έλ¬ κ²½κ³ μμ€ν </li> | |
<li>ν둬ννΈ μλ μ¦κ° κΈ°λ₯</li> | |
<li>μΈκ³Όκ΄κ³μ μΊλ¦ν° μΌκ΄μ± κ°ν</li> | |
</ul> | |
</div> | |
<div class="warning-note"> | |
β οΈ λΆλ λͺ©ν: κ° μκ°λΉ μ΅μ 800λ¨μ΄, μ 체 8,000λ¨μ΄ μ΄μ | |
</div> | |
</div> | |
""") | |
# μν κ΄λ¦¬ | |
current_session_id = gr.State(None) | |
with gr.Row(): | |
with gr.Column(scale=1): | |
with gr.Group(elem_classes=["input-section"]): | |
query_input = gr.Textbox( | |
label="μμ€ μ£Όμ / Novel Theme", | |
placeholder="μ€νΈμμ€μ μ£Όμ λ₯Ό μ λ ₯νμΈμ. μΈλ¬Όμ λ³νμ μ±μ₯μ΄ μ€μ¬μ΄ λλ μ΄μΌκΈ°...\nEnter the theme for your novella. Focus on character transformation and growth...", | |
lines=4 | |
) | |
language_select = gr.Radio( | |
choices=["Korean", "English"], | |
value="Korean", | |
label="μΈμ΄ / Language" | |
) | |
with gr.Row(): | |
submit_btn = gr.Button("π μμ€ μμ± μμ", variant="primary", scale=2) | |
clear_btn = gr.Button("ποΈ μ΄κΈ°ν", scale=1) | |
status_text = gr.Textbox( | |
label="μν", | |
interactive=False, | |
value="π μ€λΉ μλ£" | |
) | |
# μΈμ κ΄λ¦¬ | |
with gr.Group(elem_classes=["session-section"]): | |
gr.Markdown("### πΎ μ§ν μ€μΈ μΈμ ") | |
session_dropdown = gr.Dropdown( | |
label="μΈμ μ ν", | |
choices=[], | |
interactive=True | |
) | |
with gr.Row(): | |
refresh_btn = gr.Button("π λͺ©λ‘ μλ‘κ³ μΉ¨", scale=1) | |
resume_btn = gr.Button("βΆοΈ μ ν μ¬κ°", variant="secondary", scale=1) | |
auto_recover_btn = gr.Button("β»οΈ μ΅κ·Ό μΈμ 볡ꡬ", scale=1) | |
with gr.Column(scale=2): | |
with gr.Tab("π μ°½μ μ§ν"): | |
stages_display = gr.Markdown( | |
value="μ°½μ κ³Όμ μ΄ μ¬κΈ°μ νμλ©λλ€...", | |
elem_id="stages-display" | |
) | |
with gr.Tab("π μμ±λ μμ€"): | |
novel_output = gr.Markdown( | |
value="μμ±λ μμ€μ΄ μ¬κΈ°μ νμλ©λλ€...", | |
elem_id="novel-output" | |
) | |
with gr.Group(elem_classes=["download-section"]): | |
gr.Markdown("### π₯ μμ€ λ€μ΄λ‘λ") | |
with gr.Row(): | |
format_select = gr.Radio( | |
choices=["DOCX", "TXT"], | |
value="DOCX" if DOCX_AVAILABLE else "TXT", | |
label="νμ" | |
) | |
download_btn = gr.Button("β¬οΈ λ€μ΄λ‘λ", variant="secondary") | |
download_file = gr.File( | |
label="λ€μ΄λ‘λλ νμΌ", | |
visible=False | |
) | |
with gr.Tab("π νκ° λ³΄κ³ μ"): | |
report_display = gr.Markdown( | |
value="νκ° λ³΄κ³ μκ° μ¬κΈ°μ νμλ©λλ€...", | |
elem_id="report-display" | |
) | |
# μ¨κ²¨μ§ μν | |
novel_text_state = gr.State("") | |
# μμ | |
with gr.Row(): | |
gr.Examples( | |
examples=[ | |
["κΈ°μ΄μνμκΈμκ° λ μ²λ μ μμ‘΄κ³Ό μ‘΄μμ± μ°ΎκΈ°"], | |
["μ€μ§ν μ€λ λ¨μ±μ΄ μλ‘μ΄ μΆμ μλ―Έλ₯Ό μ°Ύμκ°λ μ¬μ "], | |
["λμμμ μκ³¨λ‘ μ΄μ£Όν μ²λ μ μ μκ³Ό μ±μ₯ μ΄μΌκΈ°"], | |
["μΈ μΈλκ° ν¨κ» μ¬λ κ°μ‘±μ κ°λ±κ³Ό νν΄"], | |
["A middle-aged woman's journey to rediscover herself after divorce"], | |
["The transformation of a cynical journalist through unexpected encounters"], | |
["μμ μμ μ μ΄μνλ λ ΈλΆλΆμ λ§μ§λ§ 1λ "], | |
["AI μλμ μΌμ리λ₯Ό μμ λ²μκ°μ μλ‘μ΄ λμ "], | |
["μ¬κ°λ°λ‘ μ¬λΌμ Έκ°λ λλ€μμμ λ§μ§λ§ κ³μ "] | |
], | |
inputs=query_input, | |
label="π‘ μ£Όμ μμ" | |
) | |
# μ΄λ²€νΈ νΈλ€λ¬ | |
def refresh_sessions(): | |
try: | |
sessions = get_active_sessions("Korean") | |
return gr.update(choices=sessions) | |
except Exception as e: | |
logger.error(f"Error refreshing sessions: {str(e)}") | |
logger.error(f"Full error: {e}", exc_info=True) # μ 체 μ€ν νΈλ μ΄μ€ λ‘κΉ | |
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( | |
fn=lambda x: x, | |
inputs=[novel_output], | |
outputs=[novel_text_state] | |
) | |
resume_btn.click( | |
fn=lambda x: x.split("...")[0] if x and "..." in x else x, | |
inputs=[session_dropdown], | |
outputs=[current_session_id] | |
).then( | |
fn=resume_session, | |
inputs=[current_session_id, language_select], | |
outputs=[stages_display, novel_output, status_text, current_session_id] | |
).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( | |
fn=handle_auto_recover, | |
inputs=[language_select], | |
outputs=[current_session_id, status_text] | |
).then( | |
fn=resume_session, | |
inputs=[current_session_id, language_select], | |
outputs=[stages_display, novel_output, status_text, current_session_id] | |
).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( | |
fn=refresh_sessions, | |
outputs=[session_dropdown] | |
) | |
clear_btn.click( | |
fn=lambda: ("", "", "π μ€λΉ μλ£", "", None, ""), | |
outputs=[stages_display, novel_output, status_text, novel_text_state, current_session_id, report_display] | |
) | |
def handle_download(format_type, language, session_id, novel_text): | |
if not session_id or not novel_text: | |
return gr.update(visible=False) | |
file_path = download_novel(novel_text, format_type, language, session_id) | |
if file_path: | |
return gr.update(value=file_path, visible=True) | |
else: | |
return gr.update(visible=False) | |
download_btn.click( | |
fn=handle_download, | |
inputs=[format_select, language_select, current_session_id, novel_text_state], | |
outputs=[download_file] | |
) | |
# μμ μ μΈμ λ‘λ | |
interface.load( | |
fn=refresh_sessions, | |
outputs=[session_dropdown] | |
) | |
return interface | |
# λ©μΈ μ€ν | |
if __name__ == "__main__": | |
logger.info("AI μ§νν μ₯νΈμμ€ μμ± μμ€ν v3.1 μμ...") | |
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("- μΈκ³Όκ΄κ³μ μΊλ¦ν° μΌκ΄μ± κ°ν") | |
if BRAVE_SEARCH_API_KEY: | |
logger.info("μΉ κ²μμ΄ νμ±νλμμ΅λλ€.") | |
else: | |
logger.warning("μΉ κ²μμ΄ λΉνμ±νλμμ΅λλ€.") | |
if DOCX_AVAILABLE: | |
logger.info("DOCX λ΄λ³΄λ΄κΈ°κ° νμ±νλμμ΅λλ€.") | |
else: | |
logger.warning("DOCX λ΄λ³΄λ΄κΈ°κ° λΉνμ±νλμμ΅λλ€.") | |
logger.info("=" * 60) | |
# λ°μ΄ν°λ² μ΄μ€ μ΄κΈ°ν | |
logger.info("λ°μ΄ν°λ² μ΄μ€ μ΄κΈ°ν μ€...") | |
NovelDatabase.init_db() | |
logger.info("λ°μ΄ν°λ² μ΄μ€ μ΄κΈ°ν μλ£.") | |
# μΈν°νμ΄μ€ μμ± λ° μ€ν | |
interface = create_interface() | |
interface.launch( | |
server_name="0.0.0.0", | |
server_port=7860, | |
share=False, | |
debug=True | |
) |