AGI-Screenplay-Pro / app-BACKUP2.py
openfree's picture
Rename app.py to app-BACKUP2.py
83edd7e verified
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
# --- ๋กœ๊น… ์„ค์ • ---
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"
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": "์žฅ๋ฅด์— ์ •ํ†ตํ•œ"
}
}
# ์žฅ๋ฅด ํ…œํ”Œ๋ฆฟ
GENRE_TEMPLATES = {
"์•ก์…˜": {
"pacing": "๋น ๋ฆ„",
"scene_length": "์งง์Œ",
"dialogue_ratio": 0.3,
"key_elements": ["์•ก์…˜ ์‹œํ€€์Šค", "๋ฌผ๋ฆฌ์  ๊ฐˆ๋“ฑ", "๊ธด๋ฐ•๊ฐ", "์œ„๊ธฐ ๊ณ ์กฐ", "์˜์›…์  ์ˆœ๊ฐ„"],
"structure_beats": ["ํญ๋ฐœ์  ์˜คํ”„๋‹", "์ถ”๊ฒฉ", "๋Œ€๊ฒฐ", "ํด๋ผ์ด๋งฅ์Šค ์ „ํˆฌ", "์˜์›…์˜ ์Šน๋ฆฌ"],
"scene_density": 1.2,
"action_description_ratio": 0.6
},
"์Šค๋ฆด๋Ÿฌ": {
"pacing": "๋น ๋ฆ„",
"scene_length": "์งง์Œ",
"dialogue_ratio": 0.35,
"key_elements": ["์„œ์ŠคํŽœ์Šค", "๋ฐ˜์ „", "ํŽธ์ง‘์ฆ", "์‹œ๊ฐ„ ์••๋ฐ•", "์ง„์‹ค ํญ๋กœ"],
"structure_beats": ["ํ›…", "๋ฏธ์Šคํ„ฐ๋ฆฌ ์‹ฌํ™”", "๊ฑฐ์ง“ ์Šน๋ฆฌ", "์ง„์‹ค ํญ๋กœ", "์ตœ์ข… ๋Œ€๊ฒฐ"],
"scene_density": 1.1,
"action_description_ratio": 0.5
},
"๋“œ๋ผ๋งˆ": {
"pacing": "๋ณดํ†ต",
"scene_length": "์ค‘๊ฐ„",
"dialogue_ratio": 0.55,
"key_elements": ["์บ๋ฆญํ„ฐ ๊นŠ์ด", "๊ฐ์ •์  ์ง„์‹ค", "๊ด€๊ณ„", "๋‚ด์  ๊ฐˆ๋“ฑ", "๋ณ€ํ™”"],
"structure_beats": ["์ผ์ƒ", "์ด‰๋งค", "๊ณ ๋ฏผ", "๊ฒฐ์‹ฌ", "๋ณต์žกํ™”", "์œ„๊ธฐ", "ํ•ด๊ฒฐ"],
"scene_density": 0.9,
"action_description_ratio": 0.3
},
"์ฝ”๋ฏธ๋””": {
"pacing": "๋น ๋ฆ„",
"scene_length": "์งง์Œ",
"dialogue_ratio": 0.65,
"key_elements": ["์…‹์—…๊ณผ ํŽ˜์ด์˜คํ”„", "ํƒ€์ด๋ฐ", "์บ๋ฆญํ„ฐ ์ฝ”๋ฏธ๋””", "์ƒํ™ฉ ๊ณ ์กฐ", "๋Ÿฌ๋‹ ๊ฐœ๊ทธ"],
"structure_beats": ["์›ƒ๊ธด ์˜คํ”„๋‹", "๋ณต์žกํ™”", "์˜คํ•ด ์ฆํญ", "ํ˜ผ๋ˆ์˜ ์ •์ ", "ํ•ด๊ฒฐ๊ณผ ์ฝœ๋ฐฑ"],
"scene_density": 1.0,
"action_description_ratio": 0.35
},
"๊ณตํฌ": {
"pacing": "๊ฐ€๋ณ€์ ",
"scene_length": "ํ˜ผํ•ฉ",
"dialogue_ratio": 0.3,
"key_elements": ["๋ถ„์œ„๊ธฐ", "๊ณตํฌ๊ฐ", "์ ํ”„ ์Šค์ผ€์–ด", "์‹ฌ๋ฆฌ์  ๊ณตํฌ", "๊ณ ๋ฆฝ"],
"structure_beats": ["ํ‰๋ฒ”ํ•œ ์ผ์ƒ", "์ฒซ ์ง•์กฐ", "์กฐ์‚ฌ", "์ฒซ ๊ณต๊ฒฉ", "์ƒ์กด", "์ตœ์ข… ๋Œ€๊ฒฐ"],
"scene_density": 1.0,
"action_description_ratio": 0.55
},
"SF": {
"pacing": "๋ณดํ†ต",
"scene_length": "์ค‘๊ฐ„",
"dialogue_ratio": 0.45,
"key_elements": ["์„ธ๊ณ„๊ด€ ๊ตฌ์ถ•", "๊ธฐ์ˆ ", "๊ฐœ๋…", "์‹œ๊ฐ์  ์ŠคํŽ™ํ„ฐํด", "์ฒ ํ•™์  ์งˆ๋ฌธ"],
"structure_beats": ["์ผ์ƒ ์„ธ๊ณ„", "๋ฐœ๊ฒฌ", "์ƒˆ๋กœ์šด ์„ธ๊ณ„", "๋ณต์žกํ™”", "์ดํ•ด", "์„ ํƒ", "์ƒˆ๋กœ์šด ์ผ์ƒ"],
"scene_density": 0.95,
"action_description_ratio": 0.45
},
"๋กœ๋งจ์Šค": {
"pacing": "๋ณดํ†ต",
"scene_length": "์ค‘๊ฐ„",
"dialogue_ratio": 0.6,
"key_elements": ["์ผ€๋ฏธ์ŠคํŠธ๋ฆฌ", "์žฅ์• ๋ฌผ", "๊ฐ์ •์  ์ˆœ๊ฐ„", "์นœ๋ฐ€๊ฐ", "์ทจ์•ฝ์„ฑ"],
"structure_beats": ["๋งŒ๋‚จ", "๋Œ๋ฆผ", "์ฒซ ๊ฐˆ๋“ฑ", "๊นŠ์–ด์ง", "์œ„๊ธฐ/์ด๋ณ„", "ํ™”ํ•ด", "๊ฒฐํ•ฉ"],
"scene_density": 0.85,
"action_description_ratio": 0.25
},
"ํŒํƒ€์ง€": {
"pacing": "๋ณดํ†ต",
"scene_length": "์ค‘๊ฐ„",
"dialogue_ratio": 0.4,
"key_elements": ["๋งˆ๋ฒ• ์ฒด๊ณ„", "์˜์›…์˜ ์—ฌ์ •", "์‹ ํ™”์  ์š”์†Œ", "์„ธ๊ณ„๊ด€", "์„ฑ์žฅ"],
"structure_beats": ["ํ‰๋ฒ”ํ•œ ์„ธ๊ณ„", "์†Œ๋ช…", "๊ฑฐ๋ถ€", "๋ฉ˜ํ† ", "์ฒซ ์‹œํ—˜", "์‹œ๋ จ", "๋ณด์ƒ", "๊ท€ํ™˜"],
"scene_density": 1.0,
"action_description_ratio": 0.5
}
}
# ๊ธฐํš ๋‹จ๊ณ„ - ๋‹ค์ค‘ ์—ญํ•  ํ˜‘์—…
PLANNING_STAGES = [
("ํ”„๋กœ๋“€์„œ", "producer", "๐ŸŽฌ ํ”„๋กœ๋“€์„œ: ํ•ต์‹ฌ ์ปจ์…‰ ๋ฐ ์‹œ์žฅ์„ฑ ๋ถ„์„"),
("์Šคํ† ๋ฆฌ์ž‘๊ฐ€", "story_writer", "๐Ÿ“– ์Šคํ† ๋ฆฌ ์ž‘๊ฐ€: ์‹œ๋†‰์‹œ์Šค ๋ฐ 3๋ง‰ ๊ตฌ์กฐ"),
("์บ๋ฆญํ„ฐ๋””์ž์ด๋„ˆ", "character_designer", "๐Ÿ‘ฅ ์บ๋ฆญํ„ฐ ๋””์ž์ด๋„ˆ: ์ธ๋ฌผ ํ”„๋กœํ•„ ๋ฐ ๊ด€๊ณ„๋„"),
("๊ฐ๋…", "director", "๐ŸŽญ ๊ฐ๋…: ๋น„์ฃผ์–ผ ์ปจ์…‰ ๋ฐ ์—ฐ์ถœ ๋ฐฉํ–ฅ"),
("๋น„ํ‰๊ฐ€", "critic", "๐Ÿ” ๋น„ํ‰๊ฐ€: ๊ธฐํš์•ˆ ์ข…ํ•ฉ ๊ฒ€ํ†  ๋ฐ ๊ฐœ์„ ์ "),
]
# ์ž‘์„ฑ ๋‹จ๊ณ„ - ๋‹ค์ค‘ ์—ญํ•  ํ˜‘์—…
WRITING_STAGES = [
("์Šคํ† ๋ฆฌ์ž‘๊ฐ€", "1๋ง‰ ์ดˆ๊ณ ", "โœ๏ธ 1๋ง‰ ์ดˆ๊ณ  ์ž‘์„ฑ"),
("ํŽธ์ง‘์ž", "1๋ง‰ ํŽธ์ง‘", "โœ‚๏ธ 1๋ง‰ ํŽธ์ง‘ ๋ฐ ํŽ˜์ด์‹ฑ ์กฐ์ •"),
("๊ฐ๋…", "1๋ง‰ ์—ฐ์ถœ", "๐ŸŽญ 1๋ง‰ ๋น„์ฃผ์–ผ ๊ฐ•ํ™”"),
("๋Œ€ํ™”์ „๋ฌธ๊ฐ€", "1๋ง‰ ๋Œ€์‚ฌ", "๐Ÿ’ฌ 1๋ง‰ ๋Œ€์‚ฌ ๊ฐœ์„ "),
("๋น„ํ‰๊ฐ€", "1๋ง‰ ๊ฒ€ํ† ", "๐Ÿ” 1๋ง‰ ์ข…ํ•ฉ ๊ฒ€ํ† "),
("์Šคํ† ๋ฆฌ์ž‘๊ฐ€", "1๋ง‰ ์™„์„ฑ", "โœ… 1๋ง‰ ์ตœ์ข… ์™„์„ฑ๋ณธ"),
("์Šคํ† ๋ฆฌ์ž‘๊ฐ€", "2๋ง‰A ์ดˆ๊ณ ", "โœ๏ธ 2๋ง‰A ์ดˆ๊ณ  ์ž‘์„ฑ"),
("ํŽธ์ง‘์ž", "2๋ง‰A ํŽธ์ง‘", "โœ‚๏ธ 2๋ง‰A ํŽธ์ง‘ ๋ฐ ํŽ˜์ด์‹ฑ ์กฐ์ •"),
("๊ฐ๋…", "2๋ง‰A ์—ฐ์ถœ", "๐ŸŽญ 2๋ง‰A ๋น„์ฃผ์–ผ ๊ฐ•ํ™”"),
("๋Œ€ํ™”์ „๋ฌธ๊ฐ€", "2๋ง‰A ๋Œ€์‚ฌ", "๐Ÿ’ฌ 2๋ง‰A ๋Œ€์‚ฌ ๊ฐœ์„ "),
("๋น„ํ‰๊ฐ€", "2๋ง‰A ๊ฒ€ํ† ", "๐Ÿ” 2๋ง‰A ์ข…ํ•ฉ ๊ฒ€ํ† "),
("์Šคํ† ๋ฆฌ์ž‘๊ฐ€", "2๋ง‰A ์™„์„ฑ", "โœ… 2๋ง‰A ์ตœ์ข… ์™„์„ฑ๋ณธ"),
("์Šคํ† ๋ฆฌ์ž‘๊ฐ€", "2๋ง‰B ์ดˆ๊ณ ", "โœ๏ธ 2๋ง‰B ์ดˆ๊ณ  ์ž‘์„ฑ"),
("ํŽธ์ง‘์ž", "2๋ง‰B ํŽธ์ง‘", "โœ‚๏ธ 2๋ง‰B ํŽธ์ง‘ ๋ฐ ํŽ˜์ด์‹ฑ ์กฐ์ •"),
("๊ฐ๋…", "2๋ง‰B ์—ฐ์ถœ", "๐ŸŽญ 2๋ง‰B ๋น„์ฃผ์–ผ ๊ฐ•ํ™”"),
("๋Œ€ํ™”์ „๋ฌธ๊ฐ€", "2๋ง‰B ๋Œ€์‚ฌ", "๐Ÿ’ฌ 2๋ง‰B ๋Œ€์‚ฌ ๊ฐœ์„ "),
("๋น„ํ‰๊ฐ€", "2๋ง‰B ๊ฒ€ํ† ", "๐Ÿ” 2๋ง‰B ์ข…ํ•ฉ ๊ฒ€ํ† "),
("์Šคํ† ๋ฆฌ์ž‘๊ฐ€", "2๋ง‰B ์™„์„ฑ", "โœ… 2๋ง‰B ์ตœ์ข… ์™„์„ฑ๋ณธ"),
("์Šคํ† ๋ฆฌ์ž‘๊ฐ€", "3๋ง‰ ์ดˆ๊ณ ", "โœ๏ธ 3๋ง‰ ์ดˆ๊ณ  ์ž‘์„ฑ"),
("ํŽธ์ง‘์ž", "3๋ง‰ ํŽธ์ง‘", "โœ‚๏ธ 3๋ง‰ ํŽธ์ง‘ ๋ฐ ํŽ˜์ด์‹ฑ ์กฐ์ •"),
("๊ฐ๋…", "3๋ง‰ ์—ฐ์ถœ", "๐ŸŽญ 3๋ง‰ ๋น„์ฃผ์–ผ ๊ฐ•ํ™”"),
("๋Œ€ํ™”์ „๋ฌธ๊ฐ€", "3๋ง‰ ๋Œ€์‚ฌ", "๐Ÿ’ฌ 3๋ง‰ ๋Œ€์‚ฌ ๊ฐœ์„ "),
("๋น„ํ‰๊ฐ€", "3๋ง‰ ๊ฒ€ํ† ", "๐Ÿ” 3๋ง‰ ์ข…ํ•ฉ ๊ฒ€ํ† "),
("์Šคํ† ๋ฆฌ์ž‘๊ฐ€", "3๋ง‰ ์™„์„ฑ", "โœ… 3๋ง‰ ์ตœ์ข… ์™„์„ฑ๋ณธ"),
("์žฅ๋ฅด์ „๋ฌธ๊ฐ€", "์žฅ๋ฅด ์ตœ์ ํ™”", "๐ŸŽฏ ์žฅ๋ฅด ํŠน์„ฑ ์ตœ์ ํ™”"),
("๋น„ํ‰๊ฐ€", "์ตœ์ข… ๊ฒ€ํ† ", "๐Ÿ” ์ตœ์ข… ๊ฒ€ํ†  ๋ฐ ์™„์„ฑ๋„ ํ‰๊ฐ€"),
]
# ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค
@dataclass
class ExpertFeedback:
"""์ „๋ฌธ๊ฐ€ ํ”ผ๋“œ๋ฐฑ"""
role: str
stage: str
feedback: str
suggestions: List[str]
score: float
timestamp: datetime = field(default_factory=datetime.now)
@dataclass
class ScreenplayPlan:
"""์‹œ๋‚˜๋ฆฌ์˜ค ๊ธฐํš์•ˆ"""
title: str = ""
logline: str = ""
genre: str = ""
subgenre: str = ""
themes: List[str] = field(default_factory=list)
# ์‹œ๋†‰์‹œ์Šค
synopsis: str = ""
extended_synopsis: str = ""
# 3๋ง‰ ๊ตฌ์กฐ
act1_summary: str = ""
act2a_summary: str = ""
act2b_summary: str = ""
act3_summary: str = ""
# ์ฃผ์š” ์บ๋ฆญํ„ฐ
protagonist: Dict[str, str] = field(default_factory=dict)
antagonist: Dict[str, str] = field(default_factory=dict)
supporting_characters: List[Dict[str, str]] = field(default_factory=list)
# ์„ธ๊ณ„๊ด€
time_period: str = ""
locations: List[str] = field(default_factory=list)
atmosphere: str = ""
visual_style: str = ""
# ์”ฌ ๊ตฌ์„ฑ
total_scenes: int = 0
scene_breakdown: List[Dict] = field(default_factory=list)
# ์ „๋ฌธ๊ฐ€ ํ”ผ๋“œ๋ฐฑ
expert_feedbacks: List[ExpertFeedback] = field(default_factory=list)
# ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํด๋ž˜์Šค
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,
expert_feedbacks TEXT,
screenplay_content TEXT,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now')),
status TEXT DEFAULT 'planning',
current_stage INTEGER DEFAULT 0,
total_pages REAL DEFAULT 0
)
''')
cursor.execute('''
CREATE TABLE IF NOT EXISTS expert_reviews (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL,
role TEXT NOT NULL,
stage TEXT NOT NULL,
feedback TEXT,
suggestions 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_expert_feedback(session_id: str, role: str, stage: str,
feedback: str, suggestions: List[str], score: float):
with ScreenplayDatabase.get_db() as conn:
conn.cursor().execute(
'''INSERT INTO expert_reviews
(session_id, role, stage, feedback, suggestions, score)
VALUES (?, ?, ?, ?, ?, ?)''',
(session_id, role, stage, feedback, json.dumps(suggestions, ensure_ascii=False), score)
)
conn.commit()
@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_screenplay_content(session_id: str, content: str, title: str, logline: str):
with ScreenplayDatabase.get_db() as conn:
total_pages = len(content.split('\n')) / 58
conn.cursor().execute(
'''UPDATE screenplay_sessions
SET screenplay_content = ?, title = ?, logline = ?,
total_pages = ?, status = 'complete', updated_at = datetime('now')
WHERE session_id = ?''',
(content, title, logline, total_pages, 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.current_plan = ScreenplayPlan()
self.original_query = ""
self.accumulated_content = {}
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]:
try:
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 = ""
for line in response.iter_lines():
if not line:
continue
try:
line_str = line.decode('utf-8').strip()
if not line_str.startswith("data: "):
continue
data_str = line_str[6:]
if data_str == "[DONE]":
break
data = json.loads(data_str)
if "choices" in data and len(data["choices"]) > 0:
content = data["choices"][0].get("delta", {}).get("content", "")
if content:
buffer += content
if len(buffer) >= 100 or '\n' in buffer:
yield buffer
buffer = ""
except:
continue
if buffer:
yield buffer
except Exception as e:
yield f"โŒ ์˜ค๋ฅ˜: {str(e)}"
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"])
# ์ด์ „ ์ „๋ฌธ๊ฐ€๋“ค์˜ ํ”ผ๋“œ๋ฐฑ ์š”์•ฝ
previous_feedbacks = ""
if self.expert_feedbacks:
previous_feedbacks = "\nใ€์ด์ „ ์ „๋ฌธ๊ฐ€ ์˜๊ฒฌใ€‘\n"
for fb in self.expert_feedbacks[-3:]: # ์ตœ๊ทผ 3๊ฐœ๋งŒ
previous_feedbacks += f"[{fb.role}]: {fb.feedback[:200]}...\n"
base_prompt = f"""
ใ€๋‹น์‹ ์˜ ์—ญํ• ใ€‘
๋‹น์‹ ์€ {role}์ž…๋‹ˆ๋‹ค. {expert['description']}
์„ฑ๊ฒฉ: {expert['personality']}
์ง‘์ค‘ ์˜์—ญ: {focus_areas}
ใ€์›๋ณธ ์š”์ฒญใ€‘
{query}
ใ€์žฅ๋ฅดใ€‘ {genre}
ใ€ํ˜•์‹ใ€‘ {screenplay_type}
{previous_feedbacks}
ใ€์ด์ „ ์ž‘์—… ๋‚ด์šฉใ€‘
{self._summarize_previous_content(previous_content)}
"""
# ์—ญํ• ๋ณ„ ํŠนํ™” ํ”„๋กฌํ”„ํŠธ
if role == "ํ”„๋กœ๋“€์„œ":
return base_prompt + f"""
ใ€ํ”„๋กœ๋“€์„œ๋กœ์„œ ๋ถ„์„ํ•  ๋‚ด์šฉใ€‘
1. ์ƒ์—…์  ๊ฐ€์น˜์™€ ์‹œ์žฅ์„ฑ
- ํƒ€๊ฒŸ ๊ด€๊ฐ์ธต ๋ช…ํ™•ํ™”
- ์˜ˆ์ƒ ์ œ์ž‘๋น„ ๊ทœ๋ชจ
- ๋ฐฐ๊ธ‰ ์ „๋žต
2. ์ œ๋ชฉ๊ณผ ๋กœ๊ทธ๋ผ์ธ
- ๋งˆ์ผ€ํŒ… ๊ฐ€๋Šฅํ•œ ๋งค๋ ฅ์ ์ธ ์ œ๋ชฉ
- ํ•œ ๋ฌธ์žฅ์œผ๋กœ ํ•ต์‹ฌ ๊ฐˆ๋“ฑ ํ‘œํ˜„
3. ์œ ์‚ฌ ์„ฑ๊ณต์ž‘ ๋ถ„์„
- ์ตœ๊ทผ 3๋…„ ๋‚ด ์œ ์‚ฌ ์ž‘ํ’ˆ
- ์ฐจ๋ณ„ํ™” ํฌ์ธํŠธ
4. ์ œ์ž‘ ๋ฆฌ์Šคํฌ ํ‰๊ฐ€
- ๊ธฐ์ˆ ์  ๋‚œ์ด๋„
- ์บ์ŠคํŒ… ์š”๊ตฌ์‚ฌํ•ญ
โš ๏ธ ์›๋ณธ ์š”์ฒญ์„ ์ƒ์—…์ ์œผ๋กœ ์ตœ์ ํ™”ํ•˜๋˜ ํ•ต์‹ฌ์€ ์œ ์ง€ํ•˜์„ธ์š”."""
elif role == "์Šคํ† ๋ฆฌ์ž‘๊ฐ€":
return base_prompt + f"""
ใ€์Šคํ† ๋ฆฌ ์ž‘๊ฐ€๋กœ์„œ ๊ตฌ์„ฑํ•  ๋‚ด์šฉใ€‘
1. ์‹œ๋†‰์‹œ์Šค (500-800์ž)
- ์›๋ณธ ์š”์ฒญ์˜ ํ•ต์‹ฌ ์Šคํ† ๋ฆฌ
- ๋ช…ํ™•ํ•œ ์‹œ์ž‘-์ค‘๊ฐ„-๋
2. 3๋ง‰ ๊ตฌ์กฐ ์ƒ์„ธ
- 1๋ง‰ (25%): ์„ค์ •๊ณผ ์ด‰๋ฐœ ์‚ฌ๊ฑด
- 2๋ง‰A (25%): ์ƒˆ๋กœ์šด ์„ธ๊ณ„์™€ ์žฌ๋ฏธ
- 2๋ง‰B (25%): ์‹œ๋ จ๊ณผ ์œ„๊ธฐ
- 3๋ง‰ (25%): ํด๋ผ์ด๋งฅ์Šค์™€ ํ•ด๊ฒฐ
3. ์ฃผ์š” ํ”Œ๋กฏ ํฌ์ธํŠธ
- 10๊ฐœ์˜ ํ•ต์‹ฌ ์ „ํ™˜์ 
- ๊ฐ ์ „ํ™˜์ ์˜ ๊ฐ์ •์  ์ž„ํŒฉํŠธ
4. ํ…Œ๋งˆ์™€ ๋ฉ”์‹œ์ง€
- ์ค‘์‹ฌ ํ…Œ๋งˆ
- ์„œ๋ธŒ ํ…Œ๋งˆ๋“ค
โš ๏ธ ์›๋ณธ ์Šคํ† ๋ฆฌ์˜ ๋…ผ๋ฆฌ์  ํ๋ฆ„์„ ์™„๋ฒฝํ•˜๊ฒŒ ๊ตฌํ˜„ํ•˜์„ธ์š”."""
elif role == "์บ๋ฆญํ„ฐ๋””์ž์ด๋„ˆ":
return base_prompt + f"""
ใ€์บ๋ฆญํ„ฐ ๋””์ž์ด๋„ˆ๋กœ์„œ ์ฐฝ์กฐํ•  ๋‚ด์šฉใ€‘
1. ์ฃผ์ธ๊ณต ์ƒ์„ธ ํ”„๋กœํ•„
- ์ด๋ฆ„, ๋‚˜์ด, ์ง์—…
- ์„ฑ๊ฒฉ (MBTI ํฌํ•จ)
- ํ•ต์‹ฌ ์š•๊ตฌ(Want)์™€ ํ•„์š”(Need)
- ์น˜๋ช…์  ๊ฒฐํ•จ
- ๋ณ€ํ™” ์•„ํฌ
- ํŠน์ง•์  ๋งํˆฌ์™€ ํ–‰๋™
2. ์ ๋Œ€์ž ํ”„๋กœํ•„
- ์ฃผ์ธ๊ณต๊ณผ์˜ ๋Œ€๋ฆฝ ๊ตฌ์กฐ
- ๋‚˜๋ฆ„์˜ ์ •๋‹น์„ฑ
- ์•ฝ์ ๊ณผ ๋งน์ 
3. ์กฐ์—ฐ ์บ๋ฆญํ„ฐ๋“ค (3-5๋ช…)
- ๊ฐ์ž์˜ ์—ญํ• ๊ณผ ๊ธฐ๋Šฅ
- ์ฃผ์ธ๊ณต๊ณผ์˜ ๊ด€๊ณ„
- ๊ณ ์œ ํ•œ ํŠน์ง•
4. ์บ๋ฆญํ„ฐ ๊ด€๊ณ„๋„
- ์ธ๋ฌผ๊ฐ„ ์—ญํ•™ ๊ด€๊ณ„
- ๊ฐˆ๋“ฑ ๊ตฌ์กฐ
โš ๏ธ ์›๋ณธ์— ๋ช…์‹œ๋œ ์ธ๋ฌผ ์„ค์ •์„ ์ถฉ์‹คํžˆ ๋ฐœ์ „์‹œํ‚ค์„ธ์š”."""
elif role == "๊ฐ๋…":
return base_prompt + f"""
ใ€๊ฐ๋…์œผ๋กœ์„œ ์—ฐ์ถœํ•  ๋‚ด์šฉใ€‘
1. ๋น„์ฃผ์–ผ ์ปจ์…‰
- ์ „์ฒด์ ์ธ ๋ฃฉ์•คํ•„
- ์ƒ‰๊ฐ๊ณผ ํ†ค
- ์ฐธ์กฐ ์˜ํ™”/์ด๋ฏธ์ง€
2. ํ•ต์‹ฌ ๋น„์ฃผ์–ผ ์”ฌ (5๊ฐœ)
- ์˜คํ”„๋‹ ์ด๋ฏธ์ง€
- ์ฃผ์š” ์•ก์…˜/๊ฐ์ • ์”ฌ
- ํด๋ผ์ด๋งฅ์Šค ๋น„์ฃผ์–ผ
- ์—”๋”ฉ ์ด๋ฏธ์ง€
3. ์นด๋ฉ”๋ผ ์Šคํƒ€์ผ
- ์ฃผ์š” ์ƒท ๊ตฌ์„ฑ
- ์›€์ง์ž„๊ณผ ์•ต๊ธ€
- ํŠน์ˆ˜ ์ดฌ์˜ ๊ธฐ๋ฒ•
4. ์‚ฌ์šด๋“œ ๋””์ž์ธ
- ์Œ์•… ์Šคํƒ€์ผ
- ํ•ต์‹ฌ ์‚ฌ์šด๋“œ ๋ชจํ‹ฐํ”„
- ์นจ๋ฌต์˜ ํ™œ์šฉ
โš ๏ธ ์›๋ณธ์˜ ๋ถ„์œ„๊ธฐ์™€ ํ†ค์„ ์‹œ๊ฐ์ ์œผ๋กœ ๊ตฌํ˜„ํ•˜์„ธ์š”."""
elif role == "๋น„ํ‰๊ฐ€":
return base_prompt + f"""
ใ€๋น„ํ‰๊ฐ€๋กœ์„œ ๊ฒ€ํ† ํ•  ๋‚ด์šฉใ€‘
1. ์›๋ณธ ์ถฉ์‹ค๋„ ํ‰๊ฐ€ (40์ )
- ํ•ต์‹ฌ ์š”์ฒญ์‚ฌํ•ญ ๋ฐ˜์˜๋„
- ์ž„์˜ ๋ณ€๊ฒฝ ์‚ฌํ•ญ ์ง€์ 
2. ์Šคํ† ๋ฆฌ ์™„์„ฑ๋„ (20์ )
- ๋…ผ๋ฆฌ์  ์ผ๊ด€์„ฑ
- ๊ฐ์ •์  ํ˜ธ์†Œ๋ ฅ
- ํŽ˜์ด์‹ฑ๊ณผ ๋ฆฌ๋“ฌ
3. ์บ๋ฆญํ„ฐ ํ‰๊ฐ€ (20์ )
- ์ž…์ฒด์„ฑ๊ณผ ๋งค๋ ฅ
- ์„ฑ์žฅ ์•„ํฌ
- ๊ด€๊ณ„ ์—ญํ•™
4. ์ƒ์—…์„ฑ๊ณผ ์˜ˆ์ˆ ์„ฑ (20์ )
- ์‹œ์žฅ ์–ดํ•„
- ๋…์ฐฝ์„ฑ
- ์ œ์ž‘ ๊ฐ€๋Šฅ์„ฑ
5. ๊ตฌ์ฒด์  ๊ฐœ์„  ์ œ์•ˆ
- ์šฐ์„ ์ˆœ์œ„๋ณ„ ๊ฐœ์„ ์ 
- ์‹คํ–‰ ๊ฐ€๋Šฅํ•œ ํ•ด๊ฒฐ์ฑ…
์ด์ : /100์ 
โš ๏ธ ๊ฑด์„ค์ ์ด๋ฉด์„œ๋„ ์ •์งํ•œ ํ”ผ๋“œ๋ฐฑ์„ ์ œ๊ณตํ•˜์„ธ์š”."""
elif role == "ํŽธ์ง‘์ž":
return base_prompt + f"""
ใ€ํŽธ์ง‘์ž๋กœ์„œ ์กฐ์ •ํ•  ๋‚ด์šฉใ€‘
1. ํŽ˜์ด์‹ฑ ๋ถ„์„
- ๊ฐ ์”ฌ์˜ ๊ธธ์ด ์ ์ •์„ฑ
- ๊ธด์žฅ๊ณผ ์ด์™„์˜ ๋ฆฌ๋“ฌ
- ๋ถˆํ•„์š”ํ•œ ๋ถ€๋ถ„ ์‹๋ณ„
2. ๊ตฌ์กฐ ์ตœ์ ํ™”
- ์”ฌ ์ˆœ์„œ ์กฐ์ • ์ œ์•ˆ
- ์ „ํ™˜์˜ ์ž์—ฐ์Šค๋Ÿฌ์›€
- ์ •๋ณด ๊ณต๊ฐœ ํƒ€์ด๋ฐ
3. ํŠธ๋ฆฌ๋ฐ ์ œ์•ˆ
- ์‚ญ์ œ ๊ฐ€๋Šฅํ•œ ์”ฌ/๋Œ€์‚ฌ
- ์••์ถ• ๊ฐ€๋Šฅํ•œ ๋ถ€๋ถ„
- ์ค‘๋ณต ์ œ๊ฑฐ
4. ์ž„ํŒฉํŠธ ๊ฐ•ํ™”
- ํด๋ผ์ด๋งฅ์Šค ๋นŒ๋“œ์—…
- ๊ฐ์ •์  ๋น„ํŠธ ๊ฐ•ํ™”
โš ๏ธ ์›๋ณธ ์Šคํ† ๋ฆฌ๋ฅผ ๋” ํƒ€์ดํŠธํ•˜๊ณ  ์ž„ํŒฉํŠธ์žˆ๊ฒŒ ๋งŒ๋“œ์„ธ์š”."""
elif role == "๋Œ€ํ™”์ „๋ฌธ๊ฐ€":
return base_prompt + f"""
ใ€๋Œ€ํ™” ์ „๋ฌธ๊ฐ€๋กœ์„œ ๊ฐœ์„ ํ•  ๋‚ด์šฉใ€‘
1. ์บ๋ฆญํ„ฐ๋ณ„ ๊ณ ์œ  ํ™”๋ฒ•
- ์–ดํœ˜ ์ˆ˜์ค€๊ณผ ์Šคํƒ€์ผ
- ๋ง๋ฒ„๋ฆ‡๊ณผ ํŒจํ„ด
- ๊ฐ์ • ํ‘œํ˜„ ๋ฐฉ์‹
2. ์„œ๋ธŒํ…์ŠคํŠธ ๊ฐ•ํ™”
- ํ‘œ๋ฉด ๋Œ€์‚ฌ vs ์ง„์งœ ์˜๋ฏธ
- ์นจ๋ฌต๊ณผ ํ–‰๊ฐ„
- ๋น„์–ธ์–ด์  ์†Œํ†ต
3. ํ•ต์‹ฌ ๋Œ€์‚ฌ ์ฐฝ์กฐ
- ๋ช…๋Œ€์‚ฌ 5๊ฐœ
- ์บ๋ฆญํ„ฐ ์ •์ฒด์„ฑ ๋“œ๋Ÿฌ๋‚ด๊ธฐ
- ํ…Œ๋งˆ ์ „๋‹ฌ ๋Œ€์‚ฌ
4. ๋Œ€ํ™” ๋ฆฌ๋“ฌ
- ์ž์—ฐ์Šค๋Ÿฌ์šด ์ฃผ๊ณ ๋ฐ›๊ธฐ
- ๊ธด์žฅ ๊ณ ์กฐ ํŒจํ„ด
- ์œ ๋จธ์™€ ์œ„ํŠธ
โš ๏ธ ๊ฐ ์บ๋ฆญํ„ฐ์˜ ๋ชฉ์†Œ๋ฆฌ๋ฅผ ๋šœ๋ ทํ•˜๊ฒŒ ๊ตฌ๋ถ„ํ•˜์„ธ์š”."""
elif role == "์žฅ๋ฅด์ „๋ฌธ๊ฐ€":
genre_template = GENRE_TEMPLATES.get(genre, GENRE_TEMPLATES["๋“œ๋ผ๋งˆ"])
return base_prompt + f"""
ใ€{genre} ์žฅ๋ฅด ์ „๋ฌธ๊ฐ€๋กœ์„œ ์ตœ์ ํ™”ํ•  ๋‚ด์šฉใ€‘
1. ์žฅ๋ฅด ํ•„์ˆ˜ ์š”์†Œ ์ฒดํฌ
- ํ•ต์‹ฌ ์š”์†Œ: {', '.join(genre_template['key_elements'])}
- ๊ตฌ์กฐ ๋น„ํŠธ: {', '.join(genre_template['structure_beats'])}
2. ์žฅ๋ฅด ๊ด€์Šต ์ถฉ์กฑ๋„
- ๊ด€๊ฐ ๊ธฐ๋Œ€ ์ถฉ์กฑ
- ํด๋ฆฌ์…ฐ ํ™œ์šฉ๊ณผ ์ „๋ณต
- ์žฅ๋ฅด ํŠน์œ  ์žฅ์น˜
3. ํ†ค๊ณผ ๋ถ„์œ„๊ธฐ
- ํŽ˜์ด์‹ฑ: {genre_template['pacing']}
- ์”ฌ ๊ธธ์ด: {genre_template['scene_length']}
- ๋Œ€์‚ฌ ๋น„์œจ: {genre_template['dialogue_ratio']}
4. ์žฅ๋ฅด ํŠนํ™” ๊ฐ•ํ™”
- ๋ถ€์กฑํ•œ ์š”์†Œ ์ถ”๊ฐ€
- ๊ณผ๋„ํ•œ ๋ถ€๋ถ„ ์กฐ์ •
- ์žฅ๋ฅด ์ •์ฒด์„ฑ ๋ช…ํ™•ํ™”
โš ๏ธ {genre} ์žฅ๋ฅด์˜ ์ •์ˆ˜๋ฅผ ์™„๋ฒฝํ•˜๊ฒŒ ๊ตฌํ˜„ํ•˜์„ธ์š”."""
return base_prompt
def _summarize_previous_content(self, content: Dict) -> str:
"""์ด์ „ ๋‚ด์šฉ ์š”์•ฝ"""
summary = ""
for key, value in content.items():
if value:
summary += f"\n[{key}]\n{value[:300]}...\n"
return summary if summary else "์ฒซ ์ž‘์—…์ž…๋‹ˆ๋‹ค."
def generate_planning(self, query: str, screenplay_type: str, genre: str,
progress_callback=None) -> Generator[Tuple[str, float, Dict, List], None, None]:
"""๋‹ค์ค‘ ์ „๋ฌธ๊ฐ€ ํ˜‘์—… ๊ธฐํš์•ˆ ์ƒ์„ฑ"""
try:
self.original_query = query
self.current_session_id = ScreenplayDatabase.create_session(query, screenplay_type, genre)
planning_content = {}
self.accumulated_content = {}
self.expert_feedbacks = []
total_stages = len(PLANNING_STAGES)
for idx, (role, stage_key, stage_desc) in enumerate(PLANNING_STAGES):
progress = (idx / total_stages) * 100
yield f"๐Ÿ”„ {stage_desc} ์ง„ํ–‰ ์ค‘...", progress, planning_content, self.expert_feedbacks
# ์ „๋ฌธ๊ฐ€๋ณ„ ํ”„๋กฌํ”„ํŠธ ์ƒ์„ฑ
prompt = self.get_expert_prompt(
role, stage_key, query,
self.accumulated_content, genre, screenplay_type
)
# LLM ํ˜ธ์ถœ
messages = [
{"role": "system", "content": f"""๋‹น์‹ ์€ {role}์ž…๋‹ˆ๋‹ค.
{EXPERT_ROLES[role]['description']}
์ „๋ฌธ ๋ถ„์•ผ: {', '.join(EXPERT_ROLES[role]['focus'])}
์›๋ณธ ์š”์ฒญ์„ ์ ˆ๋Œ€์ ์œผ๋กœ ์ค€์ˆ˜ํ•˜๋ฉด์„œ ์ „๋ฌธ์„ฑ์„ ๋ฐœํœ˜ํ•˜์„ธ์š”."""},
{"role": "user", "content": prompt}
]
content = ""
for chunk in self.call_llm_streaming(messages):
content += chunk
planning_content[f"{role}_{stage_key}"] = content
yield f"โœ๏ธ {stage_desc} ์ž‘์„ฑ ์ค‘...", progress, planning_content, self.expert_feedbacks
# ์ „๋ฌธ๊ฐ€ ํ”ผ๋“œ๋ฐฑ ์ €์žฅ
feedback = ExpertFeedback(
role=role,
stage=stage_key,
feedback=content[:500],
suggestions=self._extract_suggestions(content),
score=self._calculate_score(content, query)
)
self.expert_feedbacks.append(feedback)
# DB์— ํ”ผ๋“œ๋ฐฑ ์ €์žฅ
ScreenplayDatabase.save_expert_feedback(
self.current_session_id, role, stage_key,
feedback.feedback, feedback.suggestions, feedback.score
)
# ๋ˆ„์  ๋‚ด์šฉ ์ €์žฅ
self.accumulated_content[f"{role}_{stage_key}"] = content
time.sleep(0.5)
# ์ตœ์ข… ๊ธฐํš์•ˆ ์ €์žฅ
ScreenplayDatabase.save_planning_data(self.current_session_id, planning_content)
yield "โœ… ์ „๋ฌธ๊ฐ€ ํ˜‘์—… ๊ธฐํš์•ˆ ์™„์„ฑ!", 100, planning_content, self.expert_feedbacks
except Exception as e:
yield f"โŒ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}", 0, {}, []
def generate_screenplay(self, session_id: str, planning_data: Dict,
progress_callback=None) -> Generator[Tuple[str, float, str, List], None, None]:
"""๋‹ค์ค‘ ์ „๋ฌธ๊ฐ€ ํ˜‘์—… ์‹œ๋‚˜๋ฆฌ์˜ค ์ž‘์„ฑ"""
try:
total_stages = len(WRITING_STAGES)
screenplay_content = ""
act_contents = {"1๋ง‰": "", "2๋ง‰A": "", "2๋ง‰B": "", "3๋ง‰": ""}
self.expert_feedbacks = []
for idx, (role, stage_name, stage_desc) in enumerate(WRITING_STAGES):
progress = (idx / total_stages) * 100
yield f"๐Ÿ”„ {stage_desc} ์ง„ํ–‰ ์ค‘...", progress, screenplay_content, self.expert_feedbacks
# ํ˜„์žฌ ๋ง‰ ๊ฒฐ์ •
current_act = ""
if "1๋ง‰" in stage_name:
current_act = "1๋ง‰"
elif "2๋ง‰A" in stage_name:
current_act = "2๋ง‰A"
elif "2๋ง‰B" in stage_name:
current_act = "2๋ง‰B"
elif "3๋ง‰" in stage_name:
current_act = "3๋ง‰"
# ์—ญํ• ๋ณ„ ํ”„๋กฌํ”„ํŠธ ์ƒ์„ฑ
if role == "์Šคํ† ๋ฆฌ์ž‘๊ฐ€":
prompt = self._create_writer_prompt(
current_act, planning_data, act_contents, stage_name
)
elif role == "ํŽธ์ง‘์ž":
prompt = self._create_editor_prompt(
current_act, act_contents[current_act] if current_act else screenplay_content
)
elif role == "๊ฐ๋…":
prompt = self._create_director_prompt(
current_act, act_contents[current_act] if current_act else screenplay_content
)
elif role == "๋Œ€ํ™”์ „๋ฌธ๊ฐ€":
prompt = self._create_dialogue_prompt(
current_act, act_contents[current_act] if current_act else screenplay_content
)
elif role == "๋น„ํ‰๊ฐ€":
prompt = self._create_critic_prompt(
current_act, act_contents[current_act] if current_act else screenplay_content,
self.original_query
)
elif role == "์žฅ๋ฅด์ „๋ฌธ๊ฐ€":
prompt = self._create_genre_expert_prompt(
screenplay_content, planning_data
)
else:
continue
# LLM ํ˜ธ์ถœ
expert = EXPERT_ROLES.get(role, EXPERT_ROLES["์Šคํ† ๋ฆฌ์ž‘๊ฐ€"])
messages = [
{"role": "system", "content": f"""๋‹น์‹ ์€ {role}์ž…๋‹ˆ๋‹ค.
{expert['description']}
์›๋ณธ ์š”์ฒญ: {self.original_query}
ํ‘œ์ค€ ์‹œ๋‚˜๋ฆฌ์˜ค ํฌ๋งท์„ ์ค€์ˆ˜ํ•˜์„ธ์š”."""},
{"role": "user", "content": prompt}
]
content = ""
for chunk in self.call_llm_streaming(messages, max_tokens=15000):
content += chunk
# ์™„์„ฑ ๋‹จ๊ณ„์—์„œ๋งŒ ๋ง‰ ๋‚ด์šฉ ์—…๋ฐ์ดํŠธ
if current_act and "์™„์„ฑ" in stage_name:
act_contents[current_act] = content
screenplay_content = "\n\n".join([
act_contents[act] for act in ["1๋ง‰", "2๋ง‰A", "2๋ง‰B", "3๋ง‰"]
if act_contents[act]
])
yield f"โœ๏ธ {stage_desc} ์ž‘์„ฑ ์ค‘...", progress, screenplay_content, self.expert_feedbacks
# ์ „๋ฌธ๊ฐ€ ํ”ผ๋“œ๋ฐฑ ์ƒ์„ฑ
feedback = ExpertFeedback(
role=role,
stage=stage_name,
feedback=f"{role}๊ฐ€ {stage_name}์„ ๊ฒ€ํ† /์ž‘์—…ํ–ˆ์Šต๋‹ˆ๋‹ค.",
suggestions=[],
score=85.0
)
self.expert_feedbacks.append(feedback)
time.sleep(0.5)
# ์ตœ์ข… ์ €์žฅ
title = self._extract_title(planning_data)
logline = self._extract_logline(planning_data)
ScreenplayDatabase.save_screenplay_content(session_id, screenplay_content, title, logline)
yield "โœ… ์ „๋ฌธ๊ฐ€ ํ˜‘์—… ์‹œ๋‚˜๋ฆฌ์˜ค ์™„์„ฑ!", 100, screenplay_content, self.expert_feedbacks
except Exception as e:
yield f"โŒ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}", 0, "", []
def _create_writer_prompt(self, act: str, planning_data: Dict,
previous_acts: Dict, stage_name: str) -> str:
"""์Šคํ† ๋ฆฌ ์ž‘๊ฐ€ ํ”„๋กฌํ”„ํŠธ"""
if "์ดˆ๊ณ " in stage_name:
min_lines = 800
elif "์™„์„ฑ" in stage_name:
min_lines = 1200
else:
min_lines = 1000
return f"""ใ€{act} ์ž‘์„ฑใ€‘
๋ชฉํ‘œ ๋ถ„๋Ÿ‰: {min_lines}์ค„ ์ด์ƒ
ใ€๊ธฐํš์•ˆ ํ•ต์‹ฌใ€‘
{self._extract_planning_core(planning_data)}
ใ€์ด์ „ ๋ง‰ใ€‘
{previous_acts if previous_acts else "์ฒซ ๋ง‰์ž…๋‹ˆ๋‹ค"}
ใ€์ž‘์„ฑ ์š”๊ตฌ์‚ฌํ•ญใ€‘
1. ํ‘œ์ค€ ์‹œ๋‚˜๋ฆฌ์˜ค ํฌ๋งท
2. ์ถฉ๋ถ„ํ•œ ์”ฌ ๋ถ„๋Ÿ‰ (๊ฐ 5-7ํŽ˜์ด์ง€)
3. ์‹œ๊ฐ์  ์•ก์…˜ ๋ฌ˜์‚ฌ
4. ์ž์—ฐ์Šค๋Ÿฌ์šด ๋Œ€ํ™”
5. ์›๋ณธ ์Šคํ† ๋ฆฌ ์ถฉ์‹ค
๋ฐ˜๋“œ์‹œ {min_lines}์ค„ ์ด์ƒ ์ž‘์„ฑํ•˜์„ธ์š”."""
def _create_editor_prompt(self, act: str, content: str) -> str:
"""ํŽธ์ง‘์ž ํ”„๋กฌํ”„ํŠธ"""
return f"""ใ€{act} ํŽธ์ง‘ใ€‘
ํ˜„์žฌ ๋‚ด์šฉ:
{content[:2000]}...
ใ€ํŽธ์ง‘ ํฌ์ธํŠธใ€‘
1. ํŽ˜์ด์‹ฑ ์กฐ์ •
- ๋А๋ฆฐ ๋ถ€๋ถ„ ๊ฐ€์†
- ๊ธ‰ํ•œ ๋ถ€๋ถ„ ์™„ํ™”
2. ๋ถˆํ•„์š”ํ•œ ๋ถ€๋ถ„ ์ œ๊ฑฐ
- ์ค‘๋ณต ๋Œ€์‚ฌ/์•ก์…˜
- ๊ณผ๋„ํ•œ ์„ค๋ช…
3. ์”ฌ ์ „ํ™˜ ๊ฐœ์„ 
- ์ž์—ฐ์Šค๋Ÿฌ์šด ์—ฐ๊ฒฐ
- ์‹œ๊ฐ„/๊ณต๊ฐ„ ์ด๋™
4. ๊ธด์žฅ๊ฐ ์กฐ์ ˆ
- ํด๋ผ์ด๋งฅ์Šค ๋นŒ๋“œ์—…
- ์™„๊ธ‰ ์กฐ์ ˆ
๊ตฌ์ฒด์ ์ธ ํŽธ์ง‘ ์ œ์•ˆ์„ ํ•˜์„ธ์š”."""
def _create_director_prompt(self, act: str, content: str) -> str:
"""๊ฐ๋… ํ”„๋กฌํ”„ํŠธ"""
return f"""ใ€{act} ์—ฐ์ถœ ๊ฐ•ํ™”ใ€‘
ํ˜„์žฌ ์‹œ๋‚˜๋ฆฌ์˜ค:
{content[:2000]}...
ใ€๋น„์ฃผ์–ผ ๊ฐ•ํ™” ํฌ์ธํŠธใ€‘
1. ์˜คํ”„๋‹ ์”ฌ ๋น„์ฃผ์–ผ
2. ํ•ต์‹ฌ ์•ก์…˜ ์‹œํ€€์Šค
3. ๊ฐ์ •์  ํด๋กœ์ฆˆ์—…
4. ํ™˜๊ฒฝ/๋ถ„์œ„๊ธฐ ๋ฌ˜์‚ฌ
5. ์ƒ์ง•์  ์ด๋ฏธ์ง€
๊ฐ ์”ฌ์— ์‹œ๊ฐ์  ๋””ํ…Œ์ผ์„ ์ถ”๊ฐ€ํ•˜์„ธ์š”."""
def _create_dialogue_prompt(self, act: str, content: str) -> str:
"""๋Œ€ํ™” ์ „๋ฌธ๊ฐ€ ํ”„๋กฌํ”„ํŠธ"""
return f"""ใ€{act} ๋Œ€์‚ฌ ๊ฐœ์„ ใ€‘
ํ˜„์žฌ ๋Œ€์‚ฌ ๊ฒ€ํ† :
{content[:2000]}...
ใ€๊ฐœ์„  ๋ฐฉํ–ฅใ€‘
1. ์บ๋ฆญํ„ฐ๋ณ„ ๋งํˆฌ ์ฐจ๋ณ„ํ™”
2. ์„œ๋ธŒํ…์ŠคํŠธ ์ถ”๊ฐ€
3. ๋ถˆํ•„์š”ํ•œ ์„ค๋ช… ์ œ๊ฑฐ
4. ๊ฐ์ •์  ๋‰˜์•™์Šค ๊ฐ•ํ™”
5. ๋ฆฌ๋“ฌ๊ณผ ํ…œํฌ ์กฐ์ •
๊ฐœ์„ ๋œ ๋Œ€์‚ฌ ์˜ˆ์‹œ๋ฅผ ์ œ์‹œํ•˜์„ธ์š”."""
def _create_critic_prompt(self, act: str, content: str, original_query: str) -> str:
"""๋น„ํ‰๊ฐ€ ํ”„๋กฌํ”„ํŠธ"""
return f"""ใ€{act} ์ข…ํ•ฉ ๊ฒ€ํ† ใ€‘
์›๋ณธ ์š”์ฒญ: {original_query}
ํ˜„์žฌ ๋‚ด์šฉ:
{content[:2000]}...
ใ€ํ‰๊ฐ€ ํ•ญ๋ชฉใ€‘
1. ์›๋ณธ ์ถฉ์‹ค๋„ (40์ )
2. ์Šคํ† ๋ฆฌ ์™„์„ฑ๋„ (20์ )
3. ์บ๋ฆญํ„ฐ ๋งค๋ ฅ (20์ )
4. ๋Œ€์‚ฌ ํ’ˆ์งˆ (10์ )
5. ๋น„์ฃผ์–ผ ์ž„ํŒฉํŠธ (10์ )
์ด์ : /100
ใ€๊ฐœ์„  ํ•„์š”์‚ฌํ•ญใ€‘
๊ตฌ์ฒด์ ์ด๊ณ  ์‹คํ–‰ ๊ฐ€๋Šฅํ•œ ์ œ์•ˆ์„ ํ•˜์„ธ์š”."""
def _create_genre_expert_prompt(self, screenplay: str, planning_data: Dict) -> str:
"""์žฅ๋ฅด ์ „๋ฌธ๊ฐ€ ํ”„๋กฌํ”„ํŠธ"""
genre = planning_data.get("์žฅ๋ฅด", "๋“œ๋ผ๋งˆ")
genre_template = GENRE_TEMPLATES.get(genre, GENRE_TEMPLATES["๋“œ๋ผ๋งˆ"])
return f"""ใ€{genre} ์žฅ๋ฅด ์ตœ์ ํ™”ใ€‘
ใ€์žฅ๋ฅด ํŠน์„ฑใ€‘
- ํ•ต์‹ฌ ์š”์†Œ: {', '.join(genre_template['key_elements'])}
- ๊ตฌ์กฐ ๋น„ํŠธ: {', '.join(genre_template['structure_beats'])}
ใ€์ตœ์ ํ™” ํฌ์ธํŠธใ€‘
1. ์žฅ๋ฅด ๊ด€์Šต ์ถฉ์กฑ๋„
2. ํŽ˜์ด์‹ฑ ์กฐ์ •
3. ํŠน์œ  ์š”์†Œ ๊ฐ•ํ™”
4. ํ†ค ์ผ๊ด€์„ฑ
์žฅ๋ฅด ํŠน์„ฑ์„ ๊ทน๋Œ€ํ™”ํ•˜๋Š” ์ œ์•ˆ์„ ํ•˜์„ธ์š”."""
def _extract_suggestions(self, content: str) -> List[str]:
"""๋‚ด์šฉ์—์„œ ์ œ์•ˆ์‚ฌํ•ญ ์ถ”์ถœ"""
suggestions = []
lines = content.split('\n')
for line in lines:
if any(keyword in line for keyword in ['์ œ์•ˆ', '๊ฐœ์„ ', '์ถ”์ฒœ', '๊ถŒ์žฅ']):
suggestions.append(line.strip())
return suggestions[:5]
def _calculate_score(self, content: str, original_query: str) -> float:
"""์ถฉ์‹ค๋„ ์ ์ˆ˜ ๊ณ„์‚ฐ"""
score = 70.0 # ๊ธฐ๋ณธ ์ ์ˆ˜
# ์›๋ณธ ํ‚ค์›Œ๋“œ ํฌํ•จ๋„ ์ฒดํฌ
keywords = original_query.split()
matched = sum(1 for keyword in keywords if keyword in content)
score += (matched / len(keywords)) * 20 if keywords else 0
# ๊ธธ์ด ์ฒดํฌ
if len(content) > 500:
score += 10
return min(score, 100.0)
def _extract_title(self, planning_data: Dict) -> str:
"""๊ธฐํš์•ˆ์—์„œ ์ œ๋ชฉ ์ถ”์ถœ"""
for key, value in planning_data.items():
if "์ œ๋ชฉ" in value:
match = re.search(r'์ œ๋ชฉ[:\s]*([^\n]+)', value)
if match:
return match.group(1).strip()
return "๋ฌด์ œ"
def _extract_logline(self, planning_data: Dict) -> str:
"""๊ธฐํš์•ˆ์—์„œ ๋กœ๊ทธ๋ผ์ธ ์ถ”์ถœ"""
for key, value in planning_data.items():
if "๋กœ๊ทธ๋ผ์ธ" in value:
match = re.search(r'๋กœ๊ทธ๋ผ์ธ[:\s]*([^\n]+)', value)
if match:
return match.group(1).strip()
return ""
def _extract_planning_core(self, planning_data: Dict) -> str:
"""๊ธฐํš์•ˆ ํ•ต์‹ฌ ๋‚ด์šฉ ์ถ”์ถœ"""
core = ""
for key, value in planning_data.items():
if any(k in key for k in ["์Šคํ† ๋ฆฌ", "์บ๋ฆญํ„ฐ", "๊ตฌ์กฐ"]):
core += f"\n[{key}]\n{value[:500]}...\n"
return core
# ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜
def format_planning_display(planning_data: Dict) -> str:
"""๊ธฐํš์•ˆ ํ‘œ์‹œ ํฌ๋งท"""
if not planning_data:
return "๊ธฐํš์•ˆ์ด ์•„์ง ์ƒ์„ฑ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."
formatted = "# ๐Ÿ“‹ ์‹œ๋‚˜๋ฆฌ์˜ค ๊ธฐํš์•ˆ (์ „๋ฌธ๊ฐ€ ํ˜‘์—…)\n\n"
for key, content in planning_data.items():
role = key.split('_')[0] if '_' in key else key
emoji = EXPERT_ROLES.get(role, {}).get("emoji", "๐Ÿ“")
formatted += f"## {emoji} {key}\n\n"
formatted += content + "\n\n"
formatted += "---\n\n"
return formatted
def format_expert_feedbacks(feedbacks: List[ExpertFeedback]) -> str:
"""์ „๋ฌธ๊ฐ€ ํ”ผ๋“œ๋ฐฑ ํ‘œ์‹œ"""
if not feedbacks:
return ""
formatted = "## ๐ŸŽฏ ์ „๋ฌธ๊ฐ€ ํ‰๊ฐ€\n\n"
for fb in feedbacks:
emoji = EXPERT_ROLES.get(fb.role, {}).get("emoji", "๐Ÿ“")
formatted += f"### {emoji} {fb.role}\n"
formatted += f"**์ ์ˆ˜:** {fb.score:.1f}/100\n"
formatted += f"**ํ”ผ๋“œ๋ฐฑ:** {fb.feedback[:200]}...\n"
if fb.suggestions:
formatted += "**์ œ์•ˆ์‚ฌํ•ญ:**\n"
for suggestion in fb.suggestions[:3]:
formatted += f"- {suggestion}\n"
formatted += "\n"
return formatted
def format_screenplay_display(screenplay_text: str) -> str:
"""์‹œ๋‚˜๋ฆฌ์˜ค ํ‘œ์‹œ ํฌ๋งท"""
if not screenplay_text:
return "์‹œ๋‚˜๋ฆฌ์˜ค๊ฐ€ ์•„์ง ์ž‘์„ฑ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."
formatted = "# ๐ŸŽฌ ์‹œ๋‚˜๋ฆฌ์˜ค\n\n"
# ์”ฌ ํ—ค๋”ฉ ๊ฐ•์กฐ
formatted_text = re.sub(
r'^(INT\.|EXT\.).*$',
r'**\g<0>**',
screenplay_text,
flags=re.MULTILINE
)
# ์บ๋ฆญํ„ฐ๋ช… ๊ฐ•์กฐ
formatted_text = re.sub(
r'^([๊ฐ€-ํžฃA-Z][๊ฐ€-ํžฃA-Z\s]+)$',
r'**\g<0>**',
formatted_text,
flags=re.MULTILINE
)
# ํŽ˜์ด์ง€ ์ˆ˜ ๊ณ„์‚ฐ
page_count = len(screenplay_text.splitlines()) / 58
formatted = f"**์ด ํŽ˜์ด์ง€: {page_count:.1f}**\n\n" + formatted_text
return formatted
def generate_random_concept(screenplay_type: str, genre: str) -> str:
"""๋žœ๋ค ์ปจ์…‰ ์ƒ์„ฑ"""
concepts = {
'์•ก์…˜': [
"์€ํ‡ดํ•œ ํŠน์ˆ˜์š”์›์ด ๋‚ฉ์น˜๋œ ๋”ธ์„ ๊ตฌํ•˜๊ธฐ ์œ„ํ•ด ๋‹ค์‹œ ํ˜„์žฅ์œผ๋กœ ๋ณต๊ท€ํ•œ๋‹ค",
"ํ‰๋ฒ”ํ•œ ํƒ์‹œ๊ธฐ์‚ฌ๊ฐ€ ์šฐ์—ฐํžˆ ๊ตญ์ œ ํ…Œ๋Ÿฌ ์กฐ์ง์˜ ์Œ๋ชจ์— ํœ˜๋ง๋ฆฐ๋‹ค",
],
'์Šค๋ฆด๋Ÿฌ': [
"๊ธฐ์–ต์„ ์žƒ์€ ๋‚จ์ž๊ฐ€ ์ž์‹ ์ด ์—ฐ์‡„์‚ด์ธ๋ฒ”์ด๋ผ๋Š” ์ฆ๊ฑฐ๋ฅผ ๋ฐœ๊ฒฌํ•œ๋‹ค",
"์‹ค์ข…๋œ ์•„์ด๋ฅผ ์ฐพ๋Š” ๊ณผ์ •์—์„œ ๋งˆ์„์˜ ๋”์ฐํ•œ ๋น„๋ฐ€์ด ๋“œ๋Ÿฌ๋‚œ๋‹ค",
],
'๋“œ๋ผ๋งˆ': [
"๋ง๊ธฐ ์•” ํ™˜์ž๊ฐ€ ๋งˆ์ง€๋ง‰ ์†Œ์›์œผ๋กœ ๊ฐ€์กฑ๊ณผ์˜ ํ™”ํ•ด๋ฅผ ์‹œ๋„ํ•œ๋‹ค",
"์žฌ๊ฐœ๋ฐœ๋กœ ์‚ฌ๋ผ์งˆ ๋™๋„ค๋ฅผ ์ง€ํ‚ค๋ ค๋Š” ์ฃผ๋ฏผ๋“ค์˜ ๋งˆ์ง€๋ง‰ ์‹ธ์›€",
],
}
genre_concepts = concepts.get(genre, concepts['๋“œ๋ผ๋งˆ'])
return random.choice(genre_concepts)
# 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);
}
.header-title {
font-size: 3rem;
margin-bottom: 1rem;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
}
.expert-panel {
background: #f0f4f8;
padding: 1rem;
border-radius: 10px;
margin: 1rem 0;
}
.expert-badge {
display: inline-block;
padding: 0.3rem 0.8rem;
background: white;
border-radius: 20px;
margin: 0.2rem;
font-size: 0.9rem;
}
.progress-bar {
background: #e0e0e0;
border-radius: 10px;
height: 30px;
position: relative;
margin: 1rem 0;
}
.progress-fill {
background: linear-gradient(90deg, #667eea, #764ba2);
height: 100%;
border-radius: 10px;
transition: width 0.3s ease;
}
"""
with gr.Blocks(theme=gr.themes.Soft(), css=css, title="AI ์‹œ๋‚˜๋ฆฌ์˜ค ์ž‘๊ฐ€ - ์ „๋ฌธ๊ฐ€ ํ˜‘์—… ์‹œ์Šคํ…œ") as interface:
gr.HTML("""
<div class="main-header">
<h1 class="header-title">๐ŸŽฌ AI ์‹œ๋‚˜๋ฆฌ์˜ค ์ž‘๊ฐ€</h1>
<p style="font-size: 1.2rem; opacity: 0.95;">
8๋ช…์˜ ์ „๋ฌธ๊ฐ€๊ฐ€ ํ˜‘์—…ํ•˜์—ฌ ์™„๋ฒฝํ•œ ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค<br>
ํ”„๋กœ๋“€์„œ, ๊ฐ๋…, ์ž‘๊ฐ€, ๋น„ํ‰๊ฐ€๊ฐ€ ํ•จ๊ป˜ ๋งŒ๋“œ๋Š” ์ „๋ฌธ ์‹œ๋‚˜๋ฆฌ์˜ค
</p>
</div>
""")
# ์ „๋ฌธ๊ฐ€ ์†Œ๊ฐœ
gr.HTML("""
<div class="expert-panel">
<h3>๐Ÿ‘ฅ ์ฐธ์—ฌ ์ „๋ฌธ๊ฐ€</h3>
<span class="expert-badge">๐ŸŽฌ ํ”„๋กœ๋“€์„œ</span>
<span class="expert-badge">๐Ÿ“– ์Šคํ† ๋ฆฌ์ž‘๊ฐ€</span>
<span class="expert-badge">๐Ÿ‘ฅ ์บ๋ฆญํ„ฐ๋””์ž์ด๋„ˆ</span>
<span class="expert-badge">๐ŸŽญ ๊ฐ๋…</span>
<span class="expert-badge">๐Ÿ” ๋น„ํ‰๊ฐ€</span>
<span class="expert-badge">โœ‚๏ธ ํŽธ์ง‘์ž</span>
<span class="expert-badge">๐Ÿ’ฌ ๋Œ€ํ™”์ „๋ฌธ๊ฐ€</span>
<span class="expert-badge">๐ŸŽฏ ์žฅ๋ฅด์ „๋ฌธ๊ฐ€</span>
</div>
""")
current_session_id = gr.State(None)
current_planning_data = gr.State({})
current_feedbacks = gr.State([])
with gr.Tabs():
with gr.Tab("๐Ÿ“ ์ƒˆ ์‹œ๋‚˜๋ฆฌ์˜ค"):
with gr.Row():
with gr.Column(scale=2):
query_input = gr.Textbox(
label="๐Ÿ’ก ์‹œ๋‚˜๋ฆฌ์˜ค ์•„์ด๋””์–ด",
placeholder="""๊ตฌ์ฒด์ ์ธ ์Šคํ† ๋ฆฌ๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”. ์˜ˆ์‹œ:
"2045๋…„ ์„œ์šธ, AI๊ฐ€ ์ธ๊ฐ„ ๊ฐ์ •์„ ์™„๋ฒฝํžˆ ๋ชจ๋ฐฉํ•˜๋Š” ์‹œ๋Œ€.
AI ์œค๋ฆฌํ•™์ž๊ฐ€ ์ฃฝ์€ ์•„๋‚ด๋ฅผ ๋ณต์ œํ•œ AI์™€ ์‚ด๋‹ค๊ฐ€
์ง„์งœ ์•„๋‚ด๊ฐ€ ์‚ด์•„์žˆ๋‹ค๋Š” ์ฆ๊ฑฐ๋ฅผ ๋ฐœ๊ฒฌํ•˜๊ณ  ์ง„์‹ค์„ ์ฐพ์•„ ๋‚˜์„ ๋‹ค."
""",
lines=6
)
with gr.Column(scale=1):
screenplay_type = gr.Radio(
choices=list(SCREENPLAY_LENGTHS.keys()),
value="์˜ํ™”",
label="๐Ÿ“ฝ๏ธ ์‹œ๋‚˜๋ฆฌ์˜ค ์œ ํ˜•"
)
genre_select = gr.Dropdown(
choices=list(GENRE_TEMPLATES.keys()),
value="๋“œ๋ผ๋งˆ",
label="๐ŸŽญ ์žฅ๋ฅด"
)
with gr.Row():
random_btn = gr.Button("๐ŸŽฒ ๋žœ๋ค ์•„์ด๋””์–ด", scale=1)
clear_btn = gr.Button("๐Ÿ—‘๏ธ ์ดˆ๊ธฐํ™”", scale=1)
planning_btn = gr.Button("๐Ÿ“‹ ์ „๋ฌธ๊ฐ€ ๊ธฐํš ํšŒ์˜", variant="primary", scale=2)
# ์ง„ํ–‰ ์ƒํƒœ
progress_bar = gr.HTML(
value='<div class="progress-bar"><div class="progress-fill" style="width: 0%"></div></div>'
)
status_text = gr.Textbox(
label="๐Ÿ“Š ์ง„ํ–‰ ์ƒํƒœ",
interactive=False,
value="์•„์ด๋””์–ด๋ฅผ ์ž…๋ ฅํ•˜๊ณ  ์ „๋ฌธ๊ฐ€ ๊ธฐํš ํšŒ์˜๋ฅผ ์‹œ์ž‘ํ•˜์„ธ์š”"
)
# ์ „๋ฌธ๊ฐ€ ํ”ผ๋“œ๋ฐฑ
with gr.Group():
gr.Markdown("### ๐ŸŽฏ ์ „๋ฌธ๊ฐ€ ํ”ผ๋“œ๋ฐฑ")
expert_feedback_display = gr.Markdown(
value="*์ „๋ฌธ๊ฐ€๋“ค์˜ ์˜๊ฒฌ์ด ์—ฌ๊ธฐ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค...*"
)
# ๊ธฐํš์•ˆ
with gr.Group():
gr.Markdown("### ๐Ÿ“‹ ์‹œ๋‚˜๋ฆฌ์˜ค ๊ธฐํš์•ˆ")
planning_display = gr.Markdown(
value="*๊ธฐํš์•ˆ์ด ์—ฌ๊ธฐ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค...*"
)
with gr.Row():
save_planning_btn = gr.Button("๐Ÿ’พ ๊ธฐํš์•ˆ ์ €์žฅ", scale=1)
generate_screenplay_btn = gr.Button("๐ŸŽฌ ์ „๋ฌธ๊ฐ€ ์‹œ๋‚˜๋ฆฌ์˜ค ์ž‘์„ฑ", variant="primary", scale=2)
# ์‹œ๋‚˜๋ฆฌ์˜ค ์ถœ๋ ฅ
with gr.Group():
gr.Markdown("### ๐Ÿ“„ ์™„์„ฑ๋œ ์‹œ๋‚˜๋ฆฌ์˜ค")
screenplay_output = gr.Markdown(
value="*์‹œ๋‚˜๋ฆฌ์˜ค๊ฐ€ ์—ฌ๊ธฐ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค...*"
)
download_btn = gr.Button("๐Ÿ’พ ๋‹ค์šด๋กœ๋“œ (TXT)")
# ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ
def handle_planning(query, s_type, genre):
if not query:
yield "", "โŒ ์•„์ด๋””์–ด๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”", "", "", {}
return
system = ScreenplayGenerationSystem()
for status, progress, planning_data, feedbacks in system.generate_planning(query, s_type, genre):
progress_html = f'<div class="progress-bar"><div class="progress-fill" style="width: {progress}%"></div></div>'
planning_display = format_planning_display(planning_data)
feedback_display = format_expert_feedbacks(feedbacks)
yield progress_html, status, feedback_display, planning_display, planning_data
def handle_screenplay_generation(session_id, planning_data):
if not planning_data:
yield "", "โŒ ๋จผ์ € ๊ธฐํš์•ˆ์„ ์ƒ์„ฑํ•ด์ฃผ์„ธ์š”", "", ""
return
system = ScreenplayGenerationSystem()
for status, progress, screenplay, feedbacks in system.generate_screenplay(session_id, planning_data):
progress_html = f'<div class="progress-bar"><div class="progress-fill" style="width: {progress}%"></div></div>'
screenplay_display = format_screenplay_display(screenplay)
feedback_display = format_expert_feedbacks(feedbacks)
yield progress_html, status, feedback_display, screenplay_display
# ์ด๋ฒคํŠธ ์—ฐ๊ฒฐ
planning_btn.click(
fn=handle_planning,
inputs=[query_input, screenplay_type, genre_select],
outputs=[progress_bar, status_text, expert_feedback_display, planning_display, current_planning_data]
)
generate_screenplay_btn.click(
fn=handle_screenplay_generation,
inputs=[current_session_id, current_planning_data],
outputs=[progress_bar, status_text, expert_feedback_display, screenplay_output]
)
random_btn.click(
fn=generate_random_concept,
inputs=[screenplay_type, genre_select],
outputs=[query_input]
)
return interface
# ๋ฉ”์ธ ์‹คํ–‰
if __name__ == "__main__":
logger.info("=" * 60)
logger.info("AI ์‹œ๋‚˜๋ฆฌ์˜ค ์ž‘๊ฐ€ - ์ „๋ฌธ๊ฐ€ ํ˜‘์—… ์‹œ์Šคํ…œ")
logger.info("8๋ช…์˜ ์ „๋ฌธ๊ฐ€๊ฐ€ ํ˜‘์—…ํ•˜์—ฌ ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค")
logger.info("=" * 60)
if not FIREWORKS_API_KEY or FIREWORKS_API_KEY == "dummy_token_for_testing":
logger.warning("โš ๏ธ FIREWORKS_API_KEY๋ฅผ ์„ค์ •ํ•ด์ฃผ์„ธ์š”!")
logger.warning("export FIREWORKS_API_KEY='your-api-key'")
# ์ „๋ฌธ๊ฐ€ ์—ญํ•  ์†Œ๊ฐœ
logger.info("\n์ฐธ์—ฌ ์ „๋ฌธ๊ฐ€:")
for role, info in EXPERT_ROLES.items():
logger.info(f" {info['emoji']} {role}: {info['description']}")
ScreenplayDatabase.init_db()
interface = create_interface()
interface.launch(
server_name="0.0.0.0",
server_port=7860,
share=False
)