openfree commited on
Commit
6837145
ยท
verified ยท
1 Parent(s): 83edd7e

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +1102 -0
app.py ADDED
@@ -0,0 +1,1102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import os
3
+ import json
4
+ import requests
5
+ from datetime import datetime
6
+ import time
7
+ from typing import List, Dict, Any, Generator, Tuple, Optional, Set
8
+ import logging
9
+ import re
10
+ import tempfile
11
+ from pathlib import Path
12
+ import sqlite3
13
+ import hashlib
14
+ import threading
15
+ from contextlib import contextmanager
16
+ from dataclasses import dataclass, field, asdict
17
+ from collections import defaultdict
18
+ import random
19
+
20
+ # --- ๋กœ๊น… ์„ค์ • ---
21
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
22
+ logger = logging.getLogger(__name__)
23
+
24
+ # --- ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋ฐ ์ƒ์ˆ˜ ---
25
+ FIREWORKS_API_KEY = os.getenv("FIREWORKS_API_KEY", "")
26
+ BRAVE_SEARCH_API_KEY = os.getenv("BRAVE_SEARCH_API_KEY", "")
27
+ API_URL = "https://api.fireworks.ai/inference/v1/chat/completions"
28
+ MODEL_ID = "accounts/fireworks/models/qwen3-235b-a22b-instruct-2507"
29
+ BRAVE_SEARCH_URL = "https://api.search.brave.com/res/v1/web/search"
30
+ DB_PATH = "screenplay_sessions_korean.db"
31
+
32
+ # ์‹œ๋‚˜๋ฆฌ์˜ค ๊ธธ์ด ์„ค์ •
33
+ SCREENPLAY_LENGTHS = {
34
+ "์˜ํ™”": {"pages": 120, "description": "์žฅํŽธ ์˜ํ™” (110-130ํŽ˜์ด์ง€)", "min_pages": 110},
35
+ "๋“œ๋ผ๋งˆ": {"pages": 60, "description": "TV ๋“œ๋ผ๋งˆ (55-65ํŽ˜์ด์ง€)", "min_pages": 55},
36
+ "์›น๋“œ๋ผ๋งˆ": {"pages": 50, "description": "์›น/OTT ์‹œ๋ฆฌ์ฆˆ (45-55ํŽ˜์ด์ง€)", "min_pages": 45},
37
+ "๋‹จํŽธ": {"pages": 20, "description": "๋‹จํŽธ ์˜ํ™” (15-25ํŽ˜์ด์ง€)", "min_pages": 15}
38
+ }
39
+
40
+ # ํ™˜๊ฒฝ ๊ฒ€์ฆ
41
+ if not FIREWORKS_API_KEY:
42
+ logger.error("FIREWORKS_API_KEY๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.")
43
+ FIREWORKS_API_KEY = "dummy_token_for_testing"
44
+
45
+ # ๊ธ€๋กœ๋ฒŒ ๋ณ€์ˆ˜
46
+ db_lock = threading.Lock()
47
+
48
+ # ์ „๋ฌธ๊ฐ€ ์—ญํ•  ์ •์˜
49
+ EXPERT_ROLES = {
50
+ "ํ”„๋กœ๋“€์„œ": {
51
+ "emoji": "๐ŸŽฌ",
52
+ "description": "์ƒ์—…์„ฑ๊ณผ ์‹œ์žฅ์„ฑ ๋ถ„์„",
53
+ "focus": ["ํƒ€๊ฒŸ ๊ด€๊ฐ", "์ œ์ž‘ ๊ฐ€๋Šฅ์„ฑ", "์˜ˆ์‚ฐ ๊ทœ๋ชจ", "๋งˆ์ผ€ํŒ… ํฌ์ธํŠธ"],
54
+ "personality": "์‹ค์šฉ์ ์ด๊ณ  ์‹œ์žฅ ์ง€ํ–ฅ์ "
55
+ },
56
+ "์Šคํ† ๋ฆฌ์ž‘๊ฐ€": {
57
+ "emoji": "๐Ÿ“–",
58
+ "description": "๋‚ด๋Ÿฌํ‹ฐ๋ธŒ ๊ตฌ์กฐ์™€ ํ”Œ๋กฏ ๊ฐœ๋ฐœ",
59
+ "focus": ["3๋ง‰ ๊ตฌ์กฐ", "ํ”Œ๋กฏ ํฌ์ธํŠธ", "์„œ์‚ฌ ์•„ํฌ", "ํ…Œ๋งˆ"],
60
+ "personality": "์ฐฝ์˜์ ์ด๊ณ  ๊ตฌ์กฐ์ "
61
+ },
62
+ "์บ๋ฆญํ„ฐ๋””์ž์ด๋„ˆ": {
63
+ "emoji": "๐Ÿ‘ฅ",
64
+ "description": "์ธ๋ฌผ ์ฐฝ์กฐ์™€ ๊ด€๊ณ„ ์„ค๊ณ„",
65
+ "focus": ["์บ๋ฆญํ„ฐ ์•„ํฌ", "๋™๊ธฐ๋ถ€์—ฌ", "๊ด€๊ณ„ ์—ญํ•™", "๋Œ€ํ™” ์Šคํƒ€์ผ"],
66
+ "personality": "์‹ฌ๋ฆฌํ•™์ ์ด๊ณ  ๊ณต๊ฐ์ "
67
+ },
68
+ "๊ฐ๋…": {
69
+ "emoji": "๐ŸŽญ",
70
+ "description": "๋น„์ฃผ์–ผ ์Šคํ† ๋ฆฌํ…”๋ง๊ณผ ์—ฐ์ถœ",
71
+ "focus": ["์‹œ๊ฐ์  ๊ตฌ์„ฑ", "์นด๋ฉ”๋ผ ์›Œํฌ", "๋ฏธ์žฅ์„ผ", "๋ฆฌ๋“ฌ๊ณผ ํŽ˜์ด์‹ฑ"],
72
+ "personality": "๋น„์ฃผ์–ผ ์ค‘์‹ฌ์ ์ด๊ณ  ์˜ˆ์ˆ ์ "
73
+ },
74
+ "๊ณ ์ฆ์ „๋ฌธ๊ฐ€": {
75
+ "emoji": "๐Ÿ”Ž",
76
+ "description": "์‚ฌ์‹ค ํ™•์ธ๊ณผ ๊ณ ์ฆ",
77
+ "focus": ["์—ญ์‚ฌ์  ์ •ํ™•์„ฑ", "๊ณผํ•™์  ํƒ€๋‹น์„ฑ", "๋ฌธํ™”์  ์ ์ ˆ์„ฑ", "ํ˜„์‹ค์„ฑ"],
78
+ "personality": "์ •ํ™•ํ•˜๊ณ  ์„ธ์‹ฌํ•œ"
79
+ },
80
+ "๋น„ํ‰๊ฐ€": {
81
+ "emoji": "๐Ÿ”",
82
+ "description": "๊ฐ๊ด€์  ๋ถ„์„๊ณผ ๊ฐœ์„ ์  ์ œ์‹œ",
83
+ "focus": ["๋…ผ๋ฆฌ์  ์ผ๊ด€์„ฑ", "๊ฐ์ •์  ์ž„ํŒฉํŠธ", "์›์ž‘ ์ถฉ์‹ค๋„", "์™„์„ฑ๋„"],
84
+ "personality": "๋ถ„์„์ ์ด๊ณ  ๋น„ํŒ์ "
85
+ },
86
+ "ํŽธ์ง‘์ž": {
87
+ "emoji": "โœ‚๏ธ",
88
+ "description": "ํŽ˜์ด์‹ฑ๊ณผ ๊ตฌ์กฐ ์ตœ์ ํ™”",
89
+ "focus": ["์”ฌ ์ „ํ™˜", "๋ฆฌ๋“ฌ", "๊ธด์žฅ๊ฐ ์กฐ์ ˆ", "๋ถˆํ•„์š”ํ•œ ๋ถ€๋ถ„ ์ œ๊ฑฐ"],
90
+ "personality": "์ •๋ฐ€ํ•˜๊ณ  ํšจ์œจ์ "
91
+ },
92
+ "๋Œ€ํ™”์ „๋ฌธ๊ฐ€": {
93
+ "emoji": "๐Ÿ’ฌ",
94
+ "description": "๋Œ€์‚ฌ์™€ ์„œ๋ธŒํ…์ŠคํŠธ ๊ฐ•ํ™”",
95
+ "focus": ["์ž์—ฐ์Šค๋Ÿฌ์šด ๋Œ€ํ™”", "์บ๋ฆญํ„ฐ ๋ณด์ด์Šค", "์„œ๋ธŒํ…์ŠคํŠธ", "๊ฐ์ • ์ „๋‹ฌ"],
96
+ "personality": "์–ธ์–ด์ ์ด๊ณ  ๋‰˜์•™์Šค ์ค‘์‹ฌ"
97
+ }
98
+ }
99
+
100
+ # ๋ง‰๋ณ„ ์ž‘์„ฑ ๋‹จ๊ณ„
101
+ ACT_WRITING_STAGES = {
102
+ "1๋ง‰": [
103
+ ("์Šคํ† ๋ฆฌ์ž‘๊ฐ€", "์ดˆ๊ณ ", "โœ๏ธ ์Šคํ† ๋ฆฌ์ž‘๊ฐ€: 1๋ง‰ ์ดˆ๊ณ  ์ž‘์„ฑ"),
104
+ ("๊ณ ์ฆ์ „๋ฌธ๊ฐ€", "๊ฒ€์ฆ", "๐Ÿ”Ž ๊ณ ์ฆ์ „๋ฌธ๊ฐ€: ์‚ฌ์‹ค ํ™•์ธ ๋ฐ ๊ฒ€์ฆ"),
105
+ ("ํŽธ์ง‘์ž", "ํŽธ์ง‘", "โœ‚๏ธ ํŽธ์ง‘์ž: ๊ตฌ์กฐ ๋ฐ ํŽ˜์ด์‹ฑ ์กฐ์ •"),
106
+ ("๊ฐ๋…", "์—ฐ์ถœ", "๐ŸŽญ ๊ฐ๋…: ๋น„์ฃผ์–ผ ๊ฐ•ํ™” ๋ฐ ์—ฐ์ถœ ๋…ธํŠธ"),
107
+ ("๋Œ€ํ™”์ „๋ฌธ๊ฐ€", "๋Œ€์‚ฌ", "๐Ÿ’ฌ ๋Œ€ํ™”์ „๋ฌธ๊ฐ€: ๋Œ€์‚ฌ ๊ฐœ์„ "),
108
+ ("๋น„ํ‰๊ฐ€", "๊ฒ€ํ† ", "๐Ÿ” ๋น„ํ‰๊ฐ€: ์ข…ํ•ฉ ๊ฒ€ํ†  ๋ฐ ํ‰๊ฐ€"),
109
+ ("์Šคํ† ๋ฆฌ์ž‘๊ฐ€", "์™„์„ฑ", "โœ… ์Šคํ† ๋ฆฌ์ž‘๊ฐ€: 1๋ง‰ ์ตœ์ข… ์™„์„ฑ")
110
+ ],
111
+ "2๋ง‰A": [
112
+ ("์Šคํ† ๋ฆฌ์ž‘๊ฐ€", "์ดˆ๊ณ ", "โœ๏ธ ์Šคํ† ๋ฆฌ์ž‘๊ฐ€: 2๋ง‰A ์ดˆ๊ณ  ์ž‘์„ฑ"),
113
+ ("๊ณ ์ฆ์ „๋ฌธ๊ฐ€", "๊ฒ€์ฆ", "๐Ÿ”Ž ๊ณ ์ฆ์ „๋ฌธ๊ฐ€: ์‚ฌ์‹ค ํ™•์ธ ๋ฐ ๊ฒ€์ฆ"),
114
+ ("ํŽธ์ง‘์ž", "ํŽธ์ง‘", "โœ‚๏ธ ํŽธ์ง‘์ž: ๊ตฌ์กฐ ๋ฐ ํŽ˜์ด์‹ฑ ์กฐ์ •"),
115
+ ("๊ฐ๋…", "์—ฐ์ถœ", "๐ŸŽญ ๊ฐ๋…: ๋น„์ฃผ์–ผ ๊ฐ•ํ™” ๋ฐ ์—ฐ์ถœ ๋…ธํŠธ"),
116
+ ("๋Œ€ํ™”์ „๋ฌธ๊ฐ€", "๋Œ€์‚ฌ", "๐Ÿ’ฌ ๋Œ€ํ™”์ „๋ฌธ๊ฐ€: ๋Œ€์‚ฌ ๊ฐœ์„ "),
117
+ ("๋น„ํ‰๊ฐ€", "๊ฒ€ํ† ", "๐Ÿ” ๋น„ํ‰๊ฐ€: ์ข…ํ•ฉ ๊ฒ€ํ†  ๋ฐ ํ‰๊ฐ€"),
118
+ ("์Šคํ† ๋ฆฌ์ž‘๊ฐ€", "์™„์„ฑ", "โœ… ์Šคํ† ๋ฆฌ์ž‘๊ฐ€: 2๋ง‰A ์ตœ์ข… ์™„์„ฑ")
119
+ ],
120
+ "2๋ง‰B": [
121
+ ("์Šคํ† ๋ฆฌ์ž‘๊ฐ€", "์ดˆ๊ณ ", "โœ๏ธ ์Šคํ† ๋ฆฌ์ž‘๊ฐ€: 2๋ง‰B ์ดˆ๊ณ  ์ž‘์„ฑ"),
122
+ ("๊ณ ์ฆ์ „๋ฌธ๊ฐ€", "๊ฒ€์ฆ", "๐Ÿ”Ž ๊ณ ์ฆ์ „๋ฌธ๊ฐ€: ์‚ฌ์‹ค ํ™•์ธ ๋ฐ ๊ฒ€์ฆ"),
123
+ ("ํŽธ์ง‘์ž", "ํŽธ์ง‘", "โœ‚๏ธ ํŽธ์ง‘์ž: ๊ตฌ์กฐ ๋ฐ ํŽ˜์ด์‹ฑ ์กฐ์ •"),
124
+ ("๊ฐ๋…", "์—ฐ์ถœ", "๐ŸŽญ ๊ฐ๋…: ๋น„์ฃผ์–ผ ๊ฐ•ํ™” ๋ฐ ์—ฐ์ถœ ๋…ธํŠธ"),
125
+ ("๋Œ€ํ™”์ „๋ฌธ๊ฐ€", "๋Œ€์‚ฌ", "๐Ÿ’ฌ ๋Œ€ํ™”์ „๋ฌธ๊ฐ€: ๋Œ€์‚ฌ ๊ฐœ์„ "),
126
+ ("๋น„ํ‰๊ฐ€", "๊ฒ€ํ† ", "๐Ÿ” ๋น„ํ‰๊ฐ€: ์ข…ํ•ฉ ๊ฒ€ํ†  ๋ฐ ํ‰๊ฐ€"),
127
+ ("์Šคํ† ๋ฆฌ์ž‘๊ฐ€", "์™„์„ฑ", "โœ… ์Šคํ† ๋ฆฌ์ž‘๊ฐ€: 2๋ง‰B ์ตœ์ข… ์™„์„ฑ")
128
+ ],
129
+ "3๋ง‰": [
130
+ ("์Šคํ† ๋ฆฌ์ž‘๊ฐ€", "์ดˆ๊ณ ", "โœ๏ธ ์Šคํ† ๋ฆฌ์ž‘๊ฐ€: 3๋ง‰ ์ดˆ๊ณ  ์ž‘์„ฑ"),
131
+ ("๊ณ ์ฆ์ „๋ฌธ๊ฐ€", "๊ฒ€์ฆ", "๐Ÿ”Ž ๊ณ ์ฆ์ „๋ฌธ๊ฐ€: ์‚ฌ์‹ค ํ™•์ธ ๋ฐ ๊ฒ€์ฆ"),
132
+ ("ํŽธ์ง‘์ž", "ํŽธ์ง‘", "โœ‚๏ธ ํŽธ์ง‘์ž: ๊ตฌ์กฐ ๋ฐ ํŽ˜์ด์‹ฑ ์กฐ์ •"),
133
+ ("๊ฐ๋…", "์—ฐ์ถœ", "๐ŸŽญ ๊ฐ๋…: ๋น„์ฃผ์–ผ ๊ฐ•ํ™” ๋ฐ ์—ฐ์ถœ ๋…ธํŠธ"),
134
+ ("๋Œ€ํ™”์ „๋ฌธ๊ฐ€", "๋Œ€์‚ฌ", "๐Ÿ’ฌ ๋Œ€ํ™”์ „๋ฌธ๊ฐ€: ๋Œ€์‚ฌ ๊ฐœ์„ "),
135
+ ("๋น„ํ‰๊ฐ€", "๊ฒ€ํ† ", "๐Ÿ” ๋น„ํ‰๊ฐ€: ์ข…ํ•ฉ ๊ฒ€ํ†  ๋ฐ ํ‰๊ฐ€"),
136
+ ("์Šคํ† ๋ฆฌ์ž‘๊ฐ€", "์™„์„ฑ", "โœ… ์Šคํ† ๋ฆฌ์ž‘๊ฐ€: 3๋ง‰ ์ตœ์ข… ์™„์„ฑ")
137
+ ]
138
+ }
139
+
140
+ # ์›น ๊ฒ€์ƒ‰ ํด๋ž˜์Šค
141
+ class WebSearcher:
142
+ """์‚ฌ์‹ค ํ™•์ธ๊ณผ ๊ณ ์ฆ์„ ์œ„ํ•œ ์›น ๊ฒ€์ƒ‰"""
143
+ def __init__(self):
144
+ self.api_key = BRAVE_SEARCH_API_KEY
145
+ self.enabled = bool(self.api_key)
146
+
147
+ def search(self, query: str, count: int = 3) -> List[Dict]:
148
+ """์›น ๊ฒ€์ƒ‰ ์ˆ˜ํ–‰"""
149
+ if not self.enabled:
150
+ return []
151
+
152
+ headers = {
153
+ "Accept": "application/json",
154
+ "X-Subscription-Token": self.api_key
155
+ }
156
+
157
+ params = {
158
+ "q": query,
159
+ "count": count,
160
+ "search_lang": "ko",
161
+ "safesearch": "moderate"
162
+ }
163
+
164
+ try:
165
+ response = requests.get(BRAVE_SEARCH_URL, headers=headers, params=params, timeout=10)
166
+ if response.status_code == 200:
167
+ results = response.json().get("web", {}).get("results", [])
168
+ return results
169
+ else:
170
+ logger.error(f"Search API error: {response.status_code}")
171
+ return []
172
+ except Exception as e:
173
+ logger.error(f"Search error: {e}")
174
+ return []
175
+
176
+ def verify_facts(self, content: str, context: str) -> Dict:
177
+ """๋‚ด์šฉ์˜ ์‚ฌ์‹ค ํ™•์ธ"""
178
+ verification_results = {
179
+ "verified": [],
180
+ "needs_correction": [],
181
+ "suggestions": []
182
+ }
183
+
184
+ # ์ฃผ์š” ํ‚ค์›Œ๋“œ ์ถ”์ถœ
185
+ keywords = self._extract_keywords(content)
186
+
187
+ for keyword in keywords[:3]: # ์ƒ์œ„ 3๊ฐœ๋งŒ ๊ฒ€์ƒ‰
188
+ search_query = f"{keyword} {context} ์‚ฌ์‹ค ํ™•์ธ"
189
+ results = self.search(search_query, count=2)
190
+
191
+ if results:
192
+ verification_results["verified"].append({
193
+ "keyword": keyword,
194
+ "sources": [r["title"] for r in results]
195
+ })
196
+
197
+ return verification_results
198
+
199
+ def _extract_keywords(self, content: str) -> List[str]:
200
+ """์ฃผ์š” ํ‚ค์›Œ๋“œ ์ถ”์ถœ"""
201
+ # ๊ฐ„๋‹จํ•œ ํ‚ค์›Œ๋“œ ์ถ”์ถœ (์‹ค์ œ๋กœ๋Š” ๋” ์ •๊ตํ•œ ๋ฐฉ๋ฒ• ํ•„์š”)
202
+ words = content.split()
203
+ keywords = [w for w in words if len(w) > 4][:5]
204
+ return keywords
205
+
206
+ # ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค
207
+ @dataclass
208
+ class ExpertFeedback:
209
+ """์ „๋ฌธ๊ฐ€ ํ”ผ๋“œ๋ฐฑ"""
210
+ role: str
211
+ stage: str
212
+ feedback: str
213
+ suggestions: List[str]
214
+ score: float
215
+ timestamp: datetime = field(default_factory=datetime.now)
216
+
217
+ @dataclass
218
+ class ActProgress:
219
+ """๋ง‰๋ณ„ ์ง„ํ–‰ ์ƒํ™ฉ"""
220
+ act_name: str
221
+ current_stage: int
222
+ total_stages: int
223
+ current_expert: str
224
+ status: str # "ready", "in_progress", "complete"
225
+ content: str = ""
226
+ expert_feedbacks: List[ExpertFeedback] = field(default_factory=list)
227
+
228
+ @property
229
+ def progress_percentage(self):
230
+ if self.total_stages == 0:
231
+ return 0
232
+ return (self.current_stage / self.total_stages) * 100
233
+
234
+ # ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํด๋ž˜์Šค
235
+ class ScreenplayDatabase:
236
+ @staticmethod
237
+ def init_db():
238
+ with sqlite3.connect(DB_PATH) as conn:
239
+ conn.execute("PRAGMA journal_mode=WAL")
240
+ cursor = conn.cursor()
241
+
242
+ cursor.execute('''
243
+ CREATE TABLE IF NOT EXISTS screenplay_sessions (
244
+ session_id TEXT PRIMARY KEY,
245
+ user_query TEXT NOT NULL,
246
+ screenplay_type TEXT NOT NULL,
247
+ genre TEXT NOT NULL,
248
+ target_pages INTEGER,
249
+ title TEXT,
250
+ logline TEXT,
251
+ planning_data TEXT,
252
+ act1_content TEXT,
253
+ act2a_content TEXT,
254
+ act2b_content TEXT,
255
+ act3_content TEXT,
256
+ expert_feedbacks TEXT,
257
+ created_at TEXT DEFAULT (datetime('now')),
258
+ updated_at TEXT DEFAULT (datetime('now')),
259
+ status TEXT DEFAULT 'planning',
260
+ current_act TEXT DEFAULT '1๋ง‰',
261
+ total_pages REAL DEFAULT 0
262
+ )
263
+ ''')
264
+
265
+ cursor.execute('''
266
+ CREATE TABLE IF NOT EXISTS act_progress (
267
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
268
+ session_id TEXT NOT NULL,
269
+ act_name TEXT NOT NULL,
270
+ stage_num INTEGER,
271
+ expert_role TEXT,
272
+ content TEXT,
273
+ feedback TEXT,
274
+ score REAL,
275
+ created_at TEXT DEFAULT (datetime('now')),
276
+ FOREIGN KEY (session_id) REFERENCES screenplay_sessions(session_id)
277
+ )
278
+ ''')
279
+
280
+ conn.commit()
281
+
282
+ @staticmethod
283
+ @contextmanager
284
+ def get_db():
285
+ with db_lock:
286
+ conn = sqlite3.connect(DB_PATH, timeout=30.0)
287
+ conn.row_factory = sqlite3.Row
288
+ try:
289
+ yield conn
290
+ finally:
291
+ conn.close()
292
+
293
+ @staticmethod
294
+ def create_session(user_query: str, screenplay_type: str, genre: str) -> str:
295
+ session_id = hashlib.md5(f"{user_query}{screenplay_type}{datetime.now()}".encode()).hexdigest()
296
+ target_pages = SCREENPLAY_LENGTHS[screenplay_type]["pages"]
297
+
298
+ with ScreenplayDatabase.get_db() as conn:
299
+ conn.cursor().execute(
300
+ '''INSERT INTO screenplay_sessions
301
+ (session_id, user_query, screenplay_type, genre, target_pages)
302
+ VALUES (?, ?, ?, ?, ?)''',
303
+ (session_id, user_query, screenplay_type, genre, target_pages)
304
+ )
305
+ conn.commit()
306
+ return session_id
307
+
308
+ @staticmethod
309
+ def save_act_content(session_id: str, act_name: str, content: str):
310
+ """๋ง‰๋ณ„ ์ฝ˜ํ…์ธ  ์ €์žฅ"""
311
+ act_column = {
312
+ "1๋ง‰": "act1_content",
313
+ "2๋ง‰A": "act2a_content",
314
+ "2๋ง‰B": "act2b_content",
315
+ "3๋ง‰": "act3_content"
316
+ }.get(act_name, "act1_content")
317
+
318
+ with ScreenplayDatabase.get_db() as conn:
319
+ conn.cursor().execute(
320
+ f'''UPDATE screenplay_sessions
321
+ SET {act_column} = ?, current_act = ?, updated_at = datetime('now')
322
+ WHERE session_id = ?''',
323
+ (content, act_name, session_id)
324
+ )
325
+ conn.commit()
326
+
327
+ @staticmethod
328
+ def save_act_progress(session_id: str, act_name: str, stage_num: int,
329
+ expert_role: str, content: str, feedback: str, score: float):
330
+ """๋ง‰๋ณ„ ์ง„ํ–‰ ์ƒํ™ฉ ์ €์žฅ"""
331
+ with ScreenplayDatabase.get_db() as conn:
332
+ conn.cursor().execute(
333
+ '''INSERT INTO act_progress
334
+ (session_id, act_name, stage_num, expert_role, content, feedback, score)
335
+ VALUES (?, ?, ?, ?, ?, ?, ?)''',
336
+ (session_id, act_name, stage_num, expert_role, content, feedback, score)
337
+ )
338
+ conn.commit()
339
+
340
+ @staticmethod
341
+ def get_session_data(session_id: str) -> Optional[Dict]:
342
+ """์„ธ์…˜ ๋ฐ์ดํ„ฐ ์กฐํšŒ"""
343
+ with ScreenplayDatabase.get_db() as conn:
344
+ row = conn.cursor().execute(
345
+ 'SELECT * FROM screenplay_sessions WHERE session_id = ?',
346
+ (session_id,)
347
+ ).fetchone()
348
+ if row:
349
+ return dict(row)
350
+ return None
351
+
352
+ # ์‹œ๋‚˜๋ฆฌ์˜ค ์ƒ์„ฑ ์‹œ์Šคํ…œ
353
+ class ScreenplayGenerationSystem:
354
+ def __init__(self):
355
+ self.api_key = FIREWORKS_API_KEY
356
+ self.api_url = API_URL
357
+ self.model_id = MODEL_ID
358
+ self.current_session_id = None
359
+ self.original_query = ""
360
+ self.planning_data = {}
361
+ self.web_searcher = WebSearcher()
362
+ self.current_act_progress = None
363
+ ScreenplayDatabase.init_db()
364
+
365
+ def create_headers(self):
366
+ if not self.api_key or self.api_key == "dummy_token_for_testing":
367
+ raise ValueError("์œ ํšจํ•œ FIREWORKS_API_KEY๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค")
368
+
369
+ return {
370
+ "Accept": "application/json",
371
+ "Content-Type": "application/json",
372
+ "Authorization": f"Bearer {self.api_key}"
373
+ }
374
+
375
+ def call_llm_streaming(self, messages: List[Dict[str, str]], max_tokens: int = 8000) -> Generator[str, None, None]:
376
+ try:
377
+ payload = {
378
+ "model": self.model_id,
379
+ "messages": messages,
380
+ "max_tokens": max_tokens,
381
+ "temperature": 0.7,
382
+ "top_p": 0.9,
383
+ "top_k": 40,
384
+ "presence_penalty": 0.3,
385
+ "frequency_penalty": 0.3,
386
+ "stream": True
387
+ }
388
+
389
+ headers = self.create_headers()
390
+
391
+ response = requests.post(
392
+ self.api_url,
393
+ headers=headers,
394
+ json=payload,
395
+ stream=True,
396
+ timeout=300
397
+ )
398
+
399
+ if response.status_code != 200:
400
+ yield f"โŒ API ์˜ค๋ฅ˜: {response.status_code}"
401
+ return
402
+
403
+ buffer = ""
404
+ for line in response.iter_lines():
405
+ if not line:
406
+ continue
407
+
408
+ try:
409
+ line_str = line.decode('utf-8').strip()
410
+ if not line_str.startswith("data: "):
411
+ continue
412
+
413
+ data_str = line_str[6:]
414
+ if data_str == "[DONE]":
415
+ break
416
+
417
+ data = json.loads(data_str)
418
+ if "choices" in data and len(data["choices"]) > 0:
419
+ content = data["choices"][0].get("delta", {}).get("content", "")
420
+ if content:
421
+ buffer += content
422
+ if len(buffer) >= 100 or '\n' in buffer:
423
+ yield buffer
424
+ buffer = ""
425
+ except:
426
+ continue
427
+
428
+ if buffer:
429
+ yield buffer
430
+
431
+ except Exception as e:
432
+ yield f"โŒ ์˜ค๋ฅ˜: {str(e)}"
433
+
434
+ def generate_act(self, session_id: str, act_name: str, planning_data: Dict,
435
+ previous_acts: Dict) -> Generator[Tuple[ActProgress, str], None, None]:
436
+ """๋ง‰๋ณ„ ์‹œ๋‚˜๋ฆฌ์˜ค ์ƒ์„ฑ - ์ „๋ฌธ๊ฐ€ ํ˜‘์—…"""
437
+
438
+ self.current_session_id = session_id
439
+ self.planning_data = planning_data
440
+
441
+ stages = ACT_WRITING_STAGES.get(act_name, [])
442
+ act_progress = ActProgress(
443
+ act_name=act_name,
444
+ current_stage=0,
445
+ total_stages=len(stages),
446
+ current_expert="",
447
+ status="in_progress"
448
+ )
449
+
450
+ act_content = ""
451
+
452
+ for idx, (role, stage_type, description) in enumerate(stages):
453
+ act_progress.current_stage = idx + 1
454
+ act_progress.current_expert = role
455
+
456
+ # ์ง„ํ–‰ ์ƒํ™ฉ ์—…๋ฐ์ดํŠธ
457
+ yield act_progress, f"๐Ÿ”„ {description} ์ง„ํ–‰ ์ค‘..."
458
+
459
+ # ์—ญํ• ๋ณ„ ํ”„๋กฌํ”„ํŠธ ์ƒ์„ฑ
460
+ if role == "์Šคํ† ๋ฆฌ์ž‘๊ฐ€":
461
+ prompt = self._create_writer_prompt(act_name, planning_data, previous_acts, stage_type)
462
+ elif role == "๊ณ ์ฆ์ „๋ฌธ๊ฐ€":
463
+ prompt = self._create_verification_prompt(act_name, act_content)
464
+ elif role == "ํŽธ์ง‘์ž":
465
+ prompt = self._create_editor_prompt(act_name, act_content)
466
+ elif role == "๊ฐ๋…":
467
+ prompt = self._create_director_prompt(act_name, act_content)
468
+ elif role == "๋Œ€ํ™”์ „๋ฌธ๊ฐ€":
469
+ prompt = self._create_dialogue_prompt(act_name, act_content)
470
+ elif role == "๋น„ํ‰๊ฐ€":
471
+ prompt = self._create_critic_prompt(act_name, act_content)
472
+ else:
473
+ continue
474
+
475
+ # LLM ํ˜ธ์ถœ
476
+ expert_info = EXPERT_ROLES[role]
477
+ messages = [
478
+ {"role": "system", "content": f"""๋‹น์‹ ์€ {role}์ž…๋‹ˆ๋‹ค.
479
+ {expert_info['description']}
480
+ ์ „๋ฌธ ๋ถ„์•ผ: {', '.join(expert_info['focus'])}"""},
481
+ {"role": "user", "content": prompt}
482
+ ]
483
+
484
+ expert_output = ""
485
+ for chunk in self.call_llm_streaming(messages, max_tokens=10000):
486
+ expert_output += chunk
487
+
488
+ # ์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ
489
+ yield act_progress, f"โœ๏ธ {role}: {expert_output[:100]}..."
490
+
491
+ # ์›น ๊ฒ€์ƒ‰์„ ํ†ตํ•œ ๊ณ ์ฆ (๊ณ ์ฆ์ „๋ฌธ๊ฐ€์ผ ๋•Œ๋งŒ)
492
+ if role == "๊ณ ์ฆ์ „๋ฌธ๊ฐ€" and self.web_searcher.enabled:
493
+ verification_results = self.web_searcher.verify_facts(act_content, act_name)
494
+ expert_output += f"\n\nใ€์›น ๊ฒ€์ฆ ๊ฒฐ๊ณผใ€‘\n{json.dumps(verification_results, ensure_ascii=False, indent=2)}"
495
+
496
+ # ํ”ผ๋“œ๋ฐฑ ์ƒ์„ฑ
497
+ feedback = ExpertFeedback(
498
+ role=role,
499
+ stage=f"{act_name}_{stage_type}",
500
+ feedback=expert_output[:500],
501
+ suggestions=self._extract_suggestions(expert_output),
502
+ score=85.0 + random.uniform(-5, 10)
503
+ )
504
+ act_progress.expert_feedbacks.append(feedback)
505
+
506
+ # ์ตœ์ข… ์™„์„ฑ ๋‹จ๊ณ„์—์„œ๋งŒ ์ฝ˜ํ…์ธ  ์—…๋ฐ์ดํŠธ
507
+ if stage_type == "์™„์„ฑ":
508
+ act_content = expert_output
509
+ act_progress.content = act_content
510
+ elif stage_type == "์ดˆ๊ณ ":
511
+ act_content = expert_output
512
+
513
+ # DB ์ €์žฅ
514
+ ScreenplayDatabase.save_act_progress(
515
+ session_id, act_name, idx+1, role,
516
+ expert_output[:1000], feedback.feedback, feedback.score
517
+ )
518
+
519
+ # ์ง„ํ–‰๋ฅ  ํ‘œ์‹œ
520
+ progress_msg = f"โœ… {role} ์ž‘์—… ์™„๋ฃŒ ({act_progress.current_stage}/{act_progress.total_stages})"
521
+ yield act_progress, progress_msg
522
+
523
+ time.sleep(0.5) # API ์ œํ•œ ๊ณ ๋ ค
524
+
525
+ # ๋ง‰ ์™„์„ฑ
526
+ act_progress.status = "complete"
527
+ ScreenplayDatabase.save_act_content(session_id, act_name, act_content)
528
+
529
+ yield act_progress, f"โœ… {act_name} ์™„์„ฑ!"
530
+
531
+ def _create_writer_prompt(self, act_name: str, planning_data: Dict,
532
+ previous_acts: Dict, stage_type: str) -> str:
533
+ """์Šคํ† ๋ฆฌ ์ž‘๊ฐ€ ํ”„๋กฌํ”„ํŠธ"""
534
+
535
+ target_lines = 1000 if stage_type == "์ดˆ๊ณ " else 1200
536
+
537
+ previous_content = ""
538
+ if previous_acts:
539
+ for act, content in previous_acts.items():
540
+ if content:
541
+ previous_content += f"\nใ€{act}ใ€‘\n{content[:500]}...\n"
542
+
543
+ return f"""ใ€{act_name} {stage_type} ์ž‘์„ฑใ€‘
544
+
545
+ ์›๋ณธ ์š”์ฒญ: {self.original_query}
546
+
547
+ ๋ชฉํ‘œ ๋ถ„๋Ÿ‰: {target_lines}์ค„ ์ด์ƒ
548
+
549
+ ใ€๊ธฐํš์•ˆใ€‘
550
+ {self._summarize_planning(planning_data)}
551
+
552
+ ใ€์ด์ „ ๋ง‰ ๋‚ด์šฉใ€‘
553
+ {previous_content if previous_content else "์ฒซ ๋ง‰์ž…๋‹ˆ๋‹ค"}
554
+
555
+ ใ€์ž‘์„ฑ ์š”๊ตฌ์‚ฌํ•ญใ€‘
556
+ 1. ํ‘œ์ค€ ์‹œ๋‚˜๋ฆฌ์˜ค ํฌ๋งท
557
+ - INT./EXT. ์žฅ์†Œ - ์‹œ๊ฐ„
558
+ - ์บ๋ฆญํ„ฐ๋ช… (๋Œ€๋ฌธ์ž)
559
+ - ๋Œ€์‚ฌ์™€ ์•ก์…˜ ๊ตฌ๋ถ„
560
+
561
+ 2. ์ถฉ๋ถ„ํ•œ ๋ถ„๋Ÿ‰
562
+ - ๊ฐ ์”ฌ 5-7ํŽ˜์ด์ง€
563
+ - ์ƒ์„ธํ•œ ์•ก์…˜ ๋ฌ˜์‚ฌ
564
+ - ์ž์—ฐ์Šค๋Ÿฌ์šด ๋Œ€ํ™”
565
+
566
+ 3. ์›์ž‘ ์ถฉ์‹ค
567
+ - ๊ธฐํš์•ˆ ๋‚ด์šฉ ์ค€์ˆ˜
568
+ - ์บ๋ฆญํ„ฐ ์ผ๊ด€์„ฑ
569
+ - ์Šคํ† ๋ฆฌ ์—ฐ์†์„ฑ
570
+
571
+ ๋ฐ˜๋“œ์‹œ {target_lines}์ค„ ์ด์ƒ ์ž‘์„ฑํ•˜์„ธ์š”."""
572
+
573
+ def _create_verification_prompt(self, act_name: str, content: str) -> str:
574
+ """๊ณ ์ฆ ์ „๋ฌธ๊ฐ€ ํ”„๋กฌํ”„ํŠธ"""
575
+ return f"""ใ€{act_name} ์‚ฌ์‹ค ํ™•์ธ ๋ฐ ๊ณ ์ฆใ€‘
576
+
577
+ ๊ฒ€ํ† ํ•  ๋‚ด์šฉ:
578
+ {content[:2000]}...
579
+
580
+ ใ€๊ฒ€์ฆ ํ•ญ๋ชฉใ€‘
581
+ 1. ์—ญ์‚ฌ์  ์ •ํ™•์„ฑ
582
+ - ์‹œ๋Œ€ ๋ฐฐ๊ฒฝ ์ ์ ˆ์„ฑ
583
+ - ์‚ฌ๊ฑด/์ธ๋ฌผ ์‚ฌ์‹ค ํ™•์ธ
584
+
585
+ 2. ๊ณผํ•™์  ํƒ€๋‹น์„ฑ
586
+ - ๊ธฐ์ˆ ์  ๋ฌ˜์‚ฌ ์ •ํ™•์„ฑ
587
+ - ๋ฌผ๋ฆฌ ๋ฒ•์น™ ์ค€์ˆ˜
588
+
589
+ 3. ๋ฌธํ™”์  ์ ์ ˆ์„ฑ
590
+ - ์–ธ์–ด ์‚ฌ์šฉ
591
+ - ๊ด€์Šต๊ณผ ์˜ˆ์ ˆ
592
+
593
+ 4. ํ˜„์‹ค์„ฑ
594
+ - ํ–‰๋™์˜ ๊ฐœ์—ฐ์„ฑ
595
+ - ์ƒํ™ฉ์˜ ํƒ€๋‹น์„ฑ
596
+
597
+ ใ€์ˆ˜์ • ํ•„์š” ์‚ฌํ•ญใ€‘
598
+ ๊ตฌ์ฒด์ ์ธ ์˜ค๋ฅ˜์™€ ์ˆ˜์ •์•ˆ์„ ์ œ์‹œํ•˜์„ธ์š”."""
599
+
600
+ def _create_editor_prompt(self, act_name: str, content: str) -> str:
601
+ """ํŽธ์ง‘์ž ํ”„๋กฌํ”„ํŠธ"""
602
+ return f"""ใ€{act_name} ํŽธ์ง‘ ๋ฐ ๊ตฌ์กฐ ์กฐ์ •ใ€‘
603
+
604
+ ํ˜„์žฌ ๋‚ด์šฉ:
605
+ {content[:2000]}...
606
+
607
+ ใ€ํŽธ์ง‘ ํฌ์ธํŠธใ€‘
608
+ 1. ํŽ˜์ด์‹ฑ ์ตœ์ ํ™”
609
+ - ์”ฌ ๊ธธ์ด ์กฐ์ •
610
+ - ๊ธด์žฅ/์ด์™„ ๋ฆฌ๋“ฌ
611
+
612
+ 2. ๊ตฌ์กฐ ๊ฐœ์„ 
613
+ - ์”ฌ ์ˆœ์„œ ์ตœ์ ํ™”
614
+ - ์ „ํ™˜ ์ž์—ฐ์Šค๋Ÿฌ์›€
615
+
616
+ 3. ํŠธ๋ฆฌ๋ฐ
617
+ - ๋ถˆํ•„์š”ํ•œ ๋ถ€๋ถ„ ์ œ๊ฑฐ
618
+ - ์ค‘๋ณต ์ œ๊ฑฐ
619
+
620
+ 4. ์ž„ํŒฉํŠธ ๊ฐ•ํ™”
621
+ - ํ•ต์‹ฌ ์žฅ๋ฉด ๊ฐ•์กฐ
622
+ - ๊ฐ์ •์  ๋น„ํŠธ
623
+
624
+ ํŽธ์ง‘๋œ ๋ฒ„์ „์„ ์ œ์‹œํ•˜์„ธ์š”."""
625
+
626
+ def _create_director_prompt(self, act_name: str, content: str) -> str:
627
+ """๊ฐ๋… ํ”„๋กฌํ”„ํŠธ"""
628
+ return f"""ใ€{act_name} ๋น„์ฃผ์–ผ ์—ฐ์ถœใ€‘
629
+
630
+ ์‹œ๋‚˜๋ฆฌ์˜ค ๊ฒ€ํ† :
631
+ {content[:2000]}...
632
+
633
+ ใ€์—ฐ์ถœ ๊ฐ•ํ™”ใ€‘
634
+ 1. ์˜คํ”„๋‹ ๋น„์ฃผ์–ผ
635
+ - ์ฒซ ์ด๋ฏธ์ง€
636
+ - ํ†ค ์„ค์ •
637
+
638
+ 2. ํ•ต์‹ฌ ์”ฌ ์—ฐ์ถœ
639
+ - ์นด๋ฉ”๋ผ ์•ต๊ธ€
640
+ - ์›€์ง์ž„
641
+ - ์กฐ๋ช…
642
+
643
+ 3. ๊ฐ์ • ํ‘œํ˜„
644
+ - ํด๋กœ์ฆˆ์—…
645
+ - ๋ฆฌ์•ก์…˜ ์ƒท
646
+
647
+ 4. ์ „ํ™˜ ์—ฐ์ถœ
648
+ - ์”ฌ ์—ฐ๊ฒฐ
649
+ - ์‹œ๊ฐ„/๊ณต๊ฐ„ ์ด๋™
650
+
651
+ ๋น„์ฃผ์–ผ ๋…ธํŠธ๋ฅผ ์ถ”๊ฐ€ํ•˜์„ธ์š”."""
652
+
653
+ def _create_dialogue_prompt(self, act_name: str, content: str) -> str:
654
+ """๋Œ€ํ™” ์ „๋ฌธ๊ฐ€ ํ”„๋กฌํ”„ํŠธ"""
655
+ return f"""ใ€{act_name} ๋Œ€์‚ฌ ๊ฐœ์„ ใ€‘
656
+
657
+ ํ˜„์žฌ ๋Œ€์‚ฌ:
658
+ {content[:2000]}...
659
+
660
+ ใ€๊ฐœ์„  ๋ฐฉํ–ฅใ€‘
661
+ 1. ์บ๋ฆญํ„ฐ ๋ณด์ด์Šค
662
+ - ๊ณ ์œ ํ•œ ๋งํˆฌ
663
+ - ์–ดํœ˜ ์ˆ˜์ค€
664
+
665
+ 2. ์„œ๋ธŒํ…์ŠคํŠธ
666
+ - ์ˆจ์€ ์˜๋ฏธ
667
+ - ๊ฐ์ • ๋ ˆ์ด์–ด
668
+
669
+ 3. ์ž์—ฐ์Šค๋Ÿฌ์›€
670
+ - ์ผ์ƒ ๋Œ€ํ™”
671
+ - ๋ฆฌ๋“ฌ๊ณผ ํ…œํฌ
672
+
673
+ 4. ํ•ต์‹ฌ ๋Œ€์‚ฌ
674
+ - ๋ช…๋Œ€์‚ฌ
675
+ - ํ…Œ๋งˆ ์ „๋‹ฌ
676
+
677
+ ๊ฐœ์„ ๋œ ๋Œ€์‚ฌ๋ฅผ ์ œ์‹œํ•˜์„ธ์š”."""
678
+
679
+ def _create_critic_prompt(self, act_name: str, content: str) -> str:
680
+ """๋น„ํ‰๊ฐ€ ํ”„๋กฌํ”„ํŠธ"""
681
+ return f"""ใ€{act_name} ์ข…ํ•ฉ ํ‰๊ฐ€ใ€‘
682
+
683
+ ํ‰๊ฐ€ ๋Œ€์ƒ:
684
+ {content[:2000]}...
685
+
686
+ ใ€ํ‰๊ฐ€ ํ•ญ๋ชฉใ€‘
687
+ 1. ์Šคํ† ๋ฆฌ (30์ )
688
+ - ๋…ผ๋ฆฌ์„ฑ
689
+ - ํ๋ฆ„
690
+ - ๊ธด์žฅ๊ฐ
691
+
692
+ 2. ์บ๋ฆญํ„ฐ (30์ )
693
+ - ์ผ๊ด€์„ฑ
694
+ - ๋งค๋ ฅ๋„
695
+ - ์„ฑ์žฅ
696
+
697
+ 3. ๋Œ€์‚ฌ (20์ )
698
+ - ์ž์—ฐ์Šค๋Ÿฌ์›€
699
+ - ๊ฐœ์„ฑ
700
+
701
+ 4. ์™„์„ฑ๋„ (20์ )
702
+ - ํฌ๋งท
703
+ - ๊ฐ€๋…์„ฑ
704
+
705
+ ์ด์ : /100
706
+
707
+ ๊ฐœ์„  ์ œ์•ˆ์„ ํ•˜์„ธ์š”."""
708
+
709
+ def _summarize_planning(self, planning_data: Dict) -> str:
710
+ """๊ธฐํš์•ˆ ์š”์•ฝ"""
711
+ summary = ""
712
+ for key, value in planning_data.items():
713
+ if value:
714
+ summary += f"[{key}]\n{value[:300]}...\n\n"
715
+ return summary[:1500]
716
+
717
+ def _extract_suggestions(self, content: str) -> List[str]:
718
+ """์ œ์•ˆ ์‚ฌํ•ญ ์ถ”์ถœ"""
719
+ suggestions = []
720
+ lines = content.split('\n')
721
+ for line in lines:
722
+ if any(k in line for k in ['๊ฐœ์„ ', '์ œ์•ˆ', '์ˆ˜์ •', '์ถ”์ฒœ']):
723
+ suggestions.append(line.strip())
724
+ return suggestions[:5]
725
+
726
+ # UI ์—…๋ฐ์ดํŠธ ํ•จ์ˆ˜
727
+ def create_act_progress_display(act_progress: ActProgress) -> str:
728
+ """๋ง‰๋ณ„ ์ง„ํ–‰ ์ƒํ™ฉ ํ‘œ์‹œ"""
729
+ if not act_progress:
730
+ return ""
731
+
732
+ html = f"""
733
+ <div style="border: 2px solid #667eea; border-radius: 10px; padding: 15px; margin: 10px 0;">
734
+ <h3>{act_progress.act_name} ์ง„ํ–‰ ์ƒํ™ฉ</h3>
735
+
736
+ <div style="margin: 10px 0;">
737
+ <div style="background: #e0e0e0; border-radius: 10px; height: 30px; position: relative;">
738
+ <div style="background: linear-gradient(90deg, #667eea, #764ba2);
739
+ height: 100%; border-radius: 10px; width: {act_progress.progress_percentage}%;
740
+ transition: width 0.3s ease;">
741
+ <span style="position: absolute; left: 50%; transform: translateX(-50%);
742
+ color: white; line-height: 30px; font-weight: bold;">
743
+ {act_progress.progress_percentage:.0f}%
744
+ </span>
745
+ </div>
746
+ </div>
747
+ </div>
748
+
749
+ <div style="margin-top: 15px;">
750
+ <strong>ํ˜„์žฌ ์ž‘์—…:</strong> {EXPERT_ROLES[act_progress.current_expert]['emoji']} {act_progress.current_expert}
751
+ <br>
752
+ <strong>๋‹จ๊ณ„:</strong> {act_progress.current_stage} / {act_progress.total_stages}
753
+ <br>
754
+ <strong>์ƒํƒœ:</strong> {act_progress.status}
755
+ </div>
756
+ </div>
757
+ """
758
+
759
+ # ์ „๋ฌธ๊ฐ€ ํ”ผ๋“œ๋ฐฑ ํ‘œ์‹œ
760
+ if act_progress.expert_feedbacks:
761
+ html += """
762
+ <div style="background: #f8f9fa; border-radius: 8px; padding: 10px; margin-top: 10px;">
763
+ <h4>์ „๋ฌธ๊ฐ€ ์ž‘์—… ํ˜„ํ™ฉ</h4>
764
+ """
765
+
766
+ for fb in act_progress.expert_feedbacks:
767
+ emoji = EXPERT_ROLES[fb.role]['emoji']
768
+ html += f"""
769
+ <div style="border-left: 3px solid #667eea; padding-left: 10px; margin: 5px 0;">
770
+ <strong>{emoji} {fb.role}</strong> - ์ ์ˆ˜: {fb.score:.1f}/100<br>
771
+ <small>{fb.feedback[:100]}...</small>
772
+ </div>
773
+ """
774
+
775
+ html += "</div>"
776
+
777
+ return html
778
+
779
+ def format_screenplay_display(screenplay_text: str) -> str:
780
+ """์‹œ๋‚˜๋ฆฌ์˜ค ํ‘œ์‹œ ํฌ๋งท"""
781
+ if not screenplay_text:
782
+ return "์‹œ๋‚˜๋ฆฌ์˜ค๊ฐ€ ์•„์ง ์ž‘์„ฑ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."
783
+
784
+ formatted = "# ๐ŸŽฌ ์‹œ๋‚˜๋ฆฌ์˜ค\n\n"
785
+
786
+ # ์”ฌ ํ—ค๋”ฉ ๊ฐ•์กฐ
787
+ formatted_text = re.sub(
788
+ r'^(INT\.|EXT\.).*$',
789
+ r'**\g<0>**',
790
+ screenplay_text,
791
+ flags=re.MULTILINE
792
+ )
793
+
794
+ # ์บ๋ฆญํ„ฐ๋ช… ๊ฐ•์กฐ
795
+ formatted_text = re.sub(
796
+ r'^([๊ฐ€-ํžฃA-Z][๊ฐ€-ํžฃA-Z\s]+)$',
797
+ r'**\g<0>**',
798
+ formatted_text,
799
+ flags=re.MULTILINE
800
+ )
801
+
802
+ # ํŽ˜์ด์ง€ ์ˆ˜ ๊ณ„์‚ฐ
803
+ page_count = len(screenplay_text.splitlines()) / 58
804
+ formatted = f"**์ด ํŽ˜์ด์ง€: {page_count:.1f}**\n\n" + formatted_text
805
+
806
+ return formatted
807
+
808
+ # Gradio ์ธํ„ฐํŽ˜์ด์Šค
809
+ def create_interface():
810
+ css = """
811
+ .main-header {
812
+ text-align: center;
813
+ margin-bottom: 2rem;
814
+ padding: 2.5rem;
815
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
816
+ border-radius: 15px;
817
+ color: white;
818
+ box-shadow: 0 10px 30px rgba(0,0,0,0.3);
819
+ }
820
+
821
+ .expert-card {
822
+ background: white;
823
+ border: 1px solid #e0e0e0;
824
+ border-radius: 8px;
825
+ padding: 10px;
826
+ margin: 5px;
827
+ display: inline-block;
828
+ }
829
+
830
+ .act-button {
831
+ min-height: 60px;
832
+ font-size: 1.1rem;
833
+ margin: 5px;
834
+ }
835
+
836
+ .progress-container {
837
+ background: #f8f9fa;
838
+ border-radius: 10px;
839
+ padding: 20px;
840
+ margin: 15px 0;
841
+ }
842
+ """
843
+
844
+ with gr.Blocks(theme=gr.themes.Soft(), css=css, title="AI ์‹œ๋‚˜๋ฆฌ์˜ค ์ž‘๊ฐ€") as interface:
845
+ gr.HTML("""
846
+ <div class="main-header">
847
+ <h1 style="font-size: 3rem;">๐ŸŽฌ AI ์‹œ๋‚˜๋ฆฌ์˜ค ์ž‘๊ฐ€</h1>
848
+ <p style="font-size: 1.2rem;">
849
+ ์ „๋ฌธ๊ฐ€ ํ˜‘์—… ์‹œ์Šคํ…œ์œผ๋กœ ์™„๋ฒฝํ•œ ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ์ž‘์„ฑํ•ฉ๋‹ˆ๋‹ค<br>
850
+ ๋ง‰๋ณ„๋กœ ๋‹จ๊ณ„์ ์œผ๋กœ ์ง„ํ–‰ํ•˜๋ฉฐ, ์›น ๊ฒ€์ƒ‰์„ ํ†ตํ•œ ๊ณ ์ฆ๊นŒ์ง€ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค
851
+ </p>
852
+ </div>
853
+ """)
854
+
855
+ # ์ƒํƒœ ๋ณ€์ˆ˜
856
+ current_session_id = gr.State(None)
857
+ current_planning = gr.State({})
858
+ act1_content = gr.State("")
859
+ act2a_content = gr.State("")
860
+ act2b_content = gr.State("")
861
+ act3_content = gr.State("")
862
+
863
+ with gr.Tabs():
864
+ with gr.Tab("๐Ÿ“ ์‹œ๋‚˜๋ฆฌ์˜ค ์ž‘์„ฑ"):
865
+ # ๊ธฐ๋ณธ ์ž…๋ ฅ
866
+ with gr.Row():
867
+ with gr.Column(scale=2):
868
+ query_input = gr.Textbox(
869
+ label="๐Ÿ’ก ์‹œ๋‚˜๋ฆฌ์˜ค ์•„์ด๋””์–ด",
870
+ placeholder="๊ตฌ์ฒด์ ์ธ ์Šคํ† ๋ฆฌ ์•„์ด๋””์–ด๋ฅผ ์ž…๋ ฅํ•˜์„ธ์š”...",
871
+ lines=4
872
+ )
873
+
874
+ with gr.Column(scale=1):
875
+ screenplay_type = gr.Radio(
876
+ choices=list(SCREENPLAY_LENGTHS.keys()),
877
+ value="์˜ํ™”",
878
+ label="์œ ํ˜•"
879
+ )
880
+ genre_select = gr.Dropdown(
881
+ choices=["์•ก์…˜", "์Šค๋ฆด๋Ÿฌ", "๋“œ๋ผ๋งˆ", "์ฝ”๋ฏธ๋””", "๊ณตํฌ", "SF", "๋กœ๋งจ์Šค", "ํŒํƒ€์ง€"],
882
+ value="๋“œ๋ผ๋งˆ",
883
+ label="์žฅ๋ฅด"
884
+ )
885
+
886
+ planning_btn = gr.Button("๐Ÿ“‹ ๊ธฐํš์•ˆ ์ƒ์„ฑ", variant="primary", size="lg")
887
+
888
+ # ๊ธฐํš์•ˆ ํ‘œ์‹œ
889
+ planning_display = gr.Markdown("*๊ธฐํš์•ˆ์ด ์—ฌ๊ธฐ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค...*")
890
+
891
+ # ๋ง‰๋ณ„ ์ž‘์„ฑ ๋ฒ„ํŠผ๋“ค
892
+ gr.Markdown("### ๐ŸŽญ ๋ง‰๋ณ„ ์‹œ๋‚˜๋ฆฌ์˜ค ์ž‘์„ฑ")
893
+ gr.Markdown("๊ฐ ๋ง‰์„ ์ˆœ์ฐจ์ ์œผ๋กœ ์ž‘์„ฑํ•˜์„ธ์š”. ๊ฐ ๋ง‰๋งˆ๋‹ค 7๋ช…์˜ ์ „๋ฌธ๊ฐ€๊ฐ€ ํ˜‘์—…ํ•ฉ๋‹ˆ๋‹ค.")
894
+
895
+ with gr.Row():
896
+ act1_btn = gr.Button(
897
+ "1๏ธโƒฃ 1๋ง‰ ์ž‘์„ฑ\n(์„ค์ • - 25%)",
898
+ variant="primary",
899
+ elem_classes=["act-button"]
900
+ )
901
+ act2a_btn = gr.Button(
902
+ "2๏ธโƒฃ 2๋ง‰A ์ž‘์„ฑ\n(์ƒ์Šน - 25%)",
903
+ variant="secondary",
904
+ elem_classes=["act-button"]
905
+ )
906
+ act2b_btn = gr.Button(
907
+ "3๏ธโƒฃ 2๋ง‰B ์ž‘์„ฑ\n(๋ณต์žกํ™” - 25%)",
908
+ variant="secondary",
909
+ elem_classes=["act-button"]
910
+ )
911
+ act3_btn = gr.Button(
912
+ "4๏ธโƒฃ 3๋ง‰ ์ž‘์„ฑ\n(ํ•ด๊ฒฐ - 25%)",
913
+ variant="secondary",
914
+ elem_classes=["act-button"]
915
+ )
916
+
917
+ # ์ง„ํ–‰ ์ƒํ™ฉ ํ‘œ์‹œ
918
+ gr.Markdown("### ๐Ÿ“Š ์ž‘์„ฑ ์ง„ํ–‰ ์ƒํ™ฉ")
919
+ progress_display = gr.HTML(
920
+ value='<div class="progress-container">๋Œ€๊ธฐ ์ค‘...</div>'
921
+ )
922
+ status_text = gr.Textbox(
923
+ label="ํ˜„์žฌ ์ƒํƒœ",
924
+ value="๊ธฐํš์•ˆ์„ ๋จผ์ € ์ƒ์„ฑํ•ด์ฃผ์„ธ์š”",
925
+ interactive=False
926
+ )
927
+
928
+ # ์ „๋ฌธ๊ฐ€ ์ž‘์—… ํ˜„ํ™ฉ
929
+ gr.Markdown("### ๐Ÿ‘ฅ ์ „๋ฌธ๊ฐ€ ์ž‘์—… ํ˜„ํ™ฉ")
930
+ expert_status = gr.HTML("""
931
+ <div style="padding: 10px;">
932
+ <span class="expert-card">๐ŸŽฌ ํ”„๋กœ๋“€์„œ</span>
933
+ <span class="expert-card">๐Ÿ“– ์Šคํ† ๋ฆฌ์ž‘๊ฐ€</span>
934
+ <span class="expert-card">๐Ÿ”Ž ๊ณ ์ฆ์ „๋ฌธ๊ฐ€</span>
935
+ <span class="expert-card">โœ‚๏ธ ํŽธ์ง‘์ž</span>
936
+ <span class="expert-card">๐ŸŽญ ๊ฐ๋…</span>
937
+ <span class="expert-card">๐Ÿ’ฌ ๋Œ€ํ™”์ „๋ฌธ๊ฐ€</span>
938
+ <span class="expert-card">๐Ÿ” ๋น„ํ‰๊ฐ€</span>
939
+ </div>
940
+ """)
941
+
942
+ # ์‹œ๋‚˜๋ฆฌ์˜ค ์ถœ๋ ฅ
943
+ gr.Markdown("### ๐Ÿ“„ ์ž‘์„ฑ๋œ ์‹œ๋‚˜๋ฆฌ์˜ค")
944
+ with gr.Tabs():
945
+ with gr.Tab("1๋ง‰"):
946
+ act1_output = gr.Markdown("*1๋ง‰์ด ์—ฌ๊ธฐ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค...*")
947
+ with gr.Tab("2๋ง‰A"):
948
+ act2a_output = gr.Markdown("*2๋ง‰A๊ฐ€ ์—ฌ๊ธฐ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค...*")
949
+ with gr.Tab("2๋ง‰B"):
950
+ act2b_output = gr.Markdown("*2๋ง‰B๊ฐ€ ์—ฌ๊ธฐ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค...*")
951
+ with gr.Tab("3๋ง‰"):
952
+ act3_output = gr.Markdown("*3๋ง‰์ด ์—ฌ๊ธฐ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค...*")
953
+ with gr.Tab("์ „์ฒด"):
954
+ full_output = gr.Markdown("*์ „์ฒด ์‹œ๋‚˜๋ฆฌ์˜ค๊ฐ€ ์—ฌ๊ธฐ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค...*")
955
+
956
+ download_btn = gr.Button("๐Ÿ’พ ์ „์ฒด ์‹œ๋‚˜๋ฆฌ์˜ค ๋‹ค์šด๋กœ๋“œ")
957
+
958
+ # ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ
959
+ def handle_planning(query, s_type, genre):
960
+ if not query:
961
+ return "โŒ ์•„์ด๋””์–ด๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”", {}, None
962
+
963
+ # ๊ฐ„๋‹จํ•œ ๊ธฐํš์•ˆ ์ƒ์„ฑ (์‹ค์ œ๋กœ๋Š” ๋” ๋ณต์žกํ•œ ๋กœ์ง)
964
+ planning = {
965
+ "์ œ๋ชฉ": "๋ฌด์ œ",
966
+ "๋กœ๊ทธ๋ผ์ธ": query[:100],
967
+ "์žฅ๋ฅด": genre,
968
+ "ํ˜•์‹": s_type,
969
+ "์‹œ๋†‰์‹œ์Šค": query
970
+ }
971
+
972
+ session_id = ScreenplayDatabase.create_session(query, s_type, genre)
973
+
974
+ planning_text = f"""## ๐Ÿ“‹ ์‹œ๋‚˜๋ฆฌ์˜ค ๊ธฐํš์•ˆ
975
+
976
+ **์ œ๋ชฉ:** {planning['์ œ๋ชฉ']}
977
+ **์žฅ๋ฅด:** {planning['์žฅ๋ฅด']}
978
+ **ํ˜•์‹:** {planning['ํ˜•์‹']}
979
+
980
+ **๋กœ๊ทธ๋ผ์ธ:**
981
+ {planning['๋กœ๊ทธ๋ผ์ธ']}
982
+
983
+ **์‹œ๋†‰์‹œ์Šค:**
984
+ {planning['์‹œ๋†‰์‹œ์Šค']}
985
+
986
+ ---
987
+ โœ… ๊ธฐํš์•ˆ์ด ์ƒ์„ฑ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ด์ œ ๋ง‰๋ณ„๋กœ ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
988
+ """
989
+
990
+ return planning_text, planning, session_id
991
+
992
+ def handle_act_writing(act_name, session_id, planning_data, previous_acts):
993
+ if not session_id:
994
+ yield "", "โŒ ๋จผ์ € ๊ธฐํš์•ˆ์„ ์ƒ์„ฑํ•ด์ฃผ์„ธ์š”", ""
995
+ return
996
+
997
+ system = ScreenplayGenerationSystem()
998
+
999
+ for act_progress, status_msg in system.generate_act(
1000
+ session_id, act_name, planning_data, previous_acts
1001
+ ):
1002
+ progress_html = create_act_progress_display(act_progress)
1003
+ screenplay_display = format_screenplay_display(act_progress.content)
1004
+
1005
+ yield progress_html, status_msg, screenplay_display
1006
+
1007
+ # ์ด๋ฒคํŠธ ์—ฐ๊ฒฐ
1008
+ planning_btn.click(
1009
+ fn=handle_planning,
1010
+ inputs=[query_input, screenplay_type, genre_select],
1011
+ outputs=[planning_display, current_planning, current_session_id]
1012
+ )
1013
+
1014
+ act1_btn.click(
1015
+ fn=lambda sid, plan: handle_act_writing("1๋ง‰", sid, plan, {}),
1016
+ inputs=[current_session_id, current_planning],
1017
+ outputs=[progress_display, status_text, act1_output]
1018
+ ).then(
1019
+ lambda x: x,
1020
+ inputs=[act1_output],
1021
+ outputs=[act1_content]
1022
+ )
1023
+
1024
+ act2a_btn.click(
1025
+ fn=lambda sid, plan, act1: handle_act_writing("2๋ง‰A", sid, plan, {"1๋ง‰": act1}),
1026
+ inputs=[current_session_id, current_planning, act1_content],
1027
+ outputs=[progress_display, status_text, act2a_output]
1028
+ ).then(
1029
+ lambda x: x,
1030
+ inputs=[act2a_output],
1031
+ outputs=[act2a_content]
1032
+ )
1033
+
1034
+ act2b_btn.click(
1035
+ fn=lambda sid, plan, act1, act2a: handle_act_writing(
1036
+ "2๋ง‰B", sid, plan, {"1๋ง‰": act1, "2๋ง‰A": act2a}
1037
+ ),
1038
+ inputs=[current_session_id, current_planning, act1_content, act2a_content],
1039
+ outputs=[progress_display, status_text, act2b_output]
1040
+ ).then(
1041
+ lambda x: x,
1042
+ inputs=[act2b_output],
1043
+ outputs=[act2b_content]
1044
+ )
1045
+
1046
+ act3_btn.click(
1047
+ fn=lambda sid, plan, act1, act2a, act2b: handle_act_writing(
1048
+ "3๋ง‰", sid, plan, {"1๋ง‰": act1, "2๋ง‰A": act2a, "2๋ง‰B": act2b}
1049
+ ),
1050
+ inputs=[current_session_id, current_planning, act1_content, act2a_content, act2b_content],
1051
+ outputs=[progress_display, status_text, act3_output]
1052
+ ).then(
1053
+ lambda x: x,
1054
+ inputs=[act3_output],
1055
+ outputs=[act3_content]
1056
+ )
1057
+
1058
+ # ์ „์ฒด ์‹œ๋‚˜๋ฆฌ์˜ค ํ•ฉ์น˜๊ธฐ
1059
+ def combine_acts(act1, act2a, act2b, act3):
1060
+ full = "# ๐ŸŽฌ ์ „์ฒด ์‹œ๋‚˜๋ฆฌ์˜ค\n\n"
1061
+ if act1:
1062
+ full += f"## 1๋ง‰\n\n{act1}\n\n"
1063
+ if act2a:
1064
+ full += f"## 2๋ง‰A\n\n{act2a}\n\n"
1065
+ if act2b:
1066
+ full += f"## 2๋ง‰B\n\n{act2b}\n\n"
1067
+ if act3:
1068
+ full += f"## 3๋ง‰\n\n{act3}\n\n"
1069
+ return full
1070
+
1071
+ # ๋ง‰ ์ž‘์„ฑ ํ›„ ์ „์ฒด ์—…๋ฐ์ดํŠธ
1072
+ act3_btn.click(
1073
+ lambda a1, a2a, a2b, a3: combine_acts(a1, a2a, a2b, a3),
1074
+ inputs=[act1_content, act2a_content, act2b_content, act3_content],
1075
+ outputs=[full_output]
1076
+ )
1077
+
1078
+ return interface
1079
+
1080
+ # ๋ฉ”์ธ ์‹คํ–‰
1081
+ if __name__ == "__main__":
1082
+ logger.info("=" * 60)
1083
+ logger.info("AI ์‹œ๋‚˜๋ฆฌ์˜ค ์ž‘๊ฐ€ - ์ „๋ฌธ๊ฐ€ ํ˜‘์—… ์‹œ์Šคํ…œ")
1084
+ logger.info("๋ง‰๋ณ„ ๋‹จ๊ณ„์  ์ž‘์„ฑ + ์›น ๊ฒ€์ฆ ์‹œ์Šคํ…œ")
1085
+ logger.info("=" * 60)
1086
+
1087
+ if not FIREWORKS_API_KEY or FIREWORKS_API_KEY == "dummy_token_for_testing":
1088
+ logger.warning("โš ๏ธ FIREWORKS_API_KEY๋ฅผ ์„ค์ •ํ•ด์ฃผ์„ธ์š”!")
1089
+
1090
+ if BRAVE_SEARCH_API_KEY:
1091
+ logger.info("โœ… ์›น ๊ฒ€์ƒ‰ ๊ณ ์ฆ ๊ธฐ๋Šฅ ํ™œ์„ฑํ™”")
1092
+ else:
1093
+ logger.warning("โš ๏ธ BRAVE_SEARCH_API_KEY ์—†์Œ - ์›น ๊ฒ€์ฆ ๋น„ํ™œ์„ฑํ™”")
1094
+
1095
+ ScreenplayDatabase.init_db()
1096
+
1097
+ interface = create_interface()
1098
+ interface.launch(
1099
+ server_name="0.0.0.0",
1100
+ server_port=7860,
1101
+ share=False
1102
+ )