mgbam commited on
Commit
733c176
Β·
verified Β·
1 Parent(s): f4be5ea

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +172 -71
app.py CHANGED
@@ -1,13 +1,13 @@
1
  # app.py
2
  import streamlit as st
3
  from core.gemini_handler import GeminiHandler
4
- from core.visual_engine import VisualEngine
5
  from core.prompt_engineering import (
6
- create_cinematic_treatment_prompt,
7
- construct_dalle_prompt,
8
- create_narration_script_prompt_enhanced,
9
- create_scene_regeneration_prompt,
10
- create_visual_regeneration_prompt
11
  )
12
  import os
13
  import logging
@@ -17,6 +17,20 @@ st.set_page_config(page_title="CineGen AI Ultra+", layout="wide", initial_sideba
17
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
18
  logger = logging.getLogger(__name__)
19
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  # --- Global State Variables & API Key Setup ---
21
  def load_api_key(key_name_streamlit, key_name_env, service_name):
22
  key = None; secrets_available = hasattr(st, 'secrets')
@@ -38,15 +52,15 @@ if 'services_initialized' not in st.session_state:
38
  st.session_state.ELEVENLABS_API_KEY = load_api_key("ELEVENLABS_API_KEY", "ELEVENLABS_API_KEY", "ElevenLabs")
39
  st.session_state.PEXELS_API_KEY = load_api_key("PEXELS_API_KEY", "PEXELS_API_KEY", "Pexels")
40
  st.session_state.ELEVENLABS_VOICE_ID_CONFIG = load_api_key("ELEVENLABS_VOICE_ID", "ELEVENLABS_VOICE_ID", "ElevenLabs Voice ID")
41
-
42
- if not st.session_state.GEMINI_API_KEY:
43
  st.error("CRITICAL: Gemini API Key is essential and missing!"); logger.critical("Gemini API Key missing. Halting."); st.stop()
44
-
45
  try:
46
  st.session_state.gemini_handler = GeminiHandler(api_key=st.session_state.GEMINI_API_KEY)
47
  logger.info("GeminiHandler initialized successfully.")
48
  except Exception as e: st.error(f"Failed to init GeminiHandler: {e}"); logger.critical(f"GeminiHandler init failed: {e}", exc_info=True); st.stop()
49
-
50
  try:
51
  default_voice_id = "Rachel" # A common fallback if no secret is set
52
  configured_voice_id = st.session_state.ELEVENLABS_VOICE_ID_CONFIG or default_voice_id
@@ -55,7 +69,6 @@ if 'services_initialized' not in st.session_state:
55
  default_elevenlabs_voice_id=configured_voice_id # Pass it to __init__
56
  )
57
  st.session_state.visual_engine.set_openai_api_key(st.session_state.OPENAI_API_KEY)
58
- # set_elevenlabs_api_key will use the voice_id passed to __init__ or overridden by its own param
59
  st.session_state.visual_engine.set_elevenlabs_api_key(st.session_state.ELEVENLABS_API_KEY, voice_id_from_secret=st.session_state.ELEVENLABS_VOICE_ID_CONFIG)
60
  st.session_state.visual_engine.set_pexels_api_key(st.session_state.PEXELS_API_KEY)
61
  logger.info("VisualEngine initialized and API keys set (or attempted).")
@@ -72,19 +85,19 @@ for key, default_val in [
72
  ]:
73
  if key not in st.session_state: st.session_state[key] = default_val
74
 
75
- def initialize_new_project(): # Same
76
  st.session_state.story_treatment_scenes, st.session_state.scene_dalle_prompts, st.session_state.generated_visual_paths = [], [], []
77
  st.session_state.video_path, st.session_state.overall_narration_audio_path, st.session_state.narration_script_display = None, None, ""
78
  logger.info("New project initialized.")
79
 
80
- def generate_visual_for_scene_core(scene_index, scene_data, version=1): # Same
81
  dalle_prompt = construct_dalle_prompt(scene_data, st.session_state.character_definitions, st.session_state.global_style_additions)
82
  if not dalle_prompt: logger.error(f"DALL-E prompt construction failed for scene {scene_data.get('scene_number', scene_index+1)}"); return False
83
  while len(st.session_state.scene_dalle_prompts) <= scene_index: st.session_state.scene_dalle_prompts.append("")
84
  while len(st.session_state.generated_visual_paths) <= scene_index: st.session_state.generated_visual_paths.append(None)
85
  st.session_state.scene_dalle_prompts[scene_index] = dalle_prompt
86
  filename = f"scene_{scene_data.get('scene_number', scene_index+1)}_visual_v{version}.png"
87
- img_path = st.session_state.visual_engine.generate_image_visual(dalle_prompt, scene_data, filename)
88
  if img_path and os.path.exists(img_path):
89
  st.session_state.generated_visual_paths[scene_index] = img_path; logger.info(f"Visual generated for Scene {scene_data.get('scene_number', scene_index+1)}: {os.path.basename(img_path)}"); return True
90
  else:
@@ -97,7 +110,7 @@ with st.sidebar:
97
  user_idea = st.text_area("Core Story Idea / Theme:", "A lone wanderer searches for a mythical oasis in a vast, post-apocalyptic desert, haunted by mirages and mechanical scavengers.", height=120, key="user_idea_main_v5")
98
  genre = st.selectbox("Primary Genre:", ["Cyberpunk", "Sci-Fi", "Fantasy", "Noir", "Thriller", "Western", "Post-Apocalyptic", "Historical Drama", "Surreal"], index=6, key="genre_main_v5")
99
  mood = st.selectbox("Overall Mood:", ["Hopeful yet Desperate", "Mysterious & Eerie", "Gritty & Tense", "Epic & Awe-Inspiring", "Melancholy & Reflective", "Whimsical & Lighthearted"], index=0, key="mood_main_v5")
100
- num_scenes = st.slider("Number of Key Scenes:", 1, 3, 1, key="num_scenes_main_v5")
101
  creative_guidance_options = {"Standard Director": "standard", "Artistic Visionary": "more_artistic", "Experimental Storyteller": "experimental_narrative"}
102
  selected_creative_guidance_key = st.selectbox("AI Creative Director Style:", options=list(creative_guidance_options.keys()), key="creative_guidance_select_v5")
103
  actual_creative_guidance = creative_guidance_options[selected_creative_guidance_key]
@@ -109,10 +122,23 @@ with st.sidebar:
109
  with st.status("AI Director is envisioning your masterpiece...", expanded=True) as status:
110
  try:
111
  status.write("Phase 1: Gemini crafting cinematic treatment... πŸ“œ"); logger.info("Phase 1: Cinematic Treatment Gen.")
 
 
112
  treatment_prompt = create_cinematic_treatment_prompt(user_idea, genre, mood, num_scenes, actual_creative_guidance)
113
- treatment_result_json = st.session_state.gemini_handler.generate_story_breakdown(treatment_prompt)
114
- if not isinstance(treatment_result_json, list) or not treatment_result_json: raise ValueError("Gemini returned invalid scene list.")
115
- st.session_state.story_treatment_scenes = treatment_result_json; num_gen_scenes = len(st.session_state.story_treatment_scenes)
 
 
 
 
 
 
 
 
 
 
 
116
  st.session_state.scene_dalle_prompts = [""]*num_gen_scenes; st.session_state.generated_visual_paths = [None]*num_gen_scenes
117
  logger.info(f"Phase 1 complete. {num_gen_scenes} scenes."); status.update(label="Treatment complete! βœ… Generating visuals...", state="running")
118
 
@@ -122,37 +148,37 @@ with st.sidebar:
122
  sc_num_log = sc_data.get('scene_number', i+1)
123
  status.write(f" Visual for Scene {sc_num_log}..."); logger.info(f" Processing visual for Scene {sc_num_log}.")
124
  if generate_visual_for_scene_core(i, sc_data, version=1): visual_successes += 1
125
-
126
  current_status_label_ph2 = "Visuals ready! "
127
- next_step_state = "running" # Assume success unless visuals totally fail
128
- if visual_successes == 0 and num_gen_scenes > 0:
129
- logger.error("Visual gen failed all scenes."); current_status_label_ph2 = "Visual gen FAILED for all scenes."; next_step_state="error";
130
- status.update(label=current_status_label_ph2, state=next_step_state, expanded=True); st.stop() # Stop if no visuals
131
- elif visual_successes < num_gen_scenes:
132
  logger.warning(f"Visuals partial ({visual_successes}/{num_gen_scenes})."); current_status_label_ph2 = f"Visuals partially generated ({visual_successes}/{num_gen_scenes}). "
133
- status.update(label=f"{current_status_label_ph2}Generating narration script...", state=next_step_state) # Propagate error state if needed
134
- if next_step_state == "error": st.stop() # Should have already stopped
135
 
136
  status.write("Phase 3: Generating narration script..."); logger.info("Phase 3: Narration Script Gen.")
137
  voice_style_for_prompt = st.session_state.get("selected_voice_style_for_generation", "cinematic_trailer")
138
  narr_prompt = create_narration_script_prompt_enhanced(st.session_state.story_treatment_scenes, mood, genre, voice_style_for_prompt)
139
- st.session_state.narration_script_display = st.session_state.gemini_handler.generate_image_prompt(narr_prompt)
140
  logger.info("Narration script generated."); status.update(label="Narration script ready! Synthesizing voice...", state="running")
141
-
142
  status.write("Phase 4: Synthesizing voice (ElevenLabs)... πŸ”Š"); logger.info("Phase 4: Voice Synthesis.")
143
  st.session_state.overall_narration_audio_path = st.session_state.visual_engine.generate_narration_audio(st.session_state.narration_script_display)
144
-
145
  final_label = "All components ready! Storyboard below. πŸš€"
146
- final_state_val = "complete"
147
  if not st.session_state.overall_narration_audio_path:
148
  final_label = f"{current_status_label_ph2}Storyboard ready (Voiceover skipped or failed)."
149
  logger.warning("Voiceover was skipped or failed.")
150
  else: logger.info("Voiceover generated successfully.")
151
- status.update(label=final_label, state=final_state_val, expanded=False) # Always complete if this far
152
 
153
- except ValueError as ve: logger.error(f"ValueError: {ve}", exc_info=True); status.update(label=f"Input or Gemini response error: {ve}", state="error", expanded=True);
154
  except Exception as e: logger.error(f"Unhandled Exception: {e}", exc_info=True); status.update(label=f"An unexpected error occurred: {e}", state="error", expanded=True);
155
-
156
  st.markdown("---"); st.markdown("### Fine-Tuning Options")
157
  with st.expander("Define Characters", expanded=False):
158
  char_name = st.text_input("Character Name", key="char_name_adv_ultra_v5"); char_desc = st.text_area("Visual Description", key="char_desc_adv_ultra_v5", height=100, placeholder="e.g., Jax: rugged male astronaut...")
@@ -160,14 +186,14 @@ with st.sidebar:
160
  if char_name and char_desc: st.session_state.character_definitions[char_name.strip().lower()] = char_desc.strip(); st.success(f"Char '{char_name.strip()}' saved.")
161
  else: st.warning("Name and description needed.")
162
  if st.session_state.character_definitions: st.caption("Current Characters:"); [st.markdown(f"**{k.title()}:** _{v}_") for k,v in st.session_state.character_definitions.items()]
163
-
164
  with st.expander("Global Style Overrides", expanded=False):
165
  presets = { "Default (Director's Choice)": "", "Hyper-Realistic Gritty Noir": "hyper-realistic gritty neo-noir, extreme detail, deep dynamic shadows, complex reflections on wet surfaces, cinematic film grain, desaturated palette with isolated vibrant neon accents (e.g. red, cyan), anamorphic lens distortion, atmospheric haze.", "Surreal Dreamscape Fantasy": "surreal dreamscape, epic fantasy elements, painterly with photorealistic details, impossible architecture, bioluminescent flora, otherworldly color palette (e.g., magenta skies, turquoise rivers), style of Roger Dean meets ZdzisΕ‚aw BeksiΕ„ski.", "Vintage Analog Sci-Fi": "70s/80s analog sci-fi film aesthetic, tangible practical effects look, subtle light leaks, lens flares, warm filmic tones mixed with cool blues, detailed retro-futuristic technology with chunky buttons and CRT screens."}
166
  sel_preset = st.selectbox("Base Style Preset:", options=list(presets.keys()), key="style_preset_adv_ultra_v5")
167
  custom_kw = st.text_area("Additional Custom Style Keywords:", key="custom_style_adv_ultra_v5", height=80, placeholder="e.g., 'Dutch angle'")
168
  cur_style = st.session_state.global_style_additions
169
  if st.button("Apply Global Styles", key="apply_styles_adv_ultra_v5"):
170
- final_s = presets[sel_preset];
171
  if custom_kw.strip(): final_s = f"{final_s}, {custom_kw.strip()}" if final_s else custom_kw.strip()
172
  st.session_state.global_style_additions = final_s.strip(); cur_style = final_s.strip()
173
  if cur_style: st.success("Global styles applied!")
@@ -175,26 +201,25 @@ with st.sidebar:
175
  if cur_style: st.caption(f"Active global styles: \"{cur_style}\"")
176
 
177
  with st.expander("Voice & Narration Style", expanded=False):
178
- # User can override the voice ID from secrets if they want to experiment
179
- default_voice_from_engine = "Rachel" # Fallback if engine not init or no secret
180
  if hasattr(st.session_state, 'visual_engine') and st.session_state.visual_engine:
181
  default_voice_from_engine = st.session_state.visual_engine.elevenlabs_voice_id
182
 
183
  user_voice_id_override = st.text_input(
184
- "ElevenLabs Voice ID (optional override):",
185
- value=default_voice_from_engine,
186
  key="el_voice_id_override_v5",
187
  help=f"Defaulting to '{default_voice_from_engine}' from secrets/config. Enter a specific Voice ID from your ElevenLabs account to override."
188
  )
189
  prompt_v_styles = {"Cinematic Trailer": "cinematic_trailer", "Neutral Documentary": "documentary_neutral", "Character Introspection": "introspective_character"}
190
  sel_prompt_v_style_key = st.selectbox("Narration Script Style:", list(prompt_v_styles.keys()), key="narr_style_sel_v5", index=0)
191
-
192
  if st.button("Set Narrator Voice & Style", key="set_voice_btn_ultra_v5"):
193
  final_voice_id_to_use = user_voice_id_override.strip()
194
- if not final_voice_id_to_use: # If user cleared the input, revert to the one from secrets/default
195
  final_voice_id_to_use = st.session_state.get("ELEVENLABS_VOICE_ID_CONFIG", "Rachel")
196
-
197
- if hasattr(st.session_state, 'visual_engine'):
198
  st.session_state.visual_engine.elevenlabs_voice_id = final_voice_id_to_use
199
  st.session_state.selected_voice_style_for_generation = prompt_v_styles[sel_prompt_v_style_key]
200
  st.success(f"Narrator Voice ID set to: {final_voice_id_to_use}. Script Style: {sel_prompt_v_style_key}")
@@ -209,69 +234,124 @@ if not st.session_state.story_treatment_scenes: st.info("Use the sidebar to gene
209
  else:
210
  for i_main, scene_content_display in enumerate(st.session_state.story_treatment_scenes):
211
  scene_n = scene_content_display.get('scene_number', i_main + 1); scene_t = scene_content_display.get('scene_title', 'Untitled')
212
- key_base = f"s{scene_n}_{''.join(filter(str.isalnum, scene_t[:10]))}_v5"
213
  if "director_note" in scene_content_display and scene_content_display['director_note']: st.info(f"🎬 Director Note S{scene_n}: {scene_content_display['director_note']}")
214
  st.subheader(f"SCENE {scene_n}: {scene_t.upper()}"); col_d, col_v = st.columns([0.45, 0.55])
215
  with col_d:
216
- with st.expander("πŸ“ Scene Treatment", expanded=True):
217
  st.markdown(f"**Beat:** {scene_content_display.get('emotional_beat', 'N/A')}"); st.markdown(f"**Setting:** {scene_content_display.get('setting_description', 'N/A')}"); st.markdown(f"**Chars:** {', '.join(scene_content_display.get('characters_involved', ['N/A']))}"); st.markdown(f"**Focus Moment:** _{scene_content_display.get('character_focus_moment', 'N/A')}_"); st.markdown(f"**Plot Beat:** {scene_content_display.get('key_plot_beat', 'N/A')}"); st.markdown(f"**Dialogue Hook:** `\"{scene_content_display.get('suggested_dialogue_hook', '...')}\"`"); st.markdown("---"); st.markdown(f"**Dir. Visual Style:** _{scene_content_display.get('PROACTIVE_visual_style_감독', 'N/A')}_"); st.markdown(f"**Dir. Camera:** _{scene_content_display.get('PROACTIVE_camera_work_감독', 'N/A')}_"); st.markdown(f"**Dir. Sound:** _{scene_content_display.get('PROACTIVE_sound_design_감독', 'N/A')}_")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
218
  cur_d_prompt = st.session_state.scene_dalle_prompts[i_main] if i_main < len(st.session_state.scene_dalle_prompts) else None
219
  if cur_d_prompt:
220
  with st.popover("πŸ‘οΈ DALL-E Prompt"): st.markdown(f"**DALL-E Prompt:**"); st.code(cur_d_prompt, language='text')
221
  pexels_q = scene_content_display.get('pexels_search_query_감독', None)
222
  if pexels_q: st.caption(f"Pexels Fallback Query: `{pexels_q}`")
 
223
  with col_v:
224
  cur_img_p = st.session_state.generated_visual_paths[i_main] if i_main < len(st.session_state.generated_visual_paths) else None
225
- if cur_img_p and os.path.exists(cur_img_p): st.image(cur_img_p, caption=f"Scene {scene_n}: {scene_t}")
226
  else:
227
  if st.session_state.story_treatment_scenes: st.caption("Visual pending/failed.")
228
-
229
- with st.popover(f"✏️ Edit Scene {scene_n} Treatment"):
230
- fb_script = st.text_area("Changes to treatment:", key=f"treat_fb_{key_base}", height=150)
231
- if st.button(f"πŸ”„ Update Scene {scene_n} Treatment", key=f"regen_treat_btn_{key_base}"):
232
  if fb_script:
233
  with st.status(f"Updating Scene {scene_n}...", expanded=True) as s_treat_regen:
234
  prompt_text = create_scene_regeneration_prompt(scene_content_display, fb_script, st.session_state.story_treatment_scenes)
235
  try:
236
- updated_sc_data = st.session_state.gemini_handler.regenerate_scene_script_details(prompt_text)
237
- st.session_state.story_treatment_scenes[i_main] = updated_sc_data
 
 
 
 
 
 
 
 
 
 
 
 
 
238
  s_treat_regen.update(label="Treatment updated! Regenerating visual...", state="running")
239
  v_num = 1
240
- if cur_img_p and os.path.exists(cur_img_p):
241
  try: b,_=os.path.splitext(os.path.basename(cur_img_p)); v_num = int(b.split('_v')[-1])+1 if '_v' in b else 2
242
- except (ValueError, IndexError, TypeError): v_num = 2
243
- else: v_num = 1
244
  if generate_visual_for_scene_core(i_main, updated_sc_data, version=v_num): s_treat_regen.update(label="Treatment & Visual Updated! πŸŽ‰", state="complete", expanded=False)
245
- else: s_treat_regen.update(label="Treatment updated, visual failed.", state="complete", expanded=False)
246
  st.rerun()
247
  except Exception as e_regen: s_treat_regen.update(label=f"Error: {e_regen}", state="error"); logger.error(f"Scene treatment regen error: {e_regen}", exc_info=True)
248
  else: st.warning("Please provide feedback.")
249
 
250
- with st.popover(f"🎨 Edit Scene {scene_n} Visual Prompt"):
251
  d_prompt_edit = st.session_state.scene_dalle_prompts[i_main] if i_main < len(st.session_state.scene_dalle_prompts) else "No DALL-E prompt."
252
  st.caption("Current DALL-E Prompt:"); st.code(d_prompt_edit, language='text')
253
- fb_visual = st.text_area("Changes for DALL-E prompt:", key=f"visual_fb_{key_base}", height=150)
254
- if st.button(f"πŸ”„ Update Scene {scene_n} Visual", key=f"regen_visual_btn_{key_base}"):
255
  if fb_visual:
256
  with st.status(f"Refining prompt & visual for Scene {scene_n}...", expanded=True) as s_visual_regen:
257
- ref_req_prompt = create_visual_regeneration_prompt(d_prompt_edit, fb_visual, scene_content_display,
258
  st.session_state.character_definitions, st.session_state.global_style_additions)
259
  try:
260
  refined_d_prompt = st.session_state.gemini_handler.generate_image_prompt(ref_req_prompt)
261
- st.session_state.scene_dalle_prompts[i_main] = refined_d_prompt
262
  s_visual_regen.update(label="DALL-E prompt refined! Regenerating visual...", state="running")
263
  v_num = 1
264
- if cur_img_p and os.path.exists(cur_img_p):
265
  try: b,_=os.path.splitext(os.path.basename(cur_img_p)); v_num = int(b.split('_v')[-1])+1 if '_v' in b else 2
266
  except (ValueError, IndexError, TypeError): v_num=2
267
  else: v_num = 1
268
- if generate_visual_for_scene_core(i_main, scene_content_display, version=v_num): s_visual_regen.update(label="Visual Updated! πŸŽ‰", state="complete", expanded=False)
 
269
  else: s_visual_regen.update(label="Prompt refined, visual failed.", state="complete", expanded=False)
270
  st.rerun()
271
  except Exception as e_regen_vis: s_visual_regen.update(label=f"Error: {e_regen_vis}", state="error"); logger.error(f"Visual prompt regen error: {e_regen_vis}", exc_info=True)
272
  else: st.warning("Please provide feedback.")
273
  st.markdown("---")
274
-
275
  if st.session_state.story_treatment_scenes and any(p for p in st.session_state.generated_visual_paths if p is not None):
276
  if st.button("🎬 Assemble Narrated Cinematic Animatic", key="assemble_ultra_video_btn_v5", type="primary", use_container_width=True):
277
  with st.status("Assembling Ultra Animatic...", expanded=True) as status_vid:
@@ -279,21 +359,42 @@ else:
279
  for i_v, sc_c in enumerate(st.session_state.story_treatment_scenes):
280
  img_p_v = st.session_state.generated_visual_paths[i_v] if i_v < len(st.session_state.generated_visual_paths) else None
281
  if img_p_v and os.path.exists(img_p_v):
282
- img_data_vid.append({'path':img_p_v, 'scene_num':sc_c.get('scene_number',i_v+1), 'key_action':sc_c.get('key_plot_beat','')}); status_vid.write(f"Adding Scene {sc_c.get('scene_number', i_v + 1)}.")
 
 
 
 
 
 
 
 
 
 
 
 
 
283
  if img_data_vid:
284
- status_vid.write("Calling video engine..."); st.session_state.video_path = st.session_state.visual_engine.create_video_from_images(
285
- img_data_vid, overall_narration_path=st.session_state.overall_narration_audio_path,
286
- output_filename="cinegen_ultra_animatic.mp4", duration_per_image=5, fps=24)
 
 
 
 
 
 
 
 
287
  if st.session_state.video_path and os.path.exists(st.session_state.video_path): status_vid.update(label="Ultra animatic assembled! πŸŽ‰", state="complete", expanded=False); st.balloons()
288
  else: status_vid.update(label="Video assembly failed. Check logs.", state="error", expanded=False); logger.error("Video assembly returned None or file does not exist.")
289
- else: status_vid.update(label="No valid images for video.", state="error", expanded=False); logger.warning("No valid images found for video assembly.")
290
  elif st.session_state.story_treatment_scenes: st.info("Generate visuals before assembling video.")
291
 
292
  if st.session_state.video_path and os.path.exists(st.session_state.video_path):
293
- st.header("🎬 Generated Cinematic Animatic");
294
  try:
295
  with open(st.session_state.video_path, 'rb') as vf_obj: video_bytes = vf_obj.read()
296
- st.video(video_bytes, format="video/mp4")
297
  with open(st.session_state.video_path, "rb") as fp_dl:
298
  st.download_button(label="Download Ultra Animatic", data=fp_dl, file_name=os.path.basename(st.session_state.video_path), mime="video/mp4", use_container_width=True, key="download_ultra_video_btn_v5" )
299
  except Exception as e: st.error(f"Error displaying video: {e}"); logger.error(f"Error displaying video: {e}", exc_info=True)
 
1
  # app.py
2
  import streamlit as st
3
  from core.gemini_handler import GeminiHandler
4
+ from core.visual_engine import VisualEngine
5
  from core.prompt_engineering import (
6
+ create_cinematic_treatment_prompt,
7
+ construct_dalle_prompt,
8
+ create_narration_script_prompt_enhanced,
9
+ create_scene_regeneration_prompt,
10
+ create_visual_regeneration_prompt
11
  )
12
  import os
13
  import logging
 
17
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
18
  logger = logging.getLogger(__name__)
19
 
20
+ # <<< ADDED/MODIFIED START >>>
21
+ # --- Global Definitions for New Features ---
22
+ SHOT_TYPES_OPTIONS = [
23
+ "Director's Choice", "Establishing Shot", "Long Shot", "Full Shot",
24
+ "Medium Long Shot (Cowboy)", "Medium Shot", "Medium Close-up",
25
+ "Close-up", "Extreme Close-up", "Point of View (POV)",
26
+ "Over the Shoulder", "Tracking Shot", "Dolly Zoom", "Crane Shot",
27
+ "Aerial Shot", "Static Shot", "Dutch Angle", "Whip Pan"
28
+ ]
29
+ DEFAULT_SCENE_DURATION_SECS = 5 # Default duration in seconds for each scene
30
+ DEFAULT_SHOT_TYPE = "Director's Choice"
31
+ # <<< ADDED/MODIFIED END >>>
32
+
33
+
34
  # --- Global State Variables & API Key Setup ---
35
  def load_api_key(key_name_streamlit, key_name_env, service_name):
36
  key = None; secrets_available = hasattr(st, 'secrets')
 
52
  st.session_state.ELEVENLABS_API_KEY = load_api_key("ELEVENLABS_API_KEY", "ELEVENLABS_API_KEY", "ElevenLabs")
53
  st.session_state.PEXELS_API_KEY = load_api_key("PEXELS_API_KEY", "PEXELS_API_KEY", "Pexels")
54
  st.session_state.ELEVENLABS_VOICE_ID_CONFIG = load_api_key("ELEVENLABS_VOICE_ID", "ELEVENLABS_VOICE_ID", "ElevenLabs Voice ID")
55
+
56
+ if not st.session_state.GEMINI_API_KEY:
57
  st.error("CRITICAL: Gemini API Key is essential and missing!"); logger.critical("Gemini API Key missing. Halting."); st.stop()
58
+
59
  try:
60
  st.session_state.gemini_handler = GeminiHandler(api_key=st.session_state.GEMINI_API_KEY)
61
  logger.info("GeminiHandler initialized successfully.")
62
  except Exception as e: st.error(f"Failed to init GeminiHandler: {e}"); logger.critical(f"GeminiHandler init failed: {e}", exc_info=True); st.stop()
63
+
64
  try:
65
  default_voice_id = "Rachel" # A common fallback if no secret is set
66
  configured_voice_id = st.session_state.ELEVENLABS_VOICE_ID_CONFIG or default_voice_id
 
69
  default_elevenlabs_voice_id=configured_voice_id # Pass it to __init__
70
  )
71
  st.session_state.visual_engine.set_openai_api_key(st.session_state.OPENAI_API_KEY)
 
72
  st.session_state.visual_engine.set_elevenlabs_api_key(st.session_state.ELEVENLABS_API_KEY, voice_id_from_secret=st.session_state.ELEVENLABS_VOICE_ID_CONFIG)
73
  st.session_state.visual_engine.set_pexels_api_key(st.session_state.PEXELS_API_KEY)
74
  logger.info("VisualEngine initialized and API keys set (or attempted).")
 
85
  ]:
86
  if key not in st.session_state: st.session_state[key] = default_val
87
 
88
+ def initialize_new_project():
89
  st.session_state.story_treatment_scenes, st.session_state.scene_dalle_prompts, st.session_state.generated_visual_paths = [], [], []
90
  st.session_state.video_path, st.session_state.overall_narration_audio_path, st.session_state.narration_script_display = None, None, ""
91
  logger.info("New project initialized.")
92
 
93
+ def generate_visual_for_scene_core(scene_index, scene_data, version=1):
94
  dalle_prompt = construct_dalle_prompt(scene_data, st.session_state.character_definitions, st.session_state.global_style_additions)
95
  if not dalle_prompt: logger.error(f"DALL-E prompt construction failed for scene {scene_data.get('scene_number', scene_index+1)}"); return False
96
  while len(st.session_state.scene_dalle_prompts) <= scene_index: st.session_state.scene_dalle_prompts.append("")
97
  while len(st.session_state.generated_visual_paths) <= scene_index: st.session_state.generated_visual_paths.append(None)
98
  st.session_state.scene_dalle_prompts[scene_index] = dalle_prompt
99
  filename = f"scene_{scene_data.get('scene_number', scene_index+1)}_visual_v{version}.png"
100
+ img_path = st.session_state.visual_engine.generate_image_visual(dalle_prompt, scene_data, filename)
101
  if img_path and os.path.exists(img_path):
102
  st.session_state.generated_visual_paths[scene_index] = img_path; logger.info(f"Visual generated for Scene {scene_data.get('scene_number', scene_index+1)}: {os.path.basename(img_path)}"); return True
103
  else:
 
110
  user_idea = st.text_area("Core Story Idea / Theme:", "A lone wanderer searches for a mythical oasis in a vast, post-apocalyptic desert, haunted by mirages and mechanical scavengers.", height=120, key="user_idea_main_v5")
111
  genre = st.selectbox("Primary Genre:", ["Cyberpunk", "Sci-Fi", "Fantasy", "Noir", "Thriller", "Western", "Post-Apocalyptic", "Historical Drama", "Surreal"], index=6, key="genre_main_v5")
112
  mood = st.selectbox("Overall Mood:", ["Hopeful yet Desperate", "Mysterious & Eerie", "Gritty & Tense", "Epic & Awe-Inspiring", "Melancholy & Reflective", "Whimsical & Lighthearted"], index=0, key="mood_main_v5")
113
+ num_scenes = st.slider("Number of Key Scenes:", 1, 10, 3, key="num_scenes_main_v5") # MODIFIED: Increased max scenes
114
  creative_guidance_options = {"Standard Director": "standard", "Artistic Visionary": "more_artistic", "Experimental Storyteller": "experimental_narrative"}
115
  selected_creative_guidance_key = st.selectbox("AI Creative Director Style:", options=list(creative_guidance_options.keys()), key="creative_guidance_select_v5")
116
  actual_creative_guidance = creative_guidance_options[selected_creative_guidance_key]
 
122
  with st.status("AI Director is envisioning your masterpiece...", expanded=True) as status:
123
  try:
124
  status.write("Phase 1: Gemini crafting cinematic treatment... πŸ“œ"); logger.info("Phase 1: Cinematic Treatment Gen.")
125
+ # Note: Consider updating create_cinematic_treatment_prompt to also ask Gemini
126
+ # for 'suggested_shot_type' and 'estimated_duration_secs' for each scene.
127
  treatment_prompt = create_cinematic_treatment_prompt(user_idea, genre, mood, num_scenes, actual_creative_guidance)
128
+ treatment_result_json_raw = st.session_state.gemini_handler.generate_story_breakdown(treatment_prompt)
129
+ if not isinstance(treatment_result_json_raw, list) or not treatment_result_json_raw: raise ValueError("Gemini returned invalid scene list.")
130
+
131
+ # <<< ADDED/MODIFIED START >>>
132
+ # Process raw scenes and add default shot type and duration
133
+ processed_scenes = []
134
+ for scene_data_from_gemini in treatment_result_json_raw:
135
+ scene_data_from_gemini['shot_type'] = scene_data_from_gemini.get('suggested_shot_type', DEFAULT_SHOT_TYPE) # Use Gemini's suggestion if available
136
+ scene_data_from_gemini['scene_duration_secs'] = scene_data_from_gemini.get('estimated_duration_secs', DEFAULT_SCENE_DURATION_SECS) # Use Gemini's suggestion if available
137
+ processed_scenes.append(scene_data_from_gemini)
138
+ st.session_state.story_treatment_scenes = processed_scenes
139
+ # <<< ADDED/MODIFIED END >>>
140
+
141
+ num_gen_scenes = len(st.session_state.story_treatment_scenes)
142
  st.session_state.scene_dalle_prompts = [""]*num_gen_scenes; st.session_state.generated_visual_paths = [None]*num_gen_scenes
143
  logger.info(f"Phase 1 complete. {num_gen_scenes} scenes."); status.update(label="Treatment complete! βœ… Generating visuals...", state="running")
144
 
 
148
  sc_num_log = sc_data.get('scene_number', i+1)
149
  status.write(f" Visual for Scene {sc_num_log}..."); logger.info(f" Processing visual for Scene {sc_num_log}.")
150
  if generate_visual_for_scene_core(i, sc_data, version=1): visual_successes += 1
151
+
152
  current_status_label_ph2 = "Visuals ready! "
153
+ next_step_state = "running"
154
+ if visual_successes == 0 and num_gen_scenes > 0:
155
+ logger.error("Visual gen failed all scenes."); current_status_label_ph2 = "Visual gen FAILED for all scenes."; next_step_state="error";
156
+ status.update(label=current_status_label_ph2, state=next_step_state, expanded=True); st.stop()
157
+ elif visual_successes < num_gen_scenes:
158
  logger.warning(f"Visuals partial ({visual_successes}/{num_gen_scenes})."); current_status_label_ph2 = f"Visuals partially generated ({visual_successes}/{num_gen_scenes}). "
159
+ status.update(label=f"{current_status_label_ph2}Generating narration script...", state=next_step_state)
160
+ if next_step_state == "error": st.stop()
161
 
162
  status.write("Phase 3: Generating narration script..."); logger.info("Phase 3: Narration Script Gen.")
163
  voice_style_for_prompt = st.session_state.get("selected_voice_style_for_generation", "cinematic_trailer")
164
  narr_prompt = create_narration_script_prompt_enhanced(st.session_state.story_treatment_scenes, mood, genre, voice_style_for_prompt)
165
+ st.session_state.narration_script_display = st.session_state.gemini_handler.generate_image_prompt(narr_prompt) # Assuming this is correct for narration text
166
  logger.info("Narration script generated."); status.update(label="Narration script ready! Synthesizing voice...", state="running")
167
+
168
  status.write("Phase 4: Synthesizing voice (ElevenLabs)... πŸ”Š"); logger.info("Phase 4: Voice Synthesis.")
169
  st.session_state.overall_narration_audio_path = st.session_state.visual_engine.generate_narration_audio(st.session_state.narration_script_display)
170
+
171
  final_label = "All components ready! Storyboard below. πŸš€"
172
+ final_state_val = "complete"
173
  if not st.session_state.overall_narration_audio_path:
174
  final_label = f"{current_status_label_ph2}Storyboard ready (Voiceover skipped or failed)."
175
  logger.warning("Voiceover was skipped or failed.")
176
  else: logger.info("Voiceover generated successfully.")
177
+ status.update(label=final_label, state=final_state_val, expanded=False)
178
 
179
+ except ValueError as ve: logger.error(f"ValueError: {ve}", exc_info=True); status.update(label=f"Input or Gemini response error: {ve}", state="error", expanded=True);
180
  except Exception as e: logger.error(f"Unhandled Exception: {e}", exc_info=True); status.update(label=f"An unexpected error occurred: {e}", state="error", expanded=True);
181
+
182
  st.markdown("---"); st.markdown("### Fine-Tuning Options")
183
  with st.expander("Define Characters", expanded=False):
184
  char_name = st.text_input("Character Name", key="char_name_adv_ultra_v5"); char_desc = st.text_area("Visual Description", key="char_desc_adv_ultra_v5", height=100, placeholder="e.g., Jax: rugged male astronaut...")
 
186
  if char_name and char_desc: st.session_state.character_definitions[char_name.strip().lower()] = char_desc.strip(); st.success(f"Char '{char_name.strip()}' saved.")
187
  else: st.warning("Name and description needed.")
188
  if st.session_state.character_definitions: st.caption("Current Characters:"); [st.markdown(f"**{k.title()}:** _{v}_") for k,v in st.session_state.character_definitions.items()]
189
+
190
  with st.expander("Global Style Overrides", expanded=False):
191
  presets = { "Default (Director's Choice)": "", "Hyper-Realistic Gritty Noir": "hyper-realistic gritty neo-noir, extreme detail, deep dynamic shadows, complex reflections on wet surfaces, cinematic film grain, desaturated palette with isolated vibrant neon accents (e.g. red, cyan), anamorphic lens distortion, atmospheric haze.", "Surreal Dreamscape Fantasy": "surreal dreamscape, epic fantasy elements, painterly with photorealistic details, impossible architecture, bioluminescent flora, otherworldly color palette (e.g., magenta skies, turquoise rivers), style of Roger Dean meets ZdzisΕ‚aw BeksiΕ„ski.", "Vintage Analog Sci-Fi": "70s/80s analog sci-fi film aesthetic, tangible practical effects look, subtle light leaks, lens flares, warm filmic tones mixed with cool blues, detailed retro-futuristic technology with chunky buttons and CRT screens."}
192
  sel_preset = st.selectbox("Base Style Preset:", options=list(presets.keys()), key="style_preset_adv_ultra_v5")
193
  custom_kw = st.text_area("Additional Custom Style Keywords:", key="custom_style_adv_ultra_v5", height=80, placeholder="e.g., 'Dutch angle'")
194
  cur_style = st.session_state.global_style_additions
195
  if st.button("Apply Global Styles", key="apply_styles_adv_ultra_v5"):
196
+ final_s = presets[sel_preset];
197
  if custom_kw.strip(): final_s = f"{final_s}, {custom_kw.strip()}" if final_s else custom_kw.strip()
198
  st.session_state.global_style_additions = final_s.strip(); cur_style = final_s.strip()
199
  if cur_style: st.success("Global styles applied!")
 
201
  if cur_style: st.caption(f"Active global styles: \"{cur_style}\"")
202
 
203
  with st.expander("Voice & Narration Style", expanded=False):
204
+ default_voice_from_engine = "Rachel"
 
205
  if hasattr(st.session_state, 'visual_engine') and st.session_state.visual_engine:
206
  default_voice_from_engine = st.session_state.visual_engine.elevenlabs_voice_id
207
 
208
  user_voice_id_override = st.text_input(
209
+ "ElevenLabs Voice ID (optional override):",
210
+ value=default_voice_from_engine,
211
  key="el_voice_id_override_v5",
212
  help=f"Defaulting to '{default_voice_from_engine}' from secrets/config. Enter a specific Voice ID from your ElevenLabs account to override."
213
  )
214
  prompt_v_styles = {"Cinematic Trailer": "cinematic_trailer", "Neutral Documentary": "documentary_neutral", "Character Introspection": "introspective_character"}
215
  sel_prompt_v_style_key = st.selectbox("Narration Script Style:", list(prompt_v_styles.keys()), key="narr_style_sel_v5", index=0)
216
+
217
  if st.button("Set Narrator Voice & Style", key="set_voice_btn_ultra_v5"):
218
  final_voice_id_to_use = user_voice_id_override.strip()
219
+ if not final_voice_id_to_use:
220
  final_voice_id_to_use = st.session_state.get("ELEVENLABS_VOICE_ID_CONFIG", "Rachel")
221
+
222
+ if hasattr(st.session_state, 'visual_engine'):
223
  st.session_state.visual_engine.elevenlabs_voice_id = final_voice_id_to_use
224
  st.session_state.selected_voice_style_for_generation = prompt_v_styles[sel_prompt_v_style_key]
225
  st.success(f"Narrator Voice ID set to: {final_voice_id_to_use}. Script Style: {sel_prompt_v_style_key}")
 
234
  else:
235
  for i_main, scene_content_display in enumerate(st.session_state.story_treatment_scenes):
236
  scene_n = scene_content_display.get('scene_number', i_main + 1); scene_t = scene_content_display.get('scene_title', 'Untitled')
237
+ key_base = f"s{scene_n}_{''.join(filter(str.isalnum, scene_t[:10]))}_v5_{i_main}" # Ensure unique key base with index
238
  if "director_note" in scene_content_display and scene_content_display['director_note']: st.info(f"🎬 Director Note S{scene_n}: {scene_content_display['director_note']}")
239
  st.subheader(f"SCENE {scene_n}: {scene_t.upper()}"); col_d, col_v = st.columns([0.45, 0.55])
240
  with col_d:
241
+ with st.expander("πŸ“ Scene Treatment & Controls", expanded=True): # MODIFIED Expander Title
242
  st.markdown(f"**Beat:** {scene_content_display.get('emotional_beat', 'N/A')}"); st.markdown(f"**Setting:** {scene_content_display.get('setting_description', 'N/A')}"); st.markdown(f"**Chars:** {', '.join(scene_content_display.get('characters_involved', ['N/A']))}"); st.markdown(f"**Focus Moment:** _{scene_content_display.get('character_focus_moment', 'N/A')}_"); st.markdown(f"**Plot Beat:** {scene_content_display.get('key_plot_beat', 'N/A')}"); st.markdown(f"**Dialogue Hook:** `\"{scene_content_display.get('suggested_dialogue_hook', '...')}\"`"); st.markdown("---"); st.markdown(f"**Dir. Visual Style:** _{scene_content_display.get('PROACTIVE_visual_style_감독', 'N/A')}_"); st.markdown(f"**Dir. Camera:** _{scene_content_display.get('PROACTIVE_camera_work_감독', 'N/A')}_"); st.markdown(f"**Dir. Sound:** _{scene_content_display.get('PROACTIVE_sound_design_감독', 'N/A')}_")
243
+
244
+ # <<< ADDED/MODIFIED START >>>
245
+ st.markdown("---")
246
+ st.markdown("##### Shot & Pacing Controls")
247
+
248
+ # Shot Type Selection
249
+ current_shot_type = st.session_state.story_treatment_scenes[i_main].get('shot_type', DEFAULT_SHOT_TYPE)
250
+ try:
251
+ shot_type_index = SHOT_TYPES_OPTIONS.index(current_shot_type)
252
+ except ValueError:
253
+ shot_type_index = SHOT_TYPES_OPTIONS.index(DEFAULT_SHOT_TYPE) # Fallback if value is somehow invalid
254
+
255
+ new_shot_type = st.selectbox(
256
+ "Dominant Shot Type:",
257
+ options=SHOT_TYPES_OPTIONS,
258
+ index=shot_type_index,
259
+ key=f"shot_type_widget_{key_base}",
260
+ help="Suggests the primary camera shot for this scene. Influences visual generation style if DALL-E prompt is ever made aware of it."
261
+ )
262
+ if new_shot_type != st.session_state.story_treatment_scenes[i_main]['shot_type']:
263
+ st.session_state.story_treatment_scenes[i_main]['shot_type'] = new_shot_type
264
+ # No st.rerun() needed unless other UI elements immediately depend on this change
265
+
266
+ # Scene Duration Control
267
+ current_duration = st.session_state.story_treatment_scenes[i_main].get('scene_duration_secs', DEFAULT_SCENE_DURATION_SECS)
268
+ new_duration = st.number_input(
269
+ "Scene Duration (seconds):",
270
+ min_value=1,
271
+ max_value=300, # Max 5 minutes per scene image
272
+ value=current_duration,
273
+ step=1,
274
+ key=f"duration_widget_{key_base}",
275
+ help="Approximate duration this scene's visual will be shown in the final animatic."
276
+ )
277
+ if new_duration != st.session_state.story_treatment_scenes[i_main]['scene_duration_secs']:
278
+ st.session_state.story_treatment_scenes[i_main]['scene_duration_secs'] = new_duration
279
+ # No st.rerun() needed unless other UI elements immediately depend on this change
280
+ st.markdown("---")
281
+ # <<< ADDED/MODIFIED END >>>
282
+
283
  cur_d_prompt = st.session_state.scene_dalle_prompts[i_main] if i_main < len(st.session_state.scene_dalle_prompts) else None
284
  if cur_d_prompt:
285
  with st.popover("πŸ‘οΈ DALL-E Prompt"): st.markdown(f"**DALL-E Prompt:**"); st.code(cur_d_prompt, language='text')
286
  pexels_q = scene_content_display.get('pexels_search_query_감독', None)
287
  if pexels_q: st.caption(f"Pexels Fallback Query: `{pexels_q}`")
288
+
289
  with col_v:
290
  cur_img_p = st.session_state.generated_visual_paths[i_main] if i_main < len(st.session_state.generated_visual_paths) else None
291
+ if cur_img_p and os.path.exists(cur_img_p): st.image(cur_img_p, caption=f"Scene {scene_n}: {scene_t}")
292
  else:
293
  if st.session_state.story_treatment_scenes: st.caption("Visual pending/failed.")
294
+
295
+ with st.popover(f"✏️ Edit Scene {scene_n} Treatment"):
296
+ fb_script = st.text_area("Changes to treatment:", key=f"treat_fb_{key_base}", height=150)
297
+ if st.button(f"πŸ”„ Update Scene {scene_n} Treatment", key=f"regen_treat_btn_{key_base}"):
298
  if fb_script:
299
  with st.status(f"Updating Scene {scene_n}...", expanded=True) as s_treat_regen:
300
  prompt_text = create_scene_regeneration_prompt(scene_content_display, fb_script, st.session_state.story_treatment_scenes)
301
  try:
302
+ updated_sc_data_raw = st.session_state.gemini_handler.regenerate_scene_script_details(prompt_text) # This is just the script part
303
+
304
+ # <<< ADDED/MODIFIED START >>>
305
+ # Preserve user-set shot type and duration
306
+ original_shot_type = st.session_state.story_treatment_scenes[i_main].get('shot_type', DEFAULT_SHOT_TYPE)
307
+ original_duration = st.session_state.story_treatment_scenes[i_main].get('scene_duration_secs', DEFAULT_SCENE_DURATION_SECS)
308
+
309
+ # Merge Gemini's script updates with existing scene data, preserving our custom fields
310
+ updated_sc_data = {**st.session_state.story_treatment_scenes[i_main], **updated_sc_data_raw}
311
+ updated_sc_data['shot_type'] = updated_sc_data_raw.get('suggested_shot_type', original_shot_type) # If Gemini suggests it, use it, else keep old
312
+ updated_sc_data['scene_duration_secs'] = updated_sc_data_raw.get('estimated_duration_secs', original_duration) # Same for duration
313
+
314
+ st.session_state.story_treatment_scenes[i_main] = updated_sc_data
315
+ # <<< ADDED/MODIFIED END >>>
316
+
317
  s_treat_regen.update(label="Treatment updated! Regenerating visual...", state="running")
318
  v_num = 1
319
+ if cur_img_p and os.path.exists(cur_img_p):
320
  try: b,_=os.path.splitext(os.path.basename(cur_img_p)); v_num = int(b.split('_v')[-1])+1 if '_v' in b else 2
321
+ except (ValueError, IndexError, TypeError): v_num = 2
322
+ else: v_num = 1
323
  if generate_visual_for_scene_core(i_main, updated_sc_data, version=v_num): s_treat_regen.update(label="Treatment & Visual Updated! πŸŽ‰", state="complete", expanded=False)
324
+ else: s_treat_regen.update(label="Treatment updated, visual failed.", state="complete", expanded=False)
325
  st.rerun()
326
  except Exception as e_regen: s_treat_regen.update(label=f"Error: {e_regen}", state="error"); logger.error(f"Scene treatment regen error: {e_regen}", exc_info=True)
327
  else: st.warning("Please provide feedback.")
328
 
329
+ with st.popover(f"🎨 Edit Scene {scene_n} Visual Prompt"):
330
  d_prompt_edit = st.session_state.scene_dalle_prompts[i_main] if i_main < len(st.session_state.scene_dalle_prompts) else "No DALL-E prompt."
331
  st.caption("Current DALL-E Prompt:"); st.code(d_prompt_edit, language='text')
332
+ fb_visual = st.text_area("Changes for DALL-E prompt:", key=f"visual_fb_{key_base}", height=150)
333
+ if st.button(f"πŸ”„ Update Scene {scene_n} Visual", key=f"regen_visual_btn_{key_base}"):
334
  if fb_visual:
335
  with st.status(f"Refining prompt & visual for Scene {scene_n}...", expanded=True) as s_visual_regen:
336
+ ref_req_prompt = create_visual_regeneration_prompt(d_prompt_edit, fb_visual, scene_content_display,
337
  st.session_state.character_definitions, st.session_state.global_style_additions)
338
  try:
339
  refined_d_prompt = st.session_state.gemini_handler.generate_image_prompt(ref_req_prompt)
340
+ st.session_state.scene_dalle_prompts[i_main] = refined_d_prompt
341
  s_visual_regen.update(label="DALL-E prompt refined! Regenerating visual...", state="running")
342
  v_num = 1
343
+ if cur_img_p and os.path.exists(cur_img_p):
344
  try: b,_=os.path.splitext(os.path.basename(cur_img_p)); v_num = int(b.split('_v')[-1])+1 if '_v' in b else 2
345
  except (ValueError, IndexError, TypeError): v_num=2
346
  else: v_num = 1
347
+ # Pass the current scene_content_display, as DALL-E prompt is separate from treatment text
348
+ if generate_visual_for_scene_core(i_main, st.session_state.story_treatment_scenes[i_main], version=v_num): s_visual_regen.update(label="Visual Updated! πŸŽ‰", state="complete", expanded=False)
349
  else: s_visual_regen.update(label="Prompt refined, visual failed.", state="complete", expanded=False)
350
  st.rerun()
351
  except Exception as e_regen_vis: s_visual_regen.update(label=f"Error: {e_regen_vis}", state="error"); logger.error(f"Visual prompt regen error: {e_regen_vis}", exc_info=True)
352
  else: st.warning("Please provide feedback.")
353
  st.markdown("---")
354
+
355
  if st.session_state.story_treatment_scenes and any(p for p in st.session_state.generated_visual_paths if p is not None):
356
  if st.button("🎬 Assemble Narrated Cinematic Animatic", key="assemble_ultra_video_btn_v5", type="primary", use_container_width=True):
357
  with st.status("Assembling Ultra Animatic...", expanded=True) as status_vid:
 
359
  for i_v, sc_c in enumerate(st.session_state.story_treatment_scenes):
360
  img_p_v = st.session_state.generated_visual_paths[i_v] if i_v < len(st.session_state.generated_visual_paths) else None
361
  if img_p_v and os.path.exists(img_p_v):
362
+ # <<< ADDED/MODIFIED START >>>
363
+ scene_duration = sc_c.get('scene_duration_secs', DEFAULT_SCENE_DURATION_SECS)
364
+ img_data_vid.append({
365
+ 'path': img_p_v,
366
+ 'scene_num': sc_c.get('scene_number', i_v + 1),
367
+ 'key_action': sc_c.get('key_plot_beat', ''),
368
+ 'duration': scene_duration # Use per-scene duration
369
+ })
370
+ status_vid.write(f"Adding Scene {sc_c.get('scene_number', i_v + 1)} (Duration: {scene_duration}s).")
371
+ # <<< ADDED/MODIFIED END >>>
372
+ else:
373
+ logger.warning(f"Skipping Scene {sc_c.get('scene_number', i_v + 1)} for video assembly: No valid visual path.")
374
+
375
+
376
  if img_data_vid:
377
+ status_vid.write("Calling video engine...");
378
+ # <<< ADDED/MODIFIED START >>>
379
+ # IMPORTANT: Assumes visual_engine.create_video_from_images is updated to accept
380
+ # images_data (list of dicts with 'path' and 'duration') and no longer needs duration_per_image.
381
+ st.session_state.video_path = st.session_state.visual_engine.create_video_from_images(
382
+ images_data=img_data_vid, # Pass the list of dicts
383
+ overall_narration_path=st.session_state.overall_narration_audio_path,
384
+ output_filename="cinegen_ultra_animatic.mp4",
385
+ fps=24 # Keep FPS or make it configurable
386
+ )
387
+ # <<< ADDED/MODIFIED END >>>
388
  if st.session_state.video_path and os.path.exists(st.session_state.video_path): status_vid.update(label="Ultra animatic assembled! πŸŽ‰", state="complete", expanded=False); st.balloons()
389
  else: status_vid.update(label="Video assembly failed. Check logs.", state="error", expanded=False); logger.error("Video assembly returned None or file does not exist.")
390
+ else: status_vid.update(label="No valid images with duration for video.", state="error", expanded=False); logger.warning("No valid images found for video assembly.")
391
  elif st.session_state.story_treatment_scenes: st.info("Generate visuals before assembling video.")
392
 
393
  if st.session_state.video_path and os.path.exists(st.session_state.video_path):
394
+ st.header("🎬 Generated Cinematic Animatic");
395
  try:
396
  with open(st.session_state.video_path, 'rb') as vf_obj: video_bytes = vf_obj.read()
397
+ st.video(video_bytes, format="video/mp4")
398
  with open(st.session_state.video_path, "rb") as fp_dl:
399
  st.download_button(label="Download Ultra Animatic", data=fp_dl, file_name=os.path.basename(st.session_state.video_path), mime="video/mp4", use_container_width=True, key="download_ultra_video_btn_v5" )
400
  except Exception as e: st.error(f"Error displaying video: {e}"); logger.error(f"Error displaying video: {e}", exc_info=True)