Spaces:
Running
Running
Update app.py
Browse files
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 |
+
|