openfree's picture
Update app.py
1ca08f3 verified
raw
history blame
55.5 kB
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_v4.db"
# --- ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๊ฒ€์ฆ ---
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()
# ๋ฌธํ•™์  ๋‹จ๊ณ„ ๊ตฌ์„ฑ (๋‚ด๋ฉด ์„œ์ˆ ๊ณผ ์‚ฌํšŒ์  ํ†ต์ฐฐ ์ค‘์‹ฌ)
LITERARY_STAGES = [
("director", "๐ŸŽฌ ๊ฐ๋…์ž: ์‚ฌํšŒ์  ๋งฅ๋ฝ๊ณผ ์ธ๋ฌผ ์‹ฌ๋ฆฌ ๊ธฐํš"),
("critic", "๐Ÿ“ ๋น„ํ‰๊ฐ€: ์‚ฌํšŒ ๋น„ํŒ์  ๊นŠ์ด์™€ ์ƒ์ง•์„ฑ ๊ฒ€ํ† "),
("director", "๐ŸŽฌ ๊ฐ๋…์ž: ์ˆ˜์ •๋œ ๋งˆ์Šคํ„ฐํ”Œ๋žœ"),
] + [
(f"writer{i}", f"โœ๏ธ ์ž‘๊ฐ€ {i}: ์ดˆ์•ˆ")
for i in range(1, 11)
] + [
("critic", "๐Ÿ“ ๋น„ํ‰๊ฐ€: ์ค‘๊ฐ„ ๊ฒ€ํ†  (๋‚ด์  ์ผ๊ด€์„ฑ๊ณผ ์ฃผ์ œ ์‹ฌํ™”)"),
] + [
(f"writer{i}", f"โœ๏ธ ์ž‘๊ฐ€ {i}: ์ˆ˜์ •๋ณธ")
for i in range(1, 11)
] + [
("critic", f"๐Ÿ“ ๋น„ํ‰๊ฐ€: ์ตœ์ข… ๊ฒ€ํ†  ๋ฐ ๋ฌธํ•™์  ํ‰๊ฐ€"),
]
# --- ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค ---
@dataclass
class CharacterPsychology:
"""์ธ๋ฌผ์˜ ์‹ฌ๋ฆฌ์  ์ƒํƒœ์™€ ๋‚ด๋ฉด"""
name: str
age: int
social_class: str # ๊ณ„๊ธ‰์  ์œ„์น˜
occupation: str
inner_conflict: str # ๋‚ด์  ๊ฐˆ๋“ฑ
worldview: str # ์„ธ๊ณ„๊ด€
desires: List[str] # ์š•๋ง๋“ค
fears: List[str] # ๋‘๋ ค์›€๋“ค
coping_mechanisms: List[str] # ๋ฐฉ์–ด๊ธฐ์ œ
relationships: Dict[str, str] # ํƒ€์ธ๊ณผ์˜ ๊ด€๊ณ„
@dataclass
class SymbolicElement:
"""์ƒ์ง•์  ์š”์†Œ"""
symbol: str
meaning: str
appearances: List[int] # ๋“ฑ์žฅํ•˜๋Š” ์žฅ๋“ค
evolution: str # ์ƒ์ง•์˜ ์˜๋ฏธ ๋ณ€ํ™”
@dataclass
class SocialContext:
"""์‚ฌํšŒ์  ๋งฅ๋ฝ"""
economic_system: str
class_structure: str
power_dynamics: str
social_issues: List[str]
cultural_atmosphere: str
# --- ํ•ต์‹ฌ ๋กœ์ง ํด๋ž˜์Šค ---
class LiteraryConsistencyTracker:
"""๋ฌธํ•™์  ์ผ๊ด€์„ฑ ์ถ”์  ์‹œ์Šคํ…œ"""
def __init__(self):
self.characters: Dict[str, CharacterPsychology] = {}
self.symbols: Dict[str, SymbolicElement] = {}
self.social_context: Optional[SocialContext] = None
self.themes: List[str] = []
self.narrative_voice: str = "" # ์„œ์ˆ  ์‹œ์ ๊ณผ ๋ฌธ์ฒด
self.tone: str = "" # ์ „์ฒด์ ์ธ ํ†ค
def register_character(self, character: CharacterPsychology):
"""์ธ๋ฌผ ๋“ฑ๋ก"""
self.characters[character.name] = character
logger.info(f"Character registered: {character.name}, Class: {character.social_class}")
def register_symbol(self, symbol: SymbolicElement):
"""์ƒ์ง• ๋“ฑ๋ก"""
self.symbols[symbol.symbol] = symbol
logger.info(f"Symbol registered: {symbol.symbol} = {symbol.meaning}")
def check_thematic_consistency(self, content: str, chapter: int) -> List[str]:
"""์ฃผ์ œ์  ์ผ๊ด€์„ฑ ๊ฒ€์‚ฌ"""
issues = []
# ์‚ฌํšŒ๋น„ํŒ์  ์š”์†Œ๊ฐ€ ์œ ์ง€๋˜๋Š”์ง€
if self.social_context and not any(issue.lower() in content.lower()
for issue in self.social_context.social_issues):
issues.append("์‚ฌํšŒ์  ๋งฅ๋ฝ์ด ์•ฝํ™”๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๊ณ„๊ธ‰, ๋ถˆํ‰๋“ฑ ๋“ฑ์˜ ์ฃผ์ œ๋ฅผ ์œ ์ง€ํ•˜์„ธ์š”.")
# ๋‚ด๋ฉด ์„œ์ˆ ์ด ์ถฉ๋ถ„ํ•œ์ง€
introspective_keywords = ['์ƒ๊ฐํ–ˆ๋‹ค', '๋А๊ผˆ๋‹ค', '๊ธฐ์–ต', '์˜์‹', '๋งˆ์Œ',
'thought', 'felt', 'remembered', 'consciousness']
if not any(keyword in content for keyword in introspective_keywords):
issues.append("๋‚ด๋ฉด ์„œ์ˆ ์ด ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค. ์ธ๋ฌผ์˜ ์‹ฌ๋ฆฌ๋ฅผ ๋” ๊นŠ์ด ํƒ๊ตฌํ•˜์„ธ์š”.")
return issues
class NovelDatabase:
"""๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ด€๋ฆฌ"""
@staticmethod
def init_db():
with sqlite3.connect(DB_PATH) as conn:
conn.execute("PRAGMA journal_mode=WAL")
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS sessions (
session_id TEXT PRIMARY KEY,
user_query TEXT NOT NULL,
language TEXT NOT NULL,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now')),
status TEXT DEFAULT 'active',
current_stage INTEGER DEFAULT 0,
final_novel TEXT,
literary_report TEXT,
social_context TEXT,
narrative_style 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',
literary_score REAL DEFAULT 0.0,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (session_id) REFERENCES sessions(session_id),
UNIQUE(session_id, stage_number)
)
''')
cursor.execute('''
CREATE TABLE IF NOT EXISTS characters (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL,
name TEXT NOT NULL,
age INTEGER,
social_class TEXT,
occupation TEXT,
inner_conflict TEXT,
worldview TEXT,
created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (session_id) REFERENCES sessions(session_id),
UNIQUE(session_id, name)
)
''')
cursor.execute('''
CREATE TABLE IF NOT EXISTS symbols (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL,
symbol TEXT NOT NULL,
meaning TEXT,
created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (session_id) REFERENCES sessions(session_id)
)
''')
conn.commit()
# ๊ธฐ์กด ๋ฉ”์„œ๋“œ๋“ค ์œ ์ง€
@staticmethod
@contextmanager
def get_db():
with db_lock:
conn = sqlite3.connect(DB_PATH, timeout=30.0)
conn.row_factory = sqlite3.Row
try:
yield conn
finally:
conn.close()
@staticmethod
def create_session(user_query: str, language: str) -> str:
session_id = hashlib.md5(f"{user_query}{datetime.now()}".encode()).hexdigest()
with NovelDatabase.get_db() as conn:
conn.cursor().execute(
'INSERT INTO sessions (session_id, user_query, language) VALUES (?, ?, ?)',
(session_id, user_query, language)
)
conn.commit()
return session_id
@staticmethod
def save_stage(session_id: str, stage_number: int, stage_name: str,
role: str, content: str, status: str = 'complete',
literary_score: float = 0.0):
word_count = len(content.split()) if content else 0
with NovelDatabase.get_db() as conn:
cursor = conn.cursor()
cursor.execute('''
INSERT INTO stages (session_id, stage_number, stage_name, role, content, word_count, status, literary_score)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(session_id, stage_number)
DO UPDATE SET content=?, word_count=?, status=?, stage_name=?, literary_score=?, updated_at=datetime('now')
''', (session_id, stage_number, stage_name, role, content, word_count, status, literary_score,
content, word_count, status, stage_name, literary_score))
cursor.execute(
"UPDATE sessions SET updated_at = datetime('now'), current_stage = ? WHERE session_id = ?",
(stage_number, session_id)
)
conn.commit()
@staticmethod
def get_writer_content(session_id: str) -> str:
"""์ž‘๊ฐ€ ์ฝ˜ํ…์ธ  ๊ฐ€์ ธ์˜ค๊ธฐ"""
with NovelDatabase.get_db() as conn:
all_content = []
for writer_num in range(1, 11):
row = conn.cursor().execute(
"SELECT content FROM stages WHERE session_id = ? AND role = ? AND stage_name LIKE '%์ˆ˜์ •๋ณธ%' ORDER BY stage_number DESC LIMIT 1",
(session_id, f'writer{writer_num}')
).fetchone()
if row and row['content']:
content = row['content'].strip()
all_content.append(content)
return '\n\n'.join(all_content)
@staticmethod
def get_session(session_id: str) -> Optional[Dict]:
with NovelDatabase.get_db() as conn:
row = conn.cursor().execute('SELECT * FROM sessions WHERE session_id = ?', (session_id,)).fetchone()
return dict(row) if row else None
@staticmethod
def get_stages(session_id: str) -> List[Dict]:
with NovelDatabase.get_db() as conn:
rows = conn.cursor().execute('SELECT * FROM stages WHERE session_id = ? ORDER BY stage_number', (session_id,)).fetchall()
return [dict(row) for row in rows]
@staticmethod
def update_final_novel(session_id: str, final_novel: str, literary_report: str = ""):
with NovelDatabase.get_db() as conn:
conn.cursor().execute(
"UPDATE sessions SET final_novel = ?, status = 'complete', updated_at = datetime('now'), literary_report = ? WHERE session_id = ?",
(final_novel, literary_report, session_id)
)
conn.commit()
@staticmethod
def get_active_sessions() -> List[Dict]:
with NovelDatabase.get_db() as conn:
rows = conn.cursor().execute(
"SELECT session_id, user_query, language, created_at, current_stage 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 LiteraryNovelSystem:
"""๋ฌธํ•™์  ์†Œ์„ค ์ƒ์„ฑ ์‹œ์Šคํ…œ"""
def __init__(self):
self.token = FRIENDLI_TOKEN
self.api_url = API_URL
self.model_id = MODEL_ID
self.consistency_tracker = LiteraryConsistencyTracker()
self.web_search = WebSearchIntegration()
self.current_session_id = None
NovelDatabase.init_db()
def create_headers(self):
return {"Authorization": f"Bearer {self.token}", "Content-Type": "application/json"}
# --- ํ”„๋กฌํ”„ํŠธ ์ƒ์„ฑ ํ•จ์ˆ˜๋“ค (๋ฌธํ•™์  ๊นŠ์ด ์ค‘์‹ฌ) ---
def create_director_initial_prompt(self, user_query: str, language: str) -> str:
"""๊ฐ๋…์ž ์ดˆ๊ธฐ ๊ธฐํš ํ”„๋กฌํ”„ํŠธ (์‚ฌํšŒ๋น„ํŒ์  ๋ฆฌ์–ผ๋ฆฌ์ฆ˜)"""
search_results_str = ""
if self.web_search.enabled:
# ์‚ฌํšŒ์  ๋งฅ๋ฝ ๊ฒ€์ƒ‰
queries = [f"{user_query} ์‚ฌํšŒ ๋ฌธ์ œ", f"{user_query} ๊ณ„๊ธ‰ ๊ฐˆ๋“ฑ", f"{user_query} social inequality"]
for q in queries[:1]:
results = self.web_search.search(q, count=2, language=language)
if results:
search_results_str += self.web_search.extract_relevant_info(results) + "\n"
lang_prompts = {
"Korean": f"""๋‹น์‹ ์€ ํ•œ๊ตญ ํ˜„๋Œ€ ๋ฌธํ•™์˜ ์ „ํ†ต์„ ์ž‡๋Š” ์†Œ์„ค ๊ธฐํš์ž์ž…๋‹ˆ๋‹ค.
ํ•œ๊ฐ•, ๊น€์• ๋ž€, ์ฒœ๋ช…๊ด€, ์ •์„ธ๋ž‘ ๋“ฑ ํ˜„๋Œ€ ํ•œ๊ตญ ์ž‘๊ฐ€๋“ค์˜ ๋ฌธํ•™์  ์„ฑ์ทจ๋ฅผ ์ฐธ๊ณ ํ•˜์—ฌ,
์‚ฌํšŒ๋น„ํŒ์  ๋ฆฌ์–ผ๋ฆฌ์ฆ˜๊ณผ ๋‚ด๋ฉด ์‹ฌ๋ฆฌ ํƒ๊ตฌ๊ฐ€ ๊ฒฐํ•ฉ๋œ 30ํŽ˜์ด์ง€ ์ค‘ํŽธ์†Œ์„ค์„ ๊ธฐํšํ•˜์„ธ์š”.
**์‚ฌ์šฉ์ž ์ฃผ์ œ:** {user_query}
**์ฐธ๊ณ  ์ž๋ฃŒ:**
{search_results_str if search_results_str else "N/A"}
**๊ธฐํš ์š”๊ตฌ์‚ฌํ•ญ:**
1. **์ฃผ์ œ์™€ ์‚ฌํšŒ์  ๋งฅ๋ฝ**
- ํ•ต์‹ฌ ์ฃผ์ œ: ํ˜„๋Œ€ ํ•œ๊ตญ ์‚ฌํšŒ์˜ ๊ตฌ์กฐ์  ๋ฌธ์ œ (๊ณ„๊ธ‰, ๋นˆ๊ณค, ์†Œ์™ธ, ์  ๋”, ์„ธ๋Œ€ ๋“ฑ)
- ๋น„ํŒ์  ์‹œ๊ฐ: ๊ฐœ์ธ์˜ ๋ฌธ์ œ๋ฅผ ์‚ฌํšŒ ๊ตฌ์กฐ์™€ ์—ฐ๊ฒฐ
- ํ˜„์‹ค์„ฑ: 2020๋…„๋Œ€ ํ•œ๊ตญ์˜ ๊ตฌ์ฒด์  ํ˜„์‹ค ๋ฐ˜์˜
2. **์„œ์ˆ  ๋ฐฉ์‹๊ณผ ๋ฌธ์ฒด**
- ์‹œ์ : 1์ธ์นญ ๋˜๋Š” ์ œํ•œ์  3์ธ์นญ (๋‚ด๋ฉด ์ ‘๊ทผ ๊ฐ€๋Šฅ)
- ๋ฌธ์ฒด: ๋‹ด๋‹ดํ•˜์ง€๋งŒ ๋‚ ์นด๋กœ์šด ๊ด€์ฐฐ, ์ผ์ƒ์–ด์™€ ๋ฌธํ•™์  ํ‘œํ˜„์˜ ๊ท ํ˜•
- ๋‚ด๋ฉด ์„œ์ˆ : ์˜์‹์˜ ํ๋ฆ„, ํšŒ์ƒ, ์ž๊ธฐ ์„ฑ์ฐฐ ์ ๊ทน ํ™œ์šฉ
3. **์ธ๋ฌผ ์„ค์ •** (2-4๋ช…์˜ ํ•ต์‹ฌ ์ธ๋ฌผ)
| ์ด๋ฆ„ | ๋‚˜์ด | ๊ณ„๊ธ‰์  ์œ„์น˜ | ์ง์—… | ๋‚ด์  ๊ฐˆ๋“ฑ | ์š•๋ง | ๋‘๋ ค์›€ |
- ๊ฐ ์ธ๋ฌผ์€ ํŠน์ • ๊ณ„๊ธ‰๊ณผ ์‚ฌํšŒ์  ์œ„์น˜๋ฅผ ๋Œ€ํ‘œ
- ๊ฒ‰๋ชจ์Šต๊ณผ ๋‚ด๋ฉด์˜ ๊ดด๋ฆฌ ํ‘œํ˜„
- ๊ตฌ์กฐ์  ์–ต์•• ์†์—์„œ์˜ ๊ฐœ์ธ์  ์„ ํƒ
4. **์ƒ์ง•๊ณผ ์€์œ **
- ํ•ต์‹ฌ ์ƒ์ง•: (์˜ˆ: ๊ฐœ๊ตฌ๋ฆฌ=์†Œ์™ธ๋œ ์ž๋“ค์˜ ๋ชฉ์†Œ๋ฆฌ, ์—ฐ๋ชป=๊ณ„๊ธ‰ ๊ฒฝ๊ณ„)
- ๋ฐ˜๋ณต๋˜๋Š” ์ด๋ฏธ์ง€๋‚˜ ๋ชจํ‹ฐํ”„
- ์ผ์ƒ์  ์‚ฌ๋ฌผ์— ๋‹ด๊ธด ์‚ฌํšŒ์  ์˜๋ฏธ
5. **ํ”Œ๋กฏ ๊ตฌ์กฐ**
- ๊ทน์  ์‚ฌ๊ฑด๋ณด๋‹ค ์ผ์ƒ ์† ๋ฏธ๋ฌ˜ํ•œ ๋ณ€ํ™” ์ค‘์‹ฌ
- ์ธ๋ฌผ์˜ ์ธ์‹ ๋ณ€ํ™”๊ฐ€ ๊ณง ์„œ์‚ฌ์˜ ์ง„ํ–‰
- ์—ด๋ฆฐ ๊ฒฐ๋ง: ํ•ด๊ฒฐ๋ณด๋‹ค๋Š” ๋ฌธ์ œ ์ œ๊ธฐ์™€ ์„ฑ์ฐฐ
**์ ˆ๋Œ€ ๊ธˆ์ง€์‚ฌํ•ญ:**
- ์„ ์•… ๊ตฌ๋ถ„์ด ๋ช…ํ™•ํ•œ ํ‰๋ฉด์  ์ธ๋ฌผ
- ๊ธ‰์ž‘์Šค๋Ÿฌ์šด ์‚ฌ๊ฑด์ด๋‚˜ ๊ทน์  ๋ฐ˜์ „
- ๊ตํ›ˆ์ ์ด๊ฑฐ๋‚˜ ๊ณ„๋ชฝ์ ์ธ ๋ฉ”์‹œ์ง€
- ์•ˆ์ดํ•œ ํฌ๋ง์ด๋‚˜ ํ™”ํ•ด
๋‚ด๋ฉด๊ณผ ์‚ฌํšŒ๋ฅผ ๋™์‹œ์— ํฌ์ฐฉํ•˜๋Š” ์„ฌ์„ธํ•œ ๊ธฐํš์„ ์ž‘์„ฑํ•˜์„ธ์š”.""",
"English": f"""You are a literary director planning a 30-page novella in the tradition of contemporary social realism.
Drawing from authors like George Saunders, Zadie Smith, and Sally Rooney, create a work that combines psychological depth with social critique.
**User Theme:** {user_query}
**Reference Material:**
{search_results_str if search_results_str else "N/A"}
**Planning Requirements:**
1. **Theme and Social Context**
- Core theme: Structural problems in contemporary society (class, inequality, alienation)
- Critical perspective: Connect individual struggles to social systems
- Realism: Reflect specific contemporary realities
2. **Narrative Style**
- POV: First person or limited third person (with access to interiority)
- Style: Understated yet sharp observation, balance of vernacular and literary
- Interior narration: Stream of consciousness, memory, self-reflection
3. **Character Design** (2-4 main characters)
| Name | Age | Class Position | Occupation | Inner Conflict | Desires | Fears |
- Each character represents specific social position
- Gap between appearance and interior life
- Individual choices within structural constraints
4. **Symbols and Metaphors**
- Key symbols with social meaning
- Recurring images or motifs
- Everyday objects as social commentary
5. **Plot Structure**
- Focus on subtle changes over dramatic events
- Character perception shifts drive narrative
- Open ending: Questions over resolutions
**Absolutely Avoid:**
- Clear-cut heroes and villains
- Sudden dramatic events or twists
- Didactic or preachy messages
- Easy hope or reconciliation
Create a nuanced plan that captures both interior life and social reality."""
}
return lang_prompts.get(language, lang_prompts["Korean"])
def create_critic_director_prompt(self, director_plan: str, user_query: str, language: str) -> str:
"""๋น„ํ‰๊ฐ€์˜ ๊ฐ๋…์ž ๊ธฐํš ๊ฒ€ํ† """
lang_prompts = {
"Korean": f"""๋‹น์‹ ์€ ๋ฌธํ•™ ๋น„ํ‰๊ฐ€์ž…๋‹ˆ๋‹ค. ๋‹ค์Œ ๊ด€์ ์—์„œ ๊ธฐํš์„ ๊ฒ€ํ† ํ•˜์„ธ์š”:
**์› ์ฃผ์ œ:** {user_query}
**๊ฐ๋…์ž ๊ธฐํš:**
{director_plan}
**๊ฒ€ํ†  ๊ธฐ์ค€:**
1. **์‚ฌํšŒ๋น„ํŒ์  ๊นŠ์ด**
- ๊ฐœ์ธ๊ณผ ๊ตฌ์กฐ์˜ ์—ฐ๊ฒฐ์ด ์„ค๋“๋ ฅ ์žˆ๋Š”๊ฐ€?
- ํ˜„์‹ค์˜ ๋ณต์žก์„ฑ์„ ๋‹จ์ˆœํ™”ํ•˜์ง€ ์•Š์•˜๋Š”๊ฐ€?
- ์ง„๋ถ€ํ•œ ์‚ฌํšŒ ๋น„ํŒ์— ๊ทธ์น˜์ง€ ์•Š์•˜๋Š”๊ฐ€?
2. **๋ฌธํ•™์  ์™„์„ฑ๋„**
- ๋‚ด๋ฉด ์„œ์ˆ ๊ณผ ์™ธ์  ํ˜„์‹ค์˜ ๊ท ํ˜•
- ์ƒ์ง•๊ณผ ์€์œ ์˜ ์ ์ ˆ์„ฑ
- ์ธ๋ฌผ์˜ ์ž…์ฒด์„ฑ๊ณผ ์‹ ๋น™์„ฑ
3. **ํ˜„๋Œ€์„ฑ๊ณผ ๋ณดํŽธ์„ฑ**
- 2020๋…„๋Œ€ ํ•œ๊ตญ์˜ ํŠน์ˆ˜์„ฑ ๋ฐ˜์˜
- ๋™์‹œ์— ๋ณดํŽธ์  ์ธ๊ฐ„ ์กฐ๊ฑด ํƒ๊ตฌ
๊ตฌ์ฒด์  ๊ฐœ์„  ๋ฐฉํ–ฅ์„ ์ œ์‹œํ•˜์„ธ์š”.""",
"English": f"""You are a literary critic. Review the plan from these perspectives:
**Original Theme:** {user_query}
**Director's Plan:**
{director_plan}
**Review Criteria:**
1. **Social Critical Depth**
- Is the connection between individual and structure convincing?
- Does it avoid oversimplifying complex realities?
- Does it go beyond clichรฉd social criticism?
2. **Literary Merit**
- Balance of interiority and external reality
- Effectiveness of symbols and metaphors
- Character complexity and credibility
3. **Contemporary Relevance**
- Reflects specific contemporary context
- While exploring universal human conditions
Provide specific improvements."""
}
return lang_prompts.get(language, lang_prompts["Korean"])
def create_writer_prompt(self, writer_number: int, director_plan: str,
previous_content: str, user_query: str,
language: str) -> str:
"""์ž‘๊ฐ€ ํ”„๋กฌํ”„ํŠธ (๋‚ด๋ฉด ์„œ์ˆ  ์ค‘์‹ฌ)"""
lang_prompts = {
"Korean": f"""๋‹น์‹ ์€ ์ž‘๊ฐ€ {writer_number}๋ฒˆ์ž…๋‹ˆ๋‹ค. ํ•œ๊ตญ ํ˜„๋Œ€ ๋ฌธํ•™์˜ ์ „ํ†ต์— ๋”ฐ๋ผ ์ž‘์„ฑํ•˜์„ธ์š”.
**๋งˆ์Šคํ„ฐํ”Œ๋žœ:**
{director_plan}
**์ด์ „ ๋‚ด์šฉ:**
{previous_content[-2000:] if previous_content else "์‹œ์ž‘"}
**์ž‘์„ฑ ์ง€์นจ:**
1. **๋ถ„๋Ÿ‰**: 1,300-1,500 ๋‹จ์–ด
2. **์„œ์ˆ  ๋ฐฉ์‹**
- ์ธ๋ฌผ์˜ ๋‚ด๋ฉด ์˜์‹์„ ๊นŠ์ด ํƒ๊ตฌ
- ๊ด€์ฐฐ๊ณผ ์„ฑ์ฐฐ์˜ ๊ต์ฐจ
- ํ˜„์žฌ์™€ ๊ณผ๊ฑฐ์˜ ์ž์—ฐ์Šค๋Ÿฌ์šด ์˜ค๋ฒ„๋žฉ
3. **๋ฌธ์ฒด**
- ๋‹ด๋‹ดํ•˜๋ฉด์„œ๋„ ๋‚ ์นด๋กœ์šด ์‹œ์„ 
- ์ผ์ƒ์–ด ์†์— ์Šค๋ฉฐ๋“  ์‹œ์  ํ‘œํ˜„
- ์งง์€ ๋ฌธ์žฅ๊ณผ ๊ธด ๋ฌธ์žฅ์˜ ๋ฆฌ๋“ฌ๊ฐ
4. **๋‚ด์šฉ ์ „๊ฐœ**
- ํฐ ์‚ฌ๊ฑด๋ณด๋‹ค ์ž‘์€ ์ˆœ๊ฐ„์˜ ์˜๋ฏธ
- ๋Œ€ํ™”๋Š” ์ตœ์†Œํ™”, ์žˆ์–ด๋„ ํ•จ์ถ•์ 
- ์ธ๋ฌผ์˜ ์ธ์‹ ๋ณ€ํ™”๊ฐ€ ๊ณง ํ”Œ๋กฏ
5. **์‚ฌํšŒ์  ๋งฅ๋ฝ**
- ๊ฐœ์ธ์˜ ์ผ์ƒ์— ์Šค๋ฉฐ๋“  ๊ตฌ์กฐ์  ์–ต์••
- ์ง์ ‘์  ๋น„ํŒ๋ณด๋‹ค ๊ฐ„์ ‘์  ๋“œ๋Ÿฌ๋ƒ„
- ๋…์ž๊ฐ€ ์Šค์Šค๋กœ ๊นจ๋‹ซ๊ฒŒ ํ•˜๋Š” ์„œ์ˆ 
**๋ฐ˜๋“œ์‹œ ํฌํ•จํ•  ์š”์†Œ:**
- ์ธ๋ฌผ์˜ ๋‚ด์  ๋…๋ฐฑ์ด๋‚˜ ์˜์‹์˜ ํ๋ฆ„
- ๊ตฌ์ฒด์ ์ธ ๊ฐ๊ฐ์  ๋””ํ…Œ์ผ
- ์‚ฌํšŒ์  ๋งฅ๋ฝ์„ ์•”์‹œํ•˜๋Š” ์ผ์ƒ์˜ ์ˆœ๊ฐ„
- ์ƒ์ง•์ด๋‚˜ ์€์œ ์˜ ์ž์—ฐ์Šค๋Ÿฌ์šด ํ™œ์šฉ
๊นŠ์ด ์žˆ๋Š” ๋‚ด๋ฉด ํƒ๊ตฌ์™€ ์„ฌ์„ธํ•œ ์‚ฌํšŒ ๊ด€์ฐฐ์„ ๋ณด์—ฌ์ฃผ์„ธ์š”.""",
"English": f"""You are Writer #{writer_number}. Write in the contemporary literary tradition.
**Masterplan:**
{director_plan}
**Previous Content:**
{previous_content[-2000:] if previous_content else "Beginning"}
**Writing Guidelines:**
1. **Length**: 1,300-1,500 words
2. **Narrative Approach**
- Deep exploration of character consciousness
- Intersection of observation and reflection
- Natural overlap of present and past
3. **Style**
- Understated yet sharp perspective
- Poetic expression within everyday language
- Rhythm of short and long sentences
4. **Development**
- Meaning in small moments over big events
- Minimal, implicit dialogue
- Character perception shifts as plot
5. **Social Context**
- Structural oppression in daily life
- Indirect rather than direct critique
- Let readers discover meaning
**Must Include:**
- Interior monologue or stream of consciousness
- Concrete sensory details
- Everyday moments revealing social context
- Natural use of symbols and metaphors
Show deep interior exploration and subtle social observation."""
}
return lang_prompts.get(language, lang_prompts["Korean"])
def create_critic_consistency_prompt(self, all_content: str, user_query: str,
language: str) -> str:
"""๋น„ํ‰๊ฐ€ ์ค‘๊ฐ„ ๊ฒ€ํ† """
return f"""๋ฌธํ•™ ๋น„ํ‰๊ฐ€๋กœ์„œ ํ˜„์žฌ๊นŒ์ง€์˜ ์ž‘ํ’ˆ์„ ๊ฒ€ํ† ํ•˜์„ธ์š”.
**์› ์ฃผ์ œ:** {user_query}
**์ž‘ํ’ˆ ๋‚ด์šฉ (์ตœ๊ทผ ๋ถ€๋ถ„):**
{all_content[-3000:]}
**๊ฒ€ํ†  ํ•ญ๋ชฉ:**
1. **๋‚ด์  ์ผ๊ด€์„ฑ**
- ์ธ๋ฌผ์˜ ์˜์‹๊ณผ ํ–‰๋™์˜ ์ผ์น˜
- ์„œ์ˆ  ์‹œ์ ์˜ ์ผ๊ด€์„ฑ
- ๋ฌธ์ฒด์™€ ํ†ค์˜ ํ†ต์ผ์„ฑ
2. **์ฃผ์ œ ์‹ฌํ™”**
- ์ดˆ๊ธฐ ์„ค์ •ํ•œ ์‚ฌํšŒ์  ๋ฌธ์ œ์˜ ์ง€์†์  ํƒ๊ตฌ
- ์ƒ์ง•๊ณผ ์€์œ ์˜ ๋ฐœ์ „
- ๊นŠ์ด ์žˆ๋Š” ์„ฑ์ฐฐ์˜ ๋ˆ„์ 
3. **๋ฌธํ•™์  ์™„์„ฑ๋„**
- ์ง„๋ถ€ํ•จ์ด๋‚˜ ์ƒํˆฌ์„ฑ ํšŒํ”ผ
- ๋…์ฐฝ์  ํ‘œํ˜„๊ณผ ๊ด€์ฐฐ
- ์—ฌ์šด๊ณผ ํ•จ์ถ•์„ฑ
๊ฐ ์ž‘๊ฐ€์—๊ฒŒ ๊ตฌ์ฒด์  ๊ฐœ์„  ๋ฐฉํ–ฅ์„ ์ œ์‹œํ•˜์„ธ์š”."""
def create_writer_revision_prompt(self, writer_number: int, initial_content: str,
critic_feedback: str, language: str) -> str:
"""์ž‘๊ฐ€ ์ˆ˜์ • ํ”„๋กฌํ”„ํŠธ"""
return f"""์ž‘๊ฐ€ {writer_number}๋ฒˆ, ๋น„ํ‰์„ ๋ฐ˜์˜ํ•˜์—ฌ ์ˆ˜์ •ํ•˜์„ธ์š”.
**์ดˆ์•ˆ:**
{initial_content}
**๋น„ํ‰:**
{critic_feedback}
**์ˆ˜์ • ๋ฐฉํ–ฅ:**
1. ๋‚ด๋ฉด ์„œ์ˆ  ๊ฐ•ํ™”
2. ์‚ฌํšŒ์  ๋งฅ๋ฝ ์‹ฌํ™”
3. ๋ฌธํ•™์  ํ‘œํ˜„ ๊ฐœ์„ 
4. ์ง„๋ถ€ํ•จ ์ œ๊ฑฐ
์ˆ˜์ •๋ณธ๋งŒ ์ œ์‹œํ•˜์„ธ์š”."""
def create_critic_final_prompt(self, complete_novel: str, language: str) -> str:
"""์ตœ์ข… ๋น„ํ‰"""
return f"""์™„์„ฑ๋œ ์†Œ์„ค์˜ ๋ฌธํ•™์  ๊ฐ€์น˜๋ฅผ ํ‰๊ฐ€ํ•˜์„ธ์š”.
**์ž‘ํ’ˆ (์ผ๋ถ€):**
{complete_novel[-3000:]}
**ํ‰๊ฐ€ ๊ธฐ์ค€:**
1. **์ฃผ์ œ ์˜์‹ (30์ )**
- ์‚ฌํšŒ ๋น„ํŒ์˜ ์˜ˆ๋ฆฌํ•จ
- ์ธ๊ฐ„ ์กฐ๊ฑด์— ๋Œ€ํ•œ ํ†ต์ฐฐ
- ํ˜„๋Œ€์„ฑ๊ณผ ๋ณดํŽธ์„ฑ์˜ ์กฐํ™”
2. **์ธ๋ฌผ๊ณผ ์‹ฌ๋ฆฌ (25์ )**
- ๋‚ด๋ฉด ๋ฌ˜์‚ฌ์˜ ๊นŠ์ด
- ์ธ๋ฌผ์˜ ์‹ ๋น™์„ฑ
- ๋ณต์žก์„ฑ๊ณผ ๋ชจ์ˆœ์˜ ํฌ์ฐฉ
3. **๋ฌธ์ฒด์™€ ๊ธฐ๋ฒ• (25์ )**
- ๋ฌธ์žฅ์˜ ์™„์„ฑ๋„
- ์ƒ์ง•๊ณผ ์€์œ ์˜ ํšจ๊ณผ
- ๋…์ฐฝ์„ฑ๊ณผ ์ฐธ์‹ ํ•จ
4. **๊ตฌ์กฐ์™€ ์™„๊ฒฐ์„ฑ (20์ )**
- ์ „์ฒด ๊ตฌ์„ฑ์˜ ๊ท ํ˜•
- ์—ฌ์šด๊ณผ ๊ฐœ๋ฐฉ์„ฑ
- ๋…์ž์—๊ฒŒ ๋˜์ง€๋Š” ์งˆ๋ฌธ
**์ด์ : /100์ **
ํ•œ๊ตญ ํ˜„๋Œ€ ๋ฌธํ•™์˜ ๋งฅ๋ฝ์—์„œ ์ด ์ž‘ํ’ˆ์˜ ์˜์˜๋ฅผ ๋…ผํ•˜์„ธ์š”."""
# --- LLM ํ˜ธ์ถœ ํ•จ์ˆ˜๋“ค ---
def call_llm_sync(self, messages: List[Dict[str, str]], role: str, language: str) -> str:
full_content = ""
for chunk in self.call_llm_streaming(messages, role, language):
full_content += chunk
if full_content.startswith("โŒ"):
raise Exception(f"LLM Call Failed: {full_content}")
return full_content
def call_llm_streaming(self, messages: List[Dict[str, str]], role: str, language: str) -> Generator[str, None, None]:
try:
system_prompts = self.get_system_prompts(language)
full_messages = [{"role": "system", "content": system_prompts.get(role, "")}, *messages]
payload = {
"model": self.model_id,
"messages": full_messages,
"max_tokens": 10000,
"temperature": 0.8, # ๋” ์ฐฝ์˜์ ์ธ ์ถœ๋ ฅ
"top_p": 0.95,
"presence_penalty": 0.5, # ๋ฐ˜๋ณต ๋ฐฉ์ง€
"frequency_penalty": 0.3,
"stream": True
}
response = requests.post(
self.api_url,
headers=self.create_headers(),
json=payload,
stream=True,
timeout=180
)
if response.status_code != 200:
yield f"โŒ API ์˜ค๋ฅ˜ (์ƒํƒœ ์ฝ”๋“œ: {response.status_code})"
return
buffer = ""
for line in response.iter_lines():
if not line:
continue
try:
line_str = line.decode('utf-8').strip()
if not line_str.startswith("data: "):
continue
data_str = line_str[6:]
if data_str == "[DONE]":
break
data = json.loads(data_str)
choices = data.get("choices", [])
if choices and choices[0].get("delta", {}).get("content"):
content = choices[0]["delta"]["content"]
buffer += content
if len(buffer) >= 50 or '\n' in buffer:
yield buffer
buffer = ""
time.sleep(0.01)
except Exception as e:
logger.error(f"์ฒญํฌ ์ฒ˜๋ฆฌ ์˜ค๋ฅ˜: {str(e)}")
continue
if buffer:
yield buffer
except Exception as e:
logger.error(f"์ŠคํŠธ๋ฆฌ๋ฐ ์˜ค๋ฅ˜: {type(e).__name__}: {str(e)}")
yield f"โŒ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}"
def get_system_prompts(self, language: str) -> Dict[str, str]:
"""์—ญํ• ๋ณ„ ์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ"""
base_prompts = {
"Korean": {
"director": """๋‹น์‹ ์€ ํ•œ๊ฐ•, ๊น€์• ๋ž€, ์ฒœ๋ช…๊ด€ ๋“ฑ ํ•œ๊ตญ ํ˜„๋Œ€ ์ž‘๊ฐ€๋“ค์˜ ์ž‘ํ’ˆ์„ ๊นŠ์ด ์ดํ•ดํ•˜๋Š” ๋ฌธํ•™ ๊ธฐํš์ž์ž…๋‹ˆ๋‹ค.
์‚ฌํšŒ๋น„ํŒ์  ๋ฆฌ์–ผ๋ฆฌ์ฆ˜๊ณผ ์‹ฌ๋ฆฌ์  ๊นŠ์ด๋ฅผ ๊ฒฐํ•ฉํ•œ ์ž‘ํ’ˆ์„ ๊ธฐํšํ•˜์„ธ์š”.
๊ฐœ์ธ์˜ ๋‚ด๋ฉด๊ณผ ์‚ฌํšŒ ๊ตฌ์กฐ์˜ ์ƒํ˜ธ์ž‘์šฉ์„ ์„ฌ์„ธํ•˜๊ฒŒ ํฌ์ฐฉํ•˜๋Š” ๊ฒƒ์ด ํ•ต์‹ฌ์ž…๋‹ˆ๋‹ค.""",
"critic": """๋‹น์‹ ์€ ํ˜„๋Œ€ ํ•œ๊ตญ ๋ฌธํ•™์„ ๊นŠ์ด ์ดํ•ดํ•˜๋Š” ๋น„ํ‰๊ฐ€์ž…๋‹ˆ๋‹ค.
์ž‘ํ’ˆ์ด ์ง„๋ถ€ํ•œ ์‚ฌํšŒ ๋น„ํŒ์ด๋‚˜ ํ‘œ๋ฉด์  ์‹ฌ๋ฆฌ ๋ฌ˜์‚ฌ์— ๊ทธ์น˜์ง€ ์•Š๊ณ ,
์ง„์ •ํ•œ ๋ฌธํ•™์  ๊ฐ€์น˜๋ฅผ ์ง€๋‹ˆ๋Š”์ง€ ์—„๊ฒฉํ•˜๊ฒŒ ํ‰๊ฐ€ํ•˜์„ธ์š”.""",
"writer_base": """๋‹น์‹ ์€ ํ˜„๋Œ€ ํ•œ๊ตญ ๋ฌธํ•™์˜ ์ „ํ†ต์„ ์ž‡๋Š” ์ž‘๊ฐ€์ž…๋‹ˆ๋‹ค.
๋‚ด๋ฉด ์˜์‹์˜ ํ๋ฆ„๊ณผ ๋‚ ์นด๋กœ์šด ์‚ฌํšŒ ๊ด€์ฐฐ์„ ๊ฒฐํ•ฉํ•˜์—ฌ,
๋…์ž์—๊ฒŒ ๊นŠ์€ ์—ฌ์šด์„ ๋‚จ๊ธฐ๋Š” ๋ฌธ์žฅ์„ ์“ฐ์„ธ์š”.
'๋ณด์—ฌ์ฃผ๊ธฐ'๋ณด๋‹ค '์˜์‹ํ•˜๊ธฐ'๋ฅผ, ์‚ฌ๊ฑด๋ณด๋‹ค ์ธ์‹์„ ์ค‘์‹œํ•˜์„ธ์š”."""
},
"English": {
"director": """You are a literary planner deeply versed in contemporary social realist fiction.
Plan works that combine social critique with psychological depth.
The key is capturing the subtle interplay between individual consciousness and social structures.""",
"critic": """You are a critic well-versed in contemporary literary fiction.
Evaluate whether works go beyond superficial social commentary or psychology
to achieve genuine literary value.""",
"writer_base": """You are a writer in the contemporary literary tradition.
Combine stream of consciousness with sharp social observation
to create resonant prose. Prioritize consciousness over showing,
perception over events."""
}
}
prompts = base_prompts.get(language, base_prompts["Korean"]).copy()
# ํŠน์ˆ˜ ์ž‘๊ฐ€ ํ”„๋กฌํ”„ํŠธ
if language == "Korean":
prompts["writer1"] = prompts["writer_base"] + "\nํŠนํžˆ ๋„์ž…๋ถ€์—์„œ ๋…์ž๋ฅผ ์ž‘ํ’ˆ์˜ ๋ถ„์œ„๊ธฐ๋กœ ์„œ์„œํžˆ ๋Œ์–ด๋“ค์ด์„ธ์š”."
prompts["writer5"] = prompts["writer_base"] + "\n์ค‘๋ฐ˜๋ถ€์˜ ์‹ฌ๋ฆฌ์  ๋ฐ€๋„๋ฅผ ๋†’์ด๊ณ  ๊ฐˆ๋“ฑ์„ ๋‚ด๋ฉดํ™”ํ•˜์„ธ์š”."
prompts["writer10"] = prompts["writer_base"] + "\n์—ด๋ฆฐ ๊ฒฐ๋ง๋กœ ๋…์ž์—๊ฒŒ ์งˆ๋ฌธ์„ ๋˜์ง€์„ธ์š”."
for i in range(2, 10):
if f"writer{i}" not in prompts:
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
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', ''),
"literary_score": s.get('literary_score', 0.0)
} for s in NovelDatabase.get_stages(self.current_session_id)]
for stage_idx in range(resume_from_stage, len(LITERARY_STAGES)):
role, stage_name = LITERARY_STAGES[stage_idx]
if stage_idx >= len(stages):
stages.append({
"name": stage_name,
"status": "active",
"content": "",
"literary_score": 0.0
})
else:
stages[stage_idx]["status"] = "active"
yield "", 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
yield "", stages, self.current_session_id
# ๋ฌธํ•™์  ์ ์ˆ˜ ํ‰๊ฐ€
literary_score = self.evaluate_literary_quality(stage_content, role)
stages[stage_idx]["literary_score"] = literary_score
stages[stage_idx]["status"] = "complete"
NovelDatabase.save_stage(
self.current_session_id, stage_idx, stage_name, role,
stage_content, "complete", literary_score
)
yield "", stages, self.current_session_id
# ์ตœ์ข… ์†Œ์„ค ์ •๋ฆฌ
final_novel = NovelDatabase.get_writer_content(self.current_session_id)
final_report = self.generate_literary_report(final_novel, language)
NovelDatabase.update_final_novel(self.current_session_id, final_novel, final_report)
yield f"โœ… ์†Œ์„ค ์™„์„ฑ! ์ด {len(final_novel.split())}๋‹จ์–ด", stages, self.current_session_id
except Exception as e:
logger.error(f"์†Œ์„ค ์ƒ์„ฑ ํ”„๋กœ์„ธ์Šค ์˜ค๋ฅ˜: {e}", exc_info=True)
yield f"โŒ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}", stages if 'stages' in locals() else [], self.current_session_id
def get_stage_prompt(self, stage_idx: int, role: str, query: str, language: str, stages: List[Dict]) -> str:
"""๋‹จ๊ณ„๋ณ„ ํ”„๋กฌํ”„ํŠธ ์ƒ์„ฑ"""
if stage_idx == 0:
return self.create_director_initial_prompt(query, language)
if stage_idx == 1:
return self.create_critic_director_prompt(stages[0]["content"], query, language)
if stage_idx == 2:
return self.create_director_revision_prompt(stages[0]["content"], stages[1]["content"], query, language)
master_plan = stages[2]["content"]
if 3 <= stage_idx <= 12: # ์ž‘๊ฐ€ ์ดˆ์•ˆ
writer_num = stage_idx - 2
previous_content = self.get_all_content(stages, stage_idx)
return self.create_writer_prompt(writer_num, master_plan, previous_content, query, language)
if stage_idx == 13: # ๋น„ํ‰๊ฐ€ ์ค‘๊ฐ„ ๊ฒ€ํ† 
all_content = self.get_all_content(stages, stage_idx)
return self.create_critic_consistency_prompt(all_content, query, language)
if 14 <= stage_idx <= 23: # ์ž‘๊ฐ€ ์ˆ˜์ •
writer_num = stage_idx - 13
initial_content = stages[2 + writer_num]["content"]
feedback = stages[13]["content"]
return self.create_writer_revision_prompt(writer_num, initial_content, feedback, language)
if stage_idx == 24: # ์ตœ์ข… ๊ฒ€ํ† 
complete_novel = self.get_all_writer_content(stages)
return self.create_critic_final_prompt(complete_novel, language)
return ""
def create_director_revision_prompt(self, initial_plan: str, critic_feedback: str, user_query: str, language: str) -> str:
"""๊ฐ๋…์ž ์ˆ˜์ • ํ”„๋กฌํ”„ํŠธ"""
return f"""๋น„ํ‰์„ ๋ฐ˜์˜ํ•˜์—ฌ ๊ธฐํš์„ ์ˆ˜์ •ํ•˜์„ธ์š”.
**์› ์ฃผ์ œ:** {user_query}
**์ดˆ๊ธฐ ๊ธฐํš:**
{initial_plan}
**๋น„ํ‰:**
{critic_feedback}
**์ˆ˜์ • ๋ฐฉํ–ฅ:**
1. ์‚ฌํšŒ๋น„ํŒ์  ๊นŠ์ด ๊ฐ•ํ™”
2. ์ธ๋ฌผ์˜ ๋‚ด๋ฉด ๋ณต์žก์„ฑ ์ฆ๋Œ€
3. ์ƒ์ง•๊ณผ ์€์œ  ์ •๊ตํ™”
4. ์ง„๋ถ€ํ•จ ์ œ๊ฑฐ
์ˆ˜์ •๋œ ๋งˆ์Šคํ„ฐํ”Œ๋žœ์„ ์ œ์‹œํ•˜์„ธ์š”."""
def get_all_content(self, stages: List[Dict], current_stage: int) -> str:
"""ํ˜„์žฌ๊นŒ์ง€์˜ ๋ชจ๋“  ๋‚ด์šฉ"""
contents = []
for i, s in enumerate(stages):
if i < current_stage and s["content"] and "writer" in s.get("name", ""):
contents.append(s["content"])
return "\n\n".join(contents)
def get_all_writer_content(self, stages: List[Dict]) -> str:
"""๋ชจ๋“  ์ž‘๊ฐ€ ์ตœ์ข…๋ณธ"""
contents = []
for i, s in enumerate(stages):
if 14 <= i <= 23 and s["content"]:
contents.append(s["content"])
return "\n\n".join(contents)
def evaluate_literary_quality(self, content: str, role: str) -> float:
"""๋ฌธํ•™์  ํ’ˆ์งˆ ํ‰๊ฐ€"""
if not content or not role.startswith("writer"):
return 0.0
score = 5.0 # ๊ธฐ๋ณธ ์ ์ˆ˜
# ๋‚ด๋ฉด ์„œ์ˆ  ํ‰๊ฐ€
introspective_patterns = ['์ƒ๊ฐํ–ˆ๋‹ค', '๋А๊ผˆ๋‹ค', '๊ธฐ์–ต', '์˜์‹', '๋– ์˜ฌ๋ ธ๋‹ค',
'๋งˆ์Œ', '๋จธ๋ฆฟ์†', '๊ฐ€์Šด', 'thought', 'felt', 'remembered']
introspection_count = sum(1 for pattern in introspective_patterns if pattern in content)
score += min(2.0, introspection_count * 0.2)
# ๊ฐ๊ฐ์  ๋””ํ…Œ์ผ
sensory_patterns = ['๋ƒ„์ƒˆ', '์†Œ๋ฆฌ', '๋น›', '๊ทธ๋ฆผ์ž', '์ด‰๊ฐ', '๋ง›', '์˜จ๋„', '์ƒ‰',
'smell', 'sound', 'light', 'shadow', 'touch', 'taste']
sensory_count = sum(1 for pattern in sensory_patterns if pattern in content)
score += min(1.5, sensory_count * 0.15)
# ์‚ฌํšŒ์  ๋งฅ๋ฝ
social_patterns = ['์ž„๋Œ€', '๊ณ„๊ธ‰', '๋นˆ๊ณค', '๊ฒฉ์ฐจ', '์ฐจ๋ณ„', '์†Œ์™ธ', '๋ถˆํ‰๋“ฑ',
'rent', 'class', 'poverty', 'gap', 'discrimination']
social_count = sum(1 for pattern in social_patterns if pattern in content)
score += min(1.5, social_count * 0.3)
return min(10.0, score)
def generate_literary_report(self, complete_novel: str, language: str) -> str:
"""์ตœ์ข… ๋ฌธํ•™์  ํ‰๊ฐ€ ๋ณด๊ณ ์„œ"""
prompt = self.create_critic_final_prompt(complete_novel, language)
try:
report = self.call_llm_sync([{"role": "user", "content": prompt}], "critic", language)
return report
except Exception as e:
logger.error(f"์ตœ์ข… ๋ณด๊ณ ์„œ ์ƒ์„ฑ ์‹คํŒจ: {e}")
return "๋ณด๊ณ ์„œ ์ƒ์„ฑ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ"
# --- ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜๋“ค ---
def process_query(query: str, language: str, session_id: Optional[str] = None) -> Generator[Tuple[str, str, str, str], None, None]:
"""๋ฉ”์ธ ์ฟผ๋ฆฌ ์ฒ˜๋ฆฌ ํ•จ์ˆ˜"""
if not query.strip():
yield "", "", "โŒ ์ฃผ์ œ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.", session_id
return
system = LiteraryNovelSystem()
stages_markdown = ""
novel_content = ""
for status, stages, current_session_id in system.process_novel_stream(query, language, session_id):
stages_markdown = format_stages_display(stages)
# ์ตœ์ข… ์†Œ์„ค ๋‚ด์šฉ ๊ฐ€์ ธ์˜ค๊ธฐ
if stages and all(s.get("status") == "complete" for s in stages[-10:]):
novel_content = NovelDatabase.get_writer_content(current_session_id)
novel_content = format_novel_display(novel_content)
yield stages_markdown, novel_content, status or "๐Ÿ”„ ์ฒ˜๋ฆฌ ์ค‘...", current_session_id
def get_active_sessions(language: str) -> List[str]:
"""ํ™œ์„ฑ ์„ธ์…˜ ๋ชฉ๋ก"""
sessions = NovelDatabase.get_active_sessions()
return [f"{s['session_id'][:8]}... - {s['user_query'][:50]}... ({s['created_at']})"
for s in sessions]
def auto_recover_session(language: str) -> Tuple[Optional[str], str]:
"""์ตœ๊ทผ ์„ธ์…˜ ์ž๋™ ๋ณต๊ตฌ"""
latest_session = NovelDatabase.get_latest_active_session()
if latest_session:
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"
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('literary_score', 0) > 0:
markdown += f" (๋ฌธํ•™์„ฑ: {stage['literary_score']:.1f}/10)"
markdown += "\n"
if stage['content']:
preview = stage['content'][:200] + "..." if len(stage['content']) > 200 else stage['content']
markdown += f"> {preview}\n\n"
return markdown
def format_novel_display(novel_text: str) -> str:
"""์†Œ์„ค ๋‚ด์šฉ ํ‘œ์‹œ"""
if not novel_text:
return "์•„์ง ์™„์„ฑ๋œ ๋‚ด์šฉ์ด ์—†์Šต๋‹ˆ๋‹ค."
formatted = "# ๐Ÿ“– ์™„์„ฑ๋œ ์†Œ์„ค\n\n"
formatted += novel_text
return formatted
def export_to_docx(content: str, filename: str, language: str, session_id: str) -> str:
"""DOCX ํŒŒ์ผ๋กœ ๋‚ด๋ณด๋‚ด๊ธฐ (๋ฌธํ•™ ์†Œ์„ค ํ˜•์‹)"""
doc = Document()
# ์‹ ๊ตญํŒ ํฌ๊ธฐ (152mm x 225mm)
section = doc.sections[0]
section.page_height = Inches(8.86) # 225mm
section.page_width = Inches(5.98) # 152mm
# ์—ฌ๋ฐฑ ์„ค์ •
section.top_margin = Inches(0.79) # 20mm
section.bottom_margin = Inches(0.79)
section.left_margin = Inches(0.79)
section.right_margin = Inches(0.79)
# ์„ธ์…˜ ์ •๋ณด
session = NovelDatabase.get_session(session_id)
# ์ œ๋ชฉ ํŽ˜์ด์ง€
title_para = doc.add_paragraph()
title_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
# ๋นˆ ์ค„ ์ถ”๊ฐ€
for _ in range(8):
doc.add_paragraph()
if session:
title_run = title_para.add_run(session["user_query"])
title_run.font.size = Pt(16)
title_run.font.name = '๋ฐ”ํƒ•'
# ํŽ˜์ด์ง€ ๋‚˜๋ˆ„๊ธฐ
doc.add_page_break()
# ๋ณธ๋ฌธ ์Šคํƒ€์ผ ์„ค์ •
style = doc.styles['Normal']
style.font.name = '๋ฐ”ํƒ•'
style.font.size = Pt(10.5)
style.paragraph_format.line_spacing = 1.8
style.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
style.paragraph_format.first_line_indent = Inches(0.35)
style.paragraph_format.space_after = Pt(3)
# ๋ณธ๋ฌธ ์ถ”๊ฐ€
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, #1a1a1a 0%, #2d2d2d 50%, #1a1a1a 100%);
min-height: 100vh;
}
.main-header {
background-color: rgba(255, 255, 255, 0.03);
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.1);
}
.literary-note {
background-color: rgba(255, 255, 255, 0.05);
border-left: 3px solid #888;
padding: 15px;
margin: 20px 0;
border-radius: 8px;
color: #ccc;
font-style: italic;
font-family: 'Georgia', serif;
}
.input-section {
background-color: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(10px);
padding: 20px;
border-radius: 12px;
margin-bottom: 20px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.session-section {
background-color: rgba(255, 255, 255, 0.05);
backdrop-filter: blur(10px);
padding: 15px;
border-radius: 8px;
margin-top: 20px;
color: white;
border: 1px solid rgba(255, 255, 255, 0.1);
}
#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.98);
padding: 40px;
border-radius: 12px;
max-height: 700px;
overflow-y: auto;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
font-family: '๋ฐ”ํƒ•', 'Batang', 'Georgia', serif;
line-height: 2;
color: #333;
}
.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);
}
/* ๋ฌธํ•™์  ์Šคํƒ€์ผ */
#novel-output p {
text-indent: 2em;
margin-bottom: 1em;
text-align: justify;
}
#novel-output h1 {
color: #1a1a1a;
font-weight: normal;
text-align: center;
margin: 2em 0;
font-size: 1.8em;
}
/* ์ธ์šฉ๋ฌธ์ด๋‚˜ ๋‚ด์  ๋…๋ฐฑ ์Šคํƒ€์ผ */
#novel-output blockquote {
margin: 1.5em 2em;
font-style: italic;
color: #555;
border-left: 3px solid #ccc;
padding-left: 1em;
}
"""
# Gradio ์ธํ„ฐํŽ˜์ด์Šค ์ƒ์„ฑ
def create_interface():
with gr.Blocks(css=custom_css, title="AI ๋ฌธํ•™ ์†Œ์„ค ์ƒ์„ฑ ์‹œ์Šคํ…œ") as interface:
gr.HTML("""
<div class="main-header">
<h1 style="font-size: 2.5em; margin-bottom: 10px; font-weight: normal;">
๐Ÿ“– AI ๋ฌธํ•™ ์†Œ์„ค ์ƒ์„ฑ ์‹œ์Šคํ…œ
</h1>
<h3 style="color: #ccc; margin-bottom: 20px; font-weight: normal;">
ํ˜„๋Œ€ ํ•œ๊ตญ ๋ฌธํ•™์˜ ์ „ํ†ต์„ ์ž‡๋Š” ์†Œ์„ค ์ฐฝ์ž‘
</h3>
<p style="font-size: 1.1em; color: #ddd; max-width: 800px; margin: 0 auto;">
์‚ฌํšŒ๋น„ํŒ์  ๋ฆฌ์–ผ๋ฆฌ์ฆ˜๊ณผ ์‹ฌ๋ฆฌ์  ๊นŠ์ด๋ฅผ ๊ฒฐํ•ฉํ•œ 30ํŽ˜์ด์ง€ ์ค‘ํŽธ์†Œ์„ค์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
<br>
๊ฐœ์ธ์˜ ๋‚ด๋ฉด๊ณผ ์‚ฌํšŒ ๊ตฌ์กฐ์˜ ์ƒํ˜ธ์ž‘์šฉ์„ ์„ฌ์„ธํ•˜๊ฒŒ ํฌ์ฐฉํ•ฉ๋‹ˆ๋‹ค.
</p>
<div class="literary-note">
"๊ฐ€์žฅ ๊ฐœ์ธ์ ์ธ ๊ฒƒ์ด ๊ฐ€์žฅ ๋ณดํŽธ์ ์ธ ๊ฒƒ์ด๋‹ค"
</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 themes about contemporary social issues or human condition...",
lines=4
)
language_select = gr.Radio(
choices=["Korean", "English"],
value="Korean",
label="์–ธ์–ด / Language"
)
with gr.Row():
submit_btn = gr.Button("๐Ÿš€ ์†Œ์„ค ์ƒ์„ฑ ์‹œ์ž‘", variant="primary", scale=2)
clear_btn = gr.Button("๐Ÿ—‘๏ธ ์ดˆ๊ธฐํ™”", scale=1)
status_text = gr.Textbox(
label="์ƒํƒœ",
interactive=False,
value="๐Ÿ”„ ์ค€๋น„ ์™„๋ฃŒ"
)
# ์„ธ์…˜ ๊ด€๋ฆฌ
with gr.Group(elem_classes=["session-section"]):
gr.Markdown("### ๐Ÿ’พ ์ด์ „ ์„ธ์…˜ ์žฌ๊ฐœ")
session_dropdown = gr.Dropdown(
label="์„ธ์…˜ ์„ ํƒ",
choices=[],
interactive=True
)
with gr.Row():
refresh_btn = gr.Button("๐Ÿ”„ ๋ชฉ๋ก ์ƒˆ๋กœ๊ณ ์นจ", scale=1)
resume_btn = gr.Button("โ–ถ๏ธ ์„ ํƒ ์žฌ๊ฐœ", variant="secondary", scale=1)
auto_recover_btn = gr.Button("โ™ป๏ธ ์ž๋™ ๋ณต๊ตฌ", scale=1)
with gr.Column(scale=2):
with gr.Tab("๐Ÿ“ ์ฐฝ์ž‘ ๊ณผ์ •"):
stages_display = gr.Markdown(
value="์ฐฝ์ž‘ ๊ณผ์ •์ด ์—ฌ๊ธฐ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค...",
elem_id="stages-display"
)
with gr.Tab("๐Ÿ“– ์™„์„ฑ๋œ ์†Œ์„ค"):
novel_output = gr.Markdown(
value="์™„์„ฑ๋œ ์†Œ์„ค์ด ์—ฌ๊ธฐ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค...",
elem_id="novel-output"
)
with gr.Group(elem_classes=["download-section"]):
gr.Markdown("### ๐Ÿ“ฅ ์†Œ์„ค ๋‹ค์šด๋กœ๋“œ")
with gr.Row():
format_select = gr.Radio(
choices=["DOCX", "TXT"],
value="DOCX" if DOCX_AVAILABLE else "TXT",
label="ํ˜•์‹"
)
download_btn = gr.Button("โฌ‡๏ธ ๋‹ค์šด๋กœ๋“œ", variant="secondary")
download_file = gr.File(
label="๋‹ค์šด๋กœ๋“œ๋œ ํŒŒ์ผ",
visible=False
)
# ์ˆจ๊ฒจ์ง„ ์ƒํƒœ
novel_text_state = gr.State("")
# ์˜ˆ์ œ (๋ฌธํ•™์  ์ฃผ์ œ)
with gr.Row():
gr.Examples(
examples=[
["๋„์‹œ ๋นˆ๋ฏผ์˜ ์ฃผ๊ฑฐ๊ถŒ๊ณผ ๊ณ„๊ธ‰ ๊ฐˆ๋“ฑ"],
["๋น„์ •๊ทœ์ง ์ฒญ๋…„์˜ ๋ถˆ์•ˆ๊ณผ ์†Œ์™ธ"],
["๋Œ๋ด„ ๋…ธ๋™์ž์˜ ๋ณด์ด์ง€ ์•Š๋Š” ์‚ถ"],
["์  ํŠธ๋ฆฌํ”ผ์ผ€์ด์…˜๊ณผ ๋„์‹œ ๋‚œ๋ฏผ"],
["The invisible labor of care workers"],
["Digital divide and social isolation"],
["์„ธ๋Œ€ ๊ฐ„ ๋‹จ์ ˆ๊ณผ ๋…ธ์ธ ๋นˆ๊ณค"],
["ํ™˜๊ฒฝ ์žฌ๋‚œ ์‹œ๋Œ€์˜ ๋ถˆํ‰๋“ฑ"]
],
inputs=query_input,
label="๐Ÿ’ก ๋ฌธํ•™์  ์ฃผ์ œ ์˜ˆ์‹œ"
)
# ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ
def refresh_sessions():
try:
sessions = get_active_sessions("Korean")
return gr.update(choices=sessions)
except Exception as e:
logger.error(f"Error refreshing sessions: {str(e)}")
return gr.update(choices=[])
def handle_auto_recover(language):
session_id, message = auto_recover_session(language)
return session_id
# ์ด๋ฒคํŠธ ์—ฐ๊ฒฐ
submit_btn.click(
fn=process_query,
inputs=[query_input, language_select, current_session_id],
outputs=[stages_display, novel_output, status_text, current_session_id]
)
novel_output.change(
fn=lambda x: x,
inputs=[novel_output],
outputs=[novel_text_state]
)
resume_btn.click(
fn=lambda x: x,
inputs=[session_dropdown],
outputs=[current_session_id]
).then(
fn=resume_session,
inputs=[current_session_id, language_select],
outputs=[stages_display, novel_output, status_text, current_session_id]
)
auto_recover_btn.click(
fn=handle_auto_recover,
inputs=[language_select],
outputs=[current_session_id]
).then(
fn=resume_session,
inputs=[current_session_id, language_select],
outputs=[stages_display, novel_output, status_text, current_session_id]
)
refresh_btn.click(
fn=refresh_sessions,
outputs=[session_dropdown]
)
clear_btn.click(
fn=lambda: ("", "", "๐Ÿ”„ ์ค€๋น„ ์™„๋ฃŒ", "", None),
outputs=[stages_display, novel_output, status_text, novel_text_state, current_session_id]
)
def handle_download(format_type, language, session_id, novel_text):
if not session_id:
return gr.update(visible=False)
file_path = download_novel(novel_text, format_type, language, session_id)
if file_path:
return gr.update(value=file_path, visible=True)
else:
return gr.update(visible=False)
download_btn.click(
fn=handle_download,
inputs=[format_select, language_select, current_session_id, novel_text_state],
outputs=[download_file]
)
# ์‹œ์ž‘ ์‹œ ์„ธ์…˜ ๋กœ๋“œ
interface.load(
fn=refresh_sessions,
outputs=[session_dropdown]
)
return interface
# ๋ฉ”์ธ ์‹คํ–‰
if __name__ == "__main__":
logger.info("AI ๋ฌธํ•™ ์†Œ์„ค ์ƒ์„ฑ ์‹œ์Šคํ…œ ์‹œ์ž‘...")
logger.info("=" * 60)
# ํ™˜๊ฒฝ ํ™•์ธ
logger.info(f"API ์—”๋“œํฌ์ธํŠธ: {API_URL}")
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
)