Spaces:
Running
Running
| import gradio as gr | |
| import os | |
| import json | |
| import requests | |
| from datetime import datetime | |
| import time | |
| from typing import List, Dict, Any, Generator, Tuple, Optional, Set | |
| import logging | |
| import re | |
| import tempfile | |
| from pathlib import Path | |
| import sqlite3 | |
| import hashlib | |
| import threading | |
| from contextlib import contextmanager | |
| from dataclasses import dataclass, field, asdict | |
| from collections import defaultdict | |
| 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: | |
| """์บ๋ฆญํฐ ๊ด๊ณ๋์ ๊ฐ๋ฑ ๊ตฌ์กฐ ๋ค์ด์ด๊ทธ๋จ ์์ฑ""" | |
| 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 | |
| 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 | |
| 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 | |
| # ๋ฐ์ดํฐ ํด๋์ค | |
| class ExpertFeedback: | |
| """์ ๋ฌธ๊ฐ ํผ๋๋ฐฑ""" | |
| role: str | |
| stage: str | |
| feedback: str | |
| suggestions: List[str] | |
| score: float | |
| timestamp: datetime = field(default_factory=datetime.now) | |
| 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) | |
| def progress_percentage(self): | |
| if self.total_stages == 0: | |
| return 0 | |
| return (self.current_stage / self.total_stages) * 100 | |
| # ๋ฐ์ดํฐ๋ฒ ์ด์ค ํด๋์ค | |
| class ScreenplayDatabase: | |
| 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() | |
| def get_db(): | |
| with db_lock: | |
| conn = sqlite3.connect(DB_PATH, timeout=30.0) | |
| conn.row_factory = sqlite3.Row | |
| try: | |
| yield conn | |
| finally: | |
| conn.close() | |
| def create_session(user_query: str, 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 | |
| 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() | |
| 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() | |
| 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) | |
| 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) |