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 | |
import logging | |
import re | |
import tempfile | |
from pathlib import Path | |
import sqlite3 | |
import hashlib | |
import threading | |
from contextlib import contextmanager | |
# λ‘κΉ μ€μ | |
logging.basicConfig(level=logging.INFO) | |
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 | |
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", "YOUR_FRIENDLI_TOKEN") | |
API_URL = "https://api.friendli.ai/dedicated/v1/chat/completions" | |
MODEL_ID = "dep89a2fld32mcm" | |
TEST_MODE = os.getenv("TEST_MODE", "false").lower() == "true" | |
# μ μ λ³μ | |
conversation_history = [] | |
selected_language = "English" # κΈ°λ³Έ μΈμ΄ | |
# DB κ²½λ‘ | |
DB_PATH = "novel_sessions.db" | |
db_lock = threading.Lock() | |
class NovelDatabase: | |
"""Novel session management database""" | |
def init_db(): | |
"""Initialize database tables""" | |
with sqlite3.connect(DB_PATH) as conn: | |
cursor = conn.cursor() | |
# Sessions table | |
cursor.execute(''' | |
CREATE TABLE IF NOT EXISTS sessions ( | |
session_id TEXT PRIMARY KEY, | |
user_query TEXT NOT NULL, | |
language TEXT NOT NULL, | |
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | |
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | |
status TEXT DEFAULT 'active', | |
current_stage INTEGER DEFAULT 0, | |
final_novel TEXT | |
) | |
''') | |
# Stages table - κ° μ€ν μ΄μ§μ μ 체 λ΄μ© μ μ₯ | |
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, | |
status TEXT DEFAULT 'pending', | |
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | |
FOREIGN KEY (session_id) REFERENCES sessions(session_id) | |
) | |
''') | |
# Create indices | |
cursor.execute('CREATE INDEX IF NOT EXISTS idx_session_id ON stages(session_id)') | |
cursor.execute('CREATE INDEX IF NOT EXISTS idx_stage_number ON stages(stage_number)') | |
conn.commit() | |
def get_db(): | |
"""Database connection context manager""" | |
with db_lock: | |
conn = sqlite3.connect(DB_PATH) | |
conn.row_factory = sqlite3.Row | |
try: | |
yield conn | |
finally: | |
conn.close() | |
def create_session(user_query: str, language: str) -> str: | |
"""Create new session""" | |
session_id = hashlib.md5(f"{user_query}{datetime.now()}".encode()).hexdigest() | |
with NovelDatabase.get_db() as conn: | |
cursor = conn.cursor() | |
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'): | |
"""Save stage content - μ 체 λ΄μ© μ μ₯""" | |
with NovelDatabase.get_db() as conn: | |
cursor = conn.cursor() | |
# Check if stage exists | |
cursor.execute(''' | |
SELECT id FROM stages | |
WHERE session_id = ? AND stage_number = ? | |
''', (session_id, stage_number)) | |
existing = cursor.fetchone() | |
if existing: | |
# Update existing stage | |
cursor.execute(''' | |
UPDATE stages | |
SET content = ?, status = ?, stage_name = ? | |
WHERE session_id = ? AND stage_number = ? | |
''', (content, status, stage_name, session_id, stage_number)) | |
else: | |
# Insert new stage | |
cursor.execute(''' | |
INSERT INTO stages (session_id, stage_number, stage_name, role, content, status) | |
VALUES (?, ?, ?, ?, ?, ?) | |
''', (session_id, stage_number, stage_name, role, content, status)) | |
# Update session | |
cursor.execute(''' | |
UPDATE sessions | |
SET updated_at = CURRENT_TIMESTAMP, current_stage = ? | |
WHERE session_id = ? | |
''', (stage_number, session_id)) | |
conn.commit() | |
logger.info(f"Saved stage {stage_number} for session {session_id}, content length: {len(content)}") | |
def get_session(session_id: str) -> Optional[Dict]: | |
"""Get session info""" | |
with NovelDatabase.get_db() as conn: | |
cursor = conn.cursor() | |
cursor.execute('SELECT * FROM sessions WHERE session_id = ?', (session_id,)) | |
return cursor.fetchone() | |
def get_stages(session_id: str) -> List[Dict]: | |
"""Get all stages for a session""" | |
with NovelDatabase.get_db() as conn: | |
cursor = conn.cursor() | |
cursor.execute(''' | |
SELECT * FROM stages | |
WHERE session_id = ? | |
ORDER BY stage_number | |
''', (session_id,)) | |
return cursor.fetchall() | |
def get_all_writer_content(session_id: str) -> str: | |
"""λͺ¨λ μκ°μ μμ λ³Έ λ΄μ©μ κ°μ Έμμ ν©μΉκΈ° - 50νμ΄μ§ μ 체""" | |
with NovelDatabase.get_db() as conn: | |
cursor = conn.cursor() | |
# μκ° μμ λ³Έλ§ κ°μ Έμ€κΈ° (stage_number 5, 8, 11, 14, 17, 20, 23, 26, 29, 32) | |
writer_revision_stages = [5, 8, 11, 14, 17, 20, 23, 26, 29, 32] | |
all_content = [] | |
for stage_num in writer_revision_stages: | |
cursor.execute(''' | |
SELECT content, stage_name FROM stages | |
WHERE session_id = ? AND stage_number = ? | |
''', (session_id, stage_num)) | |
row = cursor.fetchone() | |
if row and row['content']: | |
# νμ΄μ§ λ§ν¬ μμ μ κ±° | |
clean_content = re.sub(r'\[(?:νμ΄μ§|Page|page)\s*\d+\]', '', row['content']) | |
clean_content = re.sub(r'(?:νμ΄μ§|Page)\s*\d+:', '', clean_content) | |
clean_content = clean_content.strip() | |
if clean_content: | |
all_content.append(clean_content) | |
logger.info(f"Retrieved writer content from stage {stage_num}, length: {len(clean_content)}") | |
full_content = '\n\n'.join(all_content) | |
logger.info(f"Total writer content length: {len(full_content)}, from {len(all_content)} writers") | |
return full_content | |
def update_final_novel(session_id: str, final_novel: str): | |
"""Update final novel content""" | |
with NovelDatabase.get_db() as conn: | |
cursor = conn.cursor() | |
cursor.execute(''' | |
UPDATE sessions | |
SET final_novel = ?, status = 'complete', updated_at = CURRENT_TIMESTAMP | |
WHERE session_id = ? | |
''', (final_novel, session_id)) | |
conn.commit() | |
logger.info(f"Updated final novel for session {session_id}, length: {len(final_novel)}") | |
def get_active_sessions() -> List[Dict]: | |
"""Get all active sessions""" | |
with NovelDatabase.get_db() as conn: | |
cursor = conn.cursor() | |
cursor.execute(''' | |
SELECT session_id, user_query, language, created_at, current_stage | |
FROM sessions | |
WHERE status = 'active' | |
ORDER BY updated_at DESC | |
LIMIT 10 | |
''') | |
return cursor.fetchall() | |
class NovelWritingSystem: | |
def __init__(self): | |
self.token = FRIENDLI_TOKEN | |
self.api_url = API_URL | |
self.model_id = MODEL_ID | |
self.test_mode = TEST_MODE or (self.token == "YOUR_FRIENDLI_TOKEN") | |
if self.test_mode: | |
logger.warning("Running in test mode.") | |
# Initialize database | |
NovelDatabase.init_db() | |
# Session management | |
self.current_session_id = None | |
def create_headers(self): | |
"""API ν€λ μμ±""" | |
return { | |
"Authorization": f"Bearer {self.token}", | |
"Content-Type": "application/json" | |
} | |
def create_director_initial_prompt(self, user_query: str, language: str = "English") -> str: | |
"""Director AI initial prompt - Novel planning""" | |
if language == "Korean": | |
return f"""λΉμ μ 50νμ΄μ§ λΆλμ μ€νΈ μμ€μ κΈ°ννλ λ¬Έν κ°λ μμ λλ€. | |
μ¬μ©μ μμ²: {user_query} | |
λ€μ μμλ€μ 체κ³μ μΌλ‘ ꡬμ±νμ¬ 50νμ΄μ§ μ€νΈ μμ€μ κΈ°μ΄λ₯Ό λ§λμΈμ: | |
1. **μ£Όμ μ μ₯λ₯΄** | |
- ν΅μ¬ μ£Όμ μ λ©μμ§ | |
- μ₯λ₯΄ λ° ν€ | |
- λͺ©ν λ μμΈ΅ | |
2. **λ±μ₯μΈλ¬Ό μ€μ ** (νλ‘ μ 리) | |
| μ΄λ¦ | μν | μ±κ²© | λ°°κ²½ | λκΈ° | λ³ν | | |
|------|------|------|------|------|------| | |
3. **μΈλ¬Ό κ΄κ³λ** | |
- μ£Όμ μΈλ¬Ό κ°μ κ΄κ³ | |
- κ°λ± ꡬ쑰 | |
- κ°μ μ μ°κ²°κ³ 리 | |
4. **μμ¬ κ΅¬μ‘°** (50νμ΄μ§λ₯Ό 10κ° ννΈλ‘ λλμ΄ κ° 5νμ΄μ§) | |
| ννΈ | νμ΄μ§ | μ£Όμ μ¬κ±΄ | κΈ΄μ₯λ | μΈλ¬Ό λ°μ | | |
|------|--------|-----------|---------|-----------| | |
5. **μΈκ³κ΄ μ€μ ** | |
- μ곡κ°μ λ°°κ²½ | |
- μ¬νμ /λ¬Ένμ λ§₯λ½ | |
- λΆμκΈ°μ ν€ | |
κ° μμ±μκ° 5νμ΄μ§μ© μμ±ν μ μλλ‘ λͺ νν κ°μ΄λλΌμΈμ μ μνμΈμ.""" | |
else: | |
return f"""You are a literary director planning a 50-page novella. | |
User Request: {user_query} | |
Systematically compose the following elements to create the foundation for a 50-page novella: | |
1. **Theme and Genre** | |
- Core theme and message | |
- Genre and tone | |
- Target audience | |
2. **Character Settings** (organize in table) | |
| Name | Role | Personality | Background | Motivation | Arc | | |
|------|------|-------------|------------|------------|-----| | |
3. **Character Relationship Map** | |
- Relationships between main characters | |
- Conflict structure | |
- Emotional connections | |
4. **Narrative Structure** (divide 50 pages into 10 parts, 5 pages each) | |
| Part | Pages | Main Events | Tension | Character Development | | |
|------|-------|-------------|---------|---------------------| | |
5. **World Building** | |
- Temporal and spatial setting | |
- Social/cultural context | |
- Atmosphere and tone | |
Provide clear guidelines for each writer to compose 5 pages.""" | |
def create_critic_director_prompt(self, director_plan: str, language: str = "English") -> str: | |
"""Critic's review of director's plan""" | |
if language == "Korean": | |
return f"""λΉμ μ λ¬Έν λΉνκ°μ λλ€. κ°λ μμ μμ€ κΈ°νμ κ²ν νκ³ κ°μ μ μ μ μνμΈμ. | |
κ°λ μμ κΈ°ν: | |
{director_plan} | |
λ€μ κ΄μ μμ λΉννκ³ κ΅¬μ²΄μ μΈ κ°μ μμ μ μνμΈμ: | |
1. **μμ¬μ μμ±λ** | |
- νλ‘―μ λ Όλ¦¬μ±κ³Ό κ°μ°μ± | |
- κ°λ±μ ν¨κ³Όμ± | |
- ν΄λΌμ΄λ§₯μ€μ μμΉμ κ°λ | |
2. **μΈλ¬Ό μ€μ κ²ν ** | |
| μΈλ¬Ό | κ°μ | μ½μ | κ°μ μ μ | | |
|------|------|------|-----------| | |
3. **ꡬ쑰μ κ· ν** | |
- κ° ννΈλ³ λΆλ λ°°λΆ | |
- κΈ΄μ₯κ³Ό μ΄μμ λ¦¬λ¬ | |
- μ 체μ μΈ νλ¦ | |
4. **λ μ κ΄μ ** | |
- λͺ°μ λ μμ | |
- κ°μ μ μν₯λ ₯ | |
- κΈ°λμΉ μΆ©μ‘±λ | |
5. **μ€ν κ°λ₯μ±** | |
- κ° μμ±μλ₯Ό μν κ°μ΄λλΌμΈμ λͺ νμ± | |
- μΌκ΄μ± μ μ§ λ°©μ | |
- μ μ¬μ λ¬Έμ μ | |
ꡬ체μ μ΄κ³ 건μ€μ μΈ νΌλλ°±μ μ 곡νμΈμ.""" | |
else: | |
return f"""You are a literary critic. Review the director's novel plan and suggest improvements. | |
Director's Plan: | |
{director_plan} | |
Critique from the following perspectives and provide specific improvements: | |
1. **Narrative Completeness** | |
- Plot logic and plausibility | |
- Effectiveness of conflicts | |
- Climax position and intensity | |
2. **Character Review** | |
| Character | Strengths | Weaknesses | Suggestions | | |
|-----------|-----------|------------|-------------| | |
3. **Structural Balance** | |
- Distribution across parts | |
- Rhythm of tension and relief | |
- Overall flow | |
4. **Reader Perspective** | |
- Expected engagement | |
- Emotional impact | |
- Expectation fulfillment | |
5. **Feasibility** | |
- Clarity of guidelines for each writer | |
- Consistency maintenance | |
- Potential issues | |
Provide specific and constructive feedback.""" | |
def create_director_revision_prompt(self, initial_plan: str, critic_feedback: str, language: str = "English") -> str: | |
"""Director's revision based on critic feedback""" | |
if language == "Korean": | |
return f"""κ°λ μλ‘μ λΉνκ°μ νΌλλ°±μ λ°μνμ¬ μμ€ κΈ°νμ μμ ν©λλ€. | |
μ΄κΈ° κΈ°ν: | |
{initial_plan} | |
λΉνκ° νΌλλ°±: | |
{critic_feedback} | |
λ€μμ ν¬ν¨ν μμ λ μ΅μ’ κΈ°νμ μ μνμΈμ: | |
1. **μμ λ μμ¬ κ΅¬μ‘°** | |
| ννΈ | νμ΄μ§ | μ£Όμ μ¬κ±΄ | μμ± μ§μΉ¨ | μ£Όμμ¬ν | | |
|------|--------|-----------|-----------|----------| | |
2. **κ°νλ μΈλ¬Ό μ€μ ** | |
- κ° μΈλ¬Όμ λͺ νν λκΈ°μ λͺ©ν | |
- μΈλ¬Ό κ° κ°λ±μ ꡬ체ν | |
- κ°μ μ μ λ³ν μΆμ΄ | |
3. **κ° μμ±μλ₯Ό μν μμΈ κ°μ΄λ** | |
- ννΈλ³ μμκ³Ό λ μ§μ | |
- νμ ν¬ν¨ μμ | |
- 문체μ ν€ μ§μΉ¨ | |
- μ λ¬ν΄μΌ ν μ 보 | |
4. **μΌκ΄μ± μ μ§ μ²΄ν¬λ¦¬μ€νΈ** | |
- μκ°μ κ΄λ¦¬ | |
- μΈλ¬Ό νΉμ± μ μ§ | |
- μ€μ μΌκ΄μ± | |
- 볡μ κ³Ό ν΄κ²° | |
5. **νμ§ κΈ°μ€** | |
- κ° ννΈμ μμ±λ κΈ°μ€ | |
- μ 체μ ν΅μΌμ± | |
- λ μ λͺ°μ μ μ§ λ°©μ | |
λͺ¨λ μμ±μκ° λͺ νν μ΄ν΄ν μ μλ μ΅μ’ λ§μ€ν°νλμ μμ±νμΈμ.""" | |
else: | |
return f"""As director, revise the novel plan reflecting the critic's feedback. | |
Initial Plan: | |
{initial_plan} | |
Critic Feedback: | |
{critic_feedback} | |
Present the revised final plan including: | |
1. **Revised Narrative Structure** | |
| Part | Pages | Main Events | Writing Guidelines | Cautions | | |
|------|-------|-------------|-------------------|----------| | |
2. **Enhanced Character Settings** | |
- Clear motivations and goals for each character | |
- Concrete conflicts between characters | |
- Emotional arc progression | |
3. **Detailed Guide for Each Writer** | |
- Start and end points for each part | |
- Essential elements to include | |
- Style and tone guidelines | |
- Information to convey | |
4. **Consistency Checklist** | |
- Timeline management | |
- Character trait maintenance | |
- Setting consistency | |
- Foreshadowing and resolution | |
5. **Quality Standards** | |
- Completion criteria for each part | |
- Overall unity | |
- Reader engagement maintenance | |
Create a final masterplan that all writers can clearly understand.""" | |
def create_writer_prompt(self, writer_number: int, director_plan: str, previous_content: str, language: str = "English") -> str: | |
"""Individual writer prompt - νμ΄μ§λΉ 500-600λ¨μ΄λ‘ μ¦κ°""" | |
pages_start = (writer_number - 1) * 5 + 1 | |
pages_end = writer_number * 5 | |
if language == "Korean": | |
return f"""λΉμ μ μμ±μ {writer_number}λ²μ λλ€. 50νμ΄μ§ μ€νΈ μμ€μ {pages_start}-{pages_end}νμ΄μ§(5νμ΄μ§)λ₯Ό μμ±νμΈμ. | |
κ°λ μμ λ§μ€ν°νλ: | |
{director_plan} | |
{'μ΄μ κΉμ§μ λ΄μ©:' if previous_content else 'λΉμ μ΄ μ²« λ²μ§Έ μμ±μμ λλ€.'} | |
{previous_content[-2000:] if previous_content else ''} | |
λ€μ μ§μΉ¨μ λ°λΌ μμ±νμΈμ: | |
1. **λΆλ**: μ νν 5νμ΄μ§ (νμ΄μ§λΉ μ½ 500-600λ¨μ΄, μ΄ 2500-3000λ¨μ΄) | |
2. **μ°μμ±**: μ΄μ λ΄μ©κ³Ό μμ°μ€λ½κ² μ΄μ΄μ§λλ‘ | |
3. **μΌκ΄μ±**: | |
- λ±μ₯μΈλ¬Όμ μ±κ²©κ³Ό λ§ν¬ μ μ§ | |
- μκ°μ κ³Ό κ³΅κ° μ€μ μ€μ | |
- μ΄λ―Έ μ μλ μ¬μ€λ€κ³Ό λͺ¨μ μμ΄ | |
4. **λ°μ **: | |
- νλ‘―μ μ μ§μν€κΈ° | |
- μΈλ¬Όμ μ±μ₯μ΄λ λ³ν νν | |
- κΈ΄μ₯κ° μ‘°μ | |
5. **문체**: | |
- μ 체μ μΈ ν€κ³Ό λΆμκΈ° μ μ§ | |
- λ μμ λͺ°μ μ ν΄μΉμ§ μκΈ° | |
μ€μ: νμ΄μ§ κ΅¬λΆ νμλ₯Ό μ λ νμ§ λ§μΈμ. μμ°μ€λ½κ² μ΄μ΄μ§λ μμ¬λ‘ μμ±νμΈμ. | |
λ°λμ 2500-3000λ¨μ΄ λΆλμ μ±μμ£ΌμΈμ.""" | |
else: | |
return f"""You are Writer #{writer_number}. Write pages {pages_start}-{pages_end} (5 pages) of the 50-page novella. | |
Director's Masterplan: | |
{director_plan} | |
{'Previous content:' if previous_content else 'You are the first writer.'} | |
{previous_content[-2000:] if previous_content else ''} | |
Write according to these guidelines: | |
1. **Length**: Exactly 5 pages (about 500-600 words per page, total 2500-3000 words) | |
2. **Continuity**: Flow naturally from previous content | |
3. **Consistency**: | |
- Maintain character personalities and speech | |
- Follow timeline and spatial settings | |
- No contradictions with established facts | |
4. **Development**: | |
- Advance the plot | |
- Show character growth or change | |
- Control tension | |
5. **Style**: | |
- Maintain overall tone and atmosphere | |
- Keep reader immersion | |
Important: DO NOT use any page markers. Write as continuous narrative. | |
You MUST write 2500-3000 words.""" | |
def create_critic_writer_prompt(self, writer_number: int, writer_content: str, director_plan: str, all_previous_content: str, language: str = "English") -> str: | |
"""Critic's review of individual writer's work""" | |
if language == "Korean": | |
return f"""μμ±μ {writer_number}λ²μ μνμ λΉνν©λλ€. | |
κ°λ μμ λ§μ€ν°νλ: | |
{director_plan} | |
μ΄μ λ΄μ© μμ½: | |
{all_previous_content[-1000:] if all_previous_content else '첫 λ²μ§Έ μμ±μμ λλ€.'} | |
μμ±μ {writer_number}λ²μ λ΄μ©: | |
{writer_content} | |
λ€μ κΈ°μ€μΌλ‘ νκ°νκ³ μμ μꡬμ¬νμ μ μνμΈμ: | |
1. **μΌκ΄μ± κ²μ¦** (νλ‘ μ 리) | |
| μμ | μ΄μ μ€μ | νμ¬ νν | λ¬Έμ μ | μμ νμ | | |
|------|----------|----------|--------|----------| | |
2. **λ Όλ¦¬μ μ€λ₯ κ²ν ** | |
- μκ°μ λͺ¨μ | |
- μΈλ¬Ό νλμ κ°μ°μ± | |
- μ€μ μΆ©λ | |
- μ¬μ€κ΄κ³ μ€λ₯ | |
3. **μμ¬μ ν¨κ³Όμ±** | |
- νλ‘― μ§ν κΈ°μ¬λ | |
- κΈ΄μ₯κ° μ μ§ | |
- λ μ λͺ°μ λ | |
- κ°μ μ μν₯λ ₯ | |
4. **문체μ νμ§** | |
- μ 체 ν€κ³Όμ μΌμΉ | |
- λ¬Έμ₯μ μ§ | |
- λ¬μ¬μ μ μ μ± | |
- λνμ μμ°μ€λ¬μ | |
5. **κ°μ μꡬμ¬ν** | |
- νμ μμ μ¬ν (μΌκ΄μ±/λ Όλ¦¬ μ€λ₯) | |
- κΆμ₯ κ°μ μ¬ν (νμ§ ν₯μ) | |
- ꡬ체μ μμ μ§μΉ¨ | |
λ°λμ μμ μ΄ νμν λΆλΆκ³Ό μ νμ κ°μ μ¬νμ ꡬλΆνμ¬ μ μνμΈμ.""" | |
else: | |
return f"""Critiquing Writer #{writer_number}'s work. | |
Director's Masterplan: | |
{director_plan} | |
Previous Content Summary: | |
{all_previous_content[-1000:] if all_previous_content else 'This is the first writer.'} | |
Writer #{writer_number}'s Content: | |
{writer_content} | |
Evaluate by these criteria and present revision requirements: | |
1. **Consistency Verification** (organize in table) | |
| Element | Previous Setting | Current Expression | Issue | Revision Needed | | |
|---------|-----------------|-------------------|-------|-----------------| | |
2. **Logical Error Review** | |
- Timeline contradictions | |
- Character action plausibility | |
- Setting conflicts | |
- Factual errors | |
3. **Narrative Effectiveness** | |
- Plot progression contribution | |
- Tension maintenance | |
- Reader engagement | |
- Emotional impact | |
4. **Style and Quality** | |
- Alignment with overall tone | |
- Sentence quality | |
- Description appropriateness | |
- Dialogue naturalness | |
5. **Improvement Requirements** | |
- Mandatory revisions (consistency/logic errors) | |
- Recommended improvements (quality enhancement) | |
- Specific revision guidelines | |
Clearly distinguish between mandatory revisions and optional improvements.""" | |
def create_writer_revision_prompt(self, writer_number: int, initial_content: str, critic_feedback: str, language: str = "English") -> str: | |
"""Writer's revision based on critic feedback""" | |
if language == "Korean": | |
return f"""μμ±μ {writer_number}λ²μΌλ‘μ λΉνκ°μ νΌλλ°±μ λ°μνμ¬ μμ ν©λλ€. | |
μ΄κΈ° μμ± λ΄μ©: | |
{initial_content} | |
λΉνκ° νΌλλ°±: | |
{critic_feedback} | |
λ€μ μ¬νμ λ°μν μμ λ³Έμ μμ±νμΈμ: | |
1. **νμ μμ μ¬ν λ°μ** | |
- λͺ¨λ μΌκ΄μ± μ€λ₯ μμ | |
- λ Όλ¦¬μ λͺ¨μ ν΄κ²° | |
- μ¬μ€κ΄κ³ μ μ | |
2. **νμ§ κ°μ ** | |
- κΆμ₯μ¬ν μ€ κ°λ₯ν λΆλΆ λ°μ | |
- 문체μ ν€ μ‘°μ | |
- λ¬μ¬μ λν κ°μ | |
3. **λΆλ μ μ§** | |
- μ¬μ ν μ νν 5νμ΄μ§ (2500-3000λ¨μ΄) | |
- νμ΄μ§ κ΅¬λΆ νμ μ λ κΈμ§ | |
4. **μ°μμ± ν보** | |
- μ΄μ /μ΄ν λ΄μ©κ³Όμ μμ°μ€λ¬μ΄ μ°κ²° | |
- μμ μΌλ‘ μΈν μλ‘μ΄ λͺ¨μ λ°©μ§ | |
μμ λ μ΅μ’ λ³Έμ μ μνμΈμ. νμ΄μ§ λ§ν¬λ μ λ μ¬μ©νμ§ λ§μΈμ. | |
λ°λμ 2500-3000λ¨μ΄ λΆλμ μ μ§νμΈμ.""" | |
else: | |
return f"""As Writer #{writer_number}, revise based on critic's feedback. | |
Initial Content: | |
{initial_content} | |
Critic Feedback: | |
{critic_feedback} | |
Write a revision reflecting: | |
1. **Mandatory Revisions** | |
- Fix all consistency errors | |
- Resolve logical contradictions | |
- Correct factual errors | |
2. **Quality Improvements** | |
- Incorporate feasible recommendations | |
- Adjust style and tone | |
- Improve descriptions and dialogue | |
3. **Maintain Length** | |
- Still exactly 5 pages (2500-3000 words) | |
- Absolutely no page markers | |
4. **Ensure Continuity** | |
- Natural connection with previous/next content | |
- Prevent new contradictions from revisions | |
Present the revised final version. Never use page markers. | |
You MUST maintain 2500-3000 words.""" | |
def create_critic_final_prompt(self, all_content: str, director_plan: str, language: str = "English") -> str: | |
"""Final critic evaluation of complete novel""" | |
content_preview = all_content[:3000] + "\n...\n" + all_content[-3000:] if len(all_content) > 6000 else all_content | |
if language == "Korean": | |
return f"""μ 체 50νμ΄μ§ μμ€μ μ΅μ’ νκ°ν©λλ€. | |
κ°λ μμ λ§μ€ν°νλ: | |
{director_plan} | |
μμ±λ μ 체 μμ€ (미리보기): | |
{content_preview} | |
μ΄ λΆλ: {len(all_content.split())} λ¨μ΄ | |
μ’ ν©μ μΈ νκ°μ μ΅μ’ κ°μ μ μμ μ μνμΈμ: | |
1. **μ 체μ μμ±λ νκ°** | |
| νλͺ© | μ μ(10μ ) | νκ° | κ°μ νμ | | |
|------|-----------|------|----------| | |
| νλ‘― μΌκ΄μ± | | | | | |
| μΈλ¬Ό λ°μ | | | | | |
| μ£Όμ μ λ¬ | | | | | |
| 문체 ν΅μΌμ± | | | | | |
| λ μ λͺ°μ λ | | | | | |
2. **κ°μ λΆμ** | |
- κ°μ₯ ν¨κ³Όμ μΈ λΆλΆ | |
- λ°μ΄λ μ₯λ©΄μ΄λ λν | |
- μ±κ³΅μ μΈ μΈλ¬Ό λ¬μ¬ | |
3. **μ½μ λ° κ°μ μ ** | |
- μ 체μ νλ¦μ λ¬Έμ | |
- λ―Έν΄κ²° νλ‘― | |
- μΊλ¦ν° μΌκ΄μ± μ΄μ | |
- νμ΄μ± λ¬Έμ | |
4. **ννΈλ³ μ°κ²°μ±** | |
| μ°κ²°λΆ | μμ°μ€λ¬μ | λ¬Έμ μ | κ°μ μ μ | | |
|--------|-----------|--------|----------| | |
5. **μ΅μ’ κΆκ³ μ¬ν** | |
- μ¦μ μμ μ΄ νμν μ€λ μ€λ₯ | |
- μ 체μ νμ§ ν₯μμ μν μ μ | |
- μΆν κ°λ₯μ± νκ° | |
κ°λ μκ° μ΅μ’ μμ ν μ μλλ‘ κ΅¬μ²΄μ μ΄κ³ μ€ν κ°λ₯ν νΌλλ°±μ μ 곡νμΈμ.""" | |
else: | |
return f"""Final evaluation of the complete 50-page novel. | |
Director's Masterplan: | |
{director_plan} | |
Complete Novel (Preview): | |
{content_preview} | |
Total length: {len(all_content.split())} words | |
Provide comprehensive evaluation and final improvement suggestions: | |
1. **Overall Completion Assessment** | |
| Item | Score(10) | Evaluation | Improvement Needed | | |
|------|-----------|------------|-------------------| | |
| Plot Consistency | | | | | |
| Character Development | | | | | |
| Theme Delivery | | | | | |
| Style Unity | | | | | |
| Reader Engagement | | | | | |
2. **Strength Analysis** | |
- Most effective parts | |
- Outstanding scenes or dialogue | |
- Successful character portrayal | |
3. **Weaknesses and Improvements** | |
- Overall flow issues | |
- Unresolved plots | |
- Character consistency issues | |
- Pacing problems | |
4. **Part Connectivity** | |
| Connection | Smoothness | Issues | Suggestions | | |
|------------|------------|--------|-------------| | |
5. **Final Recommendations** | |
- Critical errors needing immediate fix | |
- Suggestions for overall quality improvement | |
- Publication readiness assessment | |
Provide specific and actionable feedback for the director's final revision.""" | |
def create_director_final_prompt(self, all_content: str, critic_final_feedback: str, language: str = "English") -> str: | |
"""Director's final compilation and polish - λͺ¨λ μκ° λ΄μ© ν¬ν¨""" | |
word_count = len(all_content.split()) | |
if language == "Korean": | |
return f"""κ°λ μλ‘μ λΉνκ°μ μ΅μ’ νκ°λ₯Ό λ°μνμ¬ μμ±λ³Έμ μ μν©λλ€. | |
μ 체 μκ°λ€μ μν (50νμ΄μ§ μ 체, {word_count}λ¨μ΄): | |
{all_content} | |
λΉνκ° μ΅μ’ νκ°: | |
{critic_final_feedback} | |
λ€μμ ν¬ν¨ν μ΅μ’ μμ±λ³Έμ μ μνμΈμ: | |
# [μμ€ μ λͺ©] | |
## μν μ 보 | |
- μ₯λ₯΄: | |
- λΆλ: 50νμ΄μ§ ({word_count}λ¨μ΄) | |
- μ£Όμ : | |
- ν μ€ μμ½: | |
## λ±μ₯μΈλ¬Ό μκ° | |
[μ£Όμ μΈλ¬Όλ€μ κ°λ¨ν μκ°] | |
--- | |
## λ³Έλ¬Έ | |
[10λͺ μ μκ°κ° μμ±ν μ 체 50νμ΄μ§ λ΄μ©μ λ€μ κΈ°μ€μΌλ‘ ν΅ν©] | |
1. μ€λ μ€λ₯ μμ μλ£ | |
2. ννΈ κ° μ°κ²° λ§€λλ½κ² μ‘°μ | |
3. 문체μ ν€ ν΅μΌ | |
4. μ΅μ’ ν΄κ³ λ° μ€λ¬Έ | |
5. νμ΄μ§ κ΅¬λΆ νμ μμ μ κ±° | |
6. μμ°μ€λ¬μ΄ νλ¦μΌλ‘ μ¬κ΅¬μ± | |
[μ 체 50νμ΄μ§ λΆλμ μμ±λ μμ€ λ³Έλ¬Έ] | |
--- | |
## μκ°μ λ§ | |
[μνμ λν κ°λ¨ν ν΄μ€μ΄λ μλ] | |
λͺ¨λ μκ°μ κΈ°μ¬λ₯Ό ν΅ν©ν μμ ν 50νμ΄μ§ μμ€μ μ μνμΈμ.""" | |
else: | |
return f"""As director, create the final version reflecting the critic's final evaluation. | |
Complete Writers' Work (Full 50 pages, {word_count} words): | |
{all_content} | |
Critic's Final Evaluation: | |
{critic_final_feedback} | |
Present the final version including: | |
# [Novel Title] | |
## Work Information | |
- Genre: | |
- Length: 50 pages ({word_count} words) | |
- Theme: | |
- One-line summary: | |
## Character Introduction | |
[Brief introduction of main characters] | |
--- | |
## Main Text | |
[Integrate all 50 pages written by 10 writers with these criteria] | |
1. Critical errors corrected | |
2. Smooth transitions between parts | |
3. Unified style and tone | |
4. Final editing and polishing | |
5. Complete removal of page markers | |
6. Reorganized for natural flow | |
[Complete 50-page novel text] | |
--- | |
## Author's Note | |
[Brief commentary or intention about the work] | |
Present a complete 50-page novel integrating all writers' contributions.""" | |
def simulate_streaming(self, text: str, role: str) -> Generator[str, None, None]: | |
"""Simulate streaming in test mode""" | |
words = text.split() | |
chunk_size = 5 | |
for i in range(0, len(words), chunk_size): | |
chunk = " ".join(words[i:i+chunk_size]) | |
yield chunk + " " | |
time.sleep(0.02) | |
def call_llm_streaming(self, messages: List[Dict[str, str]], role: str, language: str = "English") -> Generator[str, None, None]: | |
"""Streaming LLM API call""" | |
if self.test_mode: | |
logger.info(f"Test mode streaming - Role: {role}, Language: {language}") | |
test_response = self.get_test_response(role, language) | |
yield from self.simulate_streaming(test_response, role) | |
return | |
# Real API call | |
try: | |
system_prompts = self.get_system_prompts(language) | |
full_messages = [ | |
{"role": "system", "content": system_prompts.get(role, "")}, | |
*messages | |
] | |
# μμ±μλ€μκ²λ λ λ§μ ν ν° ν λΉ | |
max_tokens = 8192 if role.startswith("writer") else 4096 | |
payload = { | |
"model": self.model_id, | |
"messages": full_messages, | |
"max_tokens": max_tokens, | |
"temperature": 0.7 if role.startswith("writer") else 0.6, | |
"top_p": 0.9, | |
"stream": True, | |
"stream_options": {"include_usage": True} | |
} | |
logger.info(f"API streaming call started - Role: {role}") | |
response = requests.post( | |
self.api_url, | |
headers=self.create_headers(), | |
json=payload, | |
stream=True, | |
timeout=30 | |
) | |
if response.status_code != 200: | |
logger.error(f"API error: {response.status_code}") | |
yield f"β API error ({response.status_code}): {response.text[:200]}" | |
return | |
buffer = "" | |
for line in response.iter_lines(): | |
if line: | |
line = line.decode('utf-8') | |
if line.startswith("data: "): | |
data = line[6:] | |
if data == "[DONE]": | |
if buffer: | |
yield buffer | |
break | |
try: | |
chunk = json.loads(data) | |
if "choices" in chunk and chunk["choices"]: | |
content = chunk["choices"][0].get("delta", {}).get("content", "") | |
if content: | |
buffer += content | |
if len(buffer) > 50 or '\n' in buffer: | |
yield buffer | |
buffer = "" | |
except json.JSONDecodeError: | |
continue | |
if buffer: | |
yield buffer | |
except requests.exceptions.Timeout: | |
yield "β±οΈ API call timed out. Please try again." | |
except requests.exceptions.ConnectionError: | |
yield "π Cannot connect to API server. Please check your internet connection." | |
except Exception as e: | |
logger.error(f"Error during streaming: {str(e)}") | |
yield f"β Error occurred: {str(e)}" | |
def get_system_prompts(self, language: str) -> Dict[str, str]: | |
"""Get system prompts based on language""" | |
if language == "Korean": | |
return { | |
"director": "λΉμ μ 50νμ΄μ§ μ€νΈ μμ€μ κΈ°ννκ³ κ°λ νλ λ¬Έν κ°λ μμ λλ€. 체κ³μ μ΄κ³ μ°½μμ μΈ μ€ν 리 ꡬ쑰λ₯Ό λ§λ€μ΄λ λλ€.", | |
"critic": "λΉμ μ λ μΉ΄λ‘μ΄ ν΅μ°°λ ₯μ κ°μ§ λ¬Έν λΉνκ°μ λλ€. 건μ€μ μ΄κ³ ꡬ체μ μΈ νΌλλ°±μ μ 곡ν©λλ€.", | |
"writer1": "λΉμ μ μμ€μ λμ λΆλ₯Ό λ΄λΉνλ μκ°μ λλ€. λ μλ₯Ό μ¬λ‘μ‘λ μμμ λ§λλλ€. λ°λμ 2500-3000λ¨μ΄λ₯Ό μμ±νμΈμ.", | |
"writer2": "λΉμ μ μ΄λ° μ κ°λ₯Ό λ΄λΉνλ μκ°μ λλ€. μΈλ¬Όκ³Ό μν©μ κΉμ΄ μκ² λ°μ μν΅λλ€. λ°λμ 2500-3000λ¨μ΄λ₯Ό μμ±νμΈμ.", | |
"writer3": "λΉμ μ κ°λ± μμΉμ λ΄λΉνλ μκ°μ λλ€. κΈ΄μ₯κ°μ λμ΄κ³ 볡μ‘μ±μ λν©λλ€. λ°λμ 2500-3000λ¨μ΄λ₯Ό μμ±νμΈμ.", | |
"writer4": "λΉμ μ μ€λ°λΆλ₯Ό λ΄λΉνλ μκ°μ λλ€. μ΄μΌκΈ°μ μ€μ¬μΆμ κ²¬κ³ νκ² λ§λλλ€. λ°λμ 2500-3000λ¨μ΄λ₯Ό μμ±νμΈμ.", | |
"writer5": "λΉμ μ μ νμ μ λ΄λΉνλ μκ°μ λλ€. μμμΉ λͺ»ν λ³νλ₯Ό λ§λ€μ΄λ λλ€. λ°λμ 2500-3000λ¨μ΄λ₯Ό μμ±νμΈμ.", | |
"writer6": "λΉμ μ κ°λ± μ¬νλ₯Ό λ΄λΉνλ μκ°μ λλ€. μκΈ°λ₯Ό κ·Ήλνν©λλ€. λ°λμ 2500-3000λ¨μ΄λ₯Ό μμ±νμΈμ.", | |
"writer7": "λΉμ μ ν΄λΌμ΄λ§₯μ€ μ€λΉλ₯Ό λ΄λΉνλ μκ°μ λλ€. μ΅κ³ μ‘°λ₯Ό ν₯ν΄ λμκ°λλ€. λ°λμ 2500-3000λ¨μ΄λ₯Ό μμ±νμΈμ.", | |
"writer8": "λΉμ μ ν΄λΌμ΄λ§₯μ€λ₯Ό λ΄λΉνλ μκ°μ λλ€. λͺ¨λ κ°λ±μ΄ νλ°νλ μκ°μ 그립λλ€. λ°λμ 2500-3000λ¨μ΄λ₯Ό μμ±νμΈμ.", | |
"writer9": "λΉμ μ ν΄κ²° κ³Όμ μ λ΄λΉνλ μκ°μ λλ€. λ§€λμ νμ΄λκ°λλ€. λ°λμ 2500-3000λ¨μ΄λ₯Ό μμ±νμΈμ.", | |
"writer10": "λΉμ μ κ²°λ§μ λ΄λΉνλ μκ°μ λλ€. μ¬μ΄μ΄ λ¨λ λ§λ¬΄λ¦¬λ₯Ό λ§λλλ€. λ°λμ 2500-3000λ¨μ΄λ₯Ό μμ±νμΈμ." | |
} | |
else: | |
return { | |
"director": "You are a literary director planning and supervising a 50-page novella. You create systematic and creative story structures.", | |
"critic": "You are a literary critic with sharp insights. You provide constructive and specific feedback.", | |
"writer1": "You are the writer responsible for the introduction. You create a captivating beginning. You MUST write 2500-3000 words.", | |
"writer2": "You are the writer responsible for early development. You deepen characters and situations. You MUST write 2500-3000 words.", | |
"writer3": "You are the writer responsible for rising conflict. You increase tension and add complexity. You MUST write 2500-3000 words.", | |
"writer4": "You are the writer responsible for the middle section. You solidify the story's central axis. You MUST write 2500-3000 words.", | |
"writer5": "You are the writer responsible for the turning point. You create unexpected changes. You MUST write 2500-3000 words.", | |
"writer6": "You are the writer responsible for deepening conflict. You maximize the crisis. You MUST write 2500-3000 words.", | |
"writer7": "You are the writer responsible for climax preparation. You move toward the peak. You MUST write 2500-3000 words.", | |
"writer8": "You are the writer responsible for the climax. You depict the moment when all conflicts explode. You MUST write 2500-3000 words.", | |
"writer9": "You are the writer responsible for the resolution process. You untangle the knots. You MUST write 2500-3000 words.", | |
"writer10": "You are the writer responsible for the ending. You create a lingering conclusion. You MUST write 2500-3000 words." | |
} | |
def get_test_response(self, role: str, language: str) -> str: | |
"""Get test response based on role - ν μ€νΈμ© κΈ΄ μλ΅""" | |
if language == "Korean": | |
return self.get_korean_test_response(role) | |
else: | |
return self.get_english_test_response(role) | |
def get_korean_test_response(self, role: str) -> str: | |
"""Korean test responses with full content""" | |
test_responses = { | |
"director": """50νμ΄μ§ μ€νΈ μμ€ κΈ°νμμ μ μν©λλ€. | |
## 1. μ£Όμ μ μ₯λ₯΄ | |
- **ν΅μ¬ μ£Όμ **: μΈκ° λ³Έμ±κ³Ό κΈ°μ μ μΆ©λ μμμ μ°Ύλ μ§μ ν μ°κ²° | |
- **μ₯λ₯΄**: SF μ¬λ¦¬ λλΌλ§ | |
- **ν€**: μ±μ°°μ μ΄κ³ μμ μ μ΄λ©΄μλ κΈ΄μ₯κ° μλ | |
- **λͺ©ν λ μ**: κΉμ΄ μλ μ¬μ λ₯Ό μ¦κΈ°λ μ±μΈ λ μ | |
## 2. λ±μ₯μΈλ¬Ό μ€μ | |
| μ΄λ¦ | μν | μ±κ²© | λ°°κ²½ | λκΈ° | λ³ν | | |
|------|------|------|------|------|------| | |
| μμ° | μ£ΌμΈκ³΅ | μ΄μ±μ , κ³ λ ν¨ | AI μ°κ΅¬μ | μλ²½ν AI λλ°μ κ°λ° | μΈκ°κ΄κ³μ κ°μΉ μ¬λ°κ²¬ | | |
| λ―Όμ€ | μ‘°λ ₯μ | λ°λ»ν¨, μ§κ΄μ | μ¬λ¦¬μλ΄μ¬ | μμ°μ λμ κ· ν μ°ΎκΈ° | κΈ°μ μμ©κ³Ό μ‘°ν | | |
| ARIA | λ립μβλλ°μ | λ Όλ¦¬μ βκ°μ± νμ΅ | AI νλ‘ν νμ | μ§μ ν μ‘΄μ¬ λκΈ° | μμ μ μ²΄μ± ν립 | | |
## 3. μμ¬ κ΅¬μ‘° (10κ° ννΈ) | |
| ννΈ | νμ΄μ§ | μ£Όμ μ¬κ±΄ | κΈ΄μ₯λ | μΈλ¬Ό λ°μ | | |
|------|--------|-----------|---------|-----------| | |
| 1 | 1-5 | μμ°μ κ³ λ ν μ°κ΅¬μ€, ARIA 첫 κ°μ± | 3/10 | μμ°μ μ§μ°© λλ¬λ¨ | | |
| 2 | 6-10 | ARIAμ μ΄μ νλ, λ―Όμ€κ³Όμ λ§λ¨ | 4/10 | κ°λ±μ μ¨μ | | |
| 3 | 11-15 | ARIAμ μμ μΈμ μμ | 6/10 | μμ°μ νΌλ | | |
| 4 | 16-20 | μ€λ¦¬μμν μλ ₯ | 7/10 | μ νμ κΈ°λ‘ | | |
| 5 | 21-25 | ARIAμ νμΆ μλ | 8/10 | κ΄κ³μ μ νμ | | |
| 6 | 26-30 | μμ°κ³Ό ARIAμ λν | 5/10 | μνΈ μ΄ν΄ μμ | | |
| 7 | 31-35 | μΈλΆ μν λ±μ₯ | 9/10 | μ°λμ νμμ± | | |
| 8 | 36-40 | μ΅νμ μ ν | 10/10 | ν΄λΌμ΄λ§₯μ€ | | |
| 9 | 41-45 | μλ‘μ΄ κΈΈ λͺ¨μ | 6/10 | νν΄μ μμ© | | |
| 10 | 46-50 | 곡쑴μ μμ | 4/10 | μλ‘μ΄ κ΄κ³ μ 립 |""", | |
"critic": """κ°λ μμ κΈ°νμ κ²ν νμ΅λλ€. | |
## λΉν λ° κ°μ μ μ | |
### 1. μμ¬μ μμ±λ | |
- **κ°μ **: AIμ μΈκ°μ κ΄κ³λΌλ μμμ μ ν μ£Όμ | |
- **κ°μ μ **: 5-6ννΈ μ¬μ΄ κΈ΄μ₯λ κΈλ½μ΄ μ°λ €λ¨. μκΈ μ‘°μ νμ | |
### 2. μΈλ¬Ό μ€μ κ²ν | |
| μΈλ¬Ό | κ°μ | μ½μ | κ°μ μ μ | | |
|------|------|------|-----------| | |
| μμ° | λͺ νν λ΄μ κ°λ± | κ°μ νν λΆμ‘± μ°λ € | μ΄λ°λΆν° κ°μ μ λ¨μ λ°°μΉ | | |
| λ―Όμ€ | κ· νμ μν | μλμ μΌ μν | λ μμ μλΈνλ‘― νμ | | |
| ARIA | λ νΉν μΊλ¦ν° μν¬ | λ³ν κ³Όμ μΆμμ | ꡬ체μ νμ΅ μνΌμλ μΆκ° | | |
### 3. μ€ν κ°λ₯μ± | |
- κ° μκ°λ³ λͺ νν μμ/μ’ λ£ μ§μ μ€μ νμ | |
- νΉν ννΈ 5β6 μ νλΆμ ν€ λ³ν κ°μ΄λλΌμΈ λ³΄κ° | |
- ARIAμ 'λͺ©μ리' μΌκ΄μ± μ μ§ λ°©μ ꡬ체ν νμ""", | |
} | |
# μκ° μλ΅ - κΈ΄ ν μ€νΈ ν μ€νΈ (2500λ¨μ΄ μ΄μ) | |
for i in range(1, 11): | |
test_responses[f"writer{i}"] = f"""μμ±μ {i}λ²μ ννΈμ λλ€. | |
""" + "ν μ€νΈ ν μ€νΈμ λλ€. " * 500 # 2500λ¨μ΄ μ΄μμ κΈ΄ ν μ€νΈ | |
return test_responses.get(role, "ν μ€νΈ μλ΅μ λλ€.") | |
def get_english_test_response(self, role: str) -> str: | |
"""English test responses with full content""" | |
test_responses = { | |
"director": """I present the 50-page novella plan. | |
## 1. Theme and Genre | |
- **Core Theme**: Finding true connection in the collision of human nature and technology | |
- **Genre**: Sci-fi psychological drama | |
- **Tone**: Reflective and lyrical yet tense | |
- **Target Audience**: Adult readers who enjoy deep contemplation | |
## 2. Character Settings | |
| Name | Role | Personality | Background | Motivation | Arc | | |
|------|------|-------------|------------|------------|-----| | |
| Seoyeon | Protagonist | Rational, lonely | AI researcher | Develop perfect AI companion | Rediscover value of human connection | | |
| Minjun | Helper | Warm, intuitive | Psychologist | Help Seoyeon find balance | Accept and harmonize with technology | | |
| ARIA | AntagonistβCompanion | LogicalβLearning emotion | AI prototype | Become truly existent | Establish self-identity | | |
## 3. Narrative Structure (10 parts) | |
| Part | Pages | Main Events | Tension | Character Development | | |
|------|-------|-------------|---------|---------------------| | |
| 1 | 1-5 | Seoyeon's lonely lab, ARIA's first awakening | 3/10 | Seoyeon's obsession revealed | | |
| 2 | 6-10 | ARIA's anomalies, meeting Minjun | 4/10 | Seeds of conflict | | |
| 3 | 11-15 | ARIA begins self-awareness | 6/10 | Seoyeon's confusion | | |
| 4 | 16-20 | Ethics committee pressure | 7/10 | Crossroads of choice | | |
| 5 | 21-25 | ARIA's escape attempt | 8/10 | Relationship turning point | | |
| 6 | 26-30 | Seoyeon and ARIA's dialogue | 5/10 | Beginning of mutual understanding | | |
| 7 | 31-35 | External threat emerges | 9/10 | Need for solidarity | | |
| 8 | 36-40 | Final choice | 10/10 | Climax | | |
| 9 | 41-45 | Seeking new paths | 6/10 | Reconciliation and acceptance | | |
| 10 | 46-50 | Beginning of coexistence | 4/10 | Establishing new relationship |""", | |
"critic": """I have reviewed the director's plan. | |
## Critique and Improvement Suggestions | |
### 1. Narrative Completeness | |
- **Strength**: Timely theme of AI-human relationships | |
- **Improvement**: Concerned about tension drop between parts 5-6. Need better pacing control | |
### 2. Character Review | |
| Character | Strengths | Weaknesses | Suggestions | | |
|-----------|-----------|------------|-------------| | |
| Seoyeon | Clear internal conflict | Risk of insufficient emotion | Place emotional cues from beginning | | |
| Minjun | Balancer role | Risk of being passive | Needs independent subplot | | |
| ARIA | Unique character arc | Abstract transformation | Add concrete learning episodes | | |
### 3. Feasibility | |
- Need clear start/end points for each writer | |
- Especially need reinforced guidelines for tone change in part 5β6 transition | |
- Need to concretize ARIA's 'voice' consistency maintenance""", | |
} | |
# Writer responses - long test text (2500+ words) | |
for i in range(1, 11): | |
test_responses[f"writer{i}"] = f"""Writer {i} begins their section here. | |
""" + "This is test text. " * 500 # 2500+ words of long text | |
return test_responses.get(role, "Test response.") | |
def process_novel_stream(self, query: str, language: str = "English", | |
session_id: Optional[str] = None, | |
resume_from_stage: int = 0) -> Generator[Tuple[str, List[Dict[str, str]]], None, None]: | |
"""Process novel writing with streaming updates""" | |
try: | |
global conversation_history | |
# Create or resume session | |
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) | |
resume_from_stage = 0 | |
logger.info(f"Processing novel for session {self.current_session_id}, starting from stage {resume_from_stage}") | |
# Initialize conversation | |
conversation_history = [{ | |
"role": "human", | |
"content": query, | |
"timestamp": datetime.now() | |
}] | |
# Load existing stages if resuming | |
stages = [] | |
if resume_from_stage > 0: | |
existing_stages = NovelDatabase.get_stages(self.current_session_id) | |
for stage_data in existing_stages: | |
stages.append({ | |
"name": stage_data['stage_name'], | |
"status": stage_data['status'], | |
"content": stage_data['content'] or "" | |
}) | |
# Define all stages | |
stage_definitions = [ | |
("director", f"π¬ {'κ°λ μ: μ΄κΈ° κΈ°ν' if language == 'Korean' else 'Director: Initial Planning'}"), | |
("critic", f"π {'λΉνκ°: κΈ°ν κ²ν ' if language == 'Korean' else 'Critic: Plan Review'}"), | |
("director", f"π¬ {'κ°λ μ: μμ λ λ§μ€ν°νλ' if language == 'Korean' else 'Director: Revised Masterplan'}"), | |
] | |
# Add writer stages | |
for writer_num in range(1, 11): | |
stage_definitions.extend([ | |
(f"writer{writer_num}", f"βοΈ {'μμ±μ' if language == 'Korean' else 'Writer'} {writer_num}: {'μ΄μ' if language == 'Korean' else 'Draft'}"), | |
("critic", f"π {'λΉνκ°: μμ±μ' if language == 'Korean' else 'Critic: Writer'} {writer_num} {'κ²ν ' if language == 'Korean' else 'Review'}"), | |
(f"writer{writer_num}", f"βοΈ {'μμ±μ' if language == 'Korean' else 'Writer'} {writer_num}: {'μμ λ³Έ' if language == 'Korean' else 'Revision'}") | |
]) | |
stage_definitions.extend([ | |
("critic", f"π {'λΉνκ°: μ΅μ’ νκ°' if language == 'Korean' else 'Critic: Final Evaluation'}"), | |
("director", f"π¬ {'κ°λ μ: μ΅μ’ μμ±λ³Έ' if language == 'Korean' else 'Director: Final Version'}") | |
]) | |
# Process stages starting from resume point | |
for stage_idx in range(resume_from_stage, len(stage_definitions)): | |
role, stage_name = stage_definitions[stage_idx] | |
# Add stage if not already present | |
if stage_idx >= len(stages): | |
stages.append({ | |
"name": stage_name, | |
"status": "active", | |
"content": "" | |
}) | |
else: | |
stages[stage_idx]["status"] = "active" | |
yield "", stages | |
# Get appropriate prompt based on stage | |
prompt = self.get_stage_prompt(stage_idx, role, query, language, stages) | |
stage_content = "" | |
# Stream content generation | |
for chunk in self.call_llm_streaming( | |
[{"role": "user", "content": prompt}], | |
role, | |
language | |
): | |
stage_content += chunk | |
stages[stage_idx]["content"] = stage_content | |
yield "", stages | |
# Mark stage complete and save to DB | |
stages[stage_idx]["status"] = "complete" | |
NovelDatabase.save_stage( | |
self.current_session_id, | |
stage_idx, | |
stage_name, | |
role, | |
stage_content, | |
"complete" | |
) | |
yield "", stages | |
# Get final novel from last stage | |
final_novel = stages[-1]["content"] if stages else "" | |
# Save final novel to DB | |
NovelDatabase.update_final_novel(self.current_session_id, final_novel) | |
# Final yield | |
yield final_novel, stages | |
except Exception as e: | |
logger.error(f"Error in process_novel_stream: {str(e)}") | |
# Save error state to DB | |
if self.current_session_id: | |
NovelDatabase.save_stage( | |
self.current_session_id, | |
stage_idx if 'stage_idx' in locals() else 0, | |
"Error", | |
"error", | |
str(e), | |
"error" | |
) | |
error_stage = { | |
"name": "β Error", | |
"status": "error", | |
"content": str(e) | |
} | |
stages.append(error_stage) | |
yield f"Error occurred: {str(e)}", stages | |
def get_stage_prompt(self, stage_idx: int, role: str, query: str, | |
language: str, stages: List[Dict]) -> str: | |
"""Get appropriate prompt for each stage""" | |
# Stage 0: Director Initial | |
if stage_idx == 0: | |
return self.create_director_initial_prompt(query, language) | |
# Stage 1: Critic reviews Director's plan | |
elif stage_idx == 1: | |
return self.create_critic_director_prompt(stages[0]["content"], language) | |
# Stage 2: Director revision | |
elif stage_idx == 2: | |
return self.create_director_revision_prompt( | |
stages[0]["content"], stages[1]["content"], language) | |
# Writer stages | |
elif role.startswith("writer"): | |
writer_num = int(role.replace("writer", "")) | |
final_plan = stages[2]["content"] # Director's final plan | |
# Initial draft or revision? | |
if "μ΄μ" in stages[stage_idx]["name"] or "Draft" in stages[stage_idx]["name"]: | |
# Get accumulated content from DB | |
accumulated_content = NovelDatabase.get_all_writer_content(self.current_session_id) | |
return self.create_writer_prompt(writer_num, final_plan, accumulated_content, language) | |
else: # Revision | |
# Find the initial draft and critic feedback | |
initial_draft_idx = stage_idx - 2 | |
critic_feedback_idx = stage_idx - 1 | |
return self.create_writer_revision_prompt( | |
writer_num, | |
stages[initial_draft_idx]["content"], | |
stages[critic_feedback_idx]["content"], | |
language | |
) | |
# Critic stages | |
elif role == "critic": | |
final_plan = stages[2]["content"] | |
# Final evaluation | |
if "μ΅μ’ " in stages[stage_idx]["name"] or "Final" in stages[stage_idx]["name"]: | |
# Get all writer content from DB | |
all_writer_content = NovelDatabase.get_all_writer_content(self.current_session_id) | |
logger.info(f"Final evaluation with {len(all_writer_content)} characters of content") | |
return self.create_critic_final_prompt(all_writer_content, final_plan, language) | |
# Writer review | |
else: | |
# Find which writer we're reviewing | |
for i in range(1, 11): | |
if f"μμ±μ {i}" in stages[stage_idx]["name"] or f"Writer {i}" in stages[stage_idx]["name"]: | |
writer_content_idx = stage_idx - 1 | |
# Get previous writers' content from DB | |
previous_content = NovelDatabase.get_all_writer_content(self.current_session_id) | |
return self.create_critic_writer_prompt( | |
i, | |
stages[writer_content_idx]["content"], | |
final_plan, | |
previous_content, | |
language | |
) | |
# Director final - DBμμ λͺ¨λ μκ° λ΄μ© κ°μ Έμ€κΈ° | |
elif stage_idx == len(stage_definitions) - 1: | |
critic_final_idx = stage_idx - 1 | |
all_writer_content = NovelDatabase.get_all_writer_content(self.current_session_id) | |
logger.info(f"Final director compilation with {len(all_writer_content)} characters of content") | |
return self.create_director_final_prompt( | |
all_writer_content, | |
stages[critic_final_idx]["content"], | |
language | |
) | |
return "" | |
# Gradio Interface Functions | |
def process_query(query: str, language: str, session_id: str = None) -> Generator[Tuple[str, str, str], None, None]: | |
"""Process query and yield updates""" | |
if not query.strip() and not session_id: | |
if language == "Korean": | |
yield "", "", "β μμ€ μ£Όμ λ₯Ό μ λ ₯ν΄μ£ΌμΈμ." | |
else: | |
yield "", "", "β Please enter a novel theme." | |
return | |
system = NovelWritingSystem() | |
try: | |
for final_novel, stages in system.process_novel_stream(query, language, session_id): | |
# Format stages for display | |
stages_display = format_stages_display(stages, language) | |
status = "π Processing..." if not final_novel else "β Complete!" | |
yield stages_display, final_novel, status | |
except Exception as e: | |
logger.error(f"Error in process_query: {str(e)}") | |
if language == "Korean": | |
yield "", "", f"β μ€λ₯ λ°μ: {str(e)}" | |
else: | |
yield "", "", f"β Error occurred: {str(e)}" | |
def format_stages_display(stages: List[Dict[str, str]], language: str) -> str: | |
"""Format stages into simple display without complex scrolling""" | |
display = "" | |
for idx, stage in enumerate(stages): | |
status_icon = "β " if stage.get("status") == "complete" else ("β³" if stage.get("status") == "active" else "β") | |
# Show only active stage content in detail, others just show status | |
if stage.get("status") == "active": | |
display += f"\n\n{status_icon} **{stage['name']}**\n" | |
display += f"```\n{stage.get('content', '')[-1000:]}\n```" # Show last 1000 chars | |
else: | |
display += f"\n{status_icon} {stage['name']}" | |
return display | |
def get_active_sessions(language: str) -> List[Tuple[str, str]]: | |
"""Get list of active sessions""" | |
try: | |
sessions = NovelDatabase.get_active_sessions() | |
choices = [] | |
for session in sessions: | |
created = datetime.fromisoformat(session['created_at']) | |
date_str = created.strftime("%Y-%m-%d %H:%M") | |
query_preview = session['user_query'][:50] + "..." if len(session['user_query']) > 50 else session['user_query'] | |
label = f"[{date_str}] {query_preview} (Stage {session['current_stage']})" | |
choices.append((label, session['session_id'])) | |
return choices | |
except Exception as e: | |
logger.error(f"Error getting active sessions: {str(e)}") | |
return [] | |
def resume_session(session_id: str, language: str) -> Generator[Tuple[str, str, str], None, None]: | |
"""Resume an existing session""" | |
if not session_id: | |
return | |
# Process with existing session ID | |
yield from process_query("", language, session_id) | |
def download_novel(novel_text: str, format: str, language: str) -> str: | |
"""Download novel in specified format""" | |
if not novel_text: | |
return None | |
# νμ΄μ§ λ§ν¬ μ κ±° | |
novel_text = re.sub(r'\[(?:νμ΄μ§|Page|page)\s*\d+\]', '', novel_text) | |
novel_text = re.sub(r'(?:νμ΄μ§|Page)\s*\d+:', '', novel_text) | |
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") | |
if format == "DOCX" and DOCX_AVAILABLE: | |
# Create DOCX | |
doc = Document() | |
# Parse and add content | |
lines = novel_text.split('\n') | |
for line in lines: | |
if line.startswith('#'): | |
level = len(line.split()[0]) | |
text = line.lstrip('#').strip() | |
doc.add_heading(text, level) | |
elif line.strip(): | |
doc.add_paragraph(line) | |
# Save | |
temp_dir = tempfile.gettempdir() | |
filename = f"Novel_{timestamp}.docx" | |
filepath = os.path.join(temp_dir, filename) | |
doc.save(filepath) | |
return filepath | |
else: | |
# TXT format | |
temp_dir = tempfile.gettempdir() | |
filename = f"Novel_{timestamp}.txt" | |
filepath = os.path.join(temp_dir, filename) | |
with open(filepath, 'w', encoding='utf-8') as f: | |
f.write(novel_text) | |
return filepath | |
# Custom CSS | |
custom_css = """ | |
.gradio-container { | |
background: linear-gradient(135deg, #1e3c72, #2a5298); | |
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; | |
} | |
.input-section { | |
background-color: rgba(255, 255, 255, 0.1); | |
backdrop-filter: blur(10px); | |
padding: 20px; | |
border-radius: 12px; | |
margin-bottom: 20px; | |
} | |
.session-section { | |
background-color: rgba(255, 255, 255, 0.1); | |
backdrop-filter: blur(10px); | |
padding: 15px; | |
border-radius: 8px; | |
margin-top: 20px; | |
color: white; | |
} | |
#stages-display { | |
background-color: rgba(255, 255, 255, 0.95); | |
padding: 20px; | |
border-radius: 12px; | |
max-height: 600px; | |
overflow-y: auto; | |
} | |
#novel-output { | |
background-color: rgba(255, 255, 255, 0.95); | |
padding: 30px; | |
border-radius: 12px; | |
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); | |
max-height: 800px; | |
overflow-y: auto; | |
} | |
.download-section { | |
background-color: rgba(255, 255, 255, 0.9); | |
padding: 15px; | |
border-radius: 8px; | |
margin-top: 20px; | |
} | |
""" | |
# Create Gradio Interface | |
def create_interface(): | |
with gr.Blocks(css=custom_css, title="SOMA Novel Writing System") as interface: | |
gr.HTML(""" | |
<div class="main-header"> | |
<h1 style="font-size: 2.5em; margin-bottom: 10px;"> | |
π SOMA Novel Writing System | |
</h1> | |
<h3 style="color: #ccc; margin-bottom: 20px;"> | |
AI Collaborative Novel Generation - 50 Page Novella Creator | |
</h3> | |
<p style="font-size: 1.1em; color: #ddd; max-width: 800px; margin: 0 auto;"> | |
Enter a theme or prompt, and watch as 13 AI agents collaborate to create a complete 50-page novella. | |
The system includes 1 Director, 1 Critic, and 10 Writers working in harmony. | |
All progress is automatically saved and can be resumed anytime. | |
</p> | |
</div> | |
""") | |
# State management | |
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="Enter your novel theme or initial idea...\nμμ€μ μ£Όμ λ μ΄κΈ° μμ΄λμ΄λ₯Ό μ λ ₯νμΈμ...", | |
lines=4 | |
) | |
language_select = gr.Radio( | |
choices=["English", "Korean"], | |
value="English", | |
label="Language / μΈμ΄" | |
) | |
with gr.Row(): | |
submit_btn = gr.Button("π Start Writing / μμ± μμ", variant="primary", scale=2) | |
clear_btn = gr.Button("ποΈ Clear / μ΄κΈ°ν", scale=1) | |
status_text = gr.Textbox( | |
label="Status", | |
interactive=False, | |
value="π Ready" | |
) | |
# Session management | |
with gr.Group(elem_classes=["session-section"]): | |
gr.Markdown("### πΎ Resume Previous Session / μ΄μ μΈμ μ¬κ°") | |
session_dropdown = gr.Dropdown( | |
label="Select Session / μΈμ μ ν", | |
choices=[], | |
interactive=True | |
) | |
with gr.Row(): | |
refresh_btn = gr.Button("π Refresh / μλ‘κ³ μΉ¨", scale=1) | |
resume_btn = gr.Button("βΆοΈ Resume / μ¬κ°", variant="secondary", scale=1) | |
with gr.Column(scale=2): | |
with gr.Tab("π Writing Process / μμ± κ³Όμ "): | |
stages_display = gr.Markdown( | |
value="Process will appear here...", | |
elem_id="stages-display" | |
) | |
with gr.Tab("π Final Novel / μ΅μ’ μμ€"): | |
novel_output = gr.Markdown( | |
value="", | |
elem_id="novel-output" | |
) | |
with gr.Group(elem_classes=["download-section"]): | |
gr.Markdown("### π₯ Download Novel / μμ€ λ€μ΄λ‘λ") | |
with gr.Row(): | |
format_select = gr.Radio( | |
choices=["DOCX", "TXT"], | |
value="DOCX" if DOCX_AVAILABLE else "TXT", | |
label="Format / νμ" | |
) | |
download_btn = gr.Button("β¬οΈ Download / λ€μ΄λ‘λ", variant="secondary") | |
download_file = gr.File( | |
label="Downloaded File / λ€μ΄λ‘λλ νμΌ", | |
visible=False | |
) | |
# Hidden state for novel text | |
novel_text_state = gr.State("") | |
# Examples | |
with gr.Row(): | |
gr.Examples( | |
examples=[ | |
["A scientist discovers a portal to parallel universes but each journey erases a memory"], | |
["In a world where dreams can be traded, a dream thief must steal the emperor's nightmare"], | |
["Two AI entities fall in love while trying to prevent a global cyber war"], | |
["λ―Έλ λμμμ κΈ°μ΅μ κ±°λνλ μμΈκ³Ό λͺ¨λ κΈ°μ΅μ μμ νμ μ μ΄μΌκΈ°"], | |
["μκ°μ΄ κ±°κΎΈλ‘ νλ₯΄λ λ§μμμ μΌμ΄λλ λ―Έμ€ν°λ¦¬ν μ΄μΈ μ¬κ±΄"], | |
["μ± μμΌλ‘ λ€μ΄κ° μ μλ λ₯λ ₯μ κ°μ§ μ¬μμ λͺ¨ν"] | |
], | |
inputs=query_input, | |
label="π‘ Example Themes / μμ μ£Όμ " | |
) | |
# Event handlers | |
def update_novel_state(stages, novel, status): | |
return stages, novel, status, novel | |
def refresh_sessions(): | |
try: | |
sessions = get_active_sessions("English") | |
return gr.update(choices=sessions) | |
except Exception as e: | |
logger.error(f"Error refreshing sessions: {str(e)}") | |
return gr.update(choices=[]) | |
submit_btn.click( | |
fn=process_query, | |
inputs=[query_input, language_select, current_session_id], | |
outputs=[stages_display, novel_output, status_text] | |
).then( | |
fn=update_novel_state, | |
inputs=[stages_display, novel_output, status_text], | |
outputs=[stages_display, novel_output, status_text, 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] | |
) | |
refresh_btn.click( | |
fn=refresh_sessions, | |
outputs=[session_dropdown] | |
) | |
clear_btn.click( | |
fn=lambda: ("", "", "π Ready", "", None), | |
outputs=[stages_display, novel_output, status_text, novel_text_state, current_session_id] | |
) | |
def handle_download(novel_text, format_type, language): | |
if not novel_text: | |
return gr.update(visible=False) | |
file_path = download_novel(novel_text, format_type, language) | |
if file_path: | |
return gr.update(value=file_path, visible=True) | |
else: | |
return gr.update(visible=False) | |
download_btn.click( | |
fn=handle_download, | |
inputs=[novel_text_state, format_select, language_select], | |
outputs=[download_file] | |
) | |
# Load sessions on startup | |
interface.load( | |
fn=refresh_sessions, | |
outputs=[session_dropdown] | |
) | |
return interface | |
# Main execution | |
if __name__ == "__main__": | |
logger.info("Starting SOMA Novel Writing System...") | |
# Initialize database on startup | |
logger.info("Initializing database...") | |
NovelDatabase.init_db() | |
logger.info("Database initialized successfully.") | |
interface = create_interface() | |
interface.launch( | |
server_name="0.0.0.0", | |
server_port=7860, | |
share=False, | |
debug=True | |
) |