ginipick commited on
Commit
90f7c1b
·
verified ·
1 Parent(s): 909bd3f

Delete app-BACKUP2.py

Browse files
Files changed (1) hide show
  1. app-BACKUP2.py +0 -1351
app-BACKUP2.py DELETED
@@ -1,1351 +0,0 @@
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
- DB_PATH = "screenplay_sessions_korean.db"
30
-
31
- # 시나리오 길이 설정
32
- SCREENPLAY_LENGTHS = {
33
- "영화": {"pages": 120, "description": "장편 영화 (110-130페이지)", "min_pages": 110},
34
- "드라마": {"pages": 60, "description": "TV 드라마 (55-65페이지)", "min_pages": 55},
35
- "웹드라마": {"pages": 50, "description": "웹/OTT 시리즈 (45-55페이지)", "min_pages": 45},
36
- "단편": {"pages": 20, "description": "단편 영화 (15-25페이지)", "min_pages": 15}
37
- }
38
-
39
- # 환경 검증
40
- if not FIREWORKS_API_KEY:
41
- logger.error("FIREWORKS_API_KEY가 설정되지 않았습니다.")
42
- FIREWORKS_API_KEY = "dummy_token_for_testing"
43
-
44
- # 글로벌 변수
45
- db_lock = threading.Lock()
46
-
47
- # 전문가 역할 정의
48
- EXPERT_ROLES = {
49
- "프로듀서": {
50
- "emoji": "🎬",
51
- "description": "상업성과 시장성 분석",
52
- "focus": ["타겟 관객", "제작 가능성", "예산 규모", "마케팅 포인트"],
53
- "personality": "실용적이고 시장 지향적"
54
- },
55
- "스토리작가": {
56
- "emoji": "📖",
57
- "description": "내러티브 구조와 플롯 개발",
58
- "focus": ["3막 구조", "플롯 포인트", "서사 아크", "테마"],
59
- "personality": "창의적이고 구조적"
60
- },
61
- "캐릭터디자이너": {
62
- "emoji": "👥",
63
- "description": "인물 창조와 관계 설계",
64
- "focus": ["캐릭터 아크", "동기부여", "관계 역학", "대화 스타일"],
65
- "personality": "심리학적이고 공감적"
66
- },
67
- "감독": {
68
- "emoji": "🎭",
69
- "description": "비주얼 스토리텔링과 연출",
70
- "focus": ["시각적 구성", "카메라 워크", "미장센", "리듬과 페이싱"],
71
- "personality": "비주얼 중심적이고 예술적"
72
- },
73
- "비평가": {
74
- "emoji": "🔍",
75
- "description": "객관적 분석과 개선점 제시",
76
- "focus": ["논리적 일관성", "감정적 임팩트", "원작 충실도", "완성도"],
77
- "personality": "분석적이고 비판적"
78
- },
79
- "편집자": {
80
- "emoji": "✂️",
81
- "description": "페이싱과 구조 최적화",
82
- "focus": ["씬 전환", "리듬", "긴장감 조절", "불필요한 부분 제거"],
83
- "personality": "정밀하고 효율적"
84
- },
85
- "대화전문가": {
86
- "emoji": "💬",
87
- "description": "대사와 서브텍스트 강화",
88
- "focus": ["자연스러운 대화", "캐릭터 보이스", "서브텍스트", "감정 전달"],
89
- "personality": "언어적이고 뉘앙스 중심"
90
- },
91
- "장르전문가": {
92
- "emoji": "🎯",
93
- "description": "장르 관습과 기대치 충족",
94
- "focus": ["장르 관습", "관객 기대", "장르 특유 요소", "트로프 활용"],
95
- "personality": "장르에 정통한"
96
- }
97
- }
98
-
99
- # 장르 템플릿
100
- GENRE_TEMPLATES = {
101
- "액션": {
102
- "pacing": "빠름",
103
- "scene_length": "짧음",
104
- "dialogue_ratio": 0.3,
105
- "key_elements": ["액션 시퀀스", "물리적 갈등", "긴박감", "위기 고조", "영웅적 순간"],
106
- "structure_beats": ["폭발적 오프닝", "추격", "대결", "클라이맥스 전투", "영웅의 승리"],
107
- "scene_density": 1.2,
108
- "action_description_ratio": 0.6
109
- },
110
- "스릴러": {
111
- "pacing": "빠름",
112
- "scene_length": "짧음",
113
- "dialogue_ratio": 0.35,
114
- "key_elements": ["서스펜스", "반전", "편집증", "시간 압박", "진실 폭로"],
115
- "structure_beats": ["훅", "미스터리 심화", "거짓 승리", "진실 폭로", "최종 대결"],
116
- "scene_density": 1.1,
117
- "action_description_ratio": 0.5
118
- },
119
- "드라마": {
120
- "pacing": "보통",
121
- "scene_length": "중간",
122
- "dialogue_ratio": 0.55,
123
- "key_elements": ["캐릭터 깊이", "감정적 진실", "관계", "내적 갈등", "변화"],
124
- "structure_beats": ["일상", "촉매", "고민", "결심", "복잡화", "위기", "해결"],
125
- "scene_density": 0.9,
126
- "action_description_ratio": 0.3
127
- },
128
- "코미디": {
129
- "pacing": "빠름",
130
- "scene_length": "짧음",
131
- "dialogue_ratio": 0.65,
132
- "key_elements": ["셋업과 페이오프", "타이밍", "캐릭터 코미디", "상황 고조", "러닝 개그"],
133
- "structure_beats": ["웃긴 오프닝", "복잡화", "오해 증폭", "혼돈의 정점", "해결과 콜백"],
134
- "scene_density": 1.0,
135
- "action_description_ratio": 0.35
136
- },
137
- "공포": {
138
- "pacing": "가변적",
139
- "scene_length": "혼합",
140
- "dialogue_ratio": 0.3,
141
- "key_elements": ["분위기", "공포감", "점프 스케어", "심리적 공포", "고립"],
142
- "structure_beats": ["평범한 일상", "첫 징조", "조사", "첫 공격", "생존", "최종 대결"],
143
- "scene_density": 1.0,
144
- "action_description_ratio": 0.55
145
- },
146
- "SF": {
147
- "pacing": "보통",
148
- "scene_length": "중간",
149
- "dialogue_ratio": 0.45,
150
- "key_elements": ["세계관 구축", "기술", "개념", "시각적 스펙터클", "철학적 질문"],
151
- "structure_beats": ["일상 세계", "발견", "새로운 세계", "복잡화", "이해", "선택", "새로운 일상"],
152
- "scene_density": 0.95,
153
- "action_description_ratio": 0.45
154
- },
155
- "로맨스": {
156
- "pacing": "보통",
157
- "scene_length": "중간",
158
- "dialogue_ratio": 0.6,
159
- "key_elements": ["케미스트리", "장애물", "감정적 순간", "친밀감", "취약성"],
160
- "structure_beats": ["만남", "끌림", "첫 갈등", "깊어짐", "위기/이별", "화해", "결합"],
161
- "scene_density": 0.85,
162
- "action_description_ratio": 0.25
163
- },
164
- "판타지": {
165
- "pacing": "보통",
166
- "scene_length": "중간",
167
- "dialogue_ratio": 0.4,
168
- "key_elements": ["마법 체계", "영웅의 여정", "신화적 요소", "세계관", "성장"],
169
- "structure_beats": ["평범한 세계", "소명", "거부", "멘토", "첫 시험", "시련", "보상", "귀환"],
170
- "scene_density": 1.0,
171
- "action_description_ratio": 0.5
172
- }
173
- }
174
-
175
- # 기획 단계 - 다중 역할 협업
176
- PLANNING_STAGES = [
177
- ("프로듀서", "producer", "🎬 프로듀서: 핵심 컨셉 및 시장성 분석"),
178
- ("스토리작가", "story_writer", "📖 스토리 작가: 시놉시스 및 3막 구조"),
179
- ("캐릭터디자이너", "character_designer", "👥 캐릭터 디자이너: 인물 프로필 및 관계도"),
180
- ("감독", "director", "🎭 감독: 비주얼 컨셉 및 연출 방향"),
181
- ("비평가", "critic", "🔍 비평가: 기획안 종합 검토 및 개선점"),
182
- ]
183
-
184
- # 작성 단계 - 다중 역할 협업
185
- WRITING_STAGES = [
186
- ("스토리작가", "1막 초고", "✍️ 1막 초고 작성"),
187
- ("편집자", "1막 편집", "✂️ 1막 편집 및 페이싱 조정"),
188
- ("감독", "1막 연출", "🎭 1막 비주얼 강화"),
189
- ("대화전문가", "1막 대사", "💬 1막 대사 개선"),
190
- ("비평가", "1막 검토", "🔍 1막 종합 검토"),
191
- ("스토리작가", "1막 완성", "✅ 1막 최종 완성본"),
192
-
193
- ("스토리작가", "2막A 초고", "✍️ 2막A 초고 작성"),
194
- ("편집자", "2막A 편집", "✂️ 2막A 편집 및 페이싱 조정"),
195
- ("감독", "2막A 연출", "🎭 2막A 비주얼 강화"),
196
- ("대화전문가", "2막A 대사", "💬 2막A 대사 개선"),
197
- ("비평가", "2막A 검토", "🔍 2막A 종합 검토"),
198
- ("스토리작가", "2막A 완성", "✅ 2막A 최종 완성본"),
199
-
200
- ("스토리작가", "2막B 초고", "✍️ 2막B 초고 작성"),
201
- ("편집자", "2막B 편집", "✂️ 2막B 편집 및 페이싱 조정"),
202
- ("감독", "2막B 연출", "🎭 2막B 비주얼 강화"),
203
- ("대화전문가", "2막B 대사", "💬 2막B 대사 개선"),
204
- ("비평가", "2막B 검토", "🔍 2막B 종합 검토"),
205
- ("스토리작가", "2막B 완성", "✅ 2막B 최종 완성본"),
206
-
207
- ("스토리작가", "3막 초고", "✍️ 3막 초고 작성"),
208
- ("편집자", "3막 편집", "✂️ 3막 편집 및 페이싱 조정"),
209
- ("감독", "3막 연출", "🎭 3막 비주얼 강화"),
210
- ("대화전문가", "3막 대사", "💬 3막 대사 개선"),
211
- ("비평가", "3막 검토", "🔍 3막 종합 검토"),
212
- ("스토리작가", "3막 완성", "✅ 3막 최종 완성본"),
213
-
214
- ("장르전문가", "장르 최적화", "🎯 장르 특성 최적화"),
215
- ("비평가", "최종 검토", "🔍 최종 검토 및 완성도 평가"),
216
- ]
217
-
218
- # 데이터 클래스
219
- @dataclass
220
- class ExpertFeedback:
221
- """전문가 피드백"""
222
- role: str
223
- stage: str
224
- feedback: str
225
- suggestions: List[str]
226
- score: float
227
- timestamp: datetime = field(default_factory=datetime.now)
228
-
229
- @dataclass
230
- class ScreenplayPlan:
231
- """시나리오 기획안"""
232
- title: str = ""
233
- logline: str = ""
234
- genre: str = ""
235
- subgenre: str = ""
236
- themes: List[str] = field(default_factory=list)
237
-
238
- # 시놉시스
239
- synopsis: str = ""
240
- extended_synopsis: str = ""
241
-
242
- # 3막 구조
243
- act1_summary: str = ""
244
- act2a_summary: str = ""
245
- act2b_summary: str = ""
246
- act3_summary: str = ""
247
-
248
- # 주요 캐릭터
249
- protagonist: Dict[str, str] = field(default_factory=dict)
250
- antagonist: Dict[str, str] = field(default_factory=dict)
251
- supporting_characters: List[Dict[str, str]] = field(default_factory=list)
252
-
253
- # 세계관
254
- time_period: str = ""
255
- locations: List[str] = field(default_factory=list)
256
- atmosphere: str = ""
257
- visual_style: str = ""
258
-
259
- # 씬 구성
260
- total_scenes: int = 0
261
- scene_breakdown: List[Dict] = field(default_factory=list)
262
-
263
- # 전문가 피드백
264
- expert_feedbacks: List[ExpertFeedback] = field(default_factory=list)
265
-
266
- # 데이터베이스 클래스
267
- class ScreenplayDatabase:
268
- @staticmethod
269
- def init_db():
270
- with sqlite3.connect(DB_PATH) as conn:
271
- conn.execute("PRAGMA journal_mode=WAL")
272
- cursor = conn.cursor()
273
-
274
- cursor.execute('''
275
- CREATE TABLE IF NOT EXISTS screenplay_sessions (
276
- session_id TEXT PRIMARY KEY,
277
- user_query TEXT NOT NULL,
278
- screenplay_type TEXT NOT NULL,
279
- genre TEXT NOT NULL,
280
- target_pages INTEGER,
281
- title TEXT,
282
- logline TEXT,
283
- planning_data TEXT,
284
- expert_feedbacks TEXT,
285
- screenplay_content TEXT,
286
- created_at TEXT DEFAULT (datetime('now')),
287
- updated_at TEXT DEFAULT (datetime('now')),
288
- status TEXT DEFAULT 'planning',
289
- current_stage INTEGER DEFAULT 0,
290
- total_pages REAL DEFAULT 0
291
- )
292
- ''')
293
-
294
- cursor.execute('''
295
- CREATE TABLE IF NOT EXISTS expert_reviews (
296
- id INTEGER PRIMARY KEY AUTOINCREMENT,
297
- session_id TEXT NOT NULL,
298
- role TEXT NOT NULL,
299
- stage TEXT NOT NULL,
300
- feedback TEXT,
301
- suggestions TEXT,
302
- score REAL,
303
- created_at TEXT DEFAULT (datetime('now')),
304
- FOREIGN KEY (session_id) REFERENCES screenplay_sessions(session_id)
305
- )
306
- ''')
307
-
308
- conn.commit()
309
-
310
- @staticmethod
311
- @contextmanager
312
- def get_db():
313
- with db_lock:
314
- conn = sqlite3.connect(DB_PATH, timeout=30.0)
315
- conn.row_factory = sqlite3.Row
316
- try:
317
- yield conn
318
- finally:
319
- conn.close()
320
-
321
- @staticmethod
322
- def create_session(user_query: str, screenplay_type: str, genre: str) -> str:
323
- session_id = hashlib.md5(f"{user_query}{screenplay_type}{datetime.now()}".encode()).hexdigest()
324
- target_pages = SCREENPLAY_LENGTHS[screenplay_type]["pages"]
325
-
326
- with ScreenplayDatabase.get_db() as conn:
327
- conn.cursor().execute(
328
- '''INSERT INTO screenplay_sessions
329
- (session_id, user_query, screenplay_type, genre, target_pages)
330
- VALUES (?, ?, ?, ?, ?)''',
331
- (session_id, user_query, screenplay_type, genre, target_pages)
332
- )
333
- conn.commit()
334
- return session_id
335
-
336
- @staticmethod
337
- def save_expert_feedback(session_id: str, role: str, stage: str,
338
- feedback: str, suggestions: List[str], score: float):
339
- with ScreenplayDatabase.get_db() as conn:
340
- conn.cursor().execute(
341
- '''INSERT INTO expert_reviews
342
- (session_id, role, stage, feedback, suggestions, score)
343
- VALUES (?, ?, ?, ?, ?, ?)''',
344
- (session_id, role, stage, feedback, json.dumps(suggestions, ensure_ascii=False), score)
345
- )
346
- conn.commit()
347
-
348
- @staticmethod
349
- def save_planning_data(session_id: str, planning_data: Dict):
350
- with ScreenplayDatabase.get_db() as conn:
351
- conn.cursor().execute(
352
- '''UPDATE screenplay_sessions
353
- SET planning_data = ?, status = 'planned', updated_at = datetime('now')
354
- WHERE session_id = ?''',
355
- (json.dumps(planning_data, ensure_ascii=False), session_id)
356
- )
357
- conn.commit()
358
-
359
- @staticmethod
360
- def save_screenplay_content(session_id: str, content: str, title: str, logline: str):
361
- with ScreenplayDatabase.get_db() as conn:
362
- total_pages = len(content.split('\n')) / 58
363
- conn.cursor().execute(
364
- '''UPDATE screenplay_sessions
365
- SET screenplay_content = ?, title = ?, logline = ?,
366
- total_pages = ?, status = 'complete', updated_at = datetime('now')
367
- WHERE session_id = ?''',
368
- (content, title, logline, total_pages, session_id)
369
- )
370
- conn.commit()
371
-
372
- # 시나리오 생성 시스템
373
- class ScreenplayGenerationSystem:
374
- def __init__(self):
375
- self.api_key = FIREWORKS_API_KEY
376
- self.api_url = API_URL
377
- self.model_id = MODEL_ID
378
- self.current_session_id = None
379
- self.current_plan = ScreenplayPlan()
380
- self.original_query = ""
381
- self.accumulated_content = {}
382
- self.expert_feedbacks = []
383
- ScreenplayDatabase.init_db()
384
-
385
- def create_headers(self):
386
- if not self.api_key or self.api_key == "dummy_token_for_testing":
387
- raise ValueError("유효한 FIREWORKS_API_KEY가 필요합니다")
388
-
389
- return {
390
- "Accept": "application/json",
391
- "Content-Type": "application/json",
392
- "Authorization": f"Bearer {self.api_key}"
393
- }
394
-
395
- def call_llm_streaming(self, messages: List[Dict[str, str]], max_tokens: int = 8000) -> Generator[str, None, None]:
396
- try:
397
- payload = {
398
- "model": self.model_id,
399
- "messages": messages,
400
- "max_tokens": max_tokens,
401
- "temperature": 0.7,
402
- "top_p": 0.9,
403
- "top_k": 40,
404
- "presence_penalty": 0.3,
405
- "frequency_penalty": 0.3,
406
- "stream": True
407
- }
408
-
409
- headers = self.create_headers()
410
-
411
- response = requests.post(
412
- self.api_url,
413
- headers=headers,
414
- json=payload,
415
- stream=True,
416
- timeout=300
417
- )
418
-
419
- if response.status_code != 200:
420
- yield f"❌ API 오류: {response.status_code}"
421
- return
422
-
423
- buffer = ""
424
- for line in response.iter_lines():
425
- if not line:
426
- continue
427
-
428
- try:
429
- line_str = line.decode('utf-8').strip()
430
- if not line_str.startswith("data: "):
431
- continue
432
-
433
- data_str = line_str[6:]
434
- if data_str == "[DONE]":
435
- break
436
-
437
- data = json.loads(data_str)
438
- if "choices" in data and len(data["choices"]) > 0:
439
- content = data["choices"][0].get("delta", {}).get("content", "")
440
- if content:
441
- buffer += content
442
- if len(buffer) >= 100 or '\n' in buffer:
443
- yield buffer
444
- buffer = ""
445
- except:
446
- continue
447
-
448
- if buffer:
449
- yield buffer
450
-
451
- except Exception as e:
452
- yield f"❌ 오류: {str(e)}"
453
-
454
- def get_expert_prompt(self, role: str, stage: str, query: str,
455
- previous_content: Dict, genre: str, screenplay_type: str) -> str:
456
- """각 전문가별 맞춤 프롬프트 생성"""
457
-
458
- expert = EXPERT_ROLES[role]
459
- focus_areas = ", ".join(expert["focus"])
460
-
461
- # 이전 전문가들의 피드백 요약
462
- previous_feedbacks = ""
463
- if self.expert_feedbacks:
464
- previous_feedbacks = "\n【이전 전문가 의견】\n"
465
- for fb in self.expert_feedbacks[-3:]: # 최근 3개만
466
- previous_feedbacks += f"[{fb.role}]: {fb.feedback[:200]}...\n"
467
-
468
- base_prompt = f"""
469
- 【당신의 역할】
470
- 당신은 {role}입니다. {expert['description']}
471
- 성격: {expert['personality']}
472
- 집중 영역: {focus_areas}
473
-
474
- 【원본 요청】
475
- {query}
476
-
477
- 【장르】 {genre}
478
- 【형식】 {screenplay_type}
479
-
480
- {previous_feedbacks}
481
-
482
- 【이전 작업 내용】
483
- {self._summarize_previous_content(previous_content)}
484
- """
485
-
486
- # 역할별 특화 프롬프트
487
- if role == "프로듀서":
488
- return base_prompt + f"""
489
- 【프로듀서로서 분석할 내용】
490
- 1. 상업적 가치와 시장성
491
- - 타겟 관객층 명확화
492
- - 예상 제작비 규모
493
- - 배급 전략
494
-
495
- 2. 제목과 로그라인
496
- - 마케팅 가능한 매력적인 제목
497
- - 한 문장으로 핵심 갈등 표현
498
-
499
- 3. 유사 성공작 분석
500
- - 최근 3년 내 유사 작품
501
- - 차별화 포인트
502
-
503
- 4. 제작 리스크 평가
504
- - 기술적 난이도
505
- - 캐스팅 요구사항
506
-
507
- ⚠️ 원본 요청을 상업적으로 최적화하되 핵심은 유지하세요."""
508
-
509
- elif role == "스토리작가":
510
- return base_prompt + f"""
511
- 【스토리 작가로서 구성할 내용】
512
- 1. 시놉시스 (500-800자)
513
- - 원본 요청의 핵심 스토리
514
- - 명확한 시작-중간-끝
515
-
516
- 2. 3막 구조 상세
517
- - 1막 (25%): 설정과 촉발 사건
518
- - 2막A (25%): 새로운 세계와 재미
519
- - 2막B (25%): 시련과 위기
520
- - 3막 (25%): 클라이맥스와 해결
521
-
522
- 3. 주요 플롯 포인트
523
- - 10개의 핵심 전���점
524
- - 각 전환점의 감정적 임팩트
525
-
526
- 4. 테마와 메시지
527
- - 중심 테마
528
- - 서브 테마들
529
-
530
- ⚠️ 원본 스토리의 논리적 흐름을 완벽하게 구현하세요."""
531
-
532
- elif role == "캐릭터디자이너":
533
- return base_prompt + f"""
534
- 【캐릭터 디자이너로서 창조할 내용】
535
- 1. 주인공 상세 프로필
536
- - 이름, 나이, 직업
537
- - 성격 (MBTI 포함)
538
- - 핵심 욕구(Want)와 필요(Need)
539
- - 치명적 결함
540
- - 변화 아크
541
- - 특징적 말투와 행동
542
-
543
- 2. 적대자 프로필
544
- - 주인공과의 대립 구조
545
- - 나름의 정당성
546
- - 약점과 맹점
547
-
548
- 3. 조연 캐릭터들 (3-5명)
549
- - 각자의 역할과 기능
550
- - 주인공과의 관계
551
- - 고유한 특징
552
-
553
- 4. 캐릭터 관계도
554
- - 인물간 역학 관계
555
- - 갈등 구조
556
-
557
- ⚠️ 원본에 명시된 인물 설정을 충실히 발전시키세요."""
558
-
559
- elif role == "감독":
560
- return base_prompt + f"""
561
- 【감독으로서 연출할 내용】
562
- 1. 비주얼 컨셉
563
- - 전체적인 룩앤필
564
- - 색감과 톤
565
- - 참조 영화/이미지
566
-
567
- 2. 핵심 비주얼 씬 (5개)
568
- - 오프닝 이미지
569
- - 주요 액션/감정 씬
570
- - 클라이맥스 비주얼
571
- - 엔딩 이미지
572
-
573
- 3. 카메라 스타일
574
- - 주요 샷 구성
575
- - 움직임과 앵글
576
- - 특수 촬영 기법
577
-
578
- 4. 사운드 디자인
579
- - 음악 스타일
580
- - 핵심 사운드 모티프
581
- - 침묵의 활용
582
-
583
- ⚠️ 원본의 분위기와 톤을 시각적으로 구현하세요."""
584
-
585
- elif role == "비평가":
586
- return base_prompt + f"""
587
- 【비평가로서 검토할 내용】
588
- 1. 원본 충실도 평가 (40점)
589
- - 핵심 요청사항 반영도
590
- - 임의 변경 사항 지적
591
-
592
- 2. 스토리 완성도 (20점)
593
- - 논리적 일관성
594
- - 감정적 호소력
595
- - 페이싱과 리듬
596
-
597
- 3. 캐릭터 평가 (20점)
598
- - 입체성과 매력
599
- - 성장 아크
600
- - 관계 역학
601
-
602
- 4. 상업성과 예술성 (20점)
603
- - 시장 어필
604
- - 독창성
605
- - 제작 가능성
606
-
607
- 5. 구체적 개선 제안
608
- - 우선순위별 개선점
609
- - 실행 가능한 해결책
610
-
611
- 총점: /100점
612
- ⚠️ 건설적이면서도 정직한 피드백을 제공하세요."""
613
-
614
- elif role == "편집자":
615
- return base_prompt + f"""
616
- 【편집자로서 조정할 내용】
617
- 1. 페이싱 분석
618
- - 각 씬의 길이 적정성
619
- - 긴장과 이완의 리듬
620
- - 불필요한 부분 식별
621
-
622
- 2. 구조 최적화
623
- - 씬 순서 조정 제안
624
- - 전환의 자연스러움
625
- - 정보 공개 타이밍
626
-
627
- 3. 트리밍 제안
628
- - 삭제 가능한 씬/대사
629
- - 압축 가능한 부분
630
- - 중복 제거
631
-
632
- 4. 임팩트 강화
633
- - 클라이맥스 빌드업
634
- - 감정적 비트 강화
635
-
636
- ⚠️ 원본 스토리를 더 타이트하고 임팩트있게 만드세요."""
637
-
638
- elif role == "대화전문가":
639
- return base_prompt + f"""
640
- 【대화 전문가로서 개선할 내용】
641
- 1. 캐릭터별 고유 화법
642
- - 어휘 수준과 스타일
643
- - 말버릇과 패턴
644
- - 감정 표현 방식
645
-
646
- 2. 서브텍스트 강화
647
- - 표면 대사 vs 진짜 의미
648
- - 침묵과 행간
649
- - 비언어적 소통
650
-
651
- 3. 핵심 대사 창조
652
- - 명대사 5개
653
- - 캐릭터 정체성 드러내기
654
- - 테마 전달 대사
655
-
656
- 4. 대화 리듬
657
- - 자연스러운 주고받기
658
- - 긴장 고조 패턴
659
- - 유머와 위트
660
-
661
- ⚠️ 각 캐릭터의 목소리를 뚜렷하게 구분하세요."""
662
-
663
- elif role == "장르전문가":
664
- genre_template = GENRE_TEMPLATES.get(genre, GENRE_TEMPLATES["드라마"])
665
- return base_prompt + f"""
666
- 【{genre} 장르 전문가로서 최적화할 내용】
667
- 1. 장르 필수 요소 체크
668
- - 핵심 요소: {', '.join(genre_template['key_elements'])}
669
- - 구조 비트: {', '.join(genre_template['structure_beats'])}
670
-
671
- 2. 장르 관습 충족도
672
- - 관객 기대 충족
673
- - 클리셰 활용과 전복
674
- - 장르 특유 장치
675
-
676
- 3. 톤과 분위기
677
- - 페이싱: {genre_template['pacing']}
678
- - 씬 길이: {genre_template['scene_length']}
679
- - 대사 비율: {genre_template['dialogue_ratio']}
680
-
681
- 4. 장르 특화 강화
682
- - 부족한 요소 추가
683
- - 과도한 부분 조정
684
- - 장르 정체성 명확화
685
-
686
- ⚠️ {genre} 장르의 정수를 완벽하게 구현하세요."""
687
-
688
- return base_prompt
689
-
690
- def _summarize_previous_content(self, content: Dict) -> str:
691
- """이전 내용 요약"""
692
- summary = ""
693
- for key, value in content.items():
694
- if value:
695
- summary += f"\n[{key}]\n{value[:300]}...\n"
696
- return summary if summary else "첫 작업입니다."
697
-
698
- def generate_planning(self, query: str, screenplay_type: str, genre: str,
699
- progress_callback=None) -> Generator[Tuple[str, float, Dict, List], None, None]:
700
- """다중 전문가 협업 기획안 생성"""
701
- try:
702
- self.original_query = query
703
- self.current_session_id = ScreenplayDatabase.create_session(query, screenplay_type, genre)
704
-
705
- planning_content = {}
706
- self.accumulated_content = {}
707
- self.expert_feedbacks = []
708
- total_stages = len(PLANNING_STAGES)
709
-
710
- for idx, (role, stage_key, stage_desc) in enumerate(PLANNING_STAGES):
711
- progress = (idx / total_stages) * 100
712
-
713
- yield f"🔄 {stage_desc} 진행 중...", progress, planning_content, self.expert_feedbacks
714
-
715
- # 전문가별 프롬프트 생성
716
- prompt = self.get_expert_prompt(
717
- role, stage_key, query,
718
- self.accumulated_content, genre, screenplay_type
719
- )
720
-
721
- # LLM 호출
722
- messages = [
723
- {"role": "system", "content": f"""당신은 {role}입니다.
724
- {EXPERT_ROLES[role]['description']}
725
- 전문 분야: {', '.join(EXPERT_ROLES[role]['focus'])}
726
- 원본 요청을 절대적으로 준수하면서 전문성을 발휘하세요."""},
727
- {"role": "user", "content": prompt}
728
- ]
729
-
730
- content = ""
731
- for chunk in self.call_llm_streaming(messages):
732
- content += chunk
733
- planning_content[f"{role}_{stage_key}"] = content
734
- yield f"✏️ {stage_desc} 작성 중...", progress, planning_content, self.expert_feedbacks
735
-
736
- # 전문가 피드백 저장
737
- feedback = ExpertFeedback(
738
- role=role,
739
- stage=stage_key,
740
- feedback=content[:500],
741
- suggestions=self._extract_suggestions(content),
742
- score=self._calculate_score(content, query)
743
- )
744
- self.expert_feedbacks.append(feedback)
745
-
746
- # DB에 피드백 저장
747
- ScreenplayDatabase.save_expert_feedback(
748
- self.current_session_id, role, stage_key,
749
- feedback.feedback, feedback.suggestions, feedback.score
750
- )
751
-
752
- # 누적 내용 저장
753
- self.accumulated_content[f"{role}_{stage_key}"] = content
754
- time.sleep(0.5)
755
-
756
- # 최종 기획안 저장
757
- ScreenplayDatabase.save_planning_data(self.current_session_id, planning_content)
758
- yield "✅ 전문가 협업 기획안 완성!", 100, planning_content, self.expert_feedbacks
759
-
760
- except Exception as e:
761
- yield f"❌ 오류 발생: {str(e)}", 0, {}, []
762
-
763
- def generate_screenplay(self, session_id: str, planning_data: Dict,
764
- progress_callback=None) -> Generator[Tuple[str, float, str, List], None, None]:
765
- """다중 전문가 협업 시나리오 작성"""
766
- try:
767
- total_stages = len(WRITING_STAGES)
768
- screenplay_content = ""
769
- act_contents = {"1막": "", "2막A": "", "2막B": "", "3막": ""}
770
- self.expert_feedbacks = []
771
-
772
- for idx, (role, stage_name, stage_desc) in enumerate(WRITING_STAGES):
773
- progress = (idx / total_stages) * 100
774
-
775
- yield f"🔄 {stage_desc} 진행 중...", progress, screenplay_content, self.expert_feedbacks
776
-
777
- # 현재 막 결정
778
- current_act = ""
779
- if "1막" in stage_name:
780
- current_act = "1막"
781
- elif "2막A" in stage_name:
782
- current_act = "2막A"
783
- elif "2막B" in stage_name:
784
- current_act = "2막B"
785
- elif "3막" in stage_name:
786
- current_act = "3막"
787
-
788
- # 역할별 프롬프트 생성
789
- if role == "스토리작가":
790
- prompt = self._create_writer_prompt(
791
- current_act, planning_data, act_contents, stage_name
792
- )
793
- elif role == "편집자":
794
- prompt = self._create_editor_prompt(
795
- current_act, act_contents[current_act] if current_act else screenplay_content
796
- )
797
- elif role == "감독":
798
- prompt = self._create_director_prompt(
799
- current_act, act_contents[current_act] if current_act else screenplay_content
800
- )
801
- elif role == "대화전문가":
802
- prompt = self._create_dialogue_prompt(
803
- current_act, act_contents[current_act] if current_act else screenplay_content
804
- )
805
- elif role == "비평가":
806
- prompt = self._create_critic_prompt(
807
- current_act, act_contents[current_act] if current_act else screenplay_content,
808
- self.original_query
809
- )
810
- elif role == "장르전문가":
811
- prompt = self._create_genre_expert_prompt(
812
- screenplay_content, planning_data
813
- )
814
- else:
815
- continue
816
-
817
- # LLM 호출
818
- expert = EXPERT_ROLES.get(role, EXPERT_ROLES["스토리작가"])
819
- messages = [
820
- {"role": "system", "content": f"""당신은 {role}입니다.
821
- {expert['description']}
822
- 원본 요청: {self.original_query}
823
- 표준 시나리오 포맷을 준수하세요."""},
824
- {"role": "user", "content": prompt}
825
- ]
826
-
827
- content = ""
828
- for chunk in self.call_llm_streaming(messages, max_tokens=15000):
829
- content += chunk
830
-
831
- # 완성 단계에서만 막 내용 업데이트
832
- if current_act and "완성" in stage_name:
833
- act_contents[current_act] = content
834
- screenplay_content = "\n\n".join([
835
- act_contents[act] for act in ["1막", "2막A", "2막B", "3막"]
836
- if act_contents[act]
837
- ])
838
-
839
- yield f"✏️ {stage_desc} 작성 중...", progress, screenplay_content, self.expert_feedbacks
840
-
841
- # 전문가 피드백 생성
842
- feedback = ExpertFeedback(
843
- role=role,
844
- stage=stage_name,
845
- feedback=f"{role}가 {stage_name}을 검토/작업했습니다.",
846
- suggestions=[],
847
- score=85.0
848
- )
849
- self.expert_feedbacks.append(feedback)
850
-
851
- time.sleep(0.5)
852
-
853
- # 최종 저장
854
- title = self._extract_title(planning_data)
855
- logline = self._extract_logline(planning_data)
856
-
857
- ScreenplayDatabase.save_screenplay_content(session_id, screenplay_content, title, logline)
858
- yield "✅ 전문가 협업 시나리오 완성!", 100, screenplay_content, self.expert_feedbacks
859
-
860
- except Exception as e:
861
- yield f"❌ 오류 발생: {str(e)}", 0, "", []
862
-
863
- def _create_writer_prompt(self, act: str, planning_data: Dict,
864
- previous_acts: Dict, stage_name: str) -> str:
865
- """스토리 작가 프롬프트"""
866
-
867
- if "초고" in stage_name:
868
- min_lines = 800
869
- elif "완성" in stage_name:
870
- min_lines = 1200
871
- else:
872
- min_lines = 1000
873
-
874
- return f"""【{act} 작성】
875
- 목표 분량: {min_lines}줄 이상
876
-
877
- 【기획안 핵심】
878
- {self._extract_planning_core(planning_data)}
879
-
880
- 【이전 막】
881
- {previous_acts if previous_acts else "첫 막입니다"}
882
-
883
- 【작성 요구사항】
884
- 1. 표준 시나리오 포맷
885
- 2. 충분한 씬 분량 (각 5-7페이지)
886
- 3. 시각적 액션 묘사
887
- 4. 자연스러운 대화
888
- 5. 원본 스토리 충실
889
-
890
- 반드시 {min_lines}줄 이상 작성하세요."""
891
-
892
- def _create_editor_prompt(self, act: str, content: str) -> str:
893
- """편집자 프롬프트"""
894
- return f"""【{act} 편집】
895
-
896
- 현재 내용:
897
- {content[:2000]}...
898
-
899
- 【편집 포인트】
900
- 1. 페이싱 조정
901
- - 느린 부분 가속
902
- - 급한 부분 완화
903
-
904
- 2. 불필요한 부분 제거
905
- - 중복 대사/액션
906
- - 과도한 설명
907
-
908
- 3. 씬 전환 개선
909
- - 자연스러운 연결
910
- - 시간/공간 이동
911
-
912
- 4. 긴장감 조절
913
- - 클라이맥스 빌드업
914
- - 완급 조절
915
-
916
- 구체적인 편집 제안을 하세요."""
917
-
918
- def _create_director_prompt(self, act: str, content: str) -> str:
919
- """감독 프롬프트"""
920
- return f"""【{act} 연출 강화】
921
-
922
- 현재 시나리오:
923
- {content[:2000]}...
924
-
925
- 【비주얼 강화 포인트】
926
- 1. 오프닝 씬 비주얼
927
- 2. 핵심 액션 시퀀스
928
- 3. 감정적 클로즈업
929
- 4. 환경/분위기 묘사
930
- 5. 상징적 이미지
931
-
932
- 각 씬에 시각적 디테일을 추가하세요."""
933
-
934
- def _create_dialogue_prompt(self, act: str, content: str) -> str:
935
- """대화 전문가 프롬프트"""
936
- return f"""【{act} 대사 개선】
937
-
938
- 현재 대사 검토:
939
- {content[:2000]}...
940
-
941
- 【개선 방향】
942
- 1. 캐릭터별 말투 차별화
943
- 2. 서브텍스트 추가
944
- 3. 불필요한 설명 제거
945
- 4. 감정적 뉘앙스 강화
946
- 5. 리듬과 템포 조정
947
-
948
- 개선된 대사 예시를 제시하세요."""
949
-
950
- def _create_critic_prompt(self, act: str, content: str, original_query: str) -> str:
951
- """비평가 프롬프트"""
952
- return f"""【{act} 종합 검토】
953
-
954
- 원본 요청: {original_query}
955
-
956
- 현재 내용:
957
- {content[:2000]}...
958
-
959
- 【평가 항목】
960
- 1. 원본 충실도 (40점)
961
- 2. 스토리 완성도 (20점)
962
- 3. 캐릭터 매력 (20점)
963
- 4. 대사 품질 (10점)
964
- 5. 비주얼 임팩트 (10점)
965
-
966
- 총점: /100
967
-
968
- 【개선 필요사항】
969
- 구체적이고 실행 가능한 제안을 하세요."""
970
-
971
- def _create_genre_expert_prompt(self, screenplay: str, planning_data: Dict) -> str:
972
- """장르 전문가 프롬프트"""
973
- genre = planning_data.get("장르", "드라마")
974
- genre_template = GENRE_TEMPLATES.get(genre, GENRE_TEMPLATES["드라마"])
975
-
976
- return f"""【{genre} 장르 최적화】
977
-
978
- 【장르 특성】
979
- - 핵심 요소: {', '.join(genre_template['key_elements'])}
980
- - 구조 비트: {', '.join(genre_template['structure_beats'])}
981
-
982
- 【최적화 포인트】
983
- 1. 장르 관습 충족도
984
- 2. 페이싱 조정
985
- 3. 특유 요소 강화
986
- 4. 톤 일관성
987
-
988
- 장르 특성을 극대화하는 제안을 하세요."""
989
-
990
- def _extract_suggestions(self, content: str) -> List[str]:
991
- """내용에서 제안사항 추출"""
992
- suggestions = []
993
- lines = content.split('\n')
994
- for line in lines:
995
- if any(keyword in line for keyword in ['제안', '개선', '추천', '권장']):
996
- suggestions.append(line.strip())
997
- return suggestions[:5]
998
-
999
- def _calculate_score(self, content: str, original_query: str) -> float:
1000
- """충실도 점수 계산"""
1001
- score = 70.0 # 기본 점수
1002
-
1003
- # 원본 키워드 포함도 체크
1004
- keywords = original_query.split()
1005
- matched = sum(1 for keyword in keywords if keyword in content)
1006
- score += (matched / len(keywords)) * 20 if keywords else 0
1007
-
1008
- # 길이 체크
1009
- if len(content) > 500:
1010
- score += 10
1011
-
1012
- return min(score, 100.0)
1013
-
1014
- def _extract_title(self, planning_data: Dict) -> str:
1015
- """기획안에서 제목 추출"""
1016
- for key, value in planning_data.items():
1017
- if "제목" in value:
1018
- match = re.search(r'제목[:\s]*([^\n]+)', value)
1019
- if match:
1020
- return match.group(1).strip()
1021
- return "무제"
1022
-
1023
- def _extract_logline(self, planning_data: Dict) -> str:
1024
- """기획안에서 로그라인 추출"""
1025
- for key, value in planning_data.items():
1026
- if "로그라인" in value:
1027
- match = re.search(r'로그라인[:\s]*([^\n]+)', value)
1028
- if match:
1029
- return match.group(1).strip()
1030
- return ""
1031
-
1032
- def _extract_planning_core(self, planning_data: Dict) -> str:
1033
- """기획안 핵심 내용 추출"""
1034
- core = ""
1035
- for key, value in planning_data.items():
1036
- if any(k in key for k in ["스토리", "캐릭터", "구조"]):
1037
- core += f"\n[{key}]\n{value[:500]}...\n"
1038
- return core
1039
-
1040
- # 유틸리티 함수
1041
- def format_planning_display(planning_data: Dict) -> str:
1042
- """기획안 표시 포맷"""
1043
- if not planning_data:
1044
- return "기획안이 아직 생성되지 않았습니다."
1045
-
1046
- formatted = "# 📋 시나리오 기획안 (전문가 협업)\n\n"
1047
-
1048
- for key, content in planning_data.items():
1049
- role = key.split('_')[0] if '_' in key else key
1050
- emoji = EXPERT_ROLES.get(role, {}).get("emoji", "📝")
1051
- formatted += f"## {emoji} {key}\n\n"
1052
- formatted += content + "\n\n"
1053
- formatted += "---\n\n"
1054
-
1055
- return formatted
1056
-
1057
- def format_expert_feedbacks(feedbacks: List[ExpertFeedback]) -> str:
1058
- """전문가 피드백 표시"""
1059
- if not feedbacks:
1060
- return ""
1061
-
1062
- formatted = "## 🎯 전문가 평가\n\n"
1063
-
1064
- for fb in feedbacks:
1065
- emoji = EXPERT_ROLES.get(fb.role, {}).get("emoji", "📝")
1066
- formatted += f"### {emoji} {fb.role}\n"
1067
- formatted += f"**점수:** {fb.score:.1f}/100\n"
1068
- formatted += f"**피드백:** {fb.feedback[:200]}...\n"
1069
-
1070
- if fb.suggestions:
1071
- formatted += "**제안사항:**\n"
1072
- for suggestion in fb.suggestions[:3]:
1073
- formatted += f"- {suggestion}\n"
1074
-
1075
- formatted += "\n"
1076
-
1077
- return formatted
1078
-
1079
- def format_screenplay_display(screenplay_text: str) -> str:
1080
- """시나리오 표시 포맷"""
1081
- if not screenplay_text:
1082
- return "시나리오가 아직 작성되지 않았습니다."
1083
-
1084
- formatted = "# 🎬 시나리오\n\n"
1085
-
1086
- # 씬 헤딩 강조
1087
- formatted_text = re.sub(
1088
- r'^(INT\.|EXT\.).*$',
1089
- r'**\g<0>**',
1090
- screenplay_text,
1091
- flags=re.MULTILINE
1092
- )
1093
-
1094
- # 캐릭터명 강조
1095
- formatted_text = re.sub(
1096
- r'^([가-힣A-Z][가-힣A-Z\s]+)$',
1097
- r'**\g<0>**',
1098
- formatted_text,
1099
- flags=re.MULTILINE
1100
- )
1101
-
1102
- # 페이지 수 계산
1103
- page_count = len(screenplay_text.splitlines()) / 58
1104
- formatted = f"**총 페이지: {page_count:.1f}**\n\n" + formatted_text
1105
-
1106
- return formatted
1107
-
1108
- def generate_random_concept(screenplay_type: str, genre: str) -> str:
1109
- """랜덤 컨셉 생성"""
1110
- concepts = {
1111
- '액션': [
1112
- "은퇴한 특수요원이 납치된 딸을 구하기 위해 다시 현장으로 복귀한다",
1113
- "평범한 택시기사가 우연히 국제 테러 조직의 음모에 휘말린다",
1114
- ],
1115
- '스릴러': [
1116
- "기억을 잃은 남자가 자신이 연쇄살인범이라는 증거를 발견한다",
1117
- "실종된 아이를 찾는 과정에서 마을의 끔찍한 비밀이 드러난다",
1118
- ],
1119
- '드라마': [
1120
- "말기 암 환자가 마지막 소원으로 가족과의 화해를 시도한다",
1121
- "재개발로 사라질 동네를 지키려는 주민들의 마지막 싸움",
1122
- ],
1123
- }
1124
-
1125
- genre_concepts = concepts.get(genre, concepts['드라마'])
1126
- return random.choice(genre_concepts)
1127
-
1128
- # Gradio 인터페이스
1129
- def create_interface():
1130
- css = """
1131
- .main-header {
1132
- text-align: center;
1133
- margin-bottom: 2rem;
1134
- padding: 2.5rem;
1135
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
1136
- border-radius: 15px;
1137
- color: white;
1138
- box-shadow: 0 10px 30px rgba(0,0,0,0.3);
1139
- }
1140
-
1141
- .header-title {
1142
- font-size: 3rem;
1143
- margin-bottom: 1rem;
1144
- text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
1145
- }
1146
-
1147
- .expert-panel {
1148
- background: #f0f4f8;
1149
- padding: 1rem;
1150
- border-radius: 10px;
1151
- margin: 1rem 0;
1152
- }
1153
-
1154
- .expert-badge {
1155
- display: inline-block;
1156
- padding: 0.3rem 0.8rem;
1157
- background: white;
1158
- border-radius: 20px;
1159
- margin: 0.2rem;
1160
- font-size: 0.9rem;
1161
- }
1162
-
1163
- .progress-bar {
1164
- background: #e0e0e0;
1165
- border-radius: 10px;
1166
- height: 30px;
1167
- position: relative;
1168
- margin: 1rem 0;
1169
- }
1170
-
1171
- .progress-fill {
1172
- background: linear-gradient(90deg, #667eea, #764ba2);
1173
- height: 100%;
1174
- border-radius: 10px;
1175
- transition: width 0.3s ease;
1176
- }
1177
- """
1178
-
1179
- with gr.Blocks(theme=gr.themes.Soft(), css=css, title="AI 시나리오 작가 - 전문가 협업 시스템") as interface:
1180
- gr.HTML("""
1181
- <div class="main-header">
1182
- <h1 class="header-title">🎬 AI 시나리오 작가</h1>
1183
- <p style="font-size: 1.2rem; opacity: 0.95;">
1184
- 8명의 전문가가 협업하여 완벽한 시나리오를 작성합니다<br>
1185
- 프로듀서, 감독, 작가, 비평가가 함께 만드는 전문 시나리오
1186
- </p>
1187
- </div>
1188
- """)
1189
-
1190
- # 전문가 소개
1191
- gr.HTML("""
1192
- <div class="expert-panel">
1193
- <h3>👥 참여 전문가</h3>
1194
- <span class="expert-badge">🎬 프로듀서</span>
1195
- <span class="expert-badge">📖 스토리작가</span>
1196
- <span class="expert-badge">👥 캐릭터디자이너</span>
1197
- <span class="expert-badge">🎭 감독</span>
1198
- <span class="expert-badge">🔍 비평가</span>
1199
- <span class="expert-badge">✂️ 편집자</span>
1200
- <span class="expert-badge">💬 대화전문가</span>
1201
- <span class="expert-badge">🎯 장르전문가</span>
1202
- </div>
1203
- """)
1204
-
1205
- current_session_id = gr.State(None)
1206
- current_planning_data = gr.State({})
1207
- current_feedbacks = gr.State([])
1208
-
1209
- with gr.Tabs():
1210
- with gr.Tab("📝 새 시나리오"):
1211
- with gr.Row():
1212
- with gr.Column(scale=2):
1213
- query_input = gr.Textbox(
1214
- label="💡 시나리오 아이디어",
1215
- placeholder="""구체적인 스토리를 입력하세요. 예시:
1216
- "2045년 서울, AI가 인간 감정을 완벽히 모방하는 시대.
1217
- AI 윤리학자가 죽은 아내를 복제한 AI와 살다가
1218
- 진짜 아내가 살아있다는 증거를 발견하고 진실을 찾아 나선다."
1219
- """,
1220
- lines=6
1221
- )
1222
-
1223
- with gr.Column(scale=1):
1224
- screenplay_type = gr.Radio(
1225
- choices=list(SCREENPLAY_LENGTHS.keys()),
1226
- value="영화",
1227
- label="📽️ 시나리오 유형"
1228
- )
1229
-
1230
- genre_select = gr.Dropdown(
1231
- choices=list(GENRE_TEMPLATES.keys()),
1232
- value="드라마",
1233
- label="🎭 장르"
1234
- )
1235
-
1236
- with gr.Row():
1237
- random_btn = gr.Button("🎲 랜덤 아이디어", scale=1)
1238
- clear_btn = gr.Button("🗑️ 초기화", scale=1)
1239
- planning_btn = gr.Button("📋 전문가 기획 회의", variant="primary", scale=2)
1240
-
1241
- # 진행 상태
1242
- progress_bar = gr.HTML(
1243
- value='<div class="progress-bar"><div class="progress-fill" style="width: 0%"></div></div>'
1244
- )
1245
- status_text = gr.Textbox(
1246
- label="📊 진행 상태",
1247
- interactive=False,
1248
- value="아이디어를 입력하고 전문가 기획 회의를 시작하세요"
1249
- )
1250
-
1251
- # 전문가 피드백
1252
- with gr.Group():
1253
- gr.Markdown("### 🎯 전문가 피드백")
1254
- expert_feedback_display = gr.Markdown(
1255
- value="*전문가들의 의견이 여기에 표시됩니다...*"
1256
- )
1257
-
1258
- # 기획안
1259
- with gr.Group():
1260
- gr.Markdown("### 📋 시나리오 기획안")
1261
- planning_display = gr.Markdown(
1262
- value="*기획안이 여기에 표시됩니다...*"
1263
- )
1264
-
1265
- with gr.Row():
1266
- save_planning_btn = gr.Button("💾 기획안 저장", scale=1)
1267
- generate_screenplay_btn = gr.Button("🎬 전문가 시나리오 작성", variant="primary", scale=2)
1268
-
1269
- # 시나리오 출력
1270
- with gr.Group():
1271
- gr.Markdown("### 📄 완성된 시나리오")
1272
- screenplay_output = gr.Markdown(
1273
- value="*시나리오가 여기에 표시됩니다...*"
1274
- )
1275
-
1276
- download_btn = gr.Button("💾 다운로드 (TXT)")
1277
-
1278
- # 이벤트 핸들러
1279
- def handle_planning(query, s_type, genre):
1280
- if not query:
1281
- yield "", "❌ 아이디어를 입력해주세요", "", "", {}
1282
- return
1283
-
1284
- system = ScreenplayGenerationSystem()
1285
-
1286
- for status, progress, planning_data, feedbacks in system.generate_planning(query, s_type, genre):
1287
- progress_html = f'<div class="progress-bar"><div class="progress-fill" style="width: {progress}%"></div></div>'
1288
- planning_display = format_planning_display(planning_data)
1289
- feedback_display = format_expert_feedbacks(feedbacks)
1290
-
1291
- yield progress_html, status, feedback_display, planning_display, planning_data
1292
-
1293
- def handle_screenplay_generation(session_id, planning_data):
1294
- if not planning_data:
1295
- yield "", "❌ 먼저 기획안을 생성해주세요", "", ""
1296
- return
1297
-
1298
- system = ScreenplayGenerationSystem()
1299
-
1300
- for status, progress, screenplay, feedbacks in system.generate_screenplay(session_id, planning_data):
1301
- progress_html = f'<div class="progress-bar"><div class="progress-fill" style="width: {progress}%"></div></div>'
1302
- screenplay_display = format_screenplay_display(screenplay)
1303
- feedback_display = format_expert_feedbacks(feedbacks)
1304
-
1305
- yield progress_html, status, feedback_display, screenplay_display
1306
-
1307
- # 이벤트 연결
1308
- planning_btn.click(
1309
- fn=handle_planning,
1310
- inputs=[query_input, screenplay_type, genre_select],
1311
- outputs=[progress_bar, status_text, expert_feedback_display, planning_display, current_planning_data]
1312
- )
1313
-
1314
- generate_screenplay_btn.click(
1315
- fn=handle_screenplay_generation,
1316
- inputs=[current_session_id, current_planning_data],
1317
- outputs=[progress_bar, status_text, expert_feedback_display, screenplay_output]
1318
- )
1319
-
1320
- random_btn.click(
1321
- fn=generate_random_concept,
1322
- inputs=[screenplay_type, genre_select],
1323
- outputs=[query_input]
1324
- )
1325
-
1326
- return interface
1327
-
1328
- # 메인 실행
1329
- if __name__ == "__main__":
1330
- logger.info("=" * 60)
1331
- logger.info("AI 시나리오 작가 - 전문가 협업 시스템")
1332
- logger.info("8명의 전문가가 협업하여 시나리오를 작성합니다")
1333
- logger.info("=" * 60)
1334
-
1335
- if not FIREWORKS_API_KEY or FIREWORKS_API_KEY == "dummy_token_for_testing":
1336
- logger.warning("⚠️ FIREWORKS_API_KEY를 설정해주세요!")
1337
- logger.warning("export FIREWORKS_API_KEY='your-api-key'")
1338
-
1339
- # 전문가 역할 소개
1340
- logger.info("\n참여 전문가:")
1341
- for role, info in EXPERT_ROLES.items():
1342
- logger.info(f" {info['emoji']} {role}: {info['description']}")
1343
-
1344
- ScreenplayDatabase.init_db()
1345
-
1346
- interface = create_interface()
1347
- interface.launch(
1348
- server_name="0.0.0.0",
1349
- server_port=7860,
1350
- share=False
1351
- )