openfree commited on
Commit
842f875
·
verified ·
1 Parent(s): 12f739f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +25 -1751
app.py CHANGED
@@ -1,1761 +1,35 @@
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
- import traceback
20
 
21
- # --- 로깅 설정 ---
22
- logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
23
- logger = logging.getLogger(__name__)
24
-
25
- # --- 환경 변수 및 상수 ---
26
- FIREWORKS_API_KEY = os.getenv("FIREWORKS_API_KEY", "")
27
- BRAVE_SEARCH_API_KEY = os.getenv("BRAVE_SEARCH_API_KEY", "")
28
- API_URL = "https://api.fireworks.ai/inference/v1/chat/completions"
29
- MODEL_ID = "accounts/fireworks/models/qwen3-235b-a22b-instruct-2507"
30
- BRAVE_SEARCH_URL = "https://api.search.brave.com/res/v1/web/search"
31
- DB_PATH = "screenplay_sessions_korean.db"
32
-
33
- # 시나리오 길이 설정
34
- SCREENPLAY_LENGTHS = {
35
- "영화": {"pages": 120, "description": "장편 영화 (110-130페이지)", "min_pages": 110},
36
- "드라마": {"pages": 60, "description": "TV 드라마 (55-65페이지)", "min_pages": 55},
37
- "웹드라마": {"pages": 50, "description": "웹/OTT 시리즈 (45-55페이지)", "min_pages": 45},
38
- "단편": {"pages": 20, "description": "단편 영화 (15-25페이지)", "min_pages": 15}
39
- }
40
-
41
- # 환경 검증
42
- if not FIREWORKS_API_KEY:
43
- logger.error("FIREWORKS_API_KEY가 설정되지 않았습니다.")
44
- FIREWORKS_API_KEY = "dummy_token_for_testing"
45
-
46
- # 글로벌 변수
47
- db_lock = threading.Lock()
48
-
49
- # 전문가 역할 정의
50
- EXPERT_ROLES = {
51
- "프로듀서": {
52
- "emoji": "🎬",
53
- "description": "상업성과 시장성 분석",
54
- "focus": ["타겟 관객", "제작 가능성", "예산 규모", "마케팅 포인트"],
55
- "personality": "실용적이고 시장 지향적"
56
- },
57
- "스토리작가": {
58
- "emoji": "📖",
59
- "description": "내러티브 구조와 플롯 개발",
60
- "focus": ["3막 구조", "플롯 포인트", "서사 아크", "테마"],
61
- "personality": "창의적이고 구조적"
62
- },
63
- "캐릭터디자이너": {
64
- "emoji": "👥",
65
- "description": "인물 창조와 관계 설계",
66
- "focus": ["캐릭터 아크", "동기부여", "관계 역학", "대화 스타일"],
67
- "personality": "심리학적이고 공감적"
68
- },
69
- "감독": {
70
- "emoji": "🎭",
71
- "description": "비주얼 스토리텔링과 연출",
72
- "focus": ["시각적 구성", "카메라 워크", "미장센", "리듬과 페이싱"],
73
- "personality": "비주얼 중심적이고 예술적"
74
- },
75
- "고증전문가": {
76
- "emoji": "🔎",
77
- "description": "사실 확인과 고증",
78
- "focus": ["역사적 정확성", "과학적 타당성", "문화적 적절성", "현실성"],
79
- "personality": "정확하고 세심한"
80
- },
81
- "비평가": {
82
- "emoji": "🔍",
83
- "description": "객관적 분석과 개선점 제시",
84
- "focus": ["논리적 일관성", "감정적 임팩트", "원작 충실도", "완성도"],
85
- "personality": "분석적이고 비판적"
86
- },
87
- "편집자": {
88
- "emoji": "✂️",
89
- "description": "페이싱과 구조 최적화",
90
- "focus": ["씬 전환", "리듬", "긴장감 조절", "불필요한 부분 제거"],
91
- "personality": "정밀하고 효율적"
92
- },
93
- "대화전문가": {
94
- "emoji": "💬",
95
- "description": "대사와 서브텍스트 강화",
96
- "focus": ["자연스러운 대화", "캐릭터 보이스", "서브텍스트", "감정 전달"],
97
- "personality": "언어적이고 뉘앙스 중심"
98
- }
99
- }
100
-
101
- # 기획 단계 전문가
102
- PLANNING_STAGES = [
103
- ("프로듀서", "producer", "🎬 프로듀서: 핵심 컨셉 및 시장성 분석"),
104
- ("스토리작가", "story_writer", "📖 스토리 작가: 시놉시스 및 3막 구조"),
105
- ("캐릭터디자이너", "character_designer", "👥 캐릭터 디자이너: 인물 프로필 및 관계도"),
106
- ("감독", "director", "🎭 감독: 비주얼 컨셉 및 연출 방향"),
107
- ("비평가", "critic", "🔍 비평가: 기획안 종합 검토 및 개선점"),
108
- ]
109
-
110
- # 막별 작성 단계
111
- ACT_WRITING_STAGES = {
112
- "1막": [
113
- ("스토리작가", "초고", "✍️ 스토리작가: 1막 초고 작성"),
114
- ("고증전문가", "검증", "🔎 고증전문가: 사실 확인 및 검증"),
115
- ("편집자", "편집", "✂️ 편집자: 구조 및 페이싱 조정"),
116
- ("감독", "연출", "🎭 감독: 비주얼 강화 및 연출 노트"),
117
- ("대화전문가", "대사", "💬 대화전문가: 대사 개선"),
118
- ("비평가", "검토", "🔍 비평가: 종합 검토 및 평가"),
119
- ("스토리작가", "완성", "✅ 스토리작가: 1막 최종 완성")
120
- ],
121
- "2막A": [
122
- ("스토리작가", "초고", "✍️ 스토리작가: 2막A 초고 작성"),
123
- ("고증전문가", "검증", "🔎 고증전문가: 사실 확인 및 검증"),
124
- ("편집자", "편집", "✂️ 편집자: 구조 및 페이싱 조정"),
125
- ("감독", "연출", "🎭 감독: 비주얼 강화 및 연출 노트"),
126
- ("대화전문가", "대사", "💬 대화전문가: 대사 개선"),
127
- ("비평가", "검토", "🔍 비평가: 종합 검토 및 평가"),
128
- ("스토리작가", "완성", "✅ 스토리작가: 2막A 최종 완성")
129
- ],
130
- "2막B": [
131
- ("스토리작가", "초고", "✍️ 스토리작가: 2막B 초고 작성"),
132
- ("고증전문가", "검증", "🔎 고증전문가: 사실 확인 및 검증"),
133
- ("편집자", "편집", "✂️ 편집자: 구조 및 페이싱 조정"),
134
- ("감독", "연출", "🎭 감독: 비주얼 강화 및 연출 노트"),
135
- ("대화전문가", "대사", "💬 대화전문가: 대사 개선"),
136
- ("비평가", "검토", "🔍 비평가: 종합 검토 및 평가"),
137
- ("스토리작가", "완성", "✅ 스토리작가: 2막B 최종 완성")
138
- ],
139
- "3막": [
140
- ("스토리작가", "초고", "✍️ 스토리작가: 3막 초고 작성"),
141
- ("고증전문가", "검증", "🔎 고증전문가: 사실 확인 및 검증"),
142
- ("편집자", "편집", "✂️ 편집자: 구조 및 페이싱 조정"),
143
- ("감독", "연출", "🎭 감독: 비주얼 강화 및 연출 노트"),
144
- ("대화전문가", "대사", "💬 대화전문가: 대사 개선"),
145
- ("비평가", "검토", "🔍 비평가: 종합 검토 및 평가"),
146
- ("스토리작가", "완성", "✅ 스토리작가: 3막 최종 완성")
147
- ]
148
- }
149
-
150
- # 웹 검색 클래스
151
- class WebSearcher:
152
- """사실 확인과 고증을 위한 웹 검색"""
153
- def __init__(self):
154
- self.api_key = BRAVE_SEARCH_API_KEY
155
- self.enabled = bool(self.api_key)
156
-
157
- def search(self, query: str, count: int = 3) -> List[Dict]:
158
- """웹 검색 수행"""
159
- if not self.enabled:
160
- return []
161
-
162
- headers = {
163
- "Accept": "application/json",
164
- "X-Subscription-Token": self.api_key
165
- }
166
-
167
- params = {
168
- "q": query,
169
- "count": count,
170
- "search_lang": "ko",
171
- "safesearch": "moderate"
172
- }
173
 
174
- try:
175
- response = requests.get(BRAVE_SEARCH_URL, headers=headers, params=params, timeout=10)
176
- if response.status_code == 200:
177
- results = response.json().get("web", {}).get("results", [])
178
- return results
179
- else:
180
- logger.error(f"Search API error: {response.status_code}")
181
- return []
182
- except Exception as e:
183
- logger.error(f"Search error: {e}")
184
- return []
185
-
186
- def verify_facts(self, content: str, context: str) -> Dict:
187
- """내용의 사실 확인"""
188
- verification_results = {
189
- "verified": [],
190
- "needs_correction": [],
191
- "suggestions": []
192
- }
193
 
194
- # 주요 키워드 추출
195
- keywords = self._extract_keywords(content)
 
 
196
 
197
- for keyword in keywords[:3]: # 상위 3개만 검색
198
- search_query = f"{keyword} {context} 사실 확인"
199
- results = self.search(search_query, count=2)
200
-
201
- if results:
202
- verification_results["verified"].append({
203
- "keyword": keyword,
204
- "sources": [r["title"] for r in results]
205
- })
206
 
207
- return verification_results
208
-
209
- def _extract_keywords(self, content: str) -> List[str]:
210
- """주요 키워드 추출"""
211
- # 간단한 키워드 추출 (실제로는 더 정교한 방법 필요)
212
- words = content.split()
213
- keywords = [w for w in words if len(w) > 4][:5]
214
- return keywords
215
-
216
- # 다이어그램 생성 클래스
217
- class DiagramGenerator:
218
- """캐릭터 관계도와 갈등 구조 다이어그램 생성"""
219
-
220
- @staticmethod
221
- def create_character_relationship_diagram(characters: Dict) -> str:
222
- """캐릭터 관계도 생성 (Mermaid 형식)"""
223
- diagram = """
224
- ```mermaid
225
- graph TB
226
- classDef protagonist fill:#f9d71c,stroke:#333,stroke-width:2px
227
- classDef antagonist fill:#ff6b6b,stroke:#333,stroke-width:2px
228
- classDef supporting fill:#95e1d3,stroke:#333,stroke-width:2px
229
- """
230
-
231
- # 주인공 노드
232
- if "주인공" in characters:
233
- protagonist = characters["주인공"]
234
- diagram += f"\n P[주인공<br/>{protagonist.get('name', '미정')}]:::protagonist"
235
-
236
- # 적대자 노드
237
- if "적대자" in characters:
238
- antagonist = characters["적대자"]
239
- diagram += f"\n A[적대자<br/>{antagonist.get('name', '미정')}]:::antagonist"
240
- diagram += f"\n P -.갈등.- A"
241
-
242
- # 조연 노드들
243
- supporting_count = 0
244
- for key, char in characters.items():
245
- if key not in ["주인공", "적대자"] and supporting_count < 5:
246
- supporting_count += 1
247
- char_id = f"S{supporting_count}"
248
- diagram += f"\n {char_id}[{key}<br/>{char.get('name', '미정')}]:::supporting"
249
-
250
- # 관계 연결
251
- if char.get("relationship_to_protagonist"):
252
- diagram += f"\n P --{char['relationship_to_protagonist']}--> {char_id}"
253
-
254
- diagram += "\n```"
255
- return diagram
256
-
257
- @staticmethod
258
- def create_conflict_structure_diagram(conflicts: List[Dict]) -> str:
259
- """갈등 구조 다이어그램 생성"""
260
- diagram = """
261
- ```mermaid
262
- flowchart LR
263
- classDef conflict fill:#ff9999,stroke:#333,stroke-width:2px
264
- classDef resolution fill:#99ff99,stroke:#333,stroke-width:2px
265
-
266
- Start([시작]) --> C1
267
- """
268
-
269
- for i, conflict in enumerate(conflicts[:5], 1):
270
- conflict_name = conflict.get("name", f"갈등{i}")
271
- resolution = conflict.get("resolution", "해결")
272
-
273
- diagram += f"\n C{i}[{conflict_name}]:::conflict"
274
- if i < len(conflicts):
275
- diagram += f" --> C{i+1}"
276
- else:
277
- diagram += f" --> End([{resolution}]):::resolution"
278
-
279
- diagram += "\n```"
280
- return diagram
281
-
282
- @staticmethod
283
- def create_story_arc_diagram(acts: Dict) -> str:
284
- """스토리 아크 다이어그램 생성"""
285
- diagram = """
286
- ```mermaid
287
- graph LR
288
- subgraph "1막 - 설정"
289
- A1[일상] --> A2[사건 발생]
290
- A2 --> A3[새로운 세계]
291
- end
292
-
293
- subgraph "2막A - 상승"
294
- B1[도전] --> B2[성장]
295
- B2 --> B3[중간점]
296
- end
297
-
298
- subgraph "2막B - 하강"
299
- C1[위기] --> C2[절망]
300
- C2 --> C3[각성]
301
- end
302
-
303
- subgraph "3막 - 해결"
304
- D1[최종 대결] --> D2[클라이맥스]
305
- D2 --> D3[새로운 일상]
306
- end
307
-
308
- A3 --> B1
309
- B3 --> C1
310
- C3 --> D1
311
-
312
- style A1 fill:#e1f5fe
313
- style D3 fill:#c8e6c9
314
- ```
315
- """
316
- return diagram
317
-
318
- # 데이터 클래스
319
- @dataclass
320
- class ExpertFeedback:
321
- """전문가 피드백"""
322
- role: str
323
- stage: str
324
- feedback: str
325
- suggestions: List[str]
326
- score: float
327
- timestamp: datetime = field(default_factory=datetime.now)
328
-
329
- @dataclass
330
- class ActProgress:
331
- """막별 진행 상황"""
332
- act_name: str
333
- current_stage: int
334
- total_stages: int
335
- current_expert: str
336
- status: str # "ready", "in_progress", "complete"
337
- content: str = ""
338
- expert_feedbacks: List[ExpertFeedback] = field(default_factory=list)
339
-
340
- @property
341
- def progress_percentage(self):
342
- if self.total_stages == 0:
343
- return 0
344
- return (self.current_stage / self.total_stages) * 100
345
-
346
- # 데이터베이스 클래스
347
- class ScreenplayDatabase:
348
- @staticmethod
349
- def init_db():
350
- with sqlite3.connect(DB_PATH) as conn:
351
- conn.execute("PRAGMA journal_mode=WAL")
352
- cursor = conn.cursor()
353
-
354
- cursor.execute('''
355
- CREATE TABLE IF NOT EXISTS screenplay_sessions (
356
- session_id TEXT PRIMARY KEY,
357
- user_query TEXT NOT NULL,
358
- screenplay_type TEXT NOT NULL,
359
- genre TEXT NOT NULL,
360
- target_pages INTEGER,
361
- title TEXT,
362
- logline TEXT,
363
- planning_data TEXT,
364
- character_diagram TEXT,
365
- conflict_diagram TEXT,
366
- act1_content TEXT,
367
- act2a_content TEXT,
368
- act2b_content TEXT,
369
- act3_content TEXT,
370
- expert_feedbacks TEXT,
371
- created_at TEXT DEFAULT (datetime('now')),
372
- updated_at TEXT DEFAULT (datetime('now')),
373
- status TEXT DEFAULT 'planning',
374
- current_act TEXT DEFAULT '1막',
375
- total_pages REAL DEFAULT 0
376
- )
377
- ''')
378
-
379
- cursor.execute('''
380
- CREATE TABLE IF NOT EXISTS act_progress (
381
- id INTEGER PRIMARY KEY AUTOINCREMENT,
382
- session_id TEXT NOT NULL,
383
- act_name TEXT NOT NULL,
384
- stage_num INTEGER,
385
- expert_role TEXT,
386
- content TEXT,
387
- feedback TEXT,
388
- score REAL,
389
- created_at TEXT DEFAULT (datetime('now')),
390
- FOREIGN KEY (session_id) REFERENCES screenplay_sessions(session_id)
391
- )
392
- ''')
393
-
394
- conn.commit()
395
-
396
- @staticmethod
397
- @contextmanager
398
- def get_db():
399
- with db_lock:
400
- conn = sqlite3.connect(DB_PATH, timeout=30.0)
401
- conn.row_factory = sqlite3.Row
402
- try:
403
- yield conn
404
- finally:
405
- conn.close()
406
-
407
- @staticmethod
408
- def create_session(user_query: str, screenplay_type: str, genre: str) -> str:
409
- session_id = hashlib.md5(f"{user_query}{screenplay_type}{datetime.now()}".encode()).hexdigest()
410
- target_pages = SCREENPLAY_LENGTHS[screenplay_type]["pages"]
411
-
412
- with ScreenplayDatabase.get_db() as conn:
413
- conn.cursor().execute(
414
- '''INSERT INTO screenplay_sessions
415
- (session_id, user_query, screenplay_type, genre, target_pages)
416
- VALUES (?, ?, ?, ?, ?)''',
417
- (session_id, user_query, screenplay_type, genre, target_pages)
418
- )
419
- conn.commit()
420
- return session_id
421
-
422
- @staticmethod
423
- def save_planning_data(session_id: str, planning_data: Dict):
424
- with ScreenplayDatabase.get_db() as conn:
425
- conn.cursor().execute(
426
- '''UPDATE screenplay_sessions
427
- SET planning_data = ?, status = 'planned', updated_at = datetime('now')
428
- WHERE session_id = ?''',
429
- (json.dumps(planning_data, ensure_ascii=False), session_id)
430
- )
431
- conn.commit()
432
-
433
- @staticmethod
434
- def save_diagrams(session_id: str, character_diagram: str, conflict_diagram: str):
435
- """다이어그램 저장"""
436
- with ScreenplayDatabase.get_db() as conn:
437
- conn.cursor().execute(
438
- '''UPDATE screenplay_sessions
439
- SET character_diagram = ?, conflict_diagram = ?, updated_at = datetime('now')
440
- WHERE session_id = ?''',
441
- (character_diagram, conflict_diagram, session_id)
442
- )
443
- conn.commit()
444
-
445
- @staticmethod
446
- def save_act_content(session_id: str, act_name: str, content: str):
447
- """막별 콘텐츠 저장"""
448
- act_column = {
449
- "1막": "act1_content",
450
- "2막A": "act2a_content",
451
- "2막B": "act2b_content",
452
- "3막": "act3_content"
453
- }.get(act_name, "act1_content")
454
-
455
- with ScreenplayDatabase.get_db() as conn:
456
- conn.cursor().execute(
457
- f'''UPDATE screenplay_sessions
458
- SET {act_column} = ?, current_act = ?, updated_at = datetime('now')
459
- WHERE session_id = ?''',
460
- (content, act_name, session_id)
461
- )
462
- conn.commit()
463
-
464
- # 시나리오 생성 시스템
465
- class ScreenplayGenerationSystem:
466
- def __init__(self):
467
- self.api_key = FIREWORKS_API_KEY
468
- self.api_url = API_URL
469
- self.model_id = MODEL_ID
470
- self.current_session_id = None
471
- self.original_query = ""
472
- self.planning_data = {}
473
- self.web_searcher = WebSearcher()
474
- self.diagram_generator = DiagramGenerator()
475
- self.expert_feedbacks = []
476
- ScreenplayDatabase.init_db()
477
-
478
- def create_headers(self):
479
- if not self.api_key or self.api_key == "dummy_token_for_testing":
480
- raise ValueError("유효한 FIREWORKS_API_KEY가 필요합니다")
481
-
482
- return {
483
- "Accept": "application/json",
484
- "Content-Type": "application/json",
485
- "Authorization": f"Bearer {self.api_key}"
486
- }
487
-
488
-
489
-
490
- def call_llm_streaming(self, messages: List[Dict[str, str]], max_tokens: int = 8000) -> Generator[str, None, None]:
491
- """LLM 호출 - 버퍼 처리 개선"""
492
  try:
493
- # 시스템 메시지 강화
494
- if messages and messages[0].get("role") == "system":
495
- messages[0]["content"] = messages[0]["content"] + """
496
-
497
- 【절대 준수 사항】
498
- 1. 모든 응답을 완전한 한국어로 작성하세요.
499
- 2. 시나리오 포맷 용어(INT. EXT. CUT TO 등)는 영어로 유지하세요.
500
- 3. 캐릭터명과 대사는 한국어로 작성하세요.
501
- 4. 미완성 문장(...) 없이 완전하게 작성하세요.
502
- 5. 영화 시나리오 포맷을 정확히 준수하세요.
503
- """
504
-
505
- payload = {
506
- "model": self.model_id,
507
- "messages": messages,
508
- "max_tokens": max_tokens,
509
- "temperature": 0.7, # 약간 높임
510
- "top_p": 0.9,
511
- "top_k": 40,
512
- "presence_penalty": 0.3,
513
- "frequency_penalty": 0.3,
514
- "stream": True
515
- }
516
-
517
- headers = self.create_headers()
518
-
519
- response = requests.post(
520
- self.api_url,
521
- headers=headers,
522
- json=payload,
523
- stream=True,
524
- timeout=300
525
- )
526
-
527
- if response.status_code != 200:
528
- yield f"❌ API 오류: {response.status_code}"
529
- return
530
-
531
- buffer = ""
532
- total_output = "" # 전체 출력 추적
533
 
534
- for line in response.iter_lines():
535
- if not line:
536
- continue
537
-
538
- try:
539
- line_str = line.decode('utf-8').strip()
540
- if not line_str.startswith("data: "):
541
- continue
542
-
543
- data_str = line_str[6:]
544
- if data_str == "[DONE]":
545
- # 남은 버퍼 모두 출력
546
- if buffer:
547
- yield buffer
548
- break
549
-
550
- data = json.loads(data_str)
551
- if "choices" in data and len(data["choices"]) > 0:
552
- content = data["choices"][0].get("delta", {}).get("content", "")
553
- if content:
554
- buffer += content
555
- total_output += content
556
-
557
- # 버퍼가 충분히 쌓이면 yield (크기 늘림)
558
- if len(buffer) >= 500 or '\n\n' in buffer:
559
- yield buffer
560
- buffer = ""
561
-
562
-
563
-
564
- except Exception as e:
565
- logger.error(f"Line processing error: {e}")
566
- continue
567
-
568
- # 남은 버퍼 처리
569
- if buffer:
570
- yield buffer
571
-
572
- # 전체 출력 길이 로깅
573
- logger.info(f"Total LLM output length: {len(total_output)} characters, {len(total_output.split())} words")
574
-
575
- except Exception as e:
576
- logger.error(f"LLM streaming error: {e}")
577
- yield f"❌ 오류: {str(e)}"
578
-
579
- def generate_planning(self, query: str, screenplay_type: str, genre: str) -> Generator[Tuple[str, float, Dict, List, str], None, None]:
580
- """기획안 생성 with 다이어그램"""
581
- try:
582
- self.original_query = query
583
- self.current_session_id = ScreenplayDatabase.create_session(query, screenplay_type, genre)
584
-
585
- planning_content = {}
586
- self.expert_feedbacks = []
587
- total_stages = len(PLANNING_STAGES)
588
- character_diagram = ""
589
- conflict_diagram = ""
590
-
591
- for idx, (role, stage_key, stage_desc) in enumerate(PLANNING_STAGES):
592
- try:
593
- progress = ((idx + 1) / total_stages) * 100
594
-
595
- # 상태 업데이트
596
- yield f"🔄 {stage_desc} 진행 중...", progress, planning_content, self.expert_feedbacks, ""
597
-
598
- # 전문가별 프롬프트 생성
599
- prompt = self._get_expert_prompt(role, stage_key, query, planning_content, genre, screenplay_type)
600
-
601
- # LLM 호출
602
- messages = [
603
- {"role": "system", "content": f"당신은 {role}입니다. {EXPERT_ROLES[role]['description']}"},
604
- {"role": "user", "content": prompt}
605
- ]
606
-
607
- content = ""
608
- for chunk in self.call_llm_streaming(messages):
609
- if chunk and not chunk.startswith("❌"):
610
- content += chunk
611
- planning_content[f"{role}_{stage_key}"] = content
612
- # 중간 상태 업데이트
613
- yield f"✏️ {stage_desc} 작성 중...", progress, planning_content, self.expert_feedbacks, ""
614
-
615
- # 캐릭터 디자이너 단계에서 다이어그램 생성
616
- if role == "캐릭터디자이너" and content:
617
- characters = self._extract_characters_from_content(content)
618
- character_diagram = self.diagram_generator.create_character_relationship_diagram(characters)
619
- conflicts = self._extract_conflicts_from_content(content)
620
- conflict_diagram = self.diagram_generator.create_conflict_structure_diagram(conflicts)
621
-
622
- # 다이어그램 저장
623
- ScreenplayDatabase.save_diagrams(self.current_session_id, character_diagram, conflict_diagram)
624
-
625
- # 전문가 피드백 생성
626
- feedback = ExpertFeedback(
627
- role=role,
628
- stage=stage_key,
629
- feedback=content[:500] if content else "작성 중...",
630
- suggestions=self._extract_suggestions(content),
631
- score=85.0 + random.uniform(-5, 10)
632
- )
633
- self.expert_feedbacks.append(feedback)
634
-
635
- time.sleep(0.5) # API 제한 고려
636
-
637
- except GeneratorExit:
638
- logger.warning(f"Generator exit at stage {idx}")
639
- break
640
- except Exception as stage_error:
641
- logger.error(f"Error in stage {idx}: {stage_error}")
642
- continue
643
-
644
- # 최종 저장
645
- ScreenplayDatabase.save_planning_data(self.current_session_id, planning_content)
646
-
647
- # 다이어그램 포함한 최종 결과
648
- final_diagrams = f"\n\n## 📊 캐릭터 관계도\n{character_diagram}\n\n## ⚔️ 갈등 구조\n{conflict_diagram}"
649
-
650
- yield "✅ 기획안 완성!", 100, planning_content, self.expert_feedbacks, final_diagrams
651
-
652
- except Exception as e:
653
- logger.error(f"Planning generation error: {e}\n{traceback.format_exc()}")
654
- yield f"❌ 오류 발생: {str(e)}", 0, {}, [], ""
655
-
656
- def _get_expert_prompt(self, role: str, stage: str, query: str,
657
- previous_content: Dict, genre: str, screenplay_type: str) -> str:
658
- """각 전문가별 프롬프트 생성"""
659
-
660
- expert = EXPERT_ROLES[role]
661
- focus_areas = ", ".join(expert["focus"])
662
-
663
- base_prompt = f"""
664
- 【당신의 역할】
665
- 당신은 {role}입니다. {expert['description']}
666
- 집중 영역: {focus_areas}
667
-
668
- 【원본 요청】
669
- {query}
670
-
671
- 【장르】 {genre}
672
- 【형식】 {screenplay_type}
673
- """
674
-
675
- if role == "캐릭터디자이너":
676
- return base_prompt + """
677
- 【작성 요구사항】
678
- 1. 주인공 프로필
679
- - 이름, 나이, 직업
680
- - 성격과 특징
681
- - 목표와 동기
682
- - 성장 아크
683
-
684
- 2. 적대자 프로필
685
- - 이름과 역할
686
- - 주인공과의 갈등
687
- - 동기와 목표
688
-
689
- 3. 조연 캐릭터들 (3-5명)
690
- - 각자의 역할
691
- - 주인공과의 관계
692
-
693
- 4. 캐릭터 관계 설명
694
- - 인물간 관계
695
- - 주요 갈등 구조
696
-
697
- 구체적으로 작성하세요."""
698
-
699
- # 다른 역할들의 프롬프트...
700
- return base_prompt + "\n구체적이고 전문적인 분석을 제공하세요."
701
-
702
- def _extract_characters_from_content(self, content: str) -> Dict:
703
- """콘텐츠에서 캐릭터 정보 추출"""
704
- characters = {}
705
-
706
- # 간단한 파싱 (실제로는 더 정교한 방법 필요)
707
- if "주인공" in content:
708
- characters["주인공"] = {"name": "주인공", "role": "protagonist"}
709
- if "적대자" in content or "악역" in content:
710
- characters["적대자"] = {"name": "적대자", "role": "antagonist"}
711
-
712
- # 조연 추출
713
- for i in range(1, 4):
714
- characters[f"조연{i}"] = {"name": f"조연{i}", "relationship_to_protagonist": "동료"}
715
-
716
- return characters
717
-
718
- def _extract_conflicts_from_content(self, content: str) -> List[Dict]:
719
- """콘텐츠에서 갈등 구조 추출"""
720
- conflicts = [
721
- {"name": "초기 갈등", "type": "external"},
722
- {"name": "내적 갈등", "type": "internal"},
723
- {"name": "관계 갈등", "type": "relationship"},
724
- {"name": "최종 대결", "type": "climax", "resolution": "해결"}
725
- ]
726
- return conflicts
727
-
728
- def _extract_suggestions(self, content: str) -> List[str]:
729
- """제안사항 추출"""
730
- suggestions = []
731
- if content:
732
- lines = content.split('\n')
733
- for line in lines:
734
- if any(k in line for k in ['개선', '제안', '추천']):
735
- suggestions.append(line.strip())
736
- return suggestions[:5]
737
-
738
- def generate_act(self, session_id: str, act_name: str, planning_data: Dict,
739
- previous_acts: Dict) -> Generator[Tuple[ActProgress, str], None, None]:
740
- """막별 시나리오 생성 - 개선된 버전"""
741
-
742
- # act_progress를 먼저 초기화
743
- act_progress = ActProgress(
744
- act_name=act_name,
745
- current_stage=0,
746
- total_stages=0,
747
- current_expert="",
748
- status="initializing"
749
- )
750
-
751
- try:
752
- self.current_session_id = session_id
753
- self.planning_data = planning_data
754
-
755
- # 세션 정보 복원
756
- session_data = ScreenplayGenerationSystem.get_session_data(session_id)
757
-
758
- if session_data:
759
- self.original_query = session_data.get('user_query', '')
760
-
761
- stages = ACT_WRITING_STAGES.get(act_name, [])
762
-
763
- # act_progress 업데이트
764
- act_progress.total_stages = len(stages)
765
- act_progress.status = "in_progress"
766
-
767
- act_content = ""
768
-
769
- for idx, (role, stage_type, description) in enumerate(stages):
770
- try:
771
- act_progress.current_stage = idx + 1
772
- act_progress.current_expert = role
773
-
774
- # 진행 상황 업데이트
775
- yield act_progress, f"🔄 {description} 진행 중..."
776
-
777
- # 역할별 프롬프트 생성
778
- prompt = self._create_act_prompt(role, act_name, act_content, planning_data, previous_acts, stage_type)
779
-
780
- # 한국어 및 영화 포맷 강조
781
- system_message = f"""당신은 {role}입니다.
782
- {EXPERT_ROLES[role]['description']}
783
-
784
- 【절대 규칙】
785
- 1. 모든 내용을 한국어로 작성하세요.
786
- 2. 영화 시나리오 포맷을 사용하세요 (연극 아님).
787
- 3. 기획안 내용을 100% 반영하세요.
788
- 4. 영어 사용 금지 (INT. EXT. 등 포맷 용어 제외).
789
- 5. 완전한 문장으로 작성 (... 사용 금지)."""
790
-
791
- messages = [
792
- {"role": "system", "content": system_message},
793
- {"role": "user", "content": prompt}
794
- ]
795
-
796
- expert_output = ""
797
- line_count = 0
798
-
799
- for chunk in self.call_llm_streaming(messages, max_tokens=14000):
800
- if chunk and not chunk.startswith("❌"):
801
- # 영어 필터링 로직 완화
802
- expert_output += chunk # _clean_output 호출 제거 (나중에 한 번만)
803
- line_count = len(expert_output.split('\n'))
804
-
805
- # 실시간 업데이트
806
- yield act_progress, f"✏️ {role}: 작업 중... ({line_count}줄 작성)"
807
-
808
- if stage_type in ["초고", "완성"]:
809
- expert_output = self._clean_output(expert_output) # 여기서만 정리
810
-
811
- # 웹 검색 고증
812
- if role == "고증전문가" and self.web_searcher.enabled:
813
- verification = self.web_searcher.verify_facts(act_content, act_name)
814
- expert_output += f"\n\n【웹 검증 결과】\n{json.dumps(verification, ensure_ascii=False, indent=2)}"
815
-
816
- # 출력 검증 및 정리
817
- if stage_type in ["초고", "완성"]:
818
- expert_output = self._validate_and_clean_screenplay(expert_output)
819
-
820
- # 피드백 저장
821
- feedback = ExpertFeedback(
822
- role=role,
823
- stage=f"{act_name}_{stage_type}",
824
- feedback=f"{role} 작업 완료. {line_count}줄 작성.",
825
- suggestions=self._extract_suggestions(expert_output),
826
- score=85.0 + random.uniform(-5, 10)
827
- )
828
- act_progress.expert_feedbacks.append(feedback)
829
-
830
- # 최종 완성 단계에서만 콘텐츠 업데이트
831
- if stage_type == "완성":
832
- act_content = expert_output
833
- act_progress.content = act_content
834
- elif stage_type == "초고":
835
- act_content = expert_output
836
-
837
- yield act_progress, f"✅ {role} 작업 완료 ({line_count}줄)"
838
-
839
- time.sleep(0.5)
840
-
841
- except GeneratorExit:
842
- logger.warning(f"Generator exit at act stage {idx}")
843
- break
844
- except Exception as e:
845
- logger.error(f"Error in act stage {idx}: {e}")
846
- yield act_progress, f"⚠️ {role} 작업 중 오류 발생"
847
- continue
848
-
849
- # 막 완성
850
- act_progress.status = "complete"
851
- ScreenplayDatabase.save_act_content(session_id, act_name, act_content)
852
-
853
- # 최종 페이지 수 계산
854
- page_count = len(act_content.split('\n')) / 58
855
- yield act_progress, f"✅ {act_name} 완성! (약 {page_count:.1f}페이지)"
856
-
857
- except Exception as e:
858
- logger.error(f"Act generation error: {e}\n{traceback.format_exc()}")
859
- act_progress.status = "error"
860
- yield act_progress, f"❌ 오류: {str(e)}"
861
-
862
- def _clean_output(self, text: str) -> str:
863
- """출력 텍스트 정리 - 개선된 버전"""
864
-
865
- # 빈 텍스트 체크
866
- if not text:
867
- return ""
868
-
869
- lines = text.split('\n')
870
- cleaned_lines = []
871
-
872
- for line in lines:
873
- # 원본 라인 유지 (strip하지 않음)
874
- original_line = line
875
- stripped = line.strip()
876
-
877
- # 빈 줄은 그대로 유지
878
- if not stripped:
879
- cleaned_lines.append(original_line)
880
- continue
881
-
882
- # 시나리오 포맷 라인은 무조건 유지
883
- # INT., EXT., CUT TO:, FADE 등이 포함된 라인
884
- format_keywords = ["INT.", "EXT.", "INT ", "EXT ", "CUT TO", "FADE",
885
- "DISSOLVE", "MONTAGE", "FLASHBACK", "THE END"]
886
-
887
- is_format_line = any(keyword in stripped.upper() for keyword in format_keywords)
888
-
889
- if is_format_line:
890
- # 포맷 라인은 그대로 유지
891
- cleaned_lines.append(original_line)
892
- else:
893
- # 영어 체크 (새로운 로직 사용)
894
- if not self._contains_english(stripped):
895
- # 불완전한 ... 제거
896
- if "..." in original_line:
897
- original_line = original_line.replace("...", ".")
898
- cleaned_lines.append(original_line)
899
- else:
900
- # 실제 영어가 포함된 경우만 제거
901
- logger.debug(f"Removing English line: {stripped[:50]}")
902
- # 중요한 대사나 지시문이 아닌 경우만 제거
903
- if len(stripped) < 5: # 너무 짧은 줄은 유지
904
- cleaned_lines.append(original_line)
905
-
906
- return '\n'.join(cleaned_lines)
907
-
908
- def _contains_english(self, text: str) -> bool:
909
- """영어 포함 여부 확인 - 시나리오 포맷 친화적 버전"""
910
-
911
- # 빈 문자열 체크
912
- if not text or not text.strip():
913
- return False
914
-
915
- # 시나리오에서 반드시 허용해야 하는 용어들 (대폭 확장)
916
- allowed_terms = [
917
- # 씬 헤더
918
- "INT", "EXT", "INT.", "EXT.",
919
- # 트랜지션
920
- "CUT", "TO", "FADE", "IN", "OUT", "DISSOLVE", "CONT'D",
921
- # 카메라 용어
922
- "O.S.", "V.O.", "POV", "CLOSE", "UP", "WIDE", "SHOT",
923
- "ESTABLISHING", "MONTAGE", "FLASHBACK", "DREAM", "SEQUENCE",
924
- # 기술 용어
925
- "CG", "SFX", "VFX", "BGM", "OST", "FX",
926
- # 일반 약어
927
- "vs", "ex", "etc", "OK", "NG", "VIP", "CEO", "AI", "IT",
928
- "DJ", "MC", "PD", "VJ", "PC", "TV", "DVD", "USB", "CD",
929
- # 알파벳 단독 문자 (A, B, C 등)
930
- "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M",
931
- "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z",
932
- # 씬 관련
933
- "SCENE", "THE", "END", "TITLE", "SUPER",
934
- # 기타 허용 단어
935
- "sir", "Mr", "Ms", "Dr", "Prof"
936
- ]
937
-
938
- # 대소문자 구분 없이 체크
939
- test_text = text.upper()
940
- for term in allowed_terms:
941
- test_text = test_text.replace(term.upper(), "")
942
-
943
- # 남은 텍스트에서 실제 영어 단어 찾기
944
- import re
945
- # 6자 이상의 연속된 알파벳만 영어로 판단
946
- english_pattern = re.compile(r'[a-zA-Z]{6,}')
947
- matches = english_pattern.findall(test_text)
948
-
949
- if matches:
950
- # 추가 필터링: 알려진 한국어 로마자 표기는 제외
951
- korean_romanization = ['hyung', 'oppa', 'noona', 'dongsaeng', 'sunbae', 'hoobae']
952
- real_english = []
953
- for match in matches:
954
- if match.lower() not in korean_romanization:
955
- real_english.append(match)
956
-
957
- if real_english:
958
- logger.debug(f"Real English words found: {real_english}")
959
- return True
960
-
961
- return False
962
-
963
-
964
- def _validate_and_clean_screenplay(self, content: str) -> str:
965
- """시나리오 검증 및 정리"""
966
- if not content:
967
- return ""
968
-
969
- lines = content.split('\n')
970
- cleaned = []
971
-
972
- for line in lines:
973
- # 빈 줄은 유지
974
- if not line.strip():
975
- cleaned.append(line)
976
- continue
977
-
978
- # 불완전한 부분 제거
979
- if "..." in line and not line.strip().endswith('.'):
980
- continue
981
-
982
- # 영어 체크 (포맷 제외)
983
- if self._contains_english(line):
984
- logger.warning(f"Removing English line: {line[:50]}")
985
- continue
986
-
987
- cleaned.append(line)
988
-
989
- return '\n'.join(cleaned)
990
-
991
-
992
-
993
- @staticmethod
994
- def get_session_data(session_id: str) -> Optional[Dict]:
995
- """세션 데이터 조회"""
996
- with ScreenplayDatabase.get_db() as conn:
997
- cursor = conn.cursor()
998
- row = cursor.execute(
999
- 'SELECT * FROM screenplay_sessions WHERE session_id = ?',
1000
- (session_id,)
1001
- ).fetchone()
1002
- if row:
1003
- columns = [description[0] for description in cursor.description]
1004
- return dict(zip(columns, row))
1005
- return None
1006
-
1007
- def _create_act_prompt(self, role: str, act_name: str, current_content: str,
1008
- planning_data: Dict, previous_acts: Dict, stage_type: str) -> str:
1009
- """막 작성 프롬프트 생성 - 개선된 버전"""
1010
-
1011
- # 기획안에서 핵심 정보 추출
1012
- title = ""
1013
- genre = ""
1014
- screenplay_type = ""
1015
- logline = ""
1016
- synopsis = ""
1017
- characters = ""
1018
-
1019
- for key, value in planning_data.items():
1020
- if "프로듀서" in key and value:
1021
- # 제목과 로그라인 추출
1022
- if "제목" in value:
1023
- title_match = re.search(r'제목[:\s]*([^\n]+)', value)
1024
- if title_match:
1025
- title = title_match.group(1).strip()
1026
- if "로그라인" in value:
1027
- logline_match = re.search(r'로그라인[:\s]*([^\n]+)', value)
1028
- if logline_match:
1029
- logline = logline_match.group(1).strip()
1030
- elif "스토리" in key and value:
1031
- synopsis = value[:1000]
1032
- elif "캐릭터" in key and value:
1033
- characters = value[:1000]
1034
-
1035
- # 원본 요청 정보 강조
1036
- core_info = f"""
1037
- 【핵심 정보 - 절대 준수】
1038
- 원본 요청: {self.original_query}
1039
- 제목: {title}
1040
- 로그라인: {logline}
1041
- 장르: {genre if genre else '드라마'}
1042
- 형식: {screenplay_type if screenplay_type else '영화'}
1043
-
1044
- ⚠️ 중요: 위 정보를 절대적으로 준수하세요. 다른 이야기로 바꾸지 마세요.
1045
- ⚠️ 중요: 모든 내용을 한국어로만 작성하세요. 영어 사용 금지.
1046
- ⚠️ 중요: 표준 시나리오 포맷을 정확히 준수하세요.
1047
- """
1048
-
1049
- if role == "스토리작가":
1050
- if stage_type == "초고":
1051
- return f"""【{act_name} 초고 작성】
1052
- {core_info}
1053
-
1054
- 【기획안 내용】
1055
- 시놉시스:
1056
- {synopsis}
1057
-
1058
- 캐릭터:
1059
- {characters}
1060
-
1061
- 【이전 막 내용】
1062
- {self._summarize_previous_acts(previous_acts)}
1063
-
1064
- 【작성 요구사항】
1065
- 1. 반드시 한국어로만 작성
1066
- 2. 표준 시나리오 포맷:
1067
- INT. 장소 - 시간 (또는 EXT. 장소 - 시간)
1068
-
1069
- 장면 설명은 현재형으로 작성.
1070
-
1071
- 캐릭터명 (대문자)
1072
- 대사는 자연스러운 한국어로.
1073
-
1074
- 3. 분량: 최소 1000줄 이상
1075
- 4. 각 씬: 5-7페이지 분량
1076
- 5. 기획안 내용 100% 반영
1077
-
1078
- 절대 영어를 사용하지 마세요.
1079
- 절대 다른 이야기로 바꾸지 마세요.
1080
- 절대 연극이나 다른 형식으로 작성하지 마세요.
1081
-
1082
- {act_name}을 완전한 시나리오 형식으로 작성하세요."""
1083
-
1084
- else: # 완성
1085
- return f"""【{act_name} 최종 완성】
1086
- {core_info}
1087
-
1088
- 이전 전문가들의 피드백을 반영하여 최종본을 작성하세요.
1089
-
1090
- 【필수 사항】
1091
- 1. 한국어로만 작성
1092
- 2. 영화 시나리오 포맷 준수
1093
- 3. 기획안 스토리 유지
1094
- 4. 1200줄 이상
1095
-
1096
- 깨진 부분이나 ... 같은 미완성 부분 없이 완전하게 작성하세요."""
1097
-
1098
- elif role == "고증전문가":
1099
- return f"""【{act_name} 사실 확인】
1100
- {core_info}
1101
-
1102
- 다음 시나리오를 검토하세요:
1103
- {current_content[:2000]}...
1104
-
1105
- 【검토 사항】
1106
- 1. 사실 오류 확인
1107
- 2. 시대 고증
1108
- 3. 영어 사용 부분을 한국어로 수정
1109
- 4. 깨진 텍스트나 ... 부분 수정
1110
-
1111
- 모든 수정 사항을 한국어로 제시하세요."""
1112
-
1113
- elif role == "편집자":
1114
- return f"""【{act_name} 편집】
1115
- {core_info}
1116
-
1117
- 【편집 지침】
1118
- 1. 영어가 있다면 모두 한국어로 변경
1119
- 2. 깨진 부분(...) 완성
1120
- 3. 기획안과 일치하지 않는 내용 수정
1121
- 4. 시나리오 포맷 교정
1122
-
1123
- 편집된 버전을 완전한 한국어로 제시하세요."""
1124
-
1125
- elif role == "감독":
1126
- return f"""【{act_name} 연출 노트】
1127
- {core_info}
1128
-
1129
- 【연출 강화】
1130
- 1. 시각적 디테일 추가
1131
- 2. 카메라 앵글 제안
1132
- 3. 분위기 묘사 강화
1133
-
1134
- 모든 연출 노트를 한국어로 작성하세요.
1135
- 영화 시나리오임을 명확히 하세요."""
1136
-
1137
- elif role == "대화전문가":
1138
- return f"""【{act_name} 대사 개선】
1139
- {core_info}
1140
-
1141
- 【대��� 수정】
1142
- 1. 모든 대사를 자연스러운 한국어로
1143
- 2. 영어 대사가 있다면 한국어로 번역
1144
- 3. 캐릭터별 말투 차별화
1145
- 4. 어색한 번역투 제거
1146
-
1147
- 완전한 한국어 대사로 수정하세요."""
1148
-
1149
- elif role == "비평가":
1150
- return f"""【{act_name} 검토】
1151
- {core_info}
1152
-
1153
- 【평가 항목】
1154
- 1. 기획안과의 일치도 (최우선)
1155
- 2. 한국어 사용 (영어 혼용 금지)
1156
- 3. 시나리오 포맷 준수
1157
- 4. 스토리 완성도
1158
-
1159
- 문제점과 개선 사항을 구체적으로 제시하세요."""
1160
-
1161
- return f"""【{role} 작업】
1162
- {core_info}
1163
-
1164
- {act_name}을(를) 검토하고 개선하세요.
1165
- 반드시 한국어로만 작성하고, 기획안 내용을 충실히 반영하세요."""
1166
-
1167
- def _summarize_planning(self, planning_data: Dict) -> str:
1168
- """기획안 요약"""
1169
- summary = ""
1170
- for key, value in planning_data.items():
1171
- if value:
1172
- summary += f"[{key}]\n{value[:200]}...\n"
1173
- return summary[:1000]
1174
-
1175
- def _summarize_previous_acts(self, previous_acts: Dict) -> str:
1176
- """이전 막 요약"""
1177
- summary = ""
1178
- for act, content in previous_acts.items():
1179
- if content:
1180
- summary += f"[{act}]\n{content[:300]}...\n"
1181
- return summary[:1000]
1182
-
1183
-
1184
- # UI 함수들
1185
- def create_act_progress_display(act_progress: ActProgress) -> str:
1186
- """막별 진행 상황 표시"""
1187
- if not act_progress:
1188
- return ""
1189
-
1190
- html = f"""
1191
- <div style="border: 2px solid #667eea; border-radius: 10px; padding: 15px; margin: 10px 0;">
1192
- <h3>{act_progress.act_name} 진행 상황</h3>
1193
-
1194
- <div style="margin: 10px 0;">
1195
- <div style="background: #e0e0e0; border-radius: 10px; height: 30px; position: relative;">
1196
- <div style="background: linear-gradient(90deg, #667eea, #764ba2);
1197
- height: 100%; border-radius: 10px; width: {act_progress.progress_percentage}%;
1198
- transition: width 0.3s ease;">
1199
- <span style="position: absolute; left: 50%; transform: translateX(-50%);
1200
- color: white; line-height: 30px; font-weight: bold;">
1201
- {act_progress.progress_percentage:.0f}%
1202
- </span>
1203
- </div>
1204
- </div>
1205
- </div>
1206
-
1207
- <div style="margin-top: 15px;">
1208
- <strong>현재 작업:</strong> {EXPERT_ROLES.get(act_progress.current_expert, {}).get('emoji', '')} {act_progress.current_expert}
1209
- <br>
1210
- <strong>단계:</strong> {act_progress.current_stage} / {act_progress.total_stages}
1211
- <br>
1212
- <strong>상태:</strong> {act_progress.status}
1213
- </div>
1214
- </div>
1215
- """
1216
-
1217
- return html
1218
-
1219
- def format_planning_with_diagrams(planning_data: Dict, diagrams: str) -> str:
1220
- """기획안과 다이어그램 포맷팅"""
1221
- formatted = "# 📋 시나리오 기획안\n\n"
1222
-
1223
- for key, content in planning_data.items():
1224
- if content:
1225
- role = key.split('_')[0]
1226
- emoji = EXPERT_ROLES.get(role, {}).get('emoji', '📝')
1227
- formatted += f"## {emoji} {key}\n\n{content}\n\n---\n\n"
1228
-
1229
- if diagrams:
1230
- formatted += diagrams
1231
-
1232
- return formatted
1233
-
1234
- # Gradio 인터페이스
1235
- def create_interface():
1236
- css = """
1237
- .main-header {
1238
- text-align: center;
1239
- margin-bottom: 2rem;
1240
- padding: 2.5rem;
1241
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
1242
- border-radius: 15px;
1243
- color: white;
1244
- box-shadow: 0 10px 30px rgba(0,0,0,0.3);
1245
- }
1246
-
1247
- /* 시나리오 출력 스타일 */
1248
- .screenplay-output {
1249
- font-family: 'Courier New', monospace;
1250
- font-size: 14px;
1251
- line-height: 1.6;
1252
- white-space: pre-wrap;
1253
- background: #f8f9fa;
1254
- padding: 20px;
1255
- border-radius: 8px;
1256
- }
1257
-
1258
- .screenplay-output h3 {
1259
- color: #2c3e50;
1260
- margin: 20px 0 10px 0;
1261
- font-size: 16px;
1262
- font-weight: bold;
1263
- }
1264
-
1265
- .screenplay-output strong {
1266
- color: #34495e;
1267
- font-weight: bold;
1268
- }
1269
-
1270
- .screenplay-output em {
1271
- font-style: italic;
1272
- color: #7f8c8d;
1273
- }
1274
-
1275
- .act-button {
1276
- min-height: 80px;
1277
- font-size: 1.1rem;
1278
- margin: 5px;
1279
- border-radius: 10px;
1280
- }
1281
-
1282
- .expert-card {
1283
- background: white;
1284
- border: 1px solid #e0e0e0;
1285
- border-radius: 8px;
1286
- padding: 10px;
1287
- margin: 5px;
1288
- display: inline-block;
1289
- }
1290
-
1291
- .progress-container {
1292
- background: #f8f9fa;
1293
- border-radius: 10px;
1294
- padding: 20px;
1295
- margin: 15px 0;
1296
- }
1297
- """
1298
-
1299
- with gr.Blocks(theme=gr.themes.Soft(), css=css, title="AI 시나리오 작가") as interface:
1300
- gr.HTML("""
1301
- <div class="main-header">
1302
- <h1>🎬 AI 시나리오 작가</h1>
1303
- <p>전문가 협업 시스템 | 다이어그램 지원 | 웹 검증</p>
1304
- </div>
1305
- """)
1306
-
1307
- # 상태 변수
1308
- current_session_id = gr.State(None)
1309
- current_planning = gr.State({})
1310
- act1_content = gr.State("")
1311
- act2a_content = gr.State("")
1312
- act2b_content = gr.State("")
1313
- act3_content = gr.State("")
1314
-
1315
- with gr.Tabs():
1316
- # 기획 탭
1317
- with gr.Tab("📋 기획안 작성"):
1318
- with gr.Row():
1319
- with gr.Column(scale=2):
1320
- query_input = gr.Textbox(
1321
- label="💡 시나리오 아이디어",
1322
- placeholder="구체적인 스토리를 입력하세요...",
1323
- lines=4
1324
- )
1325
-
1326
- with gr.Column(scale=1):
1327
- screenplay_type = gr.Radio(
1328
- choices=list(SCREENPLAY_LENGTHS.keys()),
1329
- value="영화",
1330
- label="📽️ 유형"
1331
- )
1332
- genre_select = gr.Dropdown(
1333
- choices=["액션", "스릴러", "드라마", "코미디", "공포", "SF", "로맨스", "판타지"],
1334
- value="드라마",
1335
- label="🎭 장르"
1336
- )
1337
-
1338
- planning_btn = gr.Button("📋 기획안 생성 (다이어그램 포함)", variant="primary", size="lg")
1339
-
1340
- # 진행 상태
1341
- with gr.Row():
1342
- progress_bar = gr.Slider(0, 100, value=0, label="진행률", interactive=False)
1343
- status_text = gr.Textbox(label="상태", value="대기 중", interactive=False)
1344
-
1345
- # 기획안과 다이어그램
1346
- planning_display = gr.Markdown("*기획안과 다이어그램이 여기에 표시됩니다...*")
1347
-
1348
- # 시나리오 작성 탭
1349
- with gr.Tab("🎬 시나리오 작성"):
1350
- gr.Markdown("""
1351
- ### 📝 막별 시나리오 작성
1352
-
1353
- 기획안 생성 후, 각 막을 순차적으로 작성하세요.
1354
- 각 막마다 7명의 전문가가 협업합니다:
1355
-
1356
- 📖 스토리작가 → 🔎 고증전문가 → ✂️ 편집자 → 🎭 감독 → 💬 대화전문가 → 🔍 비평가 → ✅ 최종완성
1357
- """)
1358
-
1359
- # 막별 작성 버튼들
1360
- with gr.Row():
1361
- act1_btn = gr.Button(
1362
- "1️⃣ 1막 작성\n[설정 - 25%]\n약 30페이지",
1363
- variant="primary",
1364
- elem_classes=["act-button"]
1365
- )
1366
- act2a_btn = gr.Button(
1367
- "2️⃣ 2막A 작성\n[상승 - 25%]\n약 30페이지",
1368
- variant="secondary",
1369
- elem_classes=["act-button"],
1370
- interactive=False # 1막 완성 후 활성화
1371
- )
1372
- act2b_btn = gr.Button(
1373
- "3️⃣ 2막B 작성\n[복잡화 - 25%]\n약 30페이지",
1374
- variant="secondary",
1375
- elem_classes=["act-button"],
1376
- interactive=False # 2막A 완성 후 활성화
1377
- )
1378
- act3_btn = gr.Button(
1379
- "4️⃣ 3막 작성\n[해결 - 25%]\n약 30페이지",
1380
- variant="secondary",
1381
- elem_classes=["act-button"],
1382
- interactive=False # 2막B 완성 후 활성화
1383
- )
1384
-
1385
- # 진행 상황 표시
1386
- gr.Markdown("### 📊 작성 진행 상황")
1387
- act_progress_display = gr.HTML(
1388
- value='<div class="progress-container">막을 선택하여 작성을 시작하세요...</div>'
1389
- )
1390
- act_status_text = gr.Textbox(
1391
- label="현재 상태",
1392
- value="기획안을 먼저 생성한 후 시나리오 작성을 시작하세요",
1393
- interactive=False
1394
- )
1395
-
1396
- # 전문가 작업 현황
1397
- gr.Markdown("### 👥 전문가 작업 현황")
1398
- expert_status = gr.HTML("""
1399
- <div style="padding: 10px;">
1400
- <span class="expert-card">📖 스토리작가</span>
1401
- <span class="expert-card">🔎 고증전문가</span>
1402
- <span class="expert-card">✂️ 편집자</span>
1403
- <span class="expert-card">🎭 감독</span>
1404
- <span class="expert-card">💬 대화전문가</span>
1405
- <span class="expert-card">🔍 비평가</span>
1406
- </div>
1407
- """)
1408
-
1409
- # 시나리오 출력
1410
- gr.Markdown("### 📄 작성된 시나리오")
1411
- with gr.Tabs():
1412
- with gr.Tab("1막"):
1413
- act1_output = gr.Markdown("*1막이 여기에 표시됩니다...*")
1414
- with gr.Tab("2막A"):
1415
- act2a_output = gr.Markdown("*2막A가 여기에 표시됩니다...*")
1416
- with gr.Tab("2막B"):
1417
- act2b_output = gr.Markdown("*2막B가 여기에 표시됩니다...*")
1418
- with gr.Tab("3막"):
1419
- act3_output = gr.Markdown("*3막이 여기에 표시됩니다...*")
1420
- with gr.Tab("전체 시나리오"):
1421
- full_output = gr.Markdown("*전체 시나리오가 여기에 표시됩니다...*")
1422
-
1423
- # 다운로드 버튼
1424
- with gr.Row():
1425
- download_btn = gr.Button("💾 전체 시나리오 다운로드 (TXT)", variant="secondary")
1426
- download_file = gr.File(label="다운로드 파일", visible=False)
1427
-
1428
- # 이벤트 핸들러
1429
- def handle_planning(query, s_type, genre):
1430
- if not query:
1431
- yield 0, "❌ 아이디어를 입력하세요", "*기획안이 여기에 표시됩니다...*", None, None
1432
- return
1433
-
1434
- system = ScreenplayGenerationSystem()
1435
- session_id = None
1436
-
1437
- for status, progress, planning_data, feedbacks, diagrams in system.generate_planning(query, s_type, genre):
1438
- formatted = format_planning_with_diagrams(planning_data, diagrams)
1439
- session_id = system.current_session_id
1440
- yield progress, status, formatted, planning_data, session_id
1441
-
1442
- # 4. 이벤트 핸들러 수정 - act 내용 저장 개선
1443
- def handle_act_writing(act_name, session_id, planning_data, previous_acts):
1444
- """막별 시나리오 작성 - generator 래퍼"""
1445
- if not session_id or not planning_data:
1446
- yield "", "❌ 먼저 기획안을 생성해주세요", ""
1447
- return
1448
-
1449
- try:
1450
- system = ScreenplayGenerationSystem()
1451
- final_content = "" # 최종 콘텐츠 저장
1452
-
1453
- # Generator를 순회하며 yield
1454
- for act_progress, status_msg in system.generate_act(
1455
- session_id, act_name, planning_data, previous_acts
1456
- ):
1457
- progress_html = create_act_progress_display(act_progress)
1458
- screenplay_display = format_screenplay_display(act_progress.content)
1459
- final_content = act_progress.content # 원본 콘텐츠 저장
1460
-
1461
- yield progress_html, status_msg, screenplay_display
1462
-
1463
- # 완료 후 원본 콘텐츠 반환
1464
- if final_content:
1465
- yield progress_html, f"✅ {act_name} 완성 및 저장됨", screenplay_display
1466
-
1467
- except Exception as e:
1468
- logger.error(f"Act writing error: {e}")
1469
- yield f"<div>❌ 오류 발생: {str(e)}</div>", f"오류: {str(e)}", ""
1470
-
1471
- def format_screenplay_display(content):
1472
- """시나리오 표시 포맷팅 - 개선된 버전"""
1473
- if not content:
1474
- return "*작성 중...*"
1475
-
1476
- lines = content.split('\n')
1477
- result = []
1478
- scene_number = 0
1479
-
1480
- for line in lines:
1481
- stripped = line.strip()
1482
-
1483
- if not stripped:
1484
- result.append("")
1485
- continue
1486
-
1487
- # 씬 헤더 감지 및 포맷팅 (INT. 또는 EXT.로 시작)
1488
- if stripped.startswith(('INT.', 'EXT.', 'INT ', 'EXT ')):
1489
- scene_number += 1
1490
- # 씬 구분선 추가
1491
- result.append("\n" + "="*80)
1492
- result.append(f"### 씬 {scene_number}")
1493
- result.append(f"**{stripped}**")
1494
- result.append("-"*40)
1495
- # 캐릭터명 감지 (대문자로만 된 짧은 줄)
1496
- elif stripped.isupper() and len(stripped.split()) <= 3 and not any(c.isdigit() for c in stripped):
1497
- # 캐릭터명 강조
1498
- result.append(f"\n**{stripped}**")
1499
- # 괄호로 된 지시문 (연기 지시)
1500
- elif stripped.startswith('(') and stripped.endswith(')'):
1501
- result.append(f"*{stripped}*")
1502
- # 트랜지션 (CUT TO:, FADE IN: 등)
1503
- elif any(trans in stripped.upper() for trans in ['CUT TO:', 'FADE IN:', 'FADE OUT:', 'DISSOLVE TO:']):
1504
- result.append(f"\n_{stripped}_\n")
1505
- # 일반 텍스트
1506
- else:
1507
- result.append(line)
1508
-
1509
- # 최종 정리
1510
- formatted = '\n'.join(result)
1511
-
1512
- # 연속된 빈 줄 제거
1513
- while '\n\n\n' in formatted:
1514
- formatted = formatted.replace('\n\n\n', '\n\n')
1515
-
1516
- return formatted
1517
-
1518
- # 2. combine_all_acts 함수 수정 - State 값 올바르게 처리
1519
- def combine_all_acts(act1_state, act2a_state, act2b_state, act3_state):
1520
- """모든 막 합치기 - State 값 처리 개선"""
1521
- full = "# 🎬 전체 시나리오\n\n"
1522
-
1523
- # State 객체인 경우 value 속성 접근
1524
- act1 = act1_state.value if hasattr(act1_state, 'value') else act1_state
1525
- act2a = act2a_state.value if hasattr(act2a_state, 'value') else act2a_state
1526
- act2b = act2b_state.value if hasattr(act2b_state, 'value') else act2b_state
1527
- act3 = act3_state.value if hasattr(act3_state, 'value') else act3_state
1528
-
1529
- # 실제 콘텐츠가 있는지 확인
1530
- has_content = False
1531
-
1532
- if act1 and act1 != "*1막이 여기에 표시됩니다...*" and act1 != "":
1533
- # 마크다운 제거한 원본 텍스트 추출
1534
- clean_act1 = act1.replace("**", "").replace("###", "").replace("="*80, "").replace("-"*40, "")
1535
- full += "## 제1막 - 설정\n\n" + clean_act1 + "\n\n" + "="*80 + "\n\n"
1536
- has_content = True
1537
-
1538
- if act2a and act2a != "*2막A가 여기에 표시됩니다...*" and act2a != "":
1539
- clean_act2a = act2a.replace("**", "").replace("###", "").replace("="*80, "").replace("-"*40, "")
1540
- full += "## 제2막A - 상승\n\n" + clean_act2a + "\n\n" + "="*80 + "\n\n"
1541
- has_content = True
1542
-
1543
- if act2b and act2b != "*2막B가 여기에 표시됩니다...*" and act2b != "":
1544
- clean_act2b = act2b.replace("**", "").replace("###", "").replace("="*80, "").replace("-"*40, "")
1545
- full += "## 제2막B - 복잡화\n\n" + clean_act2b + "\n\n" + "="*80 + "\n\n"
1546
- has_content = True
1547
-
1548
- if act3 and act3 != "*3막이 여기에 표시됩니다...*" and act3 != "":
1549
- clean_act3 = act3.replace("**", "").replace("###", "").replace("="*80, "").replace("-"*40, "")
1550
- full += "## 제3막 - 해결\n\n" + clean_act3 + "\n\n"
1551
- has_content = True
1552
-
1553
- if not has_content:
1554
- return "*전체 시나리오가 완성되면 여기에 표시됩니다...*"
1555
-
1556
- return full
1557
-
1558
- # 3. save_to_file 함수 수정 - 데이터베이스에서 직접 가져오기
1559
- def save_to_file(full_screenplay, session_id):
1560
- """시나리오를 파일로 저장 - 개선된 버전"""
1561
-
1562
- # 세션 ID가 있으면 DB에서 직접 가져오기
1563
- if session_id:
1564
- try:
1565
- with ScreenplayDatabase.get_db() as conn:
1566
- cursor = conn.cursor()
1567
- row = cursor.execute(
1568
- '''SELECT user_query, title, logline, genre, screenplay_type,
1569
- act1_content, act2a_content, act2b_content, act3_content
1570
- FROM screenplay_sessions WHERE session_id = ?''',
1571
- (session_id,)
1572
- ).fetchone()
1573
-
1574
- if row:
1575
- # 파일 내용 구성
1576
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
1577
- filename = f"screenplay_{timestamp}.txt"
1578
-
1579
- with open(filename, 'w', encoding='utf-8') as f:
1580
- # 헤더 정보
1581
- f.write("="*80 + "\n")
1582
- f.write("시나리오\n")
1583
- f.write("="*80 + "\n\n")
1584
-
1585
- if row['title']:
1586
- f.write(f"제목: {row['title']}\n")
1587
- if row['genre']:
1588
- f.write(f"장르: {row['genre']}\n")
1589
- if row['screenplay_type']:
1590
- f.write(f"형식: {row['screenplay_type']}\n")
1591
- if row['logline']:
1592
- f.write(f"로그라인: {row['logline']}\n")
1593
-
1594
- f.write("\n" + "="*80 + "\n\n")
1595
-
1596
- # 각 막 내용 추가
1597
- if row['act1_content']:
1598
- f.write("제1막 - 설정\n")
1599
- f.write("="*80 + "\n\n")
1600
- f.write(row['act1_content'])
1601
- f.write("\n\n" + "="*80 + "\n\n")
1602
-
1603
- if row['act2a_content']:
1604
- f.write("제2막A - 상승\n")
1605
- f.write("="*80 + "\n\n")
1606
- f.write(row['act2a_content'])
1607
- f.write("\n\n" + "="*80 + "\n\n")
1608
-
1609
- if row['act2b_content']:
1610
- f.write("제2막B - 복잡화\n")
1611
- f.write("="*80 + "\n\n")
1612
- f.write(row['act2b_content'])
1613
- f.write("\n\n" + "="*80 + "\n\n")
1614
-
1615
- if row['act3_content']:
1616
- f.write("제3막 - 해결\n")
1617
- f.write("="*80 + "\n\n")
1618
- f.write(row['act3_content'])
1619
- f.write("\n\n")
1620
-
1621
- # 작성 정보
1622
- f.write("\n" + "="*80 + "\n")
1623
- f.write(f"작성일: {datetime.now().strftime('%Y년 %m월 %d일 %H:%M')}\n")
1624
- f.write("="*80 + "\n")
1625
-
1626
- return filename
1627
- except Exception as e:
1628
- logger.error(f"DB에서 파일 저장 중 오류: {e}")
1629
-
1630
- # DB 접근 실패시 전달받은 내용으로 저장
1631
- if not full_screenplay or full_screenplay == "*전체 시나리오가 완성되면 여기에 표시됩니다...*":
1632
- return None
1633
-
1634
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
1635
- filename = f"screenplay_{timestamp}.txt"
1636
-
1637
- with open(filename, 'w', encoding='utf-8') as f:
1638
- # 모든 마크다운 및 포맷팅 제거
1639
- clean_text = full_screenplay
1640
- clean_text = clean_text.replace("**", "")
1641
- clean_text = clean_text.replace("###", "")
1642
- clean_text = clean_text.replace("#", "")
1643
- clean_text = clean_text.replace("="*80, "-"*80)
1644
- clean_text = clean_text.replace("-"*40, "")
1645
- clean_text = clean_text.replace("_", "")
1646
- clean_text = clean_text.replace("*", "")
1647
-
1648
- f.write(clean_text)
1649
-
1650
- return filename
1651
-
1652
- # 이벤트 연결
1653
- planning_output = planning_btn.click(
1654
- fn=handle_planning,
1655
- inputs=[query_input, screenplay_type, genre_select],
1656
- outputs=[progress_bar, status_text, planning_display, current_planning, current_session_id]
1657
- )
1658
-
1659
- planning_output.then(
1660
- # 기획안 생성 후 1막 버튼 활성화
1661
- lambda: gr.update(interactive=True),
1662
- outputs=[act1_btn]
1663
- )
1664
-
1665
- # 1막 작성 - generator 처리를 위해 수정
1666
- act1_output_event = act1_btn.click(
1667
- fn=handle_act_writing,
1668
- inputs=[
1669
- gr.State("1막"),
1670
- current_session_id,
1671
- current_planning,
1672
- gr.State({})
1673
- ],
1674
- outputs=[act_progress_display, act_status_text, act1_output]
1675
- )
1676
-
1677
- act1_output_event.then(
1678
- lambda x: (x, gr.update(interactive=True)),
1679
- inputs=[act1_output],
1680
- outputs=[act1_content, act2a_btn]
1681
- )
1682
-
1683
- # 2막A 작성
1684
- act2a_output_event = act2a_btn.click(
1685
- fn=handle_act_writing,
1686
- inputs=[
1687
- gr.State("2막A"),
1688
- current_session_id,
1689
- current_planning,
1690
- act1_content
1691
- ],
1692
- outputs=[act_progress_display, act_status_text, act2a_output]
1693
- )
1694
-
1695
- act2a_output_event.then(
1696
- lambda x: (x, gr.update(interactive=True)),
1697
- inputs=[act2a_output],
1698
- outputs=[act2a_content, act2b_btn]
1699
- )
1700
-
1701
- # 2막B 작성
1702
- act2b_output_event = act2b_btn.click(
1703
- fn=handle_act_writing,
1704
- inputs=[
1705
- gr.State("2막B"),
1706
- current_session_id,
1707
- current_planning,
1708
- gr.State(lambda: {"1막": act1_content.value, "2막A": act2a_content.value})
1709
- ],
1710
- outputs=[act_progress_display, act_status_text, act2b_output]
1711
- )
1712
-
1713
- act2b_output_event.then(
1714
- lambda x: (x, gr.update(interactive=True)),
1715
- inputs=[act2b_output],
1716
- outputs=[act2b_content, act3_btn]
1717
- )
1718
-
1719
- # 3막 작성
1720
- act3_output_event = act3_btn.click(
1721
- fn=handle_act_writing,
1722
- inputs=[
1723
- gr.State("3막"),
1724
- current_session_id,
1725
- current_planning,
1726
- gr.State(lambda: {"1막": act1_content.value, "2막A": act2a_content.value, "2막B": act2b_content.value})
1727
- ],
1728
- outputs=[act_progress_display, act_status_text, act3_output]
1729
- )
1730
-
1731
- act3_output_event.then(
1732
- lambda x: x,
1733
- inputs=[act3_output],
1734
- outputs=[act3_content]
1735
- ).then(
1736
- # 모든 막 완성 후 전체 시나리오 생성
1737
- fn=combine_all_acts,
1738
- inputs=[act1_content, act2a_content, act2b_content, act3_content],
1739
- outputs=[full_output]
1740
- )
1741
-
1742
- # 다운로드
1743
- download_btn.click(
1744
- fn=lambda full, sid: save_to_file(full, sid),
1745
- inputs=[full_output, current_session_id], # session_id 추가
1746
- outputs=[download_file]
1747
- ).then(
1748
- lambda x: gr.update(visible=True, value=x) if x else gr.update(visible=False),
1749
- inputs=[download_file],
1750
- outputs=[download_file]
1751
- )
1752
-
1753
-
1754
- return interface
1755
 
1756
- # 메인 실행
1757
  if __name__ == "__main__":
1758
- logger.info("AI 시나리오 작가 시작...")
1759
- ScreenplayDatabase.init_db()
1760
- interface = create_interface()
1761
- interface.launch(server_name="0.0.0.0", server_port=7860, share=False)
 
 
1
  import os
2
+ import sys
3
+ import streamlit as st
4
+ from tempfile import NamedTemporaryFile
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
+ def main():
7
+ try:
8
+ # Get the code from secrets
9
+ code = os.environ.get("MAIN_CODE")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
+ if not code:
12
+ st.error("⚠️ The application code wasn't found in secrets. Please add the MAIN_CODE secret.")
13
+ return
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
 
15
+ # Create a temporary Python file
16
+ with NamedTemporaryFile(suffix='.py', delete=False, mode='w') as tmp:
17
+ tmp.write(code)
18
+ tmp_path = tmp.name
19
 
20
+ # Execute the code
21
+ exec(compile(code, tmp_path, 'exec'), globals())
 
 
 
 
 
 
 
22
 
23
+ # Clean up the temporary file
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  try:
25
+ os.unlink(tmp_path)
26
+ except:
27
+ pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
 
29
+ except Exception as e:
30
+ st.error(f"⚠️ Error loading or executing the application: {str(e)}")
31
+ import traceback
32
+ st.code(traceback.format_exc())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
 
34
  if __name__ == "__main__":
35
+ main()