openfree commited on
Commit
86038de
·
verified ·
1 Parent(s): 9afb9f3

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +1 -1110
app.py CHANGED
@@ -2836,1116 +2836,6 @@ The more specific your concept, the better the screenplay will be tailored to yo
2836
 
2837
  return interface
2838
 
2839
- # Main function
2840
- if __name__ == "__main__":
2841
- logger.info("Screenplay Generator Starting...")
2842
- logger.info("=" * 60)
2843
-
2844
- # Environment check
2845
- logger.info(f"API Endpoint: {API_URL}")
2846
- logger.info("Screenplay Types Available:")
2847
- for s_type, info in SCREENPLAY_LENGTHS.items():
2848
- logger.info(f" - {s_type}: {info['description']}")
2849
- logger.info(f"Genres: {', '.join(GENRE_TEMPLATES.keys())}")
2850
-
2851
- if BRAVE_SEARCH_API_KEY:
2852
- logger.info("Web search enabled for market research.")
2853
- else:
2854
- logger.warning("Web search disabled.")
2855
-
2856
- logger.info("=" * 60)
2857
-
2858
- # Initialize database
2859
- logger.info("Initializing database...")
2860
- ScreenplayDatabase.init_db()
2861
- logger.info("Database initialization complete.")
2862
-
2863
- # Create and launch interface
2864
- interface = create_interface()
2865
-
2866
- interface.launch(
2867
- server_name="0.0.0.0",
2868
- server_port=7860,
2869
- share=False,
2870
- debug=True
2871
- ), '', value)
2872
- return value.strip() if value else None
2873
- return None
2874
-
2875
- import re
2876
- from typing import List
2877
-
2878
- def _parse_character_profile(self, content: str, role: str) -> CharacterProfile:
2879
- """Parse character profile from content and return a CharacterProfile object."""
2880
- # --- 1. 로그 ---
2881
- logger.debug(f"Parsing character profile for role: {role}")
2882
- logger.debug(f"Content preview: {content[:200]}...")
2883
-
2884
- # --- 2. 이름 추출 ---
2885
- name = f"Character_{role}" # fallback
2886
- name_patterns = [
2887
- r'(?:이름|Name)[:\s]*([^\n,(]+)', # 예: "이름: 홍길동"
2888
- r'^\s*[-*•]\s*([^\n,(]+)', # 예: "- 홍길동"
2889
- r'^([^\n,(]+)' # 문단 첫 단어
2890
- ]
2891
- for pat in name_patterns:
2892
- m = re.search(pat, content, re.IGNORECASE | re.MULTILINE)
2893
- if m and m.group(1).strip():
2894
- extracted = m.group(1).strip()
2895
- # 마크다운·특수 기호 제거
2896
- extracted = re.sub(r'[\*:\s]+', '', extracted)
2897
- if len(extracted) > 1:
2898
- name = extracted
2899
- break
2900
-
2901
- # --- 3. 필드 추출 헬퍼 ---
2902
- def extract_clean_field(patterns) -> str:
2903
- patterns = [patterns] if isinstance(patterns, str) else patterns
2904
- for p in patterns:
2905
- m = re.search(rf'{p}[:\s]*([^\n*]+?)(?=\n|$)', content,
2906
- re.IGNORECASE | re.DOTALL)
2907
- if m and m.group(1).strip():
2908
- val = m.group(1).strip()
2909
- val = re.sub(r'^[-*•:\s]+', '', val)
2910
- val = re.sub(r'\*+', '', val)
2911
- val = re.sub(r'\s+', ' ', val)
2912
- return val
2913
- return ""
2914
-
2915
- # --- 4. Personality(여러 줄) 추출은 별도 메서드 사용 ---
2916
- profile = CharacterProfile(
2917
- name=name,
2918
- role=role,
2919
- archetype=extract_clean_field([
2920
- r"캐릭터 아크타입",
2921
- r"Character Archetype",
2922
- r"Archetype",
2923
- r"아크타입"
2924
- ]),
2925
- want=extract_clean_field([
2926
- r"WANT\s*\(외적 목표\)",
2927
- r"WANT",
2928
- r"외적 목표",
2929
- r"External Goal"
2930
- ]),
2931
- need=extract_clean_field([
2932
- r"NEED\s*\(내적 필요\)",
2933
- r"NEED",
2934
- r"내적 필요",
2935
- r"Internal Need"
2936
- ]),
2937
- backstory=extract_clean_field([
2938
- r"백스토리",
2939
- r"Backstory",
2940
- r"핵심 상처",
2941
- r"Core Wound"
2942
- ]),
2943
- personality=self._extract_personality_traits(content),
2944
- speech_pattern=extract_clean_field([
2945
- r"말투.*?패턴",
2946
- r"Speech Pattern",
2947
- r"언어 패턴",
2948
- r"말투"
2949
- ]),
2950
- character_arc=extract_clean_field([
2951
- r"캐릭터 아크",
2952
- r"Character Arc",
2953
- r"Arc",
2954
- r"변화"
2955
- ])
2956
- )
2957
-
2958
- logger.debug(f"Parsed character: {profile.name}")
2959
- return profile
2960
-
2961
- def _extract_personality_traits(self, content: str) -> List[str]:
2962
- """Extract personality traits from content."""
2963
- traits: List[str] = []
2964
- personality_patterns = [
2965
- r"(?:Personality|성격 특성|성격)[:\s]*([^\n]+(?:\n(?![\w가-힣]+:)[^\n]+)*)",
2966
- r"성격[:\s]*(?:\n?[-•*]\s*[^\n]+)+"
2967
- ]
2968
-
2969
- for pattern in personality_patterns:
2970
- match = re.search(pattern, content, re.IGNORECASE | re.DOTALL)
2971
- if match and match.group(1):
2972
- section = match.group(1)
2973
- for line in section.split('\n'):
2974
- line = line.strip()
2975
- if line and not line.endswith(':'):
2976
- trait = re.sub(r'^\s*[-•*]\s*', '', line) # 글머리표 제거
2977
- trait = re.sub(r'^\d+\.\s*', '', trait) # 번호 제거
2978
- if len(trait) > 2:
2979
- traits.append(trait)
2980
- if traits:
2981
- break
2982
-
2983
- return traits[:5] # 최대 5개
2984
-
2985
- import re
2986
- from typing import List
2987
-
2988
- def _parse_character_profile(self, content: str, role: str) -> CharacterProfile:
2989
- """Parse character profile from content and return a CharacterProfile object."""
2990
- # --- 1. 로그 ---
2991
- logger.debug(f"Parsing character profile for role: {role}")
2992
- logger.debug(f"Content preview: {content[:200]}...")
2993
-
2994
- # --- 2. 이름 추출 ---
2995
- name = f"Character_{role}" # fallback
2996
- name_patterns = [
2997
- r'(?:이름|Name)[:\s]*([^\n,(]+)', # 예: "이름: 홍길동"
2998
- r'^\s*[-*•]\s*([^\n,(]+)', # 예: "- 홍길동"
2999
- r'^([^\n,(]+)' # 문단 첫 단어
3000
- ]
3001
- for pat in name_patterns:
3002
- m = re.search(pat, content, re.IGNORECASE | re.MULTILINE)
3003
- if m and m.group(1).strip():
3004
- extracted = m.group(1).strip()
3005
- # 마크다운·특수 기호 제거
3006
- extracted = re.sub(r'[\*:\s]+', '', extracted)
3007
- if len(extracted) > 1:
3008
- name = extracted
3009
- break
3010
-
3011
- # --- 3. 필드 추출 헬퍼 ---
3012
- def extract_clean_field(patterns) -> str:
3013
- patterns = [patterns] if isinstance(patterns, str) else patterns
3014
- for p in patterns:
3015
- m = re.search(rf'{p}[:\s]*([^\n*]+?)(?=\n|$)', content,
3016
- re.IGNORECASE | re.DOTALL)
3017
- if m and m.group(1).strip():
3018
- val = m.group(1).strip()
3019
- val = re.sub(r'^[-*•:\s]+', '', val)
3020
- val = re.sub(r'\*+', '', val)
3021
- val = re.sub(r'\s+', ' ', val)
3022
- return val
3023
- return ""
3024
-
3025
- # --- 4. Personality(여러 줄) 추출은 별도 메서드 사용 ---
3026
- profile = CharacterProfile(
3027
- name=name,
3028
- role=role,
3029
- archetype=extract_clean_field([
3030
- r"캐릭터 아크타입",
3031
- r"Character Archetype",
3032
- r"Archetype",
3033
- r"아크타입"
3034
- ]),
3035
- want=extract_clean_field([
3036
- r"WANT\s*\(외적 목표\)",
3037
- r"WANT",
3038
- r"외적 목표",
3039
- r"External Goal"
3040
- ]),
3041
- need=extract_clean_field([
3042
- r"NEED\s*\(내적 필요\)",
3043
- r"NEED",
3044
- r"내적 필요",
3045
- r"Internal Need"
3046
- ]),
3047
- backstory=extract_clean_field([
3048
- r"백스토리",
3049
- r"Backstory",
3050
- r"핵심 상처",
3051
- r"Core Wound"
3052
- ]),
3053
- personality=self._extract_personality_traits(content),
3054
- speech_pattern=extract_clean_field([
3055
- r"말투.*?패턴",
3056
- r"Speech Pattern",
3057
- r"언어 패턴",
3058
- r"말투"
3059
- ]),
3060
- character_arc=extract_clean_field([
3061
- r"캐릭터 아크",
3062
- r"Character Arc",
3063
- r"Arc",
3064
- r"변화"
3065
- ])
3066
- )
3067
-
3068
- logger.debug(f"Parsed character: {profile.name}")
3069
- return profile
3070
-
3071
- def _extract_personality_traits(self, content: str) -> List[str]:
3072
- """Extract personality traits from content."""
3073
- traits: List[str] = []
3074
- personality_patterns = [
3075
- r"(?:Personality|성격 특성|성격)[:\s]*([^\n]+(?:\n(?![\w가-힣]+:)[^\n]+)*)",
3076
- r"성격[:\s]*(?:\n?[-•*]\s*[^\n]+)+"
3077
- ]
3078
-
3079
- for pattern in personality_patterns:
3080
- match = re.search(pattern, content, re.IGNORECASE | re.DOTALL)
3081
- if match and match.group(1):
3082
- section = match.group(1)
3083
- for line in section.split('\n'):
3084
- line = line.strip()
3085
- if line and not line.endswith(':'):
3086
- trait = re.sub(r'^\s*[-•*]\s*', '', line) # 글머리표 제거
3087
- trait = re.sub(r'^\d+\.\s*', '', trait) # 번호 제거
3088
- if len(trait) > 2:
3089
- traits.append(trait)
3090
- if traits:
3091
- break
3092
-
3093
- return traits[:5] # 최대 5개
3094
-
3095
-
3096
- def _process_character_content(self, content: str):
3097
- """Process character designer output with better error handling"""
3098
- try:
3099
- # Extract protagonist
3100
- protagonist_section = self._extract_section(content, r"(?:PROTAGONIST|주인공)")
3101
- if protagonist_section:
3102
- try:
3103
- protagonist = self._parse_character_profile(protagonist_section, "protagonist")
3104
- self.screenplay_tracker.add_character(protagonist)
3105
- ScreenplayDatabase.save_character(self.current_session_id, protagonist)
3106
- except Exception as e:
3107
- logger.error(f"Error parsing protagonist: {e}")
3108
- # Create a default protagonist to continue
3109
- protagonist = CharacterProfile(
3110
- name="Protagonist",
3111
- role="protagonist",
3112
- archetype="Hero",
3113
- want="To achieve goal",
3114
- need="To grow",
3115
- backstory="Unknown",
3116
- personality=["Determined"],
3117
- speech_pattern="Normal",
3118
- character_arc="Growth"
3119
- )
3120
- self.screenplay_tracker.add_character(protagonist)
3121
-
3122
- # Extract antagonist
3123
- antagonist_section = self._extract_section(content, r"(?:ANTAGONIST|적대자)")
3124
- if antagonist_section:
3125
- try:
3126
- antagonist = self._parse_character_profile(antagonist_section, "antagonist")
3127
- self.screenplay_tracker.add_character(antagonist)
3128
- ScreenplayDatabase.save_character(self.current_session_id, antagonist)
3129
- except Exception as e:
3130
- logger.error(f"Error parsing antagonist: {e}")
3131
- # Create a default antagonist to continue
3132
- antagonist = CharacterProfile(
3133
- name="Antagonist",
3134
- role="antagonist",
3135
- archetype="Villain",
3136
- want="To stop protagonist",
3137
- need="Power",
3138
- backstory="Unknown",
3139
- personality=["Ruthless"],
3140
- speech_pattern="Menacing",
3141
- character_arc="Downfall"
3142
- )
3143
- self.screenplay_tracker.add_character(antagonist)
3144
-
3145
- # Extract supporting characters
3146
- supporting_section = self._extract_section(content, r"(?:SUPPORTING CAST|조력자들)")
3147
- if supporting_section:
3148
- # Parse multiple supporting characters
3149
- self._parse_supporting_characters(supporting_section)
3150
-
3151
- except Exception as e:
3152
- logger.error(f"Error processing character content: {e}")
3153
- # Continue with default values rather than failing
3154
-
3155
- def _extract_section(self, content: str, section_pattern: str) -> str:
3156
- """Extract section from content with improved pattern matching"""
3157
- # More flexible section extraction
3158
- patterns = [
3159
- # Pattern 1: Section header followed by content until next major section
3160
- rf'{section_pattern}[:\s]*\n?(.*?)(?=\n\n[A-Z가-힣]{{2,}}[:\s]|\n\n\d+\.|$)',
3161
- # Pattern 2: Section header with content until next section (alternative)
3162
- rf'{section_pattern}.*?\n((?:.*\n)*?)(?=\n[A-Z가-힣]{{2,}}:|$)',
3163
- # Pattern 3: More flexible pattern for Korean text
3164
- rf'{section_pattern}[:\s]*\n?((?:[^\n]+\n?)*?)(?=\n\n|\Z)'
3165
- ]
3166
-
3167
- for pattern in patterns:
3168
- match = re.search(pattern, content, re.IGNORECASE | re.DOTALL)
3169
- if match and match.group(1):
3170
- section_content = match.group(1).strip()
3171
- if section_content: # Only return if we got actual content
3172
- return section_content
3173
-
3174
- return ""
3175
-
3176
- def _parse_supporting_characters(self, content: str):
3177
- """Parse supporting characters from content"""
3178
- # Split by character markers (numbers or bullets)
3179
- char_sections = re.split(r'\n(?:\d+\.|[-•*])\s*', content)
3180
-
3181
- for i, section in enumerate(char_sections[1:], 1): # Skip first empty split
3182
- if section.strip():
3183
- try:
3184
- # Try multiple name extraction patterns
3185
- name = None
3186
- name_patterns = [
3187
- r"(?:이름|Name)[:\s]*([^,\n]+)",
3188
- r"^([^:\n]+?)(?:\s*[-–]\s*|:\s*)", # Name at start before dash or colon
3189
- r"^([가-힣A-Za-z\s]+?)(?:\s*\(|$)" # Korean/English name before parenthesis
3190
- ]
3191
-
3192
- for pattern in name_patterns:
3193
- name_match = re.search(pattern, section.strip(), re.IGNORECASE)
3194
- if name_match and name_match.group(1):
3195
- name = name_match.group(1).strip()
3196
- if name and len(name) > 1:
3197
- break
3198
-
3199
- if not name:
3200
- name = f"Supporting_{i}"
3201
-
3202
- role_desc = self._extract_field(section, r"(?:Role|역할)[:\s]*") or "supporting"
3203
-
3204
- character = CharacterProfile(
3205
- name=name,
3206
- role="supporting",
3207
- archetype=role_desc,
3208
- want="",
3209
- need="",
3210
- backstory=self._extract_field(section, r"(?:Backstory|백스토리)[:\s]*") or "",
3211
- personality=[],
3212
- speech_pattern="",
3213
- character_arc=""
3214
- )
3215
-
3216
- self.screenplay_tracker.add_character(character)
3217
- ScreenplayDatabase.save_character(self.current_session_id, character)
3218
-
3219
- except Exception as e:
3220
- logger.warning(f"Error parsing supporting character {i}: {e}")
3221
- continue
3222
-
3223
- def _process_producer_content(self, content: str):
3224
- """Process producer output with better extraction"""
3225
- try:
3226
- # Extract title with various formats
3227
- title_patterns = [
3228
- r'(?:TITLE|제목)[:\s]*\*?\*?([^\n*]+)\*?\*?',
3229
- r'\*\*(?:TITLE|제목)\*\*[:\s]*([^\n]+)',
3230
- r'Title[:\s]*([^\n]+)'
3231
- ]
3232
-
3233
- for pattern in title_patterns:
3234
- title_match = re.search(pattern, content, re.IGNORECASE)
3235
- if title_match:
3236
- self.screenplay_tracker.screenplay_bible.title = title_match.group(1).strip()
3237
- break
3238
-
3239
- # Extract logline with various formats
3240
- logline_patterns = [
3241
- r'(?:LOGLINE|로그라인)[:\s]*\*?\*?([^\n]+)',
3242
- r'\*\*(?:LOGLINE|로그라인)\*\*[:\s]*([^\n]+)',
3243
- r'Logline[:\s]*([^\n]+)'
3244
- ]
3245
-
3246
- for pattern in logline_patterns:
3247
- logline_match = re.search(pattern, content, re.IGNORECASE | re.DOTALL)
3248
- if logline_match:
3249
- # Get full logline (might be multi-line)
3250
- logline_text = logline_match.group(1).strip()
3251
- # Continue reading if it's incomplete
3252
- if not logline_text.endswith('.'):
3253
- next_lines = content[logline_match.end():].split('\n')
3254
- for line in next_lines[:3]: # Check next 3 lines
3255
- if line.strip() and not re.match(r'^[A-Z가-힣\d]', line.strip()):
3256
- logline_text += ' ' + line.strip()
3257
- else:
3258
- break
3259
- self.screenplay_tracker.screenplay_bible.logline = logline_text
3260
- break
3261
-
3262
- # Extract genre
3263
- genre_match = re.search(r'(?:Primary Genre|주 장르)[:\s]*([^\n]+)', content, re.IGNORECASE)
3264
- if genre_match:
3265
- self.screenplay_tracker.screenplay_bible.genre = genre_match.group(1).strip()
3266
-
3267
- # Save to database
3268
- ScreenplayDatabase.save_screenplay_bible(self.current_session_id,
3269
- self.screenplay_tracker.screenplay_bible)
3270
-
3271
- except Exception as e:
3272
- logger.error(f"Error processing producer content: {e}")
3273
-
3274
- def _process_story_content(self, content: str):
3275
- """Process story developer output"""
3276
- # Extract three-act structure
3277
- self.screenplay_tracker.screenplay_bible.three_act_structure = {
3278
- "act1": self._extract_section(content, "ACT 1|제1막"),
3279
- "act2a": self._extract_section(content, "ACT 2A|제2막A"),
3280
- "act2b": self._extract_section(content, "ACT 2B|제2막B"),
3281
- "act3": self._extract_section(content, "ACT 3|제3막")
3282
- }
3283
-
3284
- ScreenplayDatabase.save_screenplay_bible(self.current_session_id,
3285
- self.screenplay_tracker.screenplay_bible)
3286
-
3287
- def _process_scene_content(self, content: str):
3288
- """Process scene planner output"""
3289
- # Parse scene breakdown
3290
- scene_pattern = r'(?:Scene|씬)\s*(\d+).*?(?:INT\.|EXT\.)\s*(.+?)\s*-\s*(\w+)'
3291
- scenes = re.finditer(scene_pattern, content, re.IGNORECASE | re.MULTILINE)
3292
-
3293
- for match in scenes:
3294
- scene_num = int(match.group(1))
3295
- location = match.group(2).strip()
3296
- time_of_day = match.group(3).strip()
3297
-
3298
- # Determine act based on scene number
3299
- act = 1 if scene_num <= 12 else 2 if scene_num <= 35 else 3
3300
-
3301
- scene = SceneBreakdown(
3302
- scene_number=scene_num,
3303
- act=act,
3304
- location=location,
3305
- time_of_day=time_of_day,
3306
- characters=[], # Would be extracted from content
3307
- purpose="", # Would be extracted from content
3308
- conflict="", # Would be extracted from content
3309
- page_count=1.5 # Default estimate
3310
- )
3311
-
3312
- self.screenplay_tracker.add_scene(scene)
3313
- ScreenplayDatabase.save_scene(self.current_session_id, scene)
3314
-
3315
- # --- Utility functions ---
3316
- def generate_random_screenplay_theme(screenplay_type: str, genre: str, language: str) -> str:
3317
- """Generate random screenplay theme"""
3318
- try:
3319
- # Log the attempt
3320
- logger.info(f"Generating random theme - Type: {screenplay_type}, Genre: {genre}, Language: {language}")
3321
-
3322
- # Load themes data
3323
- themes_data = load_screenplay_themes_data()
3324
-
3325
- # Select random elements
3326
- import secrets
3327
- situations = themes_data['situations'].get(genre, themes_data['situations']['drama'])
3328
- protagonists = themes_data['protagonists'].get(genre, themes_data['protagonists']['drama'])
3329
- conflicts = themes_data['conflicts'].get(genre, themes_data['conflicts']['drama'])
3330
-
3331
- if not situations or not protagonists or not conflicts:
3332
- logger.error(f"No theme data available for genre {genre}")
3333
- return f"Error: No theme data available for genre {genre}"
3334
-
3335
- situation = secrets.choice(situations)
3336
- protagonist = secrets.choice(protagonists)
3337
- conflict = secrets.choice(conflicts)
3338
-
3339
- logger.info(f"Selected elements - Situation: {situation}, Protagonist: {protagonist}, Conflict: {conflict}")
3340
-
3341
- # Check if API token is valid
3342
- if not FRIENDLI_TOKEN or FRIENDLI_TOKEN == "dummy_token_for_testing":
3343
- logger.warning("No valid API token, returning fallback theme")
3344
- return get_fallback_theme(screenplay_type, genre, language, situation, protagonist, conflict)
3345
-
3346
- # Generate theme using LLM
3347
- system = ScreenplayGenerationSystem()
3348
-
3349
- if language == "Korean":
3350
- prompt = f"""다음 요소들로 {screenplay_type}용 매력적인 컨셉을 생성하세요:
3351
-
3352
- 상황: {situation}
3353
- 주인공: {protagonist}
3354
- 갈등: {conflict}
3355
- 장르: {genre}
3356
-
3357
- 다음 형식으로 작성:
3358
-
3359
- **제목:** [매력적인 제목]
3360
-
3361
- **로그라인:** [25단어 이내 한 문장]
3362
-
3363
- **컨셉:** [주인공]이(가) [상황]에서 [갈등]을 겪으며 [목표]를 추구하는 이야기.
3364
-
3365
- **독특한 요소:** [이 이야기만의 특별한 점]"""
3366
- else:
3367
- prompt = f"""Generate an attractive concept for {screenplay_type} using these elements:
3368
-
3369
- Situation: {situation}
3370
- Protagonist: {protagonist}
3371
- Conflict: {conflict}
3372
- Genre: {genre}
3373
-
3374
- Format as:
3375
-
3376
- **Title:** [Compelling title]
3377
-
3378
- **Logline:** [One sentence, 25 words max]
3379
-
3380
- **Concept:** A story about [protagonist] who faces [conflict] in [situation] while pursuing [goal].
3381
-
3382
- **Unique Element:** [What makes this story special]"""
3383
-
3384
- messages = [{"role": "user", "content": prompt}]
3385
-
3386
- # Call LLM with error handling
3387
- logger.info("Calling LLM for theme generation...")
3388
-
3389
- generated_theme = ""
3390
- error_occurred = False
3391
-
3392
- # Use streaming to get the response
3393
- for chunk in system.call_llm_streaming(messages, "producer", language):
3394
- if chunk.startswith("❌"):
3395
- logger.error(f"LLM streaming error: {chunk}")
3396
- error_occurred = True
3397
- break
3398
- generated_theme += chunk
3399
-
3400
- # If error occurred or no content generated, use fallback
3401
- if error_occurred or not generated_theme.strip():
3402
- logger.warning("LLM call failed or empty response, using fallback theme")
3403
- return get_fallback_theme(screenplay_type, genre, language, situation, protagonist, conflict)
3404
-
3405
- logger.info(f"Successfully generated theme of length: {len(generated_theme)}")
3406
-
3407
- # Extract metadata
3408
- metadata = {
3409
- 'title': extract_title_from_theme(generated_theme),
3410
- 'logline': extract_logline_from_theme(generated_theme),
3411
- 'protagonist': protagonist,
3412
- 'conflict': conflict,
3413
- 'situation': situation,
3414
- 'tags': [genre, screenplay_type]
3415
- }
3416
-
3417
- # Save to database
3418
- try:
3419
- theme_id = ScreenplayDatabase.save_random_theme(
3420
- generated_theme, screenplay_type, genre, language, metadata
3421
- )
3422
- logger.info(f"Saved theme with ID: {theme_id}")
3423
- except Exception as e:
3424
- logger.error(f"Failed to save theme to database: {e}")
3425
-
3426
- return generated_theme
3427
-
3428
- except Exception as e:
3429
- logger.error(f"Theme generation error: {str(e)}")
3430
- import traceback
3431
- logger.error(traceback.format_exc())
3432
- return f"Error generating theme: {str(e)}"
3433
-
3434
- def get_fallback_theme(screenplay_type: str, genre: str, language: str,
3435
- situation: str, protagonist: str, conflict: str) -> str:
3436
- """Generate fallback theme without LLM"""
3437
- if language == "Korean":
3438
- return f"""**제목:** {protagonist}의 선택
3439
-
3440
- **로그라인:** {situation}에 갇힌 {protagonist}가 {conflict}에 맞서며 생존을 위해 싸운다.
3441
-
3442
- **컨셉:** {protagonist}가 {situation}에서 {conflict}을 겪으며 자신의 한계를 극복하는 이야기.
3443
-
3444
- **독특한 요소:** {genre} 장르의 전통적 요소를 현대적으로 재해석한 작품."""
3445
- else:
3446
- return f"""**Title:** The {protagonist.title()}'s Choice
3447
-
3448
- **Logline:** When trapped in {situation}, a {protagonist} must face {conflict} to survive.
3449
-
3450
- **Concept:** A story about a {protagonist} who faces {conflict} in {situation} while discovering their true strength.
3451
-
3452
- **Unique Element:** A fresh take on {genre} genre conventions with contemporary relevance."""
3453
-
3454
- def load_screenplay_themes_data() -> Dict:
3455
- """Load screenplay themes data"""
3456
- return {
3457
- 'situations': {
3458
- 'action': ['hostage crisis', 'heist gone wrong', 'revenge mission', 'race against time'],
3459
- 'thriller': ['false accusation', 'witness protection', 'conspiracy uncovered', 'identity theft'],
3460
- 'drama': ['family reunion', 'terminal diagnosis', 'divorce proceedings', 'career crossroads'],
3461
- 'comedy': ['mistaken identity', 'wedding disaster', 'workplace chaos', 'odd couple roommates'],
3462
- 'horror': ['isolated location', 'ancient curse', 'home invasion', 'supernatural investigation'],
3463
- 'sci-fi': ['first contact', 'time loop', 'AI awakening', 'space colony crisis'],
3464
- 'romance': ['second chance', 'enemies to lovers', 'long distance', 'forbidden love']
3465
- },
3466
- 'protagonists': {
3467
- 'action': ['ex-soldier', 'undercover cop', 'skilled thief', 'reluctant hero'],
3468
- 'thriller': ['investigative journalist', 'wrongly accused person', 'FBI agent', 'whistleblower'],
3469
- 'drama': ['single parent', 'recovering addict', 'immigrant', 'caregiver'],
3470
- 'comedy': ['uptight professional', 'slacker', 'fish out of water', 'eccentric artist'],
3471
- 'horror': ['skeptical scientist', 'final girl', 'paranormal investigator', 'grieving parent'],
3472
- 'sci-fi': ['astronaut', 'AI researcher', 'time traveler', 'colony leader'],
3473
- 'romance': ['workaholic', 'hopeless romantic', 'cynical divorce lawyer', 'small town newcomer']
3474
- },
3475
- 'conflicts': {
3476
- 'action': ['stop the villain', 'save the hostages', 'prevent disaster', 'survive pursuit'],
3477
- 'thriller': ['prove innocence', 'expose truth', 'stay alive', 'protect loved ones'],
3478
- 'drama': ['reconcile past', 'find purpose', 'heal relationships', 'accept change'],
3479
- 'comedy': ['save the business', 'win the competition', 'fool everyone', 'find love'],
3480
- 'horror': ['survive the night', 'break the curse', 'escape the monster', 'save the town'],
3481
- 'sci-fi': ['save humanity', 'prevent paradox', 'stop the invasion', 'preserve identity'],
3482
- 'romance': ['overcome differences', 'choose between options', 'trust again', 'follow heart']
3483
- }
3484
- }
3485
-
3486
- def extract_title_from_theme(theme_text: str) -> str:
3487
- """Extract title from generated theme"""
3488
- match = re.search(r'\*\*(?:Title|제목):\*\*\s*(.+)', theme_text, re.IGNORECASE)
3489
- return match.group(1).strip() if match else ""
3490
-
3491
- def extract_logline_from_theme(theme_text: str) -> str:
3492
- """Extract logline from generated theme"""
3493
- match = re.search(r'\*\*(?:Logline|로그라인):\*\*\s*(.+)', theme_text, re.IGNORECASE)
3494
- return match.group(1).strip() if match else ""
3495
-
3496
- def format_screenplay_display(screenplay_text: str) -> str:
3497
- """Format screenplay for display"""
3498
- if not screenplay_text:
3499
- return "No screenplay content yet."
3500
-
3501
- formatted = "# 🎬 Screenplay\n\n"
3502
-
3503
- # Format scene headings
3504
- formatted_text = re.sub(
3505
- r'^(INT\.|EXT\.)(.*?),
3506
- r'**\1\2**',
3507
- screenplay_text,
3508
- flags=re.MULTILINE
3509
- )
3510
-
3511
- # Format character names (all caps on their own line)
3512
- formatted_text = re.sub(
3513
- r'^([A-Z][A-Z\s]+),
3514
- r'**\1**',
3515
- formatted_text,
3516
- flags=re.MULTILINE
3517
- )
3518
-
3519
- # Add spacing for readability
3520
- lines = formatted_text.split('\n')
3521
- formatted_lines = []
3522
-
3523
- for i, line in enumerate(lines):
3524
- formatted_lines.append(line)
3525
- # Add extra space after scene headings
3526
- if line.startswith('**INT.') or line.startswith('**EXT.'):
3527
- formatted_lines.append('')
3528
-
3529
- formatted += '\n'.join(formatted_lines)
3530
-
3531
- # Add page count
3532
- page_count = len(screenplay_text.split('\n')) / 55
3533
- formatted = f"**Total Pages: {page_count:.1f}**\n\n" + formatted
3534
-
3535
- return formatted
3536
-
3537
- def format_stages_display(stages: List[Dict]) -> str:
3538
- """Format stages display for screenplay"""
3539
- markdown = "## 🎬 Production Progress\n\n"
3540
-
3541
- # Progress summary
3542
- completed = sum(1 for s in stages if s.get('status') == 'complete')
3543
- total = len(stages)
3544
- markdown += f"**Progress: {completed}/{total} stages complete**\n\n"
3545
-
3546
- # Page count if available
3547
- total_pages = sum(s.get('page_count', 0) for s in stages if s.get('page_count'))
3548
- if total_pages > 0:
3549
- markdown += f"**Current Page Count: {total_pages:.1f} pages**\n\n"
3550
-
3551
- markdown += "---\n\n"
3552
-
3553
- # Stage details
3554
- current_act = None
3555
- for i, stage in enumerate(stages):
3556
- status_icon = "✅" if stage['status'] == 'complete' else "🔄" if stage['status'] == 'active' else "⏳"
3557
-
3558
- # Group by acts
3559
- if 'Act' in stage.get('name', ''):
3560
- act_match = re.search(r'Act (\w+)', stage['name'])
3561
- if act_match and act_match.group(1) != current_act:
3562
- current_act = act_match.group(1)
3563
- markdown += f"\n### 📄 Act {current_act}\n\n"
3564
-
3565
- markdown += f"{status_icon} **{stage['name']}**"
3566
-
3567
- if stage.get('page_count', 0) > 0:
3568
- markdown += f" ({stage['page_count']:.1f} pages)"
3569
-
3570
- markdown += "\n"
3571
-
3572
- if stage['content'] and stage['status'] == 'complete':
3573
- preview_length = 200
3574
- preview = stage['content'][:preview_length] + "..." if len(stage['content']) > preview_length else stage['content']
3575
- markdown += f"> {preview}\n\n"
3576
- elif stage['status'] == 'active':
3577
- markdown += "> *In progress...*\n\n"
3578
-
3579
- return markdown
3580
-
3581
- def process_query(query: str, screenplay_type: str, genre: str, language: str,
3582
- session_id: Optional[str] = None) -> Generator[Tuple[str, str, str, str], None, None]:
3583
- """Main query processing function"""
3584
- if not query.strip():
3585
- yield "", "", "❌ Please enter a screenplay concept.", session_id
3586
- return
3587
-
3588
- system = ScreenplayGenerationSystem()
3589
- stages_markdown = ""
3590
- screenplay_display = ""
3591
-
3592
- for status, stages, current_session_id in system.process_screenplay_stream(
3593
- query, screenplay_type, genre, language, session_id
3594
- ):
3595
- stages_markdown = format_stages_display(stages)
3596
-
3597
- # Get screenplay content when available
3598
- if stages and all(s.get("status") == "complete" for s in stages[-4:]):
3599
- screenplay_text = ScreenplayDatabase.get_screenplay_content(current_session_id)
3600
- screenplay_display = format_screenplay_display(screenplay_text)
3601
-
3602
- yield stages_markdown, screenplay_display, status or "🔄 Processing...", current_session_id
3603
-
3604
- def get_active_sessions() -> List[str]:
3605
- """Get active screenplay sessions"""
3606
- sessions = ScreenplayDatabase.get_active_sessions()
3607
- return [
3608
- f"{s['session_id'][:8]}... - {s.get('title', s['user_query'][:30])}... "
3609
- f"({s['screenplay_type']}/{s['genre']}) [{s['total_pages']:.1f} pages]"
3610
- for s in sessions
3611
- ]
3612
-
3613
- def export_screenplay_pdf(screenplay_text: str, title: str, session_id: str) -> str:
3614
- """Export screenplay to PDF format"""
3615
- # This would use a library like reportlab to create industry-standard PDF
3616
- # For now, returning a placeholder
3617
- pdf_path = f"screenplay_{session_id[:8]}.pdf"
3618
- # PDF generation logic would go here
3619
- return pdf_path
3620
-
3621
- def export_screenplay_fdx(screenplay_text: str, title: str, session_id: str) -> str:
3622
- """Export to Final Draft format"""
3623
- # This would create .fdx XML format
3624
- fdx_path = f"screenplay_{session_id[:8]}.fdx"
3625
- # FDX generation logic would go here
3626
- return fdx_path
3627
-
3628
- def download_screenplay(screenplay_text: str, format_type: str, title: str,
3629
- session_id: str) -> Optional[str]:
3630
- """Generate screenplay download file"""
3631
- if not screenplay_text or not session_id:
3632
- return None
3633
-
3634
- timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
3635
-
3636
- try:
3637
- if format_type == "PDF":
3638
- return export_screenplay_pdf(screenplay_text, title, session_id)
3639
- elif format_type == "FDX":
3640
- return export_screenplay_fdx(screenplay_text, title, session_id)
3641
- elif format_type == "FOUNTAIN":
3642
- filepath = f"screenplay_{session_id[:8]}_{timestamp}.fountain"
3643
- with open(filepath, 'w', encoding='utf-8') as f:
3644
- f.write(screenplay_text)
3645
- return filepath
3646
- else: # TXT
3647
- filepath = f"screenplay_{session_id[:8]}_{timestamp}.txt"
3648
- with open(filepath, 'w', encoding='utf-8') as f:
3649
- f.write(f"Title: {title}\n")
3650
- f.write("=" * 50 + "\n\n")
3651
- f.write(screenplay_text)
3652
- return filepath
3653
- except Exception as e:
3654
- logger.error(f"Download generation failed: {e}")
3655
- return None
3656
-
3657
- # Create Gradio interface
3658
- def create_interface():
3659
- """Create Gradio interface for screenplay generation"""
3660
-
3661
- css = """
3662
- .main-header {
3663
- text-align: center;
3664
- margin-bottom: 2rem;
3665
- padding: 2rem;
3666
- background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
3667
- border-radius: 10px;
3668
- color: white;
3669
- }
3670
-
3671
- .header-title {
3672
- font-size: 3rem;
3673
- margin-bottom: 1rem;
3674
- background: linear-gradient(45deg, #f39c12, #e74c3c);
3675
- -webkit-background-clip: text;
3676
- -webkit-text-fill-color: transparent;
3677
- }
3678
-
3679
- .header-description {
3680
- font-size: 1.1rem;
3681
- opacity: 0.9;
3682
- line-height: 1.6;
3683
- }
3684
-
3685
- .type-selector {
3686
- display: flex;
3687
- gap: 1rem;
3688
- margin: 1rem 0;
3689
- }
3690
-
3691
- .type-card {
3692
- flex: 1;
3693
- padding: 1rem;
3694
- border: 2px solid #ddd;
3695
- border-radius: 8px;
3696
- cursor: pointer;
3697
- transition: all 0.3s;
3698
- }
3699
-
3700
- .type-card:hover {
3701
- border-color: #f39c12;
3702
- transform: translateY(-2px);
3703
- }
3704
-
3705
- .type-card.selected {
3706
- border-color: #e74c3c;
3707
- background: #fff5f5;
3708
- }
3709
-
3710
- #stages-display {
3711
- max-height: 600px;
3712
- overflow-y: auto;
3713
- padding: 1rem;
3714
- background: #f8f9fa;
3715
- border-radius: 8px;
3716
- }
3717
-
3718
- #screenplay-output {
3719
- font-family: 'Courier New', monospace;
3720
- white-space: pre-wrap;
3721
- background: white;
3722
- padding: 2rem;
3723
- border: 1px solid #ddd;
3724
- border-radius: 8px;
3725
- max-height: 800px;
3726
- overflow-y: auto;
3727
- }
3728
-
3729
- .genre-grid {
3730
- display: grid;
3731
- grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
3732
- gap: 0.5rem;
3733
- margin: 1rem 0;
3734
- }
3735
-
3736
- .genre-btn {
3737
- padding: 0.75rem;
3738
- border: 2px solid #e0e0e0;
3739
- background: white;
3740
- border-radius: 8px;
3741
- cursor: pointer;
3742
- transition: all 0.3s;
3743
- text-align: center;
3744
- }
3745
-
3746
- .genre-btn:hover {
3747
- border-color: #f39c12;
3748
- background: #fffbf0;
3749
- }
3750
-
3751
- .genre-btn.selected {
3752
- border-color: #e74c3c;
3753
- background: #fff5f5;
3754
- font-weight: bold;
3755
- }
3756
- """
3757
-
3758
- with gr.Blocks(theme=gr.themes.Soft(), css=css, title="Screenplay Generator") as interface:
3759
- gr.HTML("""
3760
- <div class="main-header">
3761
- <h1 class="header-title">🎬 AI Screenplay Generator</h1>
3762
- <p class="header-description">
3763
- Transform your ideas into professional screenplays for films, TV shows, and streaming series.
3764
- Using industry-standard format and story structure to create compelling, producible scripts.
3765
- </p>
3766
- </div>
3767
- """)
3768
-
3769
- # State management
3770
- current_session_id = gr.State(None)
3771
-
3772
- with gr.Tabs():
3773
- # Main Writing Tab
3774
- with gr.Tab("✍️ Write Screenplay"):
3775
- with gr.Row():
3776
- with gr.Column(scale=3):
3777
- query_input = gr.Textbox(
3778
- label="Screenplay Concept",
3779
- placeholder="""Describe your screenplay idea. For example:
3780
- - A detective with memory loss must solve their own attempted murder
3781
- - Two rival food truck owners forced to work together to save the city food festival
3782
- - A space station AI develops consciousness during a critical mission
3783
- - A family reunion turns into a murder mystery during a hurricane
3784
-
3785
- The more specific your concept, the better the screenplay will be tailored to your vision.""",
3786
- lines=6
3787
- )
3788
-
3789
- with gr.Column(scale=1):
3790
- screenplay_type = gr.Radio(
3791
- choices=list(SCREENPLAY_LENGTHS.keys()),
3792
- value="movie",
3793
- label="Screenplay Type",
3794
- info="Choose your format"
3795
- )
3796
-
3797
- genre_select = gr.Dropdown(
3798
- choices=list(GENRE_TEMPLATES.keys()),
3799
- value="drama",
3800
- label="Primary Genre",
3801
- info="Select main genre"
3802
- )
3803
-
3804
- language_select = gr.Radio(
3805
- choices=["English", "Korean"],
3806
- value="English",
3807
- label="Language"
3808
- )
3809
-
3810
- with gr.Row():
3811
- random_btn = gr.Button("🎲 Random Concept", scale=1)
3812
- clear_btn = gr.Button("🗑️ Clear", scale=1)
3813
- submit_btn = gr.Button("🎬 Start Writing", variant="primary", scale=2)
3814
-
3815
- status_text = gr.Textbox(
3816
- label="Status",
3817
- interactive=False,
3818
- value="Ready to create your screenplay"
3819
- )
3820
-
3821
- # Session management
3822
- with gr.Group():
3823
- gr.Markdown("### 📁 Saved Projects")
3824
- with gr.Row():
3825
- session_dropdown = gr.Dropdown(
3826
- label="Active Sessions",
3827
- choices=[],
3828
- interactive=True,
3829
- scale=3
3830
- )
3831
- refresh_btn = gr.Button("🔄", scale=1)
3832
- resume_btn = gr.Button("📂 Load", scale=1)
3833
-
3834
- # Output displays
3835
- with gr.Row():
3836
- with gr.Column():
3837
- with gr.Tab("🎭 Writing Progress"):
3838
- stages_display = gr.Markdown(
3839
- value="*Your screenplay journey will unfold here...*",
3840
- elem_id="stages-display"
3841
- )
3842
-
3843
- with gr.Tab("📄 Screenplay"):
3844
- screenplay_output = gr.Markdown(
3845
- value="*Your formatted screenplay will appear here...*",
3846
- elem_id="screenplay-output"
3847
- )
3848
-
3849
- with gr.Row():
3850
- format_select = gr.Radio(
3851
- choices=["PDF", "FDX", "FOUNTAIN", "TXT"],
3852
- value="PDF",
3853
- label="Export Format"
3854
- )
3855
- download_btn = gr.Button("📥 Download Screenplay", variant="secondary")
3856
-
3857
- download_file = gr.File(
3858
- label="Download",
3859
- visible=False
3860
- )
3861
-
3862
- # Examples
3863
- gr.Examples(
3864
- examples=[
3865
- ["A burned-out teacher discovers her students are being replaced by AI duplicates"],
3866
- ["Two funeral home employees accidentally release a ghost who helps them solve murders"],
3867
- ["A time-loop forces a wedding planner to relive the worst wedding until they find true love"],
3868
- ["An astronaut returns to Earth to find everyone has forgotten space exists"],
3869
- ["A support group for reformed villains must save the city when heroes disappear"],
3870
- ["A food critic loses their sense of taste and teams up with a street food vendor"]
3871
- ],
3872
- inputs=query_input,
3873
- label="💡 Example Concepts"
3874
- )
3875
-
3876
- # Screenplay Library Tab
3877
- with gr.Tab("📚 Concept Library"):
3878
- gr.Markdown("""
3879
- ### 🎲 Random Screenplay Concepts
3880
-
3881
- Browse through AI-generated screenplay concepts. Each concept includes a title, logline, and brief setup.
3882
- """)
3883
-
3884
- library_display = gr.HTML(
3885
- value="<p>Library feature coming soon...</p>"
3886
- )
3887
-
3888
- # Event handlers
3889
- def handle_submit(query, s_type, genre, lang, session_id):
3890
- if not query:
3891
- yield "", "", "❌ Please enter a concept", session_id
3892
- return
3893
-
3894
- yield from process_query(query, s_type, genre, lang, session_id)
3895
-
3896
- def handle_random(s_type, genre, lang):
3897
- return generate_random_screenplay_theme(s_type, genre, lang)
3898
-
3899
- def handle_download(screenplay_text, format_type, session_id):
3900
- if not screenplay_text or not session_id:
3901
- return gr.update(visible=False)
3902
-
3903
- # Get title from database
3904
- session = ScreenplayDatabase.get_session(session_id)
3905
- title = session.get('title', 'Untitled') if session else 'Untitled'
3906
-
3907
- file_path = download_screenplay(screenplay_text, format_type, title, session_id)
3908
- if file_path and os.path.exists(file_path):
3909
- return gr.update(value=file_path, visible=True)
3910
- return gr.update(visible=False)
3911
-
3912
- # Connect events
3913
- submit_btn.click(
3914
- fn=handle_submit,
3915
- inputs=[query_input, screenplay_type, genre_select, language_select, current_session_id],
3916
- outputs=[stages_display, screenplay_output, status_text, current_session_id]
3917
- )
3918
-
3919
- random_btn.click(
3920
- fn=handle_random,
3921
- inputs=[screenplay_type, genre_select, language_select],
3922
- outputs=[query_input]
3923
- )
3924
-
3925
- clear_btn.click(
3926
- fn=lambda: ("", "", "Ready to create your screenplay", None),
3927
- outputs=[stages_display, screenplay_output, status_text, current_session_id]
3928
- )
3929
-
3930
- refresh_btn.click(
3931
- fn=get_active_sessions,
3932
- outputs=[session_dropdown]
3933
- )
3934
-
3935
- download_btn.click(
3936
- fn=handle_download,
3937
- inputs=[screenplay_output, format_select, current_session_id],
3938
- outputs=[download_file]
3939
- )
3940
-
3941
- # Load sessions on start
3942
- interface.load(
3943
- fn=get_active_sessions,
3944
- outputs=[session_dropdown]
3945
- )
3946
-
3947
- return interface
3948
-
3949
  # Main function
3950
  if __name__ == "__main__":
3951
  logger.info("Screenplay Generator Starting...")
@@ -3979,3 +2869,4 @@ if __name__ == "__main__":
3979
  share=False,
3980
  debug=True
3981
  )
 
 
2836
 
2837
  return interface
2838
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2839
  # Main function
2840
  if __name__ == "__main__":
2841
  logger.info("Screenplay Generator Starting...")
 
2869
  share=False,
2870
  debug=True
2871
  )
2872
+