openfree's picture
Update app.py
12f739f verified
raw
history blame
70.7 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
import random
import traceback
# --- ๋กœ๊น… ์„ค์ • ---
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
# --- ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋ฐ ์ƒ์ˆ˜ ---
FIREWORKS_API_KEY = os.getenv("FIREWORKS_API_KEY", "")
BRAVE_SEARCH_API_KEY = os.getenv("BRAVE_SEARCH_API_KEY", "")
API_URL = "https://api.fireworks.ai/inference/v1/chat/completions"
MODEL_ID = "accounts/fireworks/models/qwen3-235b-a22b-instruct-2507"
BRAVE_SEARCH_URL = "https://api.search.brave.com/res/v1/web/search"
DB_PATH = "screenplay_sessions_korean.db"
# ์‹œ๋‚˜๋ฆฌ์˜ค ๊ธธ์ด ์„ค์ •
SCREENPLAY_LENGTHS = {
"์˜ํ™”": {"pages": 120, "description": "์žฅํŽธ ์˜ํ™” (110-130ํŽ˜์ด์ง€)", "min_pages": 110},
"๋“œ๋ผ๋งˆ": {"pages": 60, "description": "TV ๋“œ๋ผ๋งˆ (55-65ํŽ˜์ด์ง€)", "min_pages": 55},
"์›น๋“œ๋ผ๋งˆ": {"pages": 50, "description": "์›น/OTT ์‹œ๋ฆฌ์ฆˆ (45-55ํŽ˜์ด์ง€)", "min_pages": 45},
"๋‹จํŽธ": {"pages": 20, "description": "๋‹จํŽธ ์˜ํ™” (15-25ํŽ˜์ด์ง€)", "min_pages": 15}
}
# ํ™˜๊ฒฝ ๊ฒ€์ฆ
if not FIREWORKS_API_KEY:
logger.error("FIREWORKS_API_KEY๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.")
FIREWORKS_API_KEY = "dummy_token_for_testing"
# ๊ธ€๋กœ๋ฒŒ ๋ณ€์ˆ˜
db_lock = threading.Lock()
# ์ „๋ฌธ๊ฐ€ ์—ญํ•  ์ •์˜
EXPERT_ROLES = {
"ํ”„๋กœ๋“€์„œ": {
"emoji": "๐ŸŽฌ",
"description": "์ƒ์—…์„ฑ๊ณผ ์‹œ์žฅ์„ฑ ๋ถ„์„",
"focus": ["ํƒ€๊ฒŸ ๊ด€๊ฐ", "์ œ์ž‘ ๊ฐ€๋Šฅ์„ฑ", "์˜ˆ์‚ฐ ๊ทœ๋ชจ", "๋งˆ์ผ€ํŒ… ํฌ์ธํŠธ"],
"personality": "์‹ค์šฉ์ ์ด๊ณ  ์‹œ์žฅ ์ง€ํ–ฅ์ "
},
"์Šคํ† ๋ฆฌ์ž‘๊ฐ€": {
"emoji": "๐Ÿ“–",
"description": "๋‚ด๋Ÿฌํ‹ฐ๋ธŒ ๊ตฌ์กฐ์™€ ํ”Œ๋กฏ ๊ฐœ๋ฐœ",
"focus": ["3๋ง‰ ๊ตฌ์กฐ", "ํ”Œ๋กฏ ํฌ์ธํŠธ", "์„œ์‚ฌ ์•„ํฌ", "ํ…Œ๋งˆ"],
"personality": "์ฐฝ์˜์ ์ด๊ณ  ๊ตฌ์กฐ์ "
},
"์บ๋ฆญํ„ฐ๋””์ž์ด๋„ˆ": {
"emoji": "๐Ÿ‘ฅ",
"description": "์ธ๋ฌผ ์ฐฝ์กฐ์™€ ๊ด€๊ณ„ ์„ค๊ณ„",
"focus": ["์บ๋ฆญํ„ฐ ์•„ํฌ", "๋™๊ธฐ๋ถ€์—ฌ", "๊ด€๊ณ„ ์—ญํ•™", "๋Œ€ํ™” ์Šคํƒ€์ผ"],
"personality": "์‹ฌ๋ฆฌํ•™์ ์ด๊ณ  ๊ณต๊ฐ์ "
},
"๊ฐ๋…": {
"emoji": "๐ŸŽญ",
"description": "๋น„์ฃผ์–ผ ์Šคํ† ๋ฆฌํ…”๋ง๊ณผ ์—ฐ์ถœ",
"focus": ["์‹œ๊ฐ์  ๊ตฌ์„ฑ", "์นด๋ฉ”๋ผ ์›Œํฌ", "๋ฏธ์žฅ์„ผ", "๋ฆฌ๋“ฌ๊ณผ ํŽ˜์ด์‹ฑ"],
"personality": "๋น„์ฃผ์–ผ ์ค‘์‹ฌ์ ์ด๊ณ  ์˜ˆ์ˆ ์ "
},
"๊ณ ์ฆ์ „๋ฌธ๊ฐ€": {
"emoji": "๐Ÿ”Ž",
"description": "์‚ฌ์‹ค ํ™•์ธ๊ณผ ๊ณ ์ฆ",
"focus": ["์—ญ์‚ฌ์  ์ •ํ™•์„ฑ", "๊ณผํ•™์  ํƒ€๋‹น์„ฑ", "๋ฌธํ™”์  ์ ์ ˆ์„ฑ", "ํ˜„์‹ค์„ฑ"],
"personality": "์ •ํ™•ํ•˜๊ณ  ์„ธ์‹ฌํ•œ"
},
"๋น„ํ‰๊ฐ€": {
"emoji": "๐Ÿ”",
"description": "๊ฐ๊ด€์  ๋ถ„์„๊ณผ ๊ฐœ์„ ์  ์ œ์‹œ",
"focus": ["๋…ผ๋ฆฌ์  ์ผ๊ด€์„ฑ", "๊ฐ์ •์  ์ž„ํŒฉํŠธ", "์›์ž‘ ์ถฉ์‹ค๋„", "์™„์„ฑ๋„"],
"personality": "๋ถ„์„์ ์ด๊ณ  ๋น„ํŒ์ "
},
"ํŽธ์ง‘์ž": {
"emoji": "โœ‚๏ธ",
"description": "ํŽ˜์ด์‹ฑ๊ณผ ๊ตฌ์กฐ ์ตœ์ ํ™”",
"focus": ["์”ฌ ์ „ํ™˜", "๋ฆฌ๋“ฌ", "๊ธด์žฅ๊ฐ ์กฐ์ ˆ", "๋ถˆํ•„์š”ํ•œ ๋ถ€๋ถ„ ์ œ๊ฑฐ"],
"personality": "์ •๋ฐ€ํ•˜๊ณ  ํšจ์œจ์ "
},
"๋Œ€ํ™”์ „๋ฌธ๊ฐ€": {
"emoji": "๐Ÿ’ฌ",
"description": "๋Œ€์‚ฌ์™€ ์„œ๋ธŒํ…์ŠคํŠธ ๊ฐ•ํ™”",
"focus": ["์ž์—ฐ์Šค๋Ÿฌ์šด ๋Œ€ํ™”", "์บ๋ฆญํ„ฐ ๋ณด์ด์Šค", "์„œ๋ธŒํ…์ŠคํŠธ", "๊ฐ์ • ์ „๋‹ฌ"],
"personality": "์–ธ์–ด์ ์ด๊ณ  ๋‰˜์•™์Šค ์ค‘์‹ฌ"
}
}
# ๊ธฐํš ๋‹จ๊ณ„ ์ „๋ฌธ๊ฐ€
PLANNING_STAGES = [
("ํ”„๋กœ๋“€์„œ", "producer", "๐ŸŽฌ ํ”„๋กœ๋“€์„œ: ํ•ต์‹ฌ ์ปจ์…‰ ๋ฐ ์‹œ์žฅ์„ฑ ๋ถ„์„"),
("์Šคํ† ๋ฆฌ์ž‘๊ฐ€", "story_writer", "๐Ÿ“– ์Šคํ† ๋ฆฌ ์ž‘๊ฐ€: ์‹œ๋†‰์‹œ์Šค ๋ฐ 3๋ง‰ ๊ตฌ์กฐ"),
("์บ๋ฆญํ„ฐ๋””์ž์ด๋„ˆ", "character_designer", "๐Ÿ‘ฅ ์บ๋ฆญํ„ฐ ๋””์ž์ด๋„ˆ: ์ธ๋ฌผ ํ”„๋กœํ•„ ๋ฐ ๊ด€๊ณ„๋„"),
("๊ฐ๋…", "director", "๐ŸŽญ ๊ฐ๋…: ๋น„์ฃผ์–ผ ์ปจ์…‰ ๋ฐ ์—ฐ์ถœ ๋ฐฉํ–ฅ"),
("๋น„ํ‰๊ฐ€", "critic", "๐Ÿ” ๋น„ํ‰๊ฐ€: ๊ธฐํš์•ˆ ์ข…ํ•ฉ ๊ฒ€ํ†  ๋ฐ ๊ฐœ์„ ์ "),
]
# ๋ง‰๋ณ„ ์ž‘์„ฑ ๋‹จ๊ณ„
ACT_WRITING_STAGES = {
"1๋ง‰": [
("์Šคํ† ๋ฆฌ์ž‘๊ฐ€", "์ดˆ๊ณ ", "โœ๏ธ ์Šคํ† ๋ฆฌ์ž‘๊ฐ€: 1๋ง‰ ์ดˆ๊ณ  ์ž‘์„ฑ"),
("๊ณ ์ฆ์ „๋ฌธ๊ฐ€", "๊ฒ€์ฆ", "๐Ÿ”Ž ๊ณ ์ฆ์ „๋ฌธ๊ฐ€: ์‚ฌ์‹ค ํ™•์ธ ๋ฐ ๊ฒ€์ฆ"),
("ํŽธ์ง‘์ž", "ํŽธ์ง‘", "โœ‚๏ธ ํŽธ์ง‘์ž: ๊ตฌ์กฐ ๋ฐ ํŽ˜์ด์‹ฑ ์กฐ์ •"),
("๊ฐ๋…", "์—ฐ์ถœ", "๐ŸŽญ ๊ฐ๋…: ๋น„์ฃผ์–ผ ๊ฐ•ํ™” ๋ฐ ์—ฐ์ถœ ๋…ธํŠธ"),
("๋Œ€ํ™”์ „๋ฌธ๊ฐ€", "๋Œ€์‚ฌ", "๐Ÿ’ฌ ๋Œ€ํ™”์ „๋ฌธ๊ฐ€: ๋Œ€์‚ฌ ๊ฐœ์„ "),
("๋น„ํ‰๊ฐ€", "๊ฒ€ํ† ", "๐Ÿ” ๋น„ํ‰๊ฐ€: ์ข…ํ•ฉ ๊ฒ€ํ†  ๋ฐ ํ‰๊ฐ€"),
("์Šคํ† ๋ฆฌ์ž‘๊ฐ€", "์™„์„ฑ", "โœ… ์Šคํ† ๋ฆฌ์ž‘๊ฐ€: 1๋ง‰ ์ตœ์ข… ์™„์„ฑ")
],
"2๋ง‰A": [
("์Šคํ† ๋ฆฌ์ž‘๊ฐ€", "์ดˆ๊ณ ", "โœ๏ธ ์Šคํ† ๋ฆฌ์ž‘๊ฐ€: 2๋ง‰A ์ดˆ๊ณ  ์ž‘์„ฑ"),
("๊ณ ์ฆ์ „๋ฌธ๊ฐ€", "๊ฒ€์ฆ", "๐Ÿ”Ž ๊ณ ์ฆ์ „๋ฌธ๊ฐ€: ์‚ฌ์‹ค ํ™•์ธ ๋ฐ ๊ฒ€์ฆ"),
("ํŽธ์ง‘์ž", "ํŽธ์ง‘", "โœ‚๏ธ ํŽธ์ง‘์ž: ๊ตฌ์กฐ ๋ฐ ํŽ˜์ด์‹ฑ ์กฐ์ •"),
("๊ฐ๋…", "์—ฐ์ถœ", "๐ŸŽญ ๊ฐ๋…: ๋น„์ฃผ์–ผ ๊ฐ•ํ™” ๋ฐ ์—ฐ์ถœ ๋…ธํŠธ"),
("๋Œ€ํ™”์ „๋ฌธ๊ฐ€", "๋Œ€์‚ฌ", "๐Ÿ’ฌ ๋Œ€ํ™”์ „๋ฌธ๊ฐ€: ๋Œ€์‚ฌ ๊ฐœ์„ "),
("๋น„ํ‰๊ฐ€", "๊ฒ€ํ† ", "๐Ÿ” ๋น„ํ‰๊ฐ€: ์ข…ํ•ฉ ๊ฒ€ํ†  ๋ฐ ํ‰๊ฐ€"),
("์Šคํ† ๋ฆฌ์ž‘๊ฐ€", "์™„์„ฑ", "โœ… ์Šคํ† ๋ฆฌ์ž‘๊ฐ€: 2๋ง‰A ์ตœ์ข… ์™„์„ฑ")
],
"2๋ง‰B": [
("์Šคํ† ๋ฆฌ์ž‘๊ฐ€", "์ดˆ๊ณ ", "โœ๏ธ ์Šคํ† ๋ฆฌ์ž‘๊ฐ€: 2๋ง‰B ์ดˆ๊ณ  ์ž‘์„ฑ"),
("๊ณ ์ฆ์ „๋ฌธ๊ฐ€", "๊ฒ€์ฆ", "๐Ÿ”Ž ๊ณ ์ฆ์ „๋ฌธ๊ฐ€: ์‚ฌ์‹ค ํ™•์ธ ๋ฐ ๊ฒ€์ฆ"),
("ํŽธ์ง‘์ž", "ํŽธ์ง‘", "โœ‚๏ธ ํŽธ์ง‘์ž: ๊ตฌ์กฐ ๋ฐ ํŽ˜์ด์‹ฑ ์กฐ์ •"),
("๊ฐ๋…", "์—ฐ์ถœ", "๐ŸŽญ ๊ฐ๋…: ๋น„์ฃผ์–ผ ๊ฐ•ํ™” ๋ฐ ์—ฐ์ถœ ๋…ธํŠธ"),
("๋Œ€ํ™”์ „๋ฌธ๊ฐ€", "๋Œ€์‚ฌ", "๐Ÿ’ฌ ๋Œ€ํ™”์ „๋ฌธ๊ฐ€: ๋Œ€์‚ฌ ๊ฐœ์„ "),
("๋น„ํ‰๊ฐ€", "๊ฒ€ํ† ", "๐Ÿ” ๋น„ํ‰๊ฐ€: ์ข…ํ•ฉ ๊ฒ€ํ†  ๋ฐ ํ‰๊ฐ€"),
("์Šคํ† ๋ฆฌ์ž‘๊ฐ€", "์™„์„ฑ", "โœ… ์Šคํ† ๋ฆฌ์ž‘๊ฐ€: 2๋ง‰B ์ตœ์ข… ์™„์„ฑ")
],
"3๋ง‰": [
("์Šคํ† ๋ฆฌ์ž‘๊ฐ€", "์ดˆ๊ณ ", "โœ๏ธ ์Šคํ† ๋ฆฌ์ž‘๊ฐ€: 3๋ง‰ ์ดˆ๊ณ  ์ž‘์„ฑ"),
("๊ณ ์ฆ์ „๋ฌธ๊ฐ€", "๊ฒ€์ฆ", "๐Ÿ”Ž ๊ณ ์ฆ์ „๋ฌธ๊ฐ€: ์‚ฌ์‹ค ํ™•์ธ ๋ฐ ๊ฒ€์ฆ"),
("ํŽธ์ง‘์ž", "ํŽธ์ง‘", "โœ‚๏ธ ํŽธ์ง‘์ž: ๊ตฌ์กฐ ๋ฐ ํŽ˜์ด์‹ฑ ์กฐ์ •"),
("๊ฐ๋…", "์—ฐ์ถœ", "๐ŸŽญ ๊ฐ๋…: ๋น„์ฃผ์–ผ ๊ฐ•ํ™” ๋ฐ ์—ฐ์ถœ ๋…ธํŠธ"),
("๋Œ€ํ™”์ „๋ฌธ๊ฐ€", "๋Œ€์‚ฌ", "๐Ÿ’ฌ ๋Œ€ํ™”์ „๋ฌธ๊ฐ€: ๋Œ€์‚ฌ ๊ฐœ์„ "),
("๋น„ํ‰๊ฐ€", "๊ฒ€ํ† ", "๐Ÿ” ๋น„ํ‰๊ฐ€: ์ข…ํ•ฉ ๊ฒ€ํ†  ๋ฐ ํ‰๊ฐ€"),
("์Šคํ† ๋ฆฌ์ž‘๊ฐ€", "์™„์„ฑ", "โœ… ์Šคํ† ๋ฆฌ์ž‘๊ฐ€: 3๋ง‰ ์ตœ์ข… ์™„์„ฑ")
]
}
# ์›น ๊ฒ€์ƒ‰ ํด๋ž˜์Šค
class WebSearcher:
"""์‚ฌ์‹ค ํ™•์ธ๊ณผ ๊ณ ์ฆ์„ ์œ„ํ•œ ์›น ๊ฒ€์ƒ‰"""
def __init__(self):
self.api_key = BRAVE_SEARCH_API_KEY
self.enabled = bool(self.api_key)
def search(self, query: str, count: int = 3) -> List[Dict]:
"""์›น ๊ฒ€์ƒ‰ ์ˆ˜ํ–‰"""
if not self.enabled:
return []
headers = {
"Accept": "application/json",
"X-Subscription-Token": self.api_key
}
params = {
"q": query,
"count": count,
"search_lang": "ko",
"safesearch": "moderate"
}
try:
response = requests.get(BRAVE_SEARCH_URL, headers=headers, params=params, timeout=10)
if response.status_code == 200:
results = response.json().get("web", {}).get("results", [])
return results
else:
logger.error(f"Search API error: {response.status_code}")
return []
except Exception as e:
logger.error(f"Search error: {e}")
return []
def verify_facts(self, content: str, context: str) -> Dict:
"""๋‚ด์šฉ์˜ ์‚ฌ์‹ค ํ™•์ธ"""
verification_results = {
"verified": [],
"needs_correction": [],
"suggestions": []
}
# ์ฃผ์š” ํ‚ค์›Œ๋“œ ์ถ”์ถœ
keywords = self._extract_keywords(content)
for keyword in keywords[:3]: # ์ƒ์œ„ 3๊ฐœ๋งŒ ๊ฒ€์ƒ‰
search_query = f"{keyword} {context} ์‚ฌ์‹ค ํ™•์ธ"
results = self.search(search_query, count=2)
if results:
verification_results["verified"].append({
"keyword": keyword,
"sources": [r["title"] for r in results]
})
return verification_results
def _extract_keywords(self, content: str) -> List[str]:
"""์ฃผ์š” ํ‚ค์›Œ๋“œ ์ถ”์ถœ"""
# ๊ฐ„๋‹จํ•œ ํ‚ค์›Œ๋“œ ์ถ”์ถœ (์‹ค์ œ๋กœ๋Š” ๋” ์ •๊ตํ•œ ๋ฐฉ๋ฒ• ํ•„์š”)
words = content.split()
keywords = [w for w in words if len(w) > 4][:5]
return keywords
# ๋‹ค์ด์–ด๊ทธ๋žจ ์ƒ์„ฑ ํด๋ž˜์Šค
class DiagramGenerator:
"""์บ๋ฆญํ„ฐ ๊ด€๊ณ„๋„์™€ ๊ฐˆ๋“ฑ ๊ตฌ์กฐ ๋‹ค์ด์–ด๊ทธ๋žจ ์ƒ์„ฑ"""
@staticmethod
def create_character_relationship_diagram(characters: Dict) -> str:
"""์บ๋ฆญํ„ฐ ๊ด€๊ณ„๋„ ์ƒ์„ฑ (Mermaid ํ˜•์‹)"""
diagram = """
```mermaid
graph TB
classDef protagonist fill:#f9d71c,stroke:#333,stroke-width:2px
classDef antagonist fill:#ff6b6b,stroke:#333,stroke-width:2px
classDef supporting fill:#95e1d3,stroke:#333,stroke-width:2px
"""
# ์ฃผ์ธ๊ณต ๋…ธ๋“œ
if "์ฃผ์ธ๊ณต" in characters:
protagonist = characters["์ฃผ์ธ๊ณต"]
diagram += f"\n P[์ฃผ์ธ๊ณต<br/>{protagonist.get('name', '๋ฏธ์ •')}]:::protagonist"
# ์ ๋Œ€์ž ๋…ธ๋“œ
if "์ ๋Œ€์ž" in characters:
antagonist = characters["์ ๋Œ€์ž"]
diagram += f"\n A[์ ๋Œ€์ž<br/>{antagonist.get('name', '๋ฏธ์ •')}]:::antagonist"
diagram += f"\n P -.๊ฐˆ๋“ฑ.- A"
# ์กฐ์—ฐ ๋…ธ๋“œ๋“ค
supporting_count = 0
for key, char in characters.items():
if key not in ["์ฃผ์ธ๊ณต", "์ ๋Œ€์ž"] and supporting_count < 5:
supporting_count += 1
char_id = f"S{supporting_count}"
diagram += f"\n {char_id}[{key}<br/>{char.get('name', '๋ฏธ์ •')}]:::supporting"
# ๊ด€๊ณ„ ์—ฐ๊ฒฐ
if char.get("relationship_to_protagonist"):
diagram += f"\n P --{char['relationship_to_protagonist']}--> {char_id}"
diagram += "\n```"
return diagram
@staticmethod
def create_conflict_structure_diagram(conflicts: List[Dict]) -> str:
"""๊ฐˆ๋“ฑ ๊ตฌ์กฐ ๋‹ค์ด์–ด๊ทธ๋žจ ์ƒ์„ฑ"""
diagram = """
```mermaid
flowchart LR
classDef conflict fill:#ff9999,stroke:#333,stroke-width:2px
classDef resolution fill:#99ff99,stroke:#333,stroke-width:2px
Start([์‹œ์ž‘]) --> C1
"""
for i, conflict in enumerate(conflicts[:5], 1):
conflict_name = conflict.get("name", f"๊ฐˆ๋“ฑ{i}")
resolution = conflict.get("resolution", "ํ•ด๊ฒฐ")
diagram += f"\n C{i}[{conflict_name}]:::conflict"
if i < len(conflicts):
diagram += f" --> C{i+1}"
else:
diagram += f" --> End([{resolution}]):::resolution"
diagram += "\n```"
return diagram
@staticmethod
def create_story_arc_diagram(acts: Dict) -> str:
"""์Šคํ† ๋ฆฌ ์•„ํฌ ๋‹ค์ด์–ด๊ทธ๋žจ ์ƒ์„ฑ"""
diagram = """
```mermaid
graph LR
subgraph "1๋ง‰ - ์„ค์ •"
A1[์ผ์ƒ] --> A2[์‚ฌ๊ฑด ๋ฐœ์ƒ]
A2 --> A3[์ƒˆ๋กœ์šด ์„ธ๊ณ„]
end
subgraph "2๋ง‰A - ์ƒ์Šน"
B1[๋„์ „] --> B2[์„ฑ์žฅ]
B2 --> B3[์ค‘๊ฐ„์ ]
end
subgraph "2๋ง‰B - ํ•˜๊ฐ•"
C1[์œ„๊ธฐ] --> C2[์ ˆ๋ง]
C2 --> C3[๊ฐ์„ฑ]
end
subgraph "3๋ง‰ - ํ•ด๊ฒฐ"
D1[์ตœ์ข… ๋Œ€๊ฒฐ] --> D2[ํด๋ผ์ด๋งฅ์Šค]
D2 --> D3[์ƒˆ๋กœ์šด ์ผ์ƒ]
end
A3 --> B1
B3 --> C1
C3 --> D1
style A1 fill:#e1f5fe
style D3 fill:#c8e6c9
```
"""
return diagram
# ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค
@dataclass
class ExpertFeedback:
"""์ „๋ฌธ๊ฐ€ ํ”ผ๋“œ๋ฐฑ"""
role: str
stage: str
feedback: str
suggestions: List[str]
score: float
timestamp: datetime = field(default_factory=datetime.now)
@dataclass
class ActProgress:
"""๋ง‰๋ณ„ ์ง„ํ–‰ ์ƒํ™ฉ"""
act_name: str
current_stage: int
total_stages: int
current_expert: str
status: str # "ready", "in_progress", "complete"
content: str = ""
expert_feedbacks: List[ExpertFeedback] = field(default_factory=list)
@property
def progress_percentage(self):
if self.total_stages == 0:
return 0
return (self.current_stage / self.total_stages) * 100
# ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํด๋ž˜์Šค
class ScreenplayDatabase:
@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 screenplay_sessions (
session_id TEXT PRIMARY KEY,
user_query TEXT NOT NULL,
screenplay_type TEXT NOT NULL,
genre TEXT NOT NULL,
target_pages INTEGER,
title TEXT,
logline TEXT,
planning_data TEXT,
character_diagram TEXT,
conflict_diagram TEXT,
act1_content TEXT,
act2a_content TEXT,
act2b_content TEXT,
act3_content TEXT,
expert_feedbacks TEXT,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now')),
status TEXT DEFAULT 'planning',
current_act TEXT DEFAULT '1๋ง‰',
total_pages REAL DEFAULT 0
)
''')
cursor.execute('''
CREATE TABLE IF NOT EXISTS act_progress (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL,
act_name TEXT NOT NULL,
stage_num INTEGER,
expert_role TEXT,
content TEXT,
feedback TEXT,
score REAL,
created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (session_id) REFERENCES screenplay_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, screenplay_type: str, genre: str) -> str:
session_id = hashlib.md5(f"{user_query}{screenplay_type}{datetime.now()}".encode()).hexdigest()
target_pages = SCREENPLAY_LENGTHS[screenplay_type]["pages"]
with ScreenplayDatabase.get_db() as conn:
conn.cursor().execute(
'''INSERT INTO screenplay_sessions
(session_id, user_query, screenplay_type, genre, target_pages)
VALUES (?, ?, ?, ?, ?)''',
(session_id, user_query, screenplay_type, genre, target_pages)
)
conn.commit()
return session_id
@staticmethod
def save_planning_data(session_id: str, planning_data: Dict):
with ScreenplayDatabase.get_db() as conn:
conn.cursor().execute(
'''UPDATE screenplay_sessions
SET planning_data = ?, status = 'planned', updated_at = datetime('now')
WHERE session_id = ?''',
(json.dumps(planning_data, ensure_ascii=False), session_id)
)
conn.commit()
@staticmethod
def save_diagrams(session_id: str, character_diagram: str, conflict_diagram: str):
"""๋‹ค์ด์–ด๊ทธ๋žจ ์ €์žฅ"""
with ScreenplayDatabase.get_db() as conn:
conn.cursor().execute(
'''UPDATE screenplay_sessions
SET character_diagram = ?, conflict_diagram = ?, updated_at = datetime('now')
WHERE session_id = ?''',
(character_diagram, conflict_diagram, session_id)
)
conn.commit()
@staticmethod
def save_act_content(session_id: str, act_name: str, content: str):
"""๋ง‰๋ณ„ ์ฝ˜ํ…์ธ  ์ €์žฅ"""
act_column = {
"1๋ง‰": "act1_content",
"2๋ง‰A": "act2a_content",
"2๋ง‰B": "act2b_content",
"3๋ง‰": "act3_content"
}.get(act_name, "act1_content")
with ScreenplayDatabase.get_db() as conn:
conn.cursor().execute(
f'''UPDATE screenplay_sessions
SET {act_column} = ?, current_act = ?, updated_at = datetime('now')
WHERE session_id = ?''',
(content, act_name, session_id)
)
conn.commit()
# ์‹œ๋‚˜๋ฆฌ์˜ค ์ƒ์„ฑ ์‹œ์Šคํ…œ
class ScreenplayGenerationSystem:
def __init__(self):
self.api_key = FIREWORKS_API_KEY
self.api_url = API_URL
self.model_id = MODEL_ID
self.current_session_id = None
self.original_query = ""
self.planning_data = {}
self.web_searcher = WebSearcher()
self.diagram_generator = DiagramGenerator()
self.expert_feedbacks = []
ScreenplayDatabase.init_db()
def create_headers(self):
if not self.api_key or self.api_key == "dummy_token_for_testing":
raise ValueError("์œ ํšจํ•œ FIREWORKS_API_KEY๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค")
return {
"Accept": "application/json",
"Content-Type": "application/json",
"Authorization": f"Bearer {self.api_key}"
}
def call_llm_streaming(self, messages: List[Dict[str, str]], max_tokens: int = 8000) -> Generator[str, None, None]:
"""LLM ํ˜ธ์ถœ - ๋ฒ„ํผ ์ฒ˜๋ฆฌ ๊ฐœ์„ """
try:
# ์‹œ์Šคํ…œ ๋ฉ”์‹œ์ง€ ๊ฐ•ํ™”
if messages and messages[0].get("role") == "system":
messages[0]["content"] = messages[0]["content"] + """
ใ€์ ˆ๋Œ€ ์ค€์ˆ˜ ์‚ฌํ•ญใ€‘
1. ๋ชจ๋“  ์‘๋‹ต์„ ์™„์ „ํ•œ ํ•œ๊ตญ์–ด๋กœ ์ž‘์„ฑํ•˜์„ธ์š”.
2. ์‹œ๋‚˜๋ฆฌ์˜ค ํฌ๋งท ์šฉ์–ด(INT. EXT. CUT TO ๋“ฑ)๋Š” ์˜์–ด๋กœ ์œ ์ง€ํ•˜์„ธ์š”.
3. ์บ๋ฆญํ„ฐ๋ช…๊ณผ ๋Œ€์‚ฌ๋Š” ํ•œ๊ตญ์–ด๋กœ ์ž‘์„ฑํ•˜์„ธ์š”.
4. ๋ฏธ์™„์„ฑ ๋ฌธ์žฅ(...) ์—†์ด ์™„์ „ํ•˜๊ฒŒ ์ž‘์„ฑํ•˜์„ธ์š”.
5. ์˜ํ™” ์‹œ๋‚˜๋ฆฌ์˜ค ํฌ๋งท์„ ์ •ํ™•ํžˆ ์ค€์ˆ˜ํ•˜์„ธ์š”.
"""
payload = {
"model": self.model_id,
"messages": messages,
"max_tokens": max_tokens,
"temperature": 0.7, # ์•ฝ๊ฐ„ ๋†’์ž„
"top_p": 0.9,
"top_k": 40,
"presence_penalty": 0.3,
"frequency_penalty": 0.3,
"stream": True
}
headers = self.create_headers()
response = requests.post(
self.api_url,
headers=headers,
json=payload,
stream=True,
timeout=300
)
if response.status_code != 200:
yield f"โŒ API ์˜ค๋ฅ˜: {response.status_code}"
return
buffer = ""
total_output = "" # ์ „์ฒด ์ถœ๋ ฅ ์ถ”์ 
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]":
# ๋‚จ์€ ๋ฒ„ํผ ๋ชจ๋‘ ์ถœ๋ ฅ
if buffer:
yield buffer
break
data = json.loads(data_str)
if "choices" in data and len(data["choices"]) > 0:
content = data["choices"][0].get("delta", {}).get("content", "")
if content:
buffer += content
total_output += content
# ๋ฒ„ํผ๊ฐ€ ์ถฉ๋ถ„ํžˆ ์Œ“์ด๋ฉด yield (ํฌ๊ธฐ ๋Š˜๋ฆผ)
if len(buffer) >= 500 or '\n\n' in buffer:
yield buffer
buffer = ""
except Exception as e:
logger.error(f"Line processing error: {e}")
continue
# ๋‚จ์€ ๋ฒ„ํผ ์ฒ˜๋ฆฌ
if buffer:
yield buffer
# ์ „์ฒด ์ถœ๋ ฅ ๊ธธ์ด ๋กœ๊น…
logger.info(f"Total LLM output length: {len(total_output)} characters, {len(total_output.split())} words")
except Exception as e:
logger.error(f"LLM streaming error: {e}")
yield f"โŒ ์˜ค๋ฅ˜: {str(e)}"
def generate_planning(self, query: str, screenplay_type: str, genre: str) -> Generator[Tuple[str, float, Dict, List, str], None, None]:
"""๊ธฐํš์•ˆ ์ƒ์„ฑ with ๋‹ค์ด์–ด๊ทธ๋žจ"""
try:
self.original_query = query
self.current_session_id = ScreenplayDatabase.create_session(query, screenplay_type, genre)
planning_content = {}
self.expert_feedbacks = []
total_stages = len(PLANNING_STAGES)
character_diagram = ""
conflict_diagram = ""
for idx, (role, stage_key, stage_desc) in enumerate(PLANNING_STAGES):
try:
progress = ((idx + 1) / total_stages) * 100
# ์ƒํƒœ ์—…๋ฐ์ดํŠธ
yield f"๐Ÿ”„ {stage_desc} ์ง„ํ–‰ ์ค‘...", progress, planning_content, self.expert_feedbacks, ""
# ์ „๋ฌธ๊ฐ€๋ณ„ ํ”„๋กฌํ”„ํŠธ ์ƒ์„ฑ
prompt = self._get_expert_prompt(role, stage_key, query, planning_content, genre, screenplay_type)
# LLM ํ˜ธ์ถœ
messages = [
{"role": "system", "content": f"๋‹น์‹ ์€ {role}์ž…๋‹ˆ๋‹ค. {EXPERT_ROLES[role]['description']}"},
{"role": "user", "content": prompt}
]
content = ""
for chunk in self.call_llm_streaming(messages):
if chunk and not chunk.startswith("โŒ"):
content += chunk
planning_content[f"{role}_{stage_key}"] = content
# ์ค‘๊ฐ„ ์ƒํƒœ ์—…๋ฐ์ดํŠธ
yield f"โœ๏ธ {stage_desc} ์ž‘์„ฑ ์ค‘...", progress, planning_content, self.expert_feedbacks, ""
# ์บ๋ฆญํ„ฐ ๋””์ž์ด๋„ˆ ๋‹จ๊ณ„์—์„œ ๋‹ค์ด์–ด๊ทธ๋žจ ์ƒ์„ฑ
if role == "์บ๋ฆญํ„ฐ๋””์ž์ด๋„ˆ" and content:
characters = self._extract_characters_from_content(content)
character_diagram = self.diagram_generator.create_character_relationship_diagram(characters)
conflicts = self._extract_conflicts_from_content(content)
conflict_diagram = self.diagram_generator.create_conflict_structure_diagram(conflicts)
# ๋‹ค์ด์–ด๊ทธ๋žจ ์ €์žฅ
ScreenplayDatabase.save_diagrams(self.current_session_id, character_diagram, conflict_diagram)
# ์ „๋ฌธ๊ฐ€ ํ”ผ๋“œ๋ฐฑ ์ƒ์„ฑ
feedback = ExpertFeedback(
role=role,
stage=stage_key,
feedback=content[:500] if content else "์ž‘์„ฑ ์ค‘...",
suggestions=self._extract_suggestions(content),
score=85.0 + random.uniform(-5, 10)
)
self.expert_feedbacks.append(feedback)
time.sleep(0.5) # API ์ œํ•œ ๊ณ ๋ ค
except GeneratorExit:
logger.warning(f"Generator exit at stage {idx}")
break
except Exception as stage_error:
logger.error(f"Error in stage {idx}: {stage_error}")
continue
# ์ตœ์ข… ์ €์žฅ
ScreenplayDatabase.save_planning_data(self.current_session_id, planning_content)
# ๋‹ค์ด์–ด๊ทธ๋žจ ํฌํ•จํ•œ ์ตœ์ข… ๊ฒฐ๊ณผ
final_diagrams = f"\n\n## ๐Ÿ“Š ์บ๋ฆญํ„ฐ ๊ด€๊ณ„๋„\n{character_diagram}\n\n## โš”๏ธ ๊ฐˆ๋“ฑ ๊ตฌ์กฐ\n{conflict_diagram}"
yield "โœ… ๊ธฐํš์•ˆ ์™„์„ฑ!", 100, planning_content, self.expert_feedbacks, final_diagrams
except Exception as e:
logger.error(f"Planning generation error: {e}\n{traceback.format_exc()}")
yield f"โŒ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}", 0, {}, [], ""
def _get_expert_prompt(self, role: str, stage: str, query: str,
previous_content: Dict, genre: str, screenplay_type: str) -> str:
"""๊ฐ ์ „๋ฌธ๊ฐ€๋ณ„ ํ”„๋กฌํ”„ํŠธ ์ƒ์„ฑ"""
expert = EXPERT_ROLES[role]
focus_areas = ", ".join(expert["focus"])
base_prompt = f"""
ใ€๋‹น์‹ ์˜ ์—ญํ• ใ€‘
๋‹น์‹ ์€ {role}์ž…๋‹ˆ๋‹ค. {expert['description']}
์ง‘์ค‘ ์˜์—ญ: {focus_areas}
ใ€์›๋ณธ ์š”์ฒญใ€‘
{query}
ใ€์žฅ๋ฅดใ€‘ {genre}
ใ€ํ˜•์‹ใ€‘ {screenplay_type}
"""
if role == "์บ๋ฆญํ„ฐ๋””์ž์ด๋„ˆ":
return base_prompt + """
ใ€์ž‘์„ฑ ์š”๊ตฌ์‚ฌํ•ญใ€‘
1. ์ฃผ์ธ๊ณต ํ”„๋กœํ•„
- ์ด๋ฆ„, ๋‚˜์ด, ์ง์—…
- ์„ฑ๊ฒฉ๊ณผ ํŠน์ง•
- ๋ชฉํ‘œ์™€ ๋™๊ธฐ
- ์„ฑ์žฅ ์•„ํฌ
2. ์ ๋Œ€์ž ํ”„๋กœํ•„
- ์ด๋ฆ„๊ณผ ์—ญํ• 
- ์ฃผ์ธ๊ณต๊ณผ์˜ ๊ฐˆ๋“ฑ
- ๋™๊ธฐ์™€ ๋ชฉํ‘œ
3. ์กฐ์—ฐ ์บ๋ฆญํ„ฐ๋“ค (3-5๋ช…)
- ๊ฐ์ž์˜ ์—ญํ• 
- ์ฃผ์ธ๊ณต๊ณผ์˜ ๊ด€๊ณ„
4. ์บ๋ฆญํ„ฐ ๊ด€๊ณ„ ์„ค๋ช…
- ์ธ๋ฌผ๊ฐ„ ๊ด€๊ณ„
- ์ฃผ์š” ๊ฐˆ๋“ฑ ๊ตฌ์กฐ
๊ตฌ์ฒด์ ์œผ๋กœ ์ž‘์„ฑํ•˜์„ธ์š”."""
# ๋‹ค๋ฅธ ์—ญํ• ๋“ค์˜ ํ”„๋กฌํ”„ํŠธ...
return base_prompt + "\n๊ตฌ์ฒด์ ์ด๊ณ  ์ „๋ฌธ์ ์ธ ๋ถ„์„์„ ์ œ๊ณตํ•˜์„ธ์š”."
def _extract_characters_from_content(self, content: str) -> Dict:
"""์ฝ˜ํ…์ธ ์—์„œ ์บ๋ฆญํ„ฐ ์ •๋ณด ์ถ”์ถœ"""
characters = {}
# ๊ฐ„๋‹จํ•œ ํŒŒ์‹ฑ (์‹ค์ œ๋กœ๋Š” ๋” ์ •๊ตํ•œ ๋ฐฉ๋ฒ• ํ•„์š”)
if "์ฃผ์ธ๊ณต" in content:
characters["์ฃผ์ธ๊ณต"] = {"name": "์ฃผ์ธ๊ณต", "role": "protagonist"}
if "์ ๋Œ€์ž" in content or "์•…์—ญ" in content:
characters["์ ๋Œ€์ž"] = {"name": "์ ๋Œ€์ž", "role": "antagonist"}
# ์กฐ์—ฐ ์ถ”์ถœ
for i in range(1, 4):
characters[f"์กฐ์—ฐ{i}"] = {"name": f"์กฐ์—ฐ{i}", "relationship_to_protagonist": "๋™๋ฃŒ"}
return characters
def _extract_conflicts_from_content(self, content: str) -> List[Dict]:
"""์ฝ˜ํ…์ธ ์—์„œ ๊ฐˆ๋“ฑ ๊ตฌ์กฐ ์ถ”์ถœ"""
conflicts = [
{"name": "์ดˆ๊ธฐ ๊ฐˆ๋“ฑ", "type": "external"},
{"name": "๋‚ด์  ๊ฐˆ๋“ฑ", "type": "internal"},
{"name": "๊ด€๊ณ„ ๊ฐˆ๋“ฑ", "type": "relationship"},
{"name": "์ตœ์ข… ๋Œ€๊ฒฐ", "type": "climax", "resolution": "ํ•ด๊ฒฐ"}
]
return conflicts
def _extract_suggestions(self, content: str) -> List[str]:
"""์ œ์•ˆ์‚ฌํ•ญ ์ถ”์ถœ"""
suggestions = []
if content:
lines = content.split('\n')
for line in lines:
if any(k in line for k in ['๊ฐœ์„ ', '์ œ์•ˆ', '์ถ”์ฒœ']):
suggestions.append(line.strip())
return suggestions[:5]
def generate_act(self, session_id: str, act_name: str, planning_data: Dict,
previous_acts: Dict) -> Generator[Tuple[ActProgress, str], None, None]:
"""๋ง‰๋ณ„ ์‹œ๋‚˜๋ฆฌ์˜ค ์ƒ์„ฑ - ๊ฐœ์„ ๋œ ๋ฒ„์ „"""
# act_progress๋ฅผ ๋จผ์ € ์ดˆ๊ธฐํ™”
act_progress = ActProgress(
act_name=act_name,
current_stage=0,
total_stages=0,
current_expert="",
status="initializing"
)
try:
self.current_session_id = session_id
self.planning_data = planning_data
# ์„ธ์…˜ ์ •๋ณด ๋ณต์›
session_data = ScreenplayGenerationSystem.get_session_data(session_id)
if session_data:
self.original_query = session_data.get('user_query', '')
stages = ACT_WRITING_STAGES.get(act_name, [])
# act_progress ์—…๋ฐ์ดํŠธ
act_progress.total_stages = len(stages)
act_progress.status = "in_progress"
act_content = ""
for idx, (role, stage_type, description) in enumerate(stages):
try:
act_progress.current_stage = idx + 1
act_progress.current_expert = role
# ์ง„ํ–‰ ์ƒํ™ฉ ์—…๋ฐ์ดํŠธ
yield act_progress, f"๐Ÿ”„ {description} ์ง„ํ–‰ ์ค‘..."
# ์—ญํ• ๋ณ„ ํ”„๋กฌํ”„ํŠธ ์ƒ์„ฑ
prompt = self._create_act_prompt(role, act_name, act_content, planning_data, previous_acts, stage_type)
# ํ•œ๊ตญ์–ด ๋ฐ ์˜ํ™” ํฌ๋งท ๊ฐ•์กฐ
system_message = f"""๋‹น์‹ ์€ {role}์ž…๋‹ˆ๋‹ค.
{EXPERT_ROLES[role]['description']}
ใ€์ ˆ๋Œ€ ๊ทœ์น™ใ€‘
1. ๋ชจ๋“  ๋‚ด์šฉ์„ ํ•œ๊ตญ์–ด๋กœ ์ž‘์„ฑํ•˜์„ธ์š”.
2. ์˜ํ™” ์‹œ๋‚˜๋ฆฌ์˜ค ํฌ๋งท์„ ์‚ฌ์šฉํ•˜์„ธ์š” (์—ฐ๊ทน ์•„๋‹˜).
3. ๊ธฐํš์•ˆ ๋‚ด์šฉ์„ 100% ๋ฐ˜์˜ํ•˜์„ธ์š”.
4. ์˜์–ด ์‚ฌ์šฉ ๊ธˆ์ง€ (INT. EXT. ๋“ฑ ํฌ๋งท ์šฉ์–ด ์ œ์™ธ).
5. ์™„์ „ํ•œ ๋ฌธ์žฅ์œผ๋กœ ์ž‘์„ฑ (... ์‚ฌ์šฉ ๊ธˆ์ง€)."""
messages = [
{"role": "system", "content": system_message},
{"role": "user", "content": prompt}
]
expert_output = ""
line_count = 0
for chunk in self.call_llm_streaming(messages, max_tokens=14000):
if chunk and not chunk.startswith("โŒ"):
# ์˜์–ด ํ•„ํ„ฐ๋ง ๋กœ์ง ์™„ํ™”
expert_output += chunk # _clean_output ํ˜ธ์ถœ ์ œ๊ฑฐ (๋‚˜์ค‘์— ํ•œ ๋ฒˆ๋งŒ)
line_count = len(expert_output.split('\n'))
# ์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ
yield act_progress, f"โœ๏ธ {role}: ์ž‘์—… ์ค‘... ({line_count}์ค„ ์ž‘์„ฑ)"
if stage_type in ["์ดˆ๊ณ ", "์™„์„ฑ"]:
expert_output = self._clean_output(expert_output) # ์—ฌ๊ธฐ์„œ๋งŒ ์ •๋ฆฌ
# ์›น ๊ฒ€์ƒ‰ ๊ณ ์ฆ
if role == "๊ณ ์ฆ์ „๋ฌธ๊ฐ€" and self.web_searcher.enabled:
verification = self.web_searcher.verify_facts(act_content, act_name)
expert_output += f"\n\nใ€์›น ๊ฒ€์ฆ ๊ฒฐ๊ณผใ€‘\n{json.dumps(verification, ensure_ascii=False, indent=2)}"
# ์ถœ๋ ฅ ๊ฒ€์ฆ ๋ฐ ์ •๋ฆฌ
if stage_type in ["์ดˆ๊ณ ", "์™„์„ฑ"]:
expert_output = self._validate_and_clean_screenplay(expert_output)
# ํ”ผ๋“œ๋ฐฑ ์ €์žฅ
feedback = ExpertFeedback(
role=role,
stage=f"{act_name}_{stage_type}",
feedback=f"{role} ์ž‘์—… ์™„๋ฃŒ. {line_count}์ค„ ์ž‘์„ฑ.",
suggestions=self._extract_suggestions(expert_output),
score=85.0 + random.uniform(-5, 10)
)
act_progress.expert_feedbacks.append(feedback)
# ์ตœ์ข… ์™„์„ฑ ๋‹จ๊ณ„์—์„œ๋งŒ ์ฝ˜ํ…์ธ  ์—…๋ฐ์ดํŠธ
if stage_type == "์™„์„ฑ":
act_content = expert_output
act_progress.content = act_content
elif stage_type == "์ดˆ๊ณ ":
act_content = expert_output
yield act_progress, f"โœ… {role} ์ž‘์—… ์™„๋ฃŒ ({line_count}์ค„)"
time.sleep(0.5)
except GeneratorExit:
logger.warning(f"Generator exit at act stage {idx}")
break
except Exception as e:
logger.error(f"Error in act stage {idx}: {e}")
yield act_progress, f"โš ๏ธ {role} ์ž‘์—… ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ"
continue
# ๋ง‰ ์™„์„ฑ
act_progress.status = "complete"
ScreenplayDatabase.save_act_content(session_id, act_name, act_content)
# ์ตœ์ข… ํŽ˜์ด์ง€ ์ˆ˜ ๊ณ„์‚ฐ
page_count = len(act_content.split('\n')) / 58
yield act_progress, f"โœ… {act_name} ์™„์„ฑ! (์•ฝ {page_count:.1f}ํŽ˜์ด์ง€)"
except Exception as e:
logger.error(f"Act generation error: {e}\n{traceback.format_exc()}")
act_progress.status = "error"
yield act_progress, f"โŒ ์˜ค๋ฅ˜: {str(e)}"
def _clean_output(self, text: str) -> str:
"""์ถœ๋ ฅ ํ…์ŠคํŠธ ์ •๋ฆฌ - ๊ฐœ์„ ๋œ ๋ฒ„์ „"""
# ๋นˆ ํ…์ŠคํŠธ ์ฒดํฌ
if not text:
return ""
lines = text.split('\n')
cleaned_lines = []
for line in lines:
# ์›๋ณธ ๋ผ์ธ ์œ ์ง€ (stripํ•˜์ง€ ์•Š์Œ)
original_line = line
stripped = line.strip()
# ๋นˆ ์ค„์€ ๊ทธ๋Œ€๋กœ ์œ ์ง€
if not stripped:
cleaned_lines.append(original_line)
continue
# ์‹œ๋‚˜๋ฆฌ์˜ค ํฌ๋งท ๋ผ์ธ์€ ๋ฌด์กฐ๊ฑด ์œ ์ง€
# INT., EXT., CUT TO:, FADE ๋“ฑ์ด ํฌํ•จ๋œ ๋ผ์ธ
format_keywords = ["INT.", "EXT.", "INT ", "EXT ", "CUT TO", "FADE",
"DISSOLVE", "MONTAGE", "FLASHBACK", "THE END"]
is_format_line = any(keyword in stripped.upper() for keyword in format_keywords)
if is_format_line:
# ํฌ๋งท ๋ผ์ธ์€ ๊ทธ๋Œ€๋กœ ์œ ์ง€
cleaned_lines.append(original_line)
else:
# ์˜์–ด ์ฒดํฌ (์ƒˆ๋กœ์šด ๋กœ์ง ์‚ฌ์šฉ)
if not self._contains_english(stripped):
# ๋ถˆ์™„์ „ํ•œ ... ์ œ๊ฑฐ
if "..." in original_line:
original_line = original_line.replace("...", ".")
cleaned_lines.append(original_line)
else:
# ์‹ค์ œ ์˜์–ด๊ฐ€ ํฌํ•จ๋œ ๊ฒฝ์šฐ๋งŒ ์ œ๊ฑฐ
logger.debug(f"Removing English line: {stripped[:50]}")
# ์ค‘์š”ํ•œ ๋Œ€์‚ฌ๋‚˜ ์ง€์‹œ๋ฌธ์ด ์•„๋‹Œ ๊ฒฝ์šฐ๋งŒ ์ œ๊ฑฐ
if len(stripped) < 5: # ๋„ˆ๋ฌด ์งง์€ ์ค„์€ ์œ ์ง€
cleaned_lines.append(original_line)
return '\n'.join(cleaned_lines)
def _contains_english(self, text: str) -> bool:
"""์˜์–ด ํฌํ•จ ์—ฌ๋ถ€ ํ™•์ธ - ์‹œ๋‚˜๋ฆฌ์˜ค ํฌ๋งท ์นœํ™”์  ๋ฒ„์ „"""
# ๋นˆ ๋ฌธ์ž์—ด ์ฒดํฌ
if not text or not text.strip():
return False
# ์‹œ๋‚˜๋ฆฌ์˜ค์—์„œ ๋ฐ˜๋“œ์‹œ ํ—ˆ์šฉํ•ด์•ผ ํ•˜๋Š” ์šฉ์–ด๋“ค (๋Œ€ํญ ํ™•์žฅ)
allowed_terms = [
# ์”ฌ ํ—ค๋”
"INT", "EXT", "INT.", "EXT.",
# ํŠธ๋žœ์ง€์…˜
"CUT", "TO", "FADE", "IN", "OUT", "DISSOLVE", "CONT'D",
# ์นด๋ฉ”๋ผ ์šฉ์–ด
"O.S.", "V.O.", "POV", "CLOSE", "UP", "WIDE", "SHOT",
"ESTABLISHING", "MONTAGE", "FLASHBACK", "DREAM", "SEQUENCE",
# ๊ธฐ์ˆ  ์šฉ์–ด
"CG", "SFX", "VFX", "BGM", "OST", "FX",
# ์ผ๋ฐ˜ ์•ฝ์–ด
"vs", "ex", "etc", "OK", "NG", "VIP", "CEO", "AI", "IT",
"DJ", "MC", "PD", "VJ", "PC", "TV", "DVD", "USB", "CD",
# ์•ŒํŒŒ๋ฒณ ๋‹จ๋… ๋ฌธ์ž (A, B, C ๋“ฑ)
"A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M",
"N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z",
# ์”ฌ ๊ด€๋ จ
"SCENE", "THE", "END", "TITLE", "SUPER",
# ๊ธฐํƒ€ ํ—ˆ์šฉ ๋‹จ์–ด
"sir", "Mr", "Ms", "Dr", "Prof"
]
# ๋Œ€์†Œ๋ฌธ์ž ๊ตฌ๋ถ„ ์—†์ด ์ฒดํฌ
test_text = text.upper()
for term in allowed_terms:
test_text = test_text.replace(term.upper(), "")
# ๋‚จ์€ ํ…์ŠคํŠธ์—์„œ ์‹ค์ œ ์˜์–ด ๋‹จ์–ด ์ฐพ๊ธฐ
import re
# 6์ž ์ด์ƒ์˜ ์—ฐ์†๋œ ์•ŒํŒŒ๋ฒณ๋งŒ ์˜์–ด๋กœ ํŒ๋‹จ
english_pattern = re.compile(r'[a-zA-Z]{6,}')
matches = english_pattern.findall(test_text)
if matches:
# ์ถ”๊ฐ€ ํ•„ํ„ฐ๋ง: ์•Œ๋ ค์ง„ ํ•œ๊ตญ์–ด ๋กœ๋งˆ์ž ํ‘œ๊ธฐ๋Š” ์ œ์™ธ
korean_romanization = ['hyung', 'oppa', 'noona', 'dongsaeng', 'sunbae', 'hoobae']
real_english = []
for match in matches:
if match.lower() not in korean_romanization:
real_english.append(match)
if real_english:
logger.debug(f"Real English words found: {real_english}")
return True
return False
def _validate_and_clean_screenplay(self, content: str) -> str:
"""์‹œ๋‚˜๋ฆฌ์˜ค ๊ฒ€์ฆ ๋ฐ ์ •๋ฆฌ"""
if not content:
return ""
lines = content.split('\n')
cleaned = []
for line in lines:
# ๋นˆ ์ค„์€ ์œ ์ง€
if not line.strip():
cleaned.append(line)
continue
# ๋ถˆ์™„์ „ํ•œ ๋ถ€๋ถ„ ์ œ๊ฑฐ
if "..." in line and not line.strip().endswith('.'):
continue
# ์˜์–ด ์ฒดํฌ (ํฌ๋งท ์ œ์™ธ)
if self._contains_english(line):
logger.warning(f"Removing English line: {line[:50]}")
continue
cleaned.append(line)
return '\n'.join(cleaned)
@staticmethod
def get_session_data(session_id: str) -> Optional[Dict]:
"""์„ธ์…˜ ๋ฐ์ดํ„ฐ ์กฐํšŒ"""
with ScreenplayDatabase.get_db() as conn:
cursor = conn.cursor()
row = cursor.execute(
'SELECT * FROM screenplay_sessions WHERE session_id = ?',
(session_id,)
).fetchone()
if row:
columns = [description[0] for description in cursor.description]
return dict(zip(columns, row))
return None
def _create_act_prompt(self, role: str, act_name: str, current_content: str,
planning_data: Dict, previous_acts: Dict, stage_type: str) -> str:
"""๋ง‰ ์ž‘์„ฑ ํ”„๋กฌํ”„ํŠธ ์ƒ์„ฑ - ๊ฐœ์„ ๋œ ๋ฒ„์ „"""
# ๊ธฐํš์•ˆ์—์„œ ํ•ต์‹ฌ ์ •๋ณด ์ถ”์ถœ
title = ""
genre = ""
screenplay_type = ""
logline = ""
synopsis = ""
characters = ""
for key, value in planning_data.items():
if "ํ”„๋กœ๋“€์„œ" in key and value:
# ์ œ๋ชฉ๊ณผ ๋กœ๊ทธ๋ผ์ธ ์ถ”์ถœ
if "์ œ๋ชฉ" in value:
title_match = re.search(r'์ œ๋ชฉ[:\s]*([^\n]+)', value)
if title_match:
title = title_match.group(1).strip()
if "๋กœ๊ทธ๋ผ์ธ" in value:
logline_match = re.search(r'๋กœ๊ทธ๋ผ์ธ[:\s]*([^\n]+)', value)
if logline_match:
logline = logline_match.group(1).strip()
elif "์Šคํ† ๋ฆฌ" in key and value:
synopsis = value[:1000]
elif "์บ๋ฆญํ„ฐ" in key and value:
characters = value[:1000]
# ์›๋ณธ ์š”์ฒญ ์ •๋ณด ๊ฐ•์กฐ
core_info = f"""
ใ€ํ•ต์‹ฌ ์ •๋ณด - ์ ˆ๋Œ€ ์ค€์ˆ˜ใ€‘
์›๋ณธ ์š”์ฒญ: {self.original_query}
์ œ๋ชฉ: {title}
๋กœ๊ทธ๋ผ์ธ: {logline}
์žฅ๋ฅด: {genre if genre else '๋“œ๋ผ๋งˆ'}
ํ˜•์‹: {screenplay_type if screenplay_type else '์˜ํ™”'}
โš ๏ธ ์ค‘์š”: ์œ„ ์ •๋ณด๋ฅผ ์ ˆ๋Œ€์ ์œผ๋กœ ์ค€์ˆ˜ํ•˜์„ธ์š”. ๋‹ค๋ฅธ ์ด์•ผ๊ธฐ๋กœ ๋ฐ”๊พธ์ง€ ๋งˆ์„ธ์š”.
โš ๏ธ ์ค‘์š”: ๋ชจ๋“  ๋‚ด์šฉ์„ ํ•œ๊ตญ์–ด๋กœ๋งŒ ์ž‘์„ฑํ•˜์„ธ์š”. ์˜์–ด ์‚ฌ์šฉ ๊ธˆ์ง€.
โš ๏ธ ์ค‘์š”: ํ‘œ์ค€ ์‹œ๋‚˜๋ฆฌ์˜ค ํฌ๋งท์„ ์ •ํ™•ํžˆ ์ค€์ˆ˜ํ•˜์„ธ์š”.
"""
if role == "์Šคํ† ๋ฆฌ์ž‘๊ฐ€":
if stage_type == "์ดˆ๊ณ ":
return f"""ใ€{act_name} ์ดˆ๊ณ  ์ž‘์„ฑใ€‘
{core_info}
ใ€๊ธฐํš์•ˆ ๋‚ด์šฉใ€‘
์‹œ๋†‰์‹œ์Šค:
{synopsis}
์บ๋ฆญํ„ฐ:
{characters}
ใ€์ด์ „ ๋ง‰ ๋‚ด์šฉใ€‘
{self._summarize_previous_acts(previous_acts)}
ใ€์ž‘์„ฑ ์š”๊ตฌ์‚ฌํ•ญใ€‘
1. ๋ฐ˜๋“œ์‹œ ํ•œ๊ตญ์–ด๋กœ๋งŒ ์ž‘์„ฑ
2. ํ‘œ์ค€ ์‹œ๋‚˜๋ฆฌ์˜ค ํฌ๋งท:
INT. ์žฅ์†Œ - ์‹œ๊ฐ„ (๋˜๋Š” EXT. ์žฅ์†Œ - ์‹œ๊ฐ„)
์žฅ๋ฉด ์„ค๋ช…์€ ํ˜„์žฌํ˜•์œผ๋กœ ์ž‘์„ฑ.
์บ๋ฆญํ„ฐ๋ช… (๋Œ€๋ฌธ์ž)
๋Œ€์‚ฌ๋Š” ์ž์—ฐ์Šค๋Ÿฌ์šด ํ•œ๊ตญ์–ด๋กœ.
3. ๋ถ„๋Ÿ‰: ์ตœ์†Œ 1000์ค„ ์ด์ƒ
4. ๊ฐ ์”ฌ: 5-7ํŽ˜์ด์ง€ ๋ถ„๋Ÿ‰
5. ๊ธฐํš์•ˆ ๋‚ด์šฉ 100% ๋ฐ˜์˜
์ ˆ๋Œ€ ์˜์–ด๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ๋งˆ์„ธ์š”.
์ ˆ๋Œ€ ๋‹ค๋ฅธ ์ด์•ผ๊ธฐ๋กœ ๋ฐ”๊พธ์ง€ ๋งˆ์„ธ์š”.
์ ˆ๋Œ€ ์—ฐ๊ทน์ด๋‚˜ ๋‹ค๋ฅธ ํ˜•์‹์œผ๋กœ ์ž‘์„ฑํ•˜์ง€ ๋งˆ์„ธ์š”.
{act_name}์„ ์™„์ „ํ•œ ์‹œ๋‚˜๋ฆฌ์˜ค ํ˜•์‹์œผ๋กœ ์ž‘์„ฑํ•˜์„ธ์š”."""
else: # ์™„์„ฑ
return f"""ใ€{act_name} ์ตœ์ข… ์™„์„ฑใ€‘
{core_info}
์ด์ „ ์ „๋ฌธ๊ฐ€๋“ค์˜ ํ”ผ๋“œ๋ฐฑ์„ ๋ฐ˜์˜ํ•˜์—ฌ ์ตœ์ข…๋ณธ์„ ์ž‘์„ฑํ•˜์„ธ์š”.
ใ€ํ•„์ˆ˜ ์‚ฌํ•ญใ€‘
1. ํ•œ๊ตญ์–ด๋กœ๋งŒ ์ž‘์„ฑ
2. ์˜ํ™” ์‹œ๋‚˜๋ฆฌ์˜ค ํฌ๋งท ์ค€์ˆ˜
3. ๊ธฐํš์•ˆ ์Šคํ† ๋ฆฌ ์œ ์ง€
4. 1200์ค„ ์ด์ƒ
๊นจ์ง„ ๋ถ€๋ถ„์ด๋‚˜ ... ๊ฐ™์€ ๋ฏธ์™„์„ฑ ๋ถ€๋ถ„ ์—†์ด ์™„์ „ํ•˜๊ฒŒ ์ž‘์„ฑํ•˜์„ธ์š”."""
elif role == "๊ณ ์ฆ์ „๋ฌธ๊ฐ€":
return f"""ใ€{act_name} ์‚ฌ์‹ค ํ™•์ธใ€‘
{core_info}
๋‹ค์Œ ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ๊ฒ€ํ† ํ•˜์„ธ์š”:
{current_content[:2000]}...
ใ€๊ฒ€ํ†  ์‚ฌํ•ญใ€‘
1. ์‚ฌ์‹ค ์˜ค๋ฅ˜ ํ™•์ธ
2. ์‹œ๋Œ€ ๊ณ ์ฆ
3. ์˜์–ด ์‚ฌ์šฉ ๋ถ€๋ถ„์„ ํ•œ๊ตญ์–ด๋กœ ์ˆ˜์ •
4. ๊นจ์ง„ ํ…์ŠคํŠธ๋‚˜ ... ๋ถ€๋ถ„ ์ˆ˜์ •
๋ชจ๋“  ์ˆ˜์ • ์‚ฌํ•ญ์„ ํ•œ๊ตญ์–ด๋กœ ์ œ์‹œํ•˜์„ธ์š”."""
elif role == "ํŽธ์ง‘์ž":
return f"""ใ€{act_name} ํŽธ์ง‘ใ€‘
{core_info}
ใ€ํŽธ์ง‘ ์ง€์นจใ€‘
1. ์˜์–ด๊ฐ€ ์žˆ๋‹ค๋ฉด ๋ชจ๋‘ ํ•œ๊ตญ์–ด๋กœ ๋ณ€๊ฒฝ
2. ๊นจ์ง„ ๋ถ€๋ถ„(...) ์™„์„ฑ
3. ๊ธฐํš์•ˆ๊ณผ ์ผ์น˜ํ•˜์ง€ ์•Š๋Š” ๋‚ด์šฉ ์ˆ˜์ •
4. ์‹œ๋‚˜๋ฆฌ์˜ค ํฌ๋งท ๊ต์ •
ํŽธ์ง‘๋œ ๋ฒ„์ „์„ ์™„์ „ํ•œ ํ•œ๊ตญ์–ด๋กœ ์ œ์‹œํ•˜์„ธ์š”."""
elif role == "๊ฐ๋…":
return f"""ใ€{act_name} ์—ฐ์ถœ ๋…ธํŠธใ€‘
{core_info}
ใ€์—ฐ์ถœ ๊ฐ•ํ™”ใ€‘
1. ์‹œ๊ฐ์  ๋””ํ…Œ์ผ ์ถ”๊ฐ€
2. ์นด๋ฉ”๋ผ ์•ต๊ธ€ ์ œ์•ˆ
3. ๋ถ„์œ„๊ธฐ ๋ฌ˜์‚ฌ ๊ฐ•ํ™”
๋ชจ๋“  ์—ฐ์ถœ ๋…ธํŠธ๋ฅผ ํ•œ๊ตญ์–ด๋กœ ์ž‘์„ฑํ•˜์„ธ์š”.
์˜ํ™” ์‹œ๋‚˜๋ฆฌ์˜ค์ž„์„ ๋ช…ํ™•ํžˆ ํ•˜์„ธ์š”."""
elif role == "๋Œ€ํ™”์ „๋ฌธ๊ฐ€":
return f"""ใ€{act_name} ๋Œ€์‚ฌ ๊ฐœ์„ ใ€‘
{core_info}
ใ€๋Œ€์‚ฌ ์ˆ˜์ •ใ€‘
1. ๋ชจ๋“  ๋Œ€์‚ฌ๋ฅผ ์ž์—ฐ์Šค๋Ÿฌ์šด ํ•œ๊ตญ์–ด๋กœ
2. ์˜์–ด ๋Œ€์‚ฌ๊ฐ€ ์žˆ๋‹ค๋ฉด ํ•œ๊ตญ์–ด๋กœ ๋ฒˆ์—ญ
3. ์บ๋ฆญํ„ฐ๋ณ„ ๋งํˆฌ ์ฐจ๋ณ„ํ™”
4. ์–ด์ƒ‰ํ•œ ๋ฒˆ์—ญํˆฌ ์ œ๊ฑฐ
์™„์ „ํ•œ ํ•œ๊ตญ์–ด ๋Œ€์‚ฌ๋กœ ์ˆ˜์ •ํ•˜์„ธ์š”."""
elif role == "๋น„ํ‰๊ฐ€":
return f"""ใ€{act_name} ๊ฒ€ํ† ใ€‘
{core_info}
ใ€ํ‰๊ฐ€ ํ•ญ๋ชฉใ€‘
1. ๊ธฐํš์•ˆ๊ณผ์˜ ์ผ์น˜๋„ (์ตœ์šฐ์„ )
2. ํ•œ๊ตญ์–ด ์‚ฌ์šฉ (์˜์–ด ํ˜ผ์šฉ ๊ธˆ์ง€)
3. ์‹œ๋‚˜๋ฆฌ์˜ค ํฌ๋งท ์ค€์ˆ˜
4. ์Šคํ† ๋ฆฌ ์™„์„ฑ๋„
๋ฌธ์ œ์ ๊ณผ ๊ฐœ์„  ์‚ฌํ•ญ์„ ๊ตฌ์ฒด์ ์œผ๋กœ ์ œ์‹œํ•˜์„ธ์š”."""
return f"""ใ€{role} ์ž‘์—…ใ€‘
{core_info}
{act_name}์„(๋ฅผ) ๊ฒ€ํ† ํ•˜๊ณ  ๊ฐœ์„ ํ•˜์„ธ์š”.
๋ฐ˜๋“œ์‹œ ํ•œ๊ตญ์–ด๋กœ๋งŒ ์ž‘์„ฑํ•˜๊ณ , ๊ธฐํš์•ˆ ๋‚ด์šฉ์„ ์ถฉ์‹คํžˆ ๋ฐ˜์˜ํ•˜์„ธ์š”."""
def _summarize_planning(self, planning_data: Dict) -> str:
"""๊ธฐํš์•ˆ ์š”์•ฝ"""
summary = ""
for key, value in planning_data.items():
if value:
summary += f"[{key}]\n{value[:200]}...\n"
return summary[:1000]
def _summarize_previous_acts(self, previous_acts: Dict) -> str:
"""์ด์ „ ๋ง‰ ์š”์•ฝ"""
summary = ""
for act, content in previous_acts.items():
if content:
summary += f"[{act}]\n{content[:300]}...\n"
return summary[:1000]
# UI ํ•จ์ˆ˜๋“ค
def create_act_progress_display(act_progress: ActProgress) -> str:
"""๋ง‰๋ณ„ ์ง„ํ–‰ ์ƒํ™ฉ ํ‘œ์‹œ"""
if not act_progress:
return ""
html = f"""
<div style="border: 2px solid #667eea; border-radius: 10px; padding: 15px; margin: 10px 0;">
<h3>{act_progress.act_name} ์ง„ํ–‰ ์ƒํ™ฉ</h3>
<div style="margin: 10px 0;">
<div style="background: #e0e0e0; border-radius: 10px; height: 30px; position: relative;">
<div style="background: linear-gradient(90deg, #667eea, #764ba2);
height: 100%; border-radius: 10px; width: {act_progress.progress_percentage}%;
transition: width 0.3s ease;">
<span style="position: absolute; left: 50%; transform: translateX(-50%);
color: white; line-height: 30px; font-weight: bold;">
{act_progress.progress_percentage:.0f}%
</span>
</div>
</div>
</div>
<div style="margin-top: 15px;">
<strong>ํ˜„์žฌ ์ž‘์—…:</strong> {EXPERT_ROLES.get(act_progress.current_expert, {}).get('emoji', '')} {act_progress.current_expert}
<br>
<strong>๋‹จ๊ณ„:</strong> {act_progress.current_stage} / {act_progress.total_stages}
<br>
<strong>์ƒํƒœ:</strong> {act_progress.status}
</div>
</div>
"""
return html
def format_planning_with_diagrams(planning_data: Dict, diagrams: str) -> str:
"""๊ธฐํš์•ˆ๊ณผ ๋‹ค์ด์–ด๊ทธ๋žจ ํฌ๋งทํŒ…"""
formatted = "# ๐Ÿ“‹ ์‹œ๋‚˜๋ฆฌ์˜ค ๊ธฐํš์•ˆ\n\n"
for key, content in planning_data.items():
if content:
role = key.split('_')[0]
emoji = EXPERT_ROLES.get(role, {}).get('emoji', '๐Ÿ“')
formatted += f"## {emoji} {key}\n\n{content}\n\n---\n\n"
if diagrams:
formatted += diagrams
return formatted
# Gradio ์ธํ„ฐํŽ˜์ด์Šค
def create_interface():
css = """
.main-header {
text-align: center;
margin-bottom: 2rem;
padding: 2.5rem;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 15px;
color: white;
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
}
/* ์‹œ๋‚˜๋ฆฌ์˜ค ์ถœ๋ ฅ ์Šคํƒ€์ผ */
.screenplay-output {
font-family: 'Courier New', monospace;
font-size: 14px;
line-height: 1.6;
white-space: pre-wrap;
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
}
.screenplay-output h3 {
color: #2c3e50;
margin: 20px 0 10px 0;
font-size: 16px;
font-weight: bold;
}
.screenplay-output strong {
color: #34495e;
font-weight: bold;
}
.screenplay-output em {
font-style: italic;
color: #7f8c8d;
}
.act-button {
min-height: 80px;
font-size: 1.1rem;
margin: 5px;
border-radius: 10px;
}
.expert-card {
background: white;
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 10px;
margin: 5px;
display: inline-block;
}
.progress-container {
background: #f8f9fa;
border-radius: 10px;
padding: 20px;
margin: 15px 0;
}
"""
with gr.Blocks(theme=gr.themes.Soft(), css=css, title="AI ์‹œ๋‚˜๋ฆฌ์˜ค ์ž‘๊ฐ€") as interface:
gr.HTML("""
<div class="main-header">
<h1>๐ŸŽฌ AI ์‹œ๋‚˜๋ฆฌ์˜ค ์ž‘๊ฐ€</h1>
<p>์ „๋ฌธ๊ฐ€ ํ˜‘์—… ์‹œ์Šคํ…œ | ๋‹ค์ด์–ด๊ทธ๋žจ ์ง€์› | ์›น ๊ฒ€์ฆ</p>
</div>
""")
# ์ƒํƒœ ๋ณ€์ˆ˜
current_session_id = gr.State(None)
current_planning = gr.State({})
act1_content = gr.State("")
act2a_content = gr.State("")
act2b_content = gr.State("")
act3_content = gr.State("")
with gr.Tabs():
# ๊ธฐํš ํƒญ
with gr.Tab("๐Ÿ“‹ ๊ธฐํš์•ˆ ์ž‘์„ฑ"):
with gr.Row():
with gr.Column(scale=2):
query_input = gr.Textbox(
label="๐Ÿ’ก ์‹œ๋‚˜๋ฆฌ์˜ค ์•„์ด๋””์–ด",
placeholder="๊ตฌ์ฒด์ ์ธ ์Šคํ† ๋ฆฌ๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”...",
lines=4
)
with gr.Column(scale=1):
screenplay_type = gr.Radio(
choices=list(SCREENPLAY_LENGTHS.keys()),
value="์˜ํ™”",
label="๐Ÿ“ฝ๏ธ ์œ ํ˜•"
)
genre_select = gr.Dropdown(
choices=["์•ก์…˜", "์Šค๋ฆด๋Ÿฌ", "๋“œ๋ผ๋งˆ", "์ฝ”๋ฏธ๋””", "๊ณตํฌ", "SF", "๋กœ๋งจ์Šค", "ํŒํƒ€์ง€"],
value="๋“œ๋ผ๋งˆ",
label="๐ŸŽญ ์žฅ๋ฅด"
)
planning_btn = gr.Button("๐Ÿ“‹ ๊ธฐํš์•ˆ ์ƒ์„ฑ (๋‹ค์ด์–ด๊ทธ๋žจ ํฌํ•จ)", variant="primary", size="lg")
# ์ง„ํ–‰ ์ƒํƒœ
with gr.Row():
progress_bar = gr.Slider(0, 100, value=0, label="์ง„ํ–‰๋ฅ ", interactive=False)
status_text = gr.Textbox(label="์ƒํƒœ", value="๋Œ€๊ธฐ ์ค‘", interactive=False)
# ๊ธฐํš์•ˆ๊ณผ ๋‹ค์ด์–ด๊ทธ๋žจ
planning_display = gr.Markdown("*๊ธฐํš์•ˆ๊ณผ ๋‹ค์ด์–ด๊ทธ๋žจ์ด ์—ฌ๊ธฐ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค...*")
# ์‹œ๋‚˜๋ฆฌ์˜ค ์ž‘์„ฑ ํƒญ
with gr.Tab("๐ŸŽฌ ์‹œ๋‚˜๋ฆฌ์˜ค ์ž‘์„ฑ"):
gr.Markdown("""
### ๐Ÿ“ ๋ง‰๋ณ„ ์‹œ๋‚˜๋ฆฌ์˜ค ์ž‘์„ฑ
๊ธฐํš์•ˆ ์ƒ์„ฑ ํ›„, ๊ฐ ๋ง‰์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ž‘์„ฑํ•˜์„ธ์š”.
๊ฐ ๋ง‰๋งˆ๋‹ค 7๋ช…์˜ ์ „๋ฌธ๊ฐ€๊ฐ€ ํ˜‘์—…ํ•ฉ๋‹ˆ๋‹ค:
๐Ÿ“– ์Šคํ† ๋ฆฌ์ž‘๊ฐ€ โ†’ ๐Ÿ”Ž ๊ณ ์ฆ์ „๋ฌธ๊ฐ€ โ†’ โœ‚๏ธ ํŽธ์ง‘์ž โ†’ ๐ŸŽญ ๊ฐ๋… โ†’ ๐Ÿ’ฌ ๋Œ€ํ™”์ „๋ฌธ๊ฐ€ โ†’ ๐Ÿ” ๋น„ํ‰๊ฐ€ โ†’ โœ… ์ตœ์ข…์™„์„ฑ
""")
# ๋ง‰๋ณ„ ์ž‘์„ฑ ๋ฒ„ํŠผ๋“ค
with gr.Row():
act1_btn = gr.Button(
"1๏ธโƒฃ 1๋ง‰ ์ž‘์„ฑ\n[์„ค์ • - 25%]\n์•ฝ 30ํŽ˜์ด์ง€",
variant="primary",
elem_classes=["act-button"]
)
act2a_btn = gr.Button(
"2๏ธโƒฃ 2๋ง‰A ์ž‘์„ฑ\n[์ƒ์Šน - 25%]\n์•ฝ 30ํŽ˜์ด์ง€",
variant="secondary",
elem_classes=["act-button"],
interactive=False # 1๋ง‰ ์™„์„ฑ ํ›„ ํ™œ์„ฑํ™”
)
act2b_btn = gr.Button(
"3๏ธโƒฃ 2๋ง‰B ์ž‘์„ฑ\n[๋ณต์žกํ™” - 25%]\n์•ฝ 30ํŽ˜์ด์ง€",
variant="secondary",
elem_classes=["act-button"],
interactive=False # 2๋ง‰A ์™„์„ฑ ํ›„ ํ™œ์„ฑํ™”
)
act3_btn = gr.Button(
"4๏ธโƒฃ 3๋ง‰ ์ž‘์„ฑ\n[ํ•ด๊ฒฐ - 25%]\n์•ฝ 30ํŽ˜์ด์ง€",
variant="secondary",
elem_classes=["act-button"],
interactive=False # 2๋ง‰B ์™„์„ฑ ํ›„ ํ™œ์„ฑํ™”
)
# ์ง„ํ–‰ ์ƒํ™ฉ ํ‘œ์‹œ
gr.Markdown("### ๐Ÿ“Š ์ž‘์„ฑ ์ง„ํ–‰ ์ƒํ™ฉ")
act_progress_display = gr.HTML(
value='<div class="progress-container">๋ง‰์„ ์„ ํƒํ•˜์—ฌ ์ž‘์„ฑ์„ ์‹œ์ž‘ํ•˜์„ธ์š”...</div>'
)
act_status_text = gr.Textbox(
label="ํ˜„์žฌ ์ƒํƒœ",
value="๊ธฐํš์•ˆ์„ ๋จผ์ € ์ƒ์„ฑํ•œ ํ›„ ์‹œ๋‚˜๋ฆฌ์˜ค ์ž‘์„ฑ์„ ์‹œ์ž‘ํ•˜์„ธ์š”",
interactive=False
)
# ์ „๋ฌธ๊ฐ€ ์ž‘์—… ํ˜„ํ™ฉ
gr.Markdown("### ๐Ÿ‘ฅ ์ „๋ฌธ๊ฐ€ ์ž‘์—… ํ˜„ํ™ฉ")
expert_status = gr.HTML("""
<div style="padding: 10px;">
<span class="expert-card">๐Ÿ“– ์Šคํ† ๋ฆฌ์ž‘๊ฐ€</span>
<span class="expert-card">๐Ÿ”Ž ๊ณ ์ฆ์ „๋ฌธ๊ฐ€</span>
<span class="expert-card">โœ‚๏ธ ํŽธ์ง‘์ž</span>
<span class="expert-card">๐ŸŽญ ๊ฐ๋…</span>
<span class="expert-card">๐Ÿ’ฌ ๋Œ€ํ™”์ „๋ฌธ๊ฐ€</span>
<span class="expert-card">๐Ÿ” ๋น„ํ‰๊ฐ€</span>
</div>
""")
# ์‹œ๋‚˜๋ฆฌ์˜ค ์ถœ๋ ฅ
gr.Markdown("### ๐Ÿ“„ ์ž‘์„ฑ๋œ ์‹œ๋‚˜๋ฆฌ์˜ค")
with gr.Tabs():
with gr.Tab("1๋ง‰"):
act1_output = gr.Markdown("*1๋ง‰์ด ์—ฌ๊ธฐ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค...*")
with gr.Tab("2๋ง‰A"):
act2a_output = gr.Markdown("*2๋ง‰A๊ฐ€ ์—ฌ๊ธฐ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค...*")
with gr.Tab("2๋ง‰B"):
act2b_output = gr.Markdown("*2๋ง‰B๊ฐ€ ์—ฌ๊ธฐ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค...*")
with gr.Tab("3๋ง‰"):
act3_output = gr.Markdown("*3๋ง‰์ด ์—ฌ๊ธฐ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค...*")
with gr.Tab("์ „์ฒด ์‹œ๋‚˜๋ฆฌ์˜ค"):
full_output = gr.Markdown("*์ „์ฒด ์‹œ๋‚˜๋ฆฌ์˜ค๊ฐ€ ์—ฌ๊ธฐ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค...*")
# ๋‹ค์šด๋กœ๋“œ ๋ฒ„ํŠผ
with gr.Row():
download_btn = gr.Button("๐Ÿ’พ ์ „์ฒด ์‹œ๋‚˜๋ฆฌ์˜ค ๋‹ค์šด๋กœ๋“œ (TXT)", variant="secondary")
download_file = gr.File(label="๋‹ค์šด๋กœ๋“œ ํŒŒ์ผ", visible=False)
# ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ
def handle_planning(query, s_type, genre):
if not query:
yield 0, "โŒ ์•„์ด๋””์–ด๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”", "*๊ธฐํš์•ˆ์ด ์—ฌ๊ธฐ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค...*", None, None
return
system = ScreenplayGenerationSystem()
session_id = None
for status, progress, planning_data, feedbacks, diagrams in system.generate_planning(query, s_type, genre):
formatted = format_planning_with_diagrams(planning_data, diagrams)
session_id = system.current_session_id
yield progress, status, formatted, planning_data, session_id
# 4. ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ ์ˆ˜์ • - act ๋‚ด์šฉ ์ €์žฅ ๊ฐœ์„ 
def handle_act_writing(act_name, session_id, planning_data, previous_acts):
"""๋ง‰๋ณ„ ์‹œ๋‚˜๋ฆฌ์˜ค ์ž‘์„ฑ - generator ๋ž˜ํผ"""
if not session_id or not planning_data:
yield "", "โŒ ๋จผ์ € ๊ธฐํš์•ˆ์„ ์ƒ์„ฑํ•ด์ฃผ์„ธ์š”", ""
return
try:
system = ScreenplayGenerationSystem()
final_content = "" # ์ตœ์ข… ์ฝ˜ํ…์ธ  ์ €์žฅ
# Generator๋ฅผ ์ˆœํšŒํ•˜๋ฉฐ yield
for act_progress, status_msg in system.generate_act(
session_id, act_name, planning_data, previous_acts
):
progress_html = create_act_progress_display(act_progress)
screenplay_display = format_screenplay_display(act_progress.content)
final_content = act_progress.content # ์›๋ณธ ์ฝ˜ํ…์ธ  ์ €์žฅ
yield progress_html, status_msg, screenplay_display
# ์™„๋ฃŒ ํ›„ ์›๋ณธ ์ฝ˜ํ…์ธ  ๋ฐ˜ํ™˜
if final_content:
yield progress_html, f"โœ… {act_name} ์™„์„ฑ ๋ฐ ์ €์žฅ๋จ", screenplay_display
except Exception as e:
logger.error(f"Act writing error: {e}")
yield f"<div>โŒ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}</div>", f"์˜ค๋ฅ˜: {str(e)}", ""
def format_screenplay_display(content):
"""์‹œ๋‚˜๋ฆฌ์˜ค ํ‘œ์‹œ ํฌ๋งทํŒ… - ๊ฐœ์„ ๋œ ๋ฒ„์ „"""
if not content:
return "*์ž‘์„ฑ ์ค‘...*"
lines = content.split('\n')
result = []
scene_number = 0
for line in lines:
stripped = line.strip()
if not stripped:
result.append("")
continue
# ์”ฌ ํ—ค๋” ๊ฐ์ง€ ๋ฐ ํฌ๋งทํŒ… (INT. ๋˜๋Š” EXT.๋กœ ์‹œ์ž‘)
if stripped.startswith(('INT.', 'EXT.', 'INT ', 'EXT ')):
scene_number += 1
# ์”ฌ ๊ตฌ๋ถ„์„  ์ถ”๊ฐ€
result.append("\n" + "="*80)
result.append(f"### ์”ฌ {scene_number}")
result.append(f"**{stripped}**")
result.append("-"*40)
# ์บ๋ฆญํ„ฐ๋ช… ๊ฐ์ง€ (๋Œ€๋ฌธ์ž๋กœ๋งŒ ๋œ ์งง์€ ์ค„)
elif stripped.isupper() and len(stripped.split()) <= 3 and not any(c.isdigit() for c in stripped):
# ์บ๋ฆญํ„ฐ๋ช… ๊ฐ•์กฐ
result.append(f"\n**{stripped}**")
# ๊ด„ํ˜ธ๋กœ ๋œ ์ง€์‹œ๋ฌธ (์—ฐ๊ธฐ ์ง€์‹œ)
elif stripped.startswith('(') and stripped.endswith(')'):
result.append(f"*{stripped}*")
# ํŠธ๋žœ์ง€์…˜ (CUT TO:, FADE IN: ๋“ฑ)
elif any(trans in stripped.upper() for trans in ['CUT TO:', 'FADE IN:', 'FADE OUT:', 'DISSOLVE TO:']):
result.append(f"\n_{stripped}_\n")
# ์ผ๋ฐ˜ ํ…์ŠคํŠธ
else:
result.append(line)
# ์ตœ์ข… ์ •๋ฆฌ
formatted = '\n'.join(result)
# ์—ฐ์†๋œ ๋นˆ ์ค„ ์ œ๊ฑฐ
while '\n\n\n' in formatted:
formatted = formatted.replace('\n\n\n', '\n\n')
return formatted
# 2. combine_all_acts ํ•จ์ˆ˜ ์ˆ˜์ • - State ๊ฐ’ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ฒ˜๋ฆฌ
def combine_all_acts(act1_state, act2a_state, act2b_state, act3_state):
"""๋ชจ๋“  ๋ง‰ ํ•ฉ์น˜๊ธฐ - State ๊ฐ’ ์ฒ˜๋ฆฌ ๊ฐœ์„ """
full = "# ๐ŸŽฌ ์ „์ฒด ์‹œ๋‚˜๋ฆฌ์˜ค\n\n"
# State ๊ฐ์ฒด์ธ ๊ฒฝ์šฐ value ์†์„ฑ ์ ‘๊ทผ
act1 = act1_state.value if hasattr(act1_state, 'value') else act1_state
act2a = act2a_state.value if hasattr(act2a_state, 'value') else act2a_state
act2b = act2b_state.value if hasattr(act2b_state, 'value') else act2b_state
act3 = act3_state.value if hasattr(act3_state, 'value') else act3_state
# ์‹ค์ œ ์ฝ˜ํ…์ธ ๊ฐ€ ์žˆ๋Š”์ง€ ํ™•์ธ
has_content = False
if act1 and act1 != "*1๋ง‰์ด ์—ฌ๊ธฐ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค...*" and act1 != "":
# ๋งˆํฌ๋‹ค์šด ์ œ๊ฑฐํ•œ ์›๋ณธ ํ…์ŠคํŠธ ์ถ”์ถœ
clean_act1 = act1.replace("**", "").replace("###", "").replace("="*80, "").replace("-"*40, "")
full += "## ์ œ1๋ง‰ - ์„ค์ •\n\n" + clean_act1 + "\n\n" + "="*80 + "\n\n"
has_content = True
if act2a and act2a != "*2๋ง‰A๊ฐ€ ์—ฌ๊ธฐ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค...*" and act2a != "":
clean_act2a = act2a.replace("**", "").replace("###", "").replace("="*80, "").replace("-"*40, "")
full += "## ์ œ2๋ง‰A - ์ƒ์Šน\n\n" + clean_act2a + "\n\n" + "="*80 + "\n\n"
has_content = True
if act2b and act2b != "*2๋ง‰B๊ฐ€ ์—ฌ๊ธฐ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค...*" and act2b != "":
clean_act2b = act2b.replace("**", "").replace("###", "").replace("="*80, "").replace("-"*40, "")
full += "## ์ œ2๋ง‰B - ๋ณต์žกํ™”\n\n" + clean_act2b + "\n\n" + "="*80 + "\n\n"
has_content = True
if act3 and act3 != "*3๋ง‰์ด ์—ฌ๊ธฐ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค...*" and act3 != "":
clean_act3 = act3.replace("**", "").replace("###", "").replace("="*80, "").replace("-"*40, "")
full += "## ์ œ3๋ง‰ - ํ•ด๊ฒฐ\n\n" + clean_act3 + "\n\n"
has_content = True
if not has_content:
return "*์ „์ฒด ์‹œ๋‚˜๋ฆฌ์˜ค๊ฐ€ ์™„์„ฑ๋˜๋ฉด ์—ฌ๊ธฐ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค...*"
return full
# 3. save_to_file ํ•จ์ˆ˜ ์ˆ˜์ • - ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์—์„œ ์ง์ ‘ ๊ฐ€์ ธ์˜ค๊ธฐ
def save_to_file(full_screenplay, session_id):
"""์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ํŒŒ์ผ๋กœ ์ €์žฅ - ๊ฐœ์„ ๋œ ๋ฒ„์ „"""
# ์„ธ์…˜ ID๊ฐ€ ์žˆ์œผ๋ฉด DB์—์„œ ์ง์ ‘ ๊ฐ€์ ธ์˜ค๊ธฐ
if session_id:
try:
with ScreenplayDatabase.get_db() as conn:
cursor = conn.cursor()
row = cursor.execute(
'''SELECT user_query, title, logline, genre, screenplay_type,
act1_content, act2a_content, act2b_content, act3_content
FROM screenplay_sessions WHERE session_id = ?''',
(session_id,)
).fetchone()
if row:
# ํŒŒ์ผ ๋‚ด์šฉ ๊ตฌ์„ฑ
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"screenplay_{timestamp}.txt"
with open(filename, 'w', encoding='utf-8') as f:
# ํ—ค๋” ์ •๋ณด
f.write("="*80 + "\n")
f.write("์‹œ๋‚˜๋ฆฌ์˜ค\n")
f.write("="*80 + "\n\n")
if row['title']:
f.write(f"์ œ๋ชฉ: {row['title']}\n")
if row['genre']:
f.write(f"์žฅ๋ฅด: {row['genre']}\n")
if row['screenplay_type']:
f.write(f"ํ˜•์‹: {row['screenplay_type']}\n")
if row['logline']:
f.write(f"๋กœ๊ทธ๋ผ์ธ: {row['logline']}\n")
f.write("\n" + "="*80 + "\n\n")
# ๊ฐ ๋ง‰ ๋‚ด์šฉ ์ถ”๊ฐ€
if row['act1_content']:
f.write("์ œ1๋ง‰ - ์„ค์ •\n")
f.write("="*80 + "\n\n")
f.write(row['act1_content'])
f.write("\n\n" + "="*80 + "\n\n")
if row['act2a_content']:
f.write("์ œ2๋ง‰A - ์ƒ์Šน\n")
f.write("="*80 + "\n\n")
f.write(row['act2a_content'])
f.write("\n\n" + "="*80 + "\n\n")
if row['act2b_content']:
f.write("์ œ2๋ง‰B - ๋ณต์žกํ™”\n")
f.write("="*80 + "\n\n")
f.write(row['act2b_content'])
f.write("\n\n" + "="*80 + "\n\n")
if row['act3_content']:
f.write("์ œ3๋ง‰ - ํ•ด๊ฒฐ\n")
f.write("="*80 + "\n\n")
f.write(row['act3_content'])
f.write("\n\n")
# ์ž‘์„ฑ ์ •๋ณด
f.write("\n" + "="*80 + "\n")
f.write(f"์ž‘์„ฑ์ผ: {datetime.now().strftime('%Y๋…„ %m์›” %d์ผ %H:%M')}\n")
f.write("="*80 + "\n")
return filename
except Exception as e:
logger.error(f"DB์—์„œ ํŒŒ์ผ ์ €์žฅ ์ค‘ ์˜ค๋ฅ˜: {e}")
# DB ์ ‘๊ทผ ์‹คํŒจ์‹œ ์ „๋‹ฌ๋ฐ›์€ ๋‚ด์šฉ์œผ๋กœ ์ €์žฅ
if not full_screenplay or full_screenplay == "*์ „์ฒด ์‹œ๋‚˜๋ฆฌ์˜ค๊ฐ€ ์™„์„ฑ๋˜๋ฉด ์—ฌ๊ธฐ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค...*":
return None
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"screenplay_{timestamp}.txt"
with open(filename, 'w', encoding='utf-8') as f:
# ๋ชจ๋“  ๋งˆํฌ๋‹ค์šด ๋ฐ ํฌ๋งทํŒ… ์ œ๊ฑฐ
clean_text = full_screenplay
clean_text = clean_text.replace("**", "")
clean_text = clean_text.replace("###", "")
clean_text = clean_text.replace("#", "")
clean_text = clean_text.replace("="*80, "-"*80)
clean_text = clean_text.replace("-"*40, "")
clean_text = clean_text.replace("_", "")
clean_text = clean_text.replace("*", "")
f.write(clean_text)
return filename
# ์ด๋ฒคํŠธ ์—ฐ๊ฒฐ
planning_output = planning_btn.click(
fn=handle_planning,
inputs=[query_input, screenplay_type, genre_select],
outputs=[progress_bar, status_text, planning_display, current_planning, current_session_id]
)
planning_output.then(
# ๊ธฐํš์•ˆ ์ƒ์„ฑ ํ›„ 1๋ง‰ ๋ฒ„ํŠผ ํ™œ์„ฑํ™”
lambda: gr.update(interactive=True),
outputs=[act1_btn]
)
# 1๋ง‰ ์ž‘์„ฑ - generator ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•ด ์ˆ˜์ •
act1_output_event = act1_btn.click(
fn=handle_act_writing,
inputs=[
gr.State("1๋ง‰"),
current_session_id,
current_planning,
gr.State({})
],
outputs=[act_progress_display, act_status_text, act1_output]
)
act1_output_event.then(
lambda x: (x, gr.update(interactive=True)),
inputs=[act1_output],
outputs=[act1_content, act2a_btn]
)
# 2๋ง‰A ์ž‘์„ฑ
act2a_output_event = act2a_btn.click(
fn=handle_act_writing,
inputs=[
gr.State("2๋ง‰A"),
current_session_id,
current_planning,
act1_content
],
outputs=[act_progress_display, act_status_text, act2a_output]
)
act2a_output_event.then(
lambda x: (x, gr.update(interactive=True)),
inputs=[act2a_output],
outputs=[act2a_content, act2b_btn]
)
# 2๋ง‰B ์ž‘์„ฑ
act2b_output_event = act2b_btn.click(
fn=handle_act_writing,
inputs=[
gr.State("2๋ง‰B"),
current_session_id,
current_planning,
gr.State(lambda: {"1๋ง‰": act1_content.value, "2๋ง‰A": act2a_content.value})
],
outputs=[act_progress_display, act_status_text, act2b_output]
)
act2b_output_event.then(
lambda x: (x, gr.update(interactive=True)),
inputs=[act2b_output],
outputs=[act2b_content, act3_btn]
)
# 3๋ง‰ ์ž‘์„ฑ
act3_output_event = act3_btn.click(
fn=handle_act_writing,
inputs=[
gr.State("3๋ง‰"),
current_session_id,
current_planning,
gr.State(lambda: {"1๋ง‰": act1_content.value, "2๋ง‰A": act2a_content.value, "2๋ง‰B": act2b_content.value})
],
outputs=[act_progress_display, act_status_text, act3_output]
)
act3_output_event.then(
lambda x: x,
inputs=[act3_output],
outputs=[act3_content]
).then(
# ๋ชจ๋“  ๋ง‰ ์™„์„ฑ ํ›„ ์ „์ฒด ์‹œ๋‚˜๋ฆฌ์˜ค ์ƒ์„ฑ
fn=combine_all_acts,
inputs=[act1_content, act2a_content, act2b_content, act3_content],
outputs=[full_output]
)
# ๋‹ค์šด๋กœ๋“œ
download_btn.click(
fn=lambda full, sid: save_to_file(full, sid),
inputs=[full_output, current_session_id], # session_id ์ถ”๊ฐ€
outputs=[download_file]
).then(
lambda x: gr.update(visible=True, value=x) if x else gr.update(visible=False),
inputs=[download_file],
outputs=[download_file]
)
return interface
# ๋ฉ”์ธ ์‹คํ–‰
if __name__ == "__main__":
logger.info("AI ์‹œ๋‚˜๋ฆฌ์˜ค ์ž‘๊ฐ€ ์‹œ์ž‘...")
ScreenplayDatabase.init_db()
interface = create_interface()
interface.launch(server_name="0.0.0.0", server_port=7860, share=False)