mgbam commited on
Commit
1813b8c
Β·
verified Β·
1 Parent(s): 17602ca

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +265 -150
app.py CHANGED
@@ -5,6 +5,7 @@ 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
@@ -17,7 +18,6 @@ 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
- # <<< ADDED/MODIFIED START >>>
21
  # --- Global Definitions for New Features ---
22
  SHOT_TYPES_OPTIONS = [
23
  "Director's Choice", "Establishing Shot", "Long Shot", "Full Shot",
@@ -26,9 +26,9 @@ SHOT_TYPES_OPTIONS = [
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 ---
@@ -52,6 +52,7 @@ if 'services_initialized' not in st.session_state:
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()
@@ -62,15 +63,16 @@ if 'services_initialized' not in st.session_state:
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
67
  st.session_state.visual_engine = VisualEngine(
68
  output_dir="temp_cinegen_media",
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).")
75
  except Exception as e:
76
  st.error(f"Failed to init VisualEngine or set its API keys: {e}"); logger.critical(f"VisualEngine init/key setting failed: {e}", exc_info=True)
@@ -78,30 +80,86 @@ if 'services_initialized' not in st.session_state:
78
  st.session_state.services_initialized = True; logger.info("Service initialization sequence complete.")
79
 
80
  # Initialize other session state variables
 
81
  for key, default_val in [
82
- ('story_treatment_scenes', []), ('scene_dalle_prompts', []), ('generated_visual_paths', []),
83
  ('video_path', None), ('character_definitions', {}), ('global_style_additions', ""),
84
  ('overall_narration_audio_path', None), ('narration_script_display', "")
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:
104
- st.session_state.generated_visual_paths[scene_index] = None; logger.warning(f"Visual generation FAILED for Scene {scene_data.get('scene_number', scene_index+1)}. img_path was: {img_path}"); return False
 
 
 
 
 
105
 
106
  # --- UI Sidebar ---
107
  with st.sidebar:
@@ -110,7 +168,7 @@ with st.sidebar:
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,47 +180,50 @@ with st.sidebar:
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
 
145
- status.write("Phase 2: Creating visuals (DALL-E/Pexels)... πŸ–ΌοΈ"); logger.info("Phase 2: Visual Gen.")
146
  visual_successes = 0
147
  for i, sc_data in enumerate(st.session_state.story_treatment_scenes):
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.")
@@ -180,6 +241,7 @@ with st.sidebar:
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...")
185
  if st.button("Save Character", key="add_char_adv_ultra_v5"):
@@ -225,6 +287,7 @@ with st.sidebar:
225
  st.success(f"Narrator Voice ID set to: {final_voice_id_to_use}. Script Style: {sel_prompt_v_style_key}")
226
  logger.info(f"User updated ElevenLabs Voice ID to: {final_voice_id_to_use}, Script Style: {sel_prompt_v_style_key}")
227
 
 
228
  # --- Main Content Area ---
229
  st.header("🎬 Cinematic Storyboard & Treatment")
230
  if st.session_state.narration_script_display:
@@ -234,169 +297,221 @@ if not st.session_state.story_treatment_scenes: st.info("Use the sidebar to gene
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:
358
- img_data_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)
401
 
402
  # --- Footer ---
 
5
  from core.prompt_engineering import (
6
  create_cinematic_treatment_prompt,
7
  construct_dalle_prompt,
8
+ construct_text_to_video_prompt, # Import new function
9
  create_narration_script_prompt_enhanced,
10
  create_scene_regeneration_prompt,
11
  create_visual_regeneration_prompt
 
18
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
19
  logger = logging.getLogger(__name__)
20
 
 
21
  # --- Global Definitions for New Features ---
22
  SHOT_TYPES_OPTIONS = [
23
  "Director's Choice", "Establishing Shot", "Long Shot", "Full Shot",
 
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
30
  DEFAULT_SHOT_TYPE = "Director's Choice"
31
+ ASSET_TYPE_OPTIONS = ["Auto (Director's Choice)", "Image", "Video Clip"] # For user selection
32
 
33
 
34
  # --- Global State Variables & API Key Setup ---
 
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
+ st.session_state.RUNWAY_API_KEY = load_api_key("RUNWAY_API_KEY", "RUNWAY_API_KEY", "RunwayML") # Load Runway Key
56
 
57
  if not st.session_state.GEMINI_API_KEY:
58
  st.error("CRITICAL: Gemini API Key is essential and missing!"); logger.critical("Gemini API Key missing. Halting."); st.stop()
 
63
  except Exception as e: st.error(f"Failed to init GeminiHandler: {e}"); logger.critical(f"GeminiHandler init failed: {e}", exc_info=True); st.stop()
64
 
65
  try:
66
+ default_voice_id = "Rachel"
67
  configured_voice_id = st.session_state.ELEVENLABS_VOICE_ID_CONFIG or default_voice_id
68
  st.session_state.visual_engine = VisualEngine(
69
  output_dir="temp_cinegen_media",
70
+ default_elevenlabs_voice_id=configured_voice_id
71
  )
72
  st.session_state.visual_engine.set_openai_api_key(st.session_state.OPENAI_API_KEY)
73
  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)
74
  st.session_state.visual_engine.set_pexels_api_key(st.session_state.PEXELS_API_KEY)
75
+ st.session_state.visual_engine.set_runway_api_key(st.session_state.RUNWAY_API_KEY) # Set Runway Key
76
  logger.info("VisualEngine initialized and API keys set (or attempted).")
77
  except Exception as e:
78
  st.error(f"Failed to init VisualEngine or set its API keys: {e}"); logger.critical(f"VisualEngine init/key setting failed: {e}", exc_info=True)
 
80
  st.session_state.services_initialized = True; logger.info("Service initialization sequence complete.")
81
 
82
  # Initialize other session state variables
83
+ # <<< MODIFIED START >>> : Renamed generated_visual_paths to generated_scene_assets
84
  for key, default_val in [
85
+ ('story_treatment_scenes', []), ('scene_prompts', []), ('generated_scene_assets', []), # Stores dicts: {'path':..., 'type':...}
86
  ('video_path', None), ('character_definitions', {}), ('global_style_additions', ""),
87
  ('overall_narration_audio_path', None), ('narration_script_display', "")
88
  ]:
89
  if key not in st.session_state: st.session_state[key] = default_val
90
 
91
  def initialize_new_project():
92
+ st.session_state.story_treatment_scenes = []
93
+ st.session_state.scene_prompts = [] # Stores DALL-E or Text-to-Video prompts
94
+ st.session_state.generated_scene_assets = [] # Stores dicts {'path': ..., 'type': ..., 'error': ...}
95
  st.session_state.video_path, st.session_state.overall_narration_audio_path, st.session_state.narration_script_display = None, None, ""
96
  logger.info("New project initialized.")
97
+ # <<< MODIFIED END >>>
98
+
99
+ # <<< MODIFIED START >>> : Updated function to use generate_scene_asset
100
+ def generate_asset_for_scene_core(scene_index, scene_data, version=1, user_selected_asset_type="Auto (Director's Choice)"):
101
+ """
102
+ Generates a visual asset (image or video clip) for a scene.
103
+ Returns True on success, False on failure.
104
+ """
105
+ # Determine asset type: user override > Gemini suggestion > default to image
106
+ final_asset_type_decision = "image" # Default
107
+ gemini_suggested_type = scene_data.get('suggested_asset_type_감독', 'image').lower()
108
+
109
+ if user_selected_asset_type == "Image":
110
+ final_asset_type_decision = "image"
111
+ elif user_selected_asset_type == "Video Clip":
112
+ final_asset_type_decision = "video_clip"
113
+ elif user_selected_asset_type == "Auto (Director's Choice)":
114
+ final_asset_type_decision = gemini_suggested_type if gemini_suggested_type == "video_clip" else "image"
115
+
116
+ generate_as_video = (final_asset_type_decision == "video_clip")
117
+ prompt_text_for_visual = ""
118
+
119
+ if generate_as_video:
120
+ # Construct prompt for text-to-video (e.g., RunwayML)
121
+ prompt_text_for_visual = construct_text_to_video_prompt(scene_data, st.session_state.character_definitions, st.session_state.global_style_additions)
122
+ # Note: seed_image_path could be an enhancement if DALL-E image is generated first
123
+ else:
124
+ # Construct prompt for DALL-E (image)
125
+ prompt_text_for_visual = construct_dalle_prompt(scene_data, st.session_state.character_definitions, st.session_state.global_style_additions)
126
+
127
+ if not prompt_text_for_visual:
128
+ logger.error(f"Visual prompt construction failed for scene {scene_data.get('scene_number', scene_index+1)} (Type: {final_asset_type_decision})")
129
+ return False
130
+
131
+ # Ensure session state lists are long enough
132
+ while len(st.session_state.scene_prompts) <= scene_index: st.session_state.scene_prompts.append("")
133
+ while len(st.session_state.generated_scene_assets) <= scene_index: st.session_state.generated_scene_assets.append(None)
134
+
135
+ st.session_state.scene_prompts[scene_index] = prompt_text_for_visual
136
+
137
+ # Filename base (extension will be added by visual_engine)
138
+ filename_base = f"scene_{scene_data.get('scene_number', scene_index+1)}_asset_v{version}"
139
+ runway_duration = scene_data.get('video_clip_duration_estimate_secs_감독', DEFAULT_SCENE_DURATION_SECS)
140
+ if runway_duration <= 0 : runway_duration = DEFAULT_SCENE_DURATION_SECS # Ensure positive duration
141
+
142
+ asset_result = st.session_state.visual_engine.generate_scene_asset(
143
+ image_prompt_text=prompt_text_for_visual, # This is generic, used for DALL-E or T2V
144
+ scene_data=scene_data,
145
+ scene_identifier_filename_base=filename_base,
146
+ generate_as_video_clip=generate_as_video,
147
+ runway_target_duration=runway_duration
148
+ # input_image_for_runway=None # TODO: Could be an enhancement
149
+ )
150
+
151
+ st.session_state.generated_scene_assets[scene_index] = asset_result # Store the whole dict
152
+
153
+ if asset_result and not asset_result['error'] and asset_result.get('path') and os.path.exists(asset_result['path']):
154
+ logger.info(f"Asset ({asset_result.get('type')}) generated for Scene {scene_data.get('scene_number', scene_index+1)}: {os.path.basename(asset_result['path'])}")
155
+ return True
156
  else:
157
+ err_msg = asset_result.get('error_message', 'Unknown error') if asset_result else 'Asset result is None'
158
+ logger.warning(f"Asset generation FAILED for Scene {scene_data.get('scene_number', scene_index+1)}. Type attempted: {final_asset_type_decision}. Path was: {asset_result.get('path') if asset_result else 'N/A'}. Error: {err_msg}")
159
+ # Store a failure state
160
+ st.session_state.generated_scene_assets[scene_index] = {'path': None, 'type': 'none', 'error': True, 'error_message': err_msg, 'prompt_used': prompt_text_for_visual}
161
+ return False
162
+ # <<< MODIFIED END >>>
163
 
164
  # --- UI Sidebar ---
165
  with st.sidebar:
 
168
  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")
169
  genre = st.selectbox("Primary Genre:", ["Cyberpunk", "Sci-Fi", "Fantasy", "Noir", "Thriller", "Western", "Post-Apocalyptic", "Historical Drama", "Surreal"], index=6, key="genre_main_v5")
170
  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")
171
+ num_scenes = st.slider("Number of Key Scenes:", 1, 10, 2, key="num_scenes_main_v5")
172
  creative_guidance_options = {"Standard Director": "standard", "Artistic Visionary": "more_artistic", "Experimental Storyteller": "experimental_narrative"}
173
  selected_creative_guidance_key = st.selectbox("AI Creative Director Style:", options=list(creative_guidance_options.keys()), key="creative_guidance_select_v5")
174
  actual_creative_guidance = creative_guidance_options[selected_creative_guidance_key]
 
180
  with st.status("AI Director is envisioning your masterpiece...", expanded=True) as status:
181
  try:
182
  status.write("Phase 1: Gemini crafting cinematic treatment... πŸ“œ"); logger.info("Phase 1: Cinematic Treatment Gen.")
 
 
183
  treatment_prompt = create_cinematic_treatment_prompt(user_idea, genre, mood, num_scenes, actual_creative_guidance)
184
+ treatment_result_json_raw = st.session_state.gemini_handler.generate_story_breakdown(treatment_prompt) # Expect list of dicts
185
+ if not isinstance(treatment_result_json_raw, list) or not treatment_result_json_raw: raise ValueError("Gemini returned invalid scene list format.")
186
 
 
 
187
  processed_scenes = []
188
  for scene_data_from_gemini in treatment_result_json_raw:
189
+ scene_data_from_gemini['user_shot_type'] = scene_data_from_gemini.get('PROACTIVE_camera_work_감독', DEFAULT_SHOT_TYPE) # Default from Gemini's suggestion
190
+ scene_data_from_gemini['user_scene_duration_secs'] = scene_data_from_gemini.get('video_clip_duration_estimate_secs_감독', DEFAULT_SCENE_DURATION_SECS)
191
+ if scene_data_from_gemini['user_scene_duration_secs'] <=0: scene_data_from_gemini['user_scene_duration_secs'] = DEFAULT_SCENE_DURATION_SECS
192
+ scene_data_from_gemini['user_selected_asset_type'] = "Auto (Director's Choice)" # Default for UI
193
  processed_scenes.append(scene_data_from_gemini)
194
  st.session_state.story_treatment_scenes = processed_scenes
 
195
 
196
  num_gen_scenes = len(st.session_state.story_treatment_scenes)
197
+ # <<< MODIFIED START >>>
198
+ st.session_state.scene_prompts = [""]*num_gen_scenes
199
+ st.session_state.generated_scene_assets = [None]*num_gen_scenes # Initialize list for asset dicts
200
+ # <<< MODIFIED END >>>
201
  logger.info(f"Phase 1 complete. {num_gen_scenes} scenes."); status.update(label="Treatment complete! βœ… Generating visuals...", state="running")
202
 
203
+ status.write("Phase 2: Creating visual assets (Image/Video)... πŸ–ΌοΈπŸŽ¬"); logger.info("Phase 2: Visual Asset Gen.")
204
  visual_successes = 0
205
  for i, sc_data in enumerate(st.session_state.story_treatment_scenes):
206
  sc_num_log = sc_data.get('scene_number', i+1)
207
+ status.write(f" Asset for Scene {sc_num_log}..."); logger.info(f" Processing asset for Scene {sc_num_log}.")
208
+ # <<< MODIFIED START >>> : Calling new function
209
+ if generate_asset_for_scene_core(i, sc_data, version=1): # Default to 'Auto' asset type for initial gen
210
+ visual_successes += 1
211
+ # <<< MODIFIED END >>>
212
 
213
+ current_status_label_ph2 = "Visual assets ready! "
214
  next_step_state = "running"
215
  if visual_successes == 0 and num_gen_scenes > 0:
216
+ logger.error("Visual asset gen failed for all scenes."); current_status_label_ph2 = "Asset gen FAILED for all scenes."; next_step_state="error";
217
  status.update(label=current_status_label_ph2, state=next_step_state, expanded=True); st.stop()
218
  elif visual_successes < num_gen_scenes:
219
+ logger.warning(f"Assets partially generated ({visual_successes}/{num_gen_scenes})."); current_status_label_ph2 = f"Assets partially generated ({visual_successes}/{num_gen_scenes}). "
220
  status.update(label=f"{current_status_label_ph2}Generating narration script...", state=next_step_state)
221
  if next_step_state == "error": st.stop()
222
 
223
  status.write("Phase 3: Generating narration script..."); logger.info("Phase 3: Narration Script Gen.")
224
  voice_style_for_prompt = st.session_state.get("selected_voice_style_for_generation", "cinematic_trailer")
225
  narr_prompt = create_narration_script_prompt_enhanced(st.session_state.story_treatment_scenes, mood, genre, voice_style_for_prompt)
226
+ st.session_state.narration_script_display = st.session_state.gemini_handler.generate_image_prompt(narr_prompt) # This generates a string
227
  logger.info("Narration script generated."); status.update(label="Narration script ready! Synthesizing voice...", state="running")
228
 
229
  status.write("Phase 4: Synthesizing voice (ElevenLabs)... πŸ”Š"); logger.info("Phase 4: Voice Synthesis.")
 
241
  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);
242
 
243
  st.markdown("---"); st.markdown("### Fine-Tuning Options")
244
+ # ... (Character, Global Style, Voice expanders - no changes needed here for this fix) ...
245
  with st.expander("Define Characters", expanded=False):
246
  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...")
247
  if st.button("Save Character", key="add_char_adv_ultra_v5"):
 
287
  st.success(f"Narrator Voice ID set to: {final_voice_id_to_use}. Script Style: {sel_prompt_v_style_key}")
288
  logger.info(f"User updated ElevenLabs Voice ID to: {final_voice_id_to_use}, Script Style: {sel_prompt_v_style_key}")
289
 
290
+
291
  # --- Main Content Area ---
292
  st.header("🎬 Cinematic Storyboard & Treatment")
293
  if st.session_state.narration_script_display:
 
297
  else:
298
  for i_main, scene_content_display in enumerate(st.session_state.story_treatment_scenes):
299
  scene_n = scene_content_display.get('scene_number', i_main + 1); scene_t = scene_content_display.get('scene_title', 'Untitled')
300
+ key_base = f"s{scene_n}_{''.join(filter(str.isalnum, scene_t[:10]))}_v5_{i_main}"
301
  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']}")
302
  st.subheader(f"SCENE {scene_n}: {scene_t.upper()}"); col_d, col_v = st.columns([0.45, 0.55])
 
 
 
303
 
304
+ with col_d: # Treatment and Controls Column
305
+ with st.expander("πŸ“ Scene Treatment & Controls", expanded=True):
306
+ # Display scene textual details (emotional_beat, setting, etc.)
307
+ st.markdown(f"**Beat:** {scene_content_display.get('emotional_beat', 'N/A')}")
308
+ st.markdown(f"**Setting:** {scene_content_display.get('setting_description', 'N/A')}")
309
+ st.markdown(f"**Chars:** {', '.join(scene_content_display.get('characters_involved', ['N/A']))}")
310
+ st.markdown(f"**Focus Moment:** _{scene_content_display.get('character_focus_moment', 'N/A')}_")
311
+ st.markdown(f"**Plot Beat:** {scene_content_display.get('key_plot_beat', 'N/A')}")
312
+ st.markdown(f"**Dialogue Hook:** `\"{scene_content_display.get('suggested_dialogue_hook', '...')}\"`")
313
+ st.markdown("---")
314
+ st.markdown(f"**Dir. Visual Style:** _{scene_content_display.get('PROACTIVE_visual_style_감독', 'N/A')}_")
315
+ st.markdown(f"**Dir. Camera:** _{scene_content_display.get('PROACTIVE_camera_work_감독', 'N/A')}_")
316
+ st.markdown(f"**Dir. Sound:** _{scene_content_display.get('PROACTIVE_sound_design_감독', 'N/A')}_")
317
  st.markdown("---")
318
+ st.markdown("##### Shot, Pacing & Asset Controls")
319
+
320
+ # User Shot Type (Camera Angle)
321
+ current_shot_type = st.session_state.story_treatment_scenes[i_main].get('user_shot_type', DEFAULT_SHOT_TYPE)
322
+ try: shot_type_index = SHOT_TYPES_OPTIONS.index(current_shot_type)
323
+ except ValueError: shot_type_index = SHOT_TYPES_OPTIONS.index(DEFAULT_SHOT_TYPE)
324
+ new_shot_type = st.selectbox("Dominant Shot Type:", options=SHOT_TYPES_OPTIONS, index=shot_type_index, key=f"shot_type_widget_{key_base}")
325
+ if new_shot_type != current_shot_type:
326
+ st.session_state.story_treatment_scenes[i_main]['user_shot_type'] = new_shot_type
327
+ # Consider if a re-run is needed or if DALL-E prompt should be updated based on this
328
+
329
+ # User Scene Duration
330
+ current_duration = st.session_state.story_treatment_scenes[i_main].get('user_scene_duration_secs', DEFAULT_SCENE_DURATION_SECS)
331
+ new_duration = st.number_input("Scene Duration (seconds):", min_value=1, max_value=300, value=current_duration, step=1, key=f"duration_widget_{key_base}")
332
+ if new_duration != current_duration:
333
+ st.session_state.story_treatment_scenes[i_main]['user_scene_duration_secs'] = new_duration
334
+
335
+ # <<< MODIFIED START >>> : User Asset Type Selection
336
+ current_user_asset_type = st.session_state.story_treatment_scenes[i_main].get('user_selected_asset_type', "Auto (Director's Choice)")
337
+ try: asset_type_idx = ASSET_TYPE_OPTIONS.index(current_user_asset_type)
338
+ except ValueError: asset_type_idx = 0 # Default to Auto
339
+ new_user_asset_type = st.selectbox("Asset Type Override:", ASSET_TYPE_OPTIONS, index=asset_type_idx, key=f"asset_type_sel_{key_base}",
340
+ help="Choose 'Image' or 'Video Clip'. 'Auto' uses Gemini's suggestion.")
341
+ if new_user_asset_type != current_user_asset_type:
342
+ st.session_state.story_treatment_scenes[i_main]['user_selected_asset_type'] = new_user_asset_type
343
+ # This change will be picked up by regeneration buttons
344
+ # <<< MODIFIED END >>>
345
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
346
  st.markdown("---")
347
+ # Display generated prompt for the asset
348
+ current_prompt_for_asset = st.session_state.scene_prompts[i_main] if i_main < len(st.session_state.scene_prompts) else None
349
+ if current_prompt_for_asset:
350
+ with st.popover("πŸ‘οΈ View Asset Generation Prompt"):
351
+ st.markdown(f"**Prompt used for current asset:**"); st.code(current_prompt_for_asset, language='text')
352
 
 
 
 
353
  pexels_q = scene_content_display.get('pexels_search_query_감독', None)
354
  if pexels_q: st.caption(f"Pexels Fallback Query: `{pexels_q}`")
355
 
356
+ with col_v: # Visuals Column
357
+ # <<< MODIFIED START >>> : Display logic for different asset types
358
+ current_asset_data = st.session_state.generated_scene_assets[i_main] if i_main < len(st.session_state.generated_scene_assets) else None
359
+ if current_asset_data and not current_asset_data.get('error') and current_asset_data.get('path') and os.path.exists(current_asset_data['path']):
360
+ asset_path = current_asset_data['path']
361
+ asset_type = current_asset_data.get('type', 'image') # Default to image if type missing
362
+ if asset_type == 'image':
363
+ st.image(asset_path, caption=f"Scene {scene_n} ({asset_type}): {scene_t}")
364
+ elif asset_type == 'video':
365
+ try:
366
+ with open(asset_path, 'rb') as vf: video_bytes = vf.read()
367
+ st.video(video_bytes, format="video/mp4", start_time=0)
368
+ st.caption(f"Scene {scene_n} ({asset_type}): {scene_t}")
369
+ except Exception as e_vid:
370
+ st.error(f"Error displaying video {asset_path}: {e_vid}")
371
+ logger.error(f"Error displaying video {asset_path}: {e_vid}", exc_info=True)
372
+ else:
373
+ st.warning(f"Unknown asset type '{asset_type}' for Scene {scene_n}.")
374
+ else: # No asset, or error during generation
375
+ if st.session_state.story_treatment_scenes: # Check if treatment exists
376
+ error_msg = current_asset_data.get('error_message', 'Visual pending or failed.') if current_asset_data else 'Visual pending or failed.'
377
+ st.caption(error_msg)
378
+ # <<< MODIFIED END >>>
379
 
380
  with st.popover(f"✏️ Edit Scene {scene_n} Treatment"):
381
  fb_script = st.text_area("Changes to treatment:", key=f"treat_fb_{key_base}", height=150)
382
  if st.button(f"πŸ”„ Update Scene {scene_n} Treatment", key=f"regen_treat_btn_{key_base}"):
383
  if fb_script:
384
+ with st.status(f"Updating Scene {scene_n} Treatment & Asset...", expanded=True) as s_treat_regen:
385
+ # Preserve user's shot type, duration, and asset type choices
386
+ user_shot_type = st.session_state.story_treatment_scenes[i_main]['user_shot_type']
387
+ user_duration = st.session_state.story_treatment_scenes[i_main]['user_scene_duration_secs']
388
+ user_asset_type_choice = st.session_state.story_treatment_scenes[i_main]['user_selected_asset_type']
389
+
390
  prompt_text = create_scene_regeneration_prompt(scene_content_display, fb_script, st.session_state.story_treatment_scenes)
391
  try:
392
+ updated_sc_data_from_gemini = st.session_state.gemini_handler.regenerate_scene_script_details(prompt_text)
393
+ # Merge, but prioritize user's UI choices for duration/shot/asset type
394
+ updated_sc_data = {**updated_sc_data_from_gemini} # Start with Gemini's new script
395
+ updated_sc_data['user_shot_type'] = user_shot_type
396
+ updated_sc_data['user_scene_duration_secs'] = user_duration
397
+ updated_sc_data['user_selected_asset_type'] = user_asset_type_choice
398
+ # Gemini might re-suggest asset type/duration, but user's direct settings take precedence for next gen
399
+ # We can log if Gemini's suggestion differs from user's explicit choice.
400
+ if updated_sc_data.get('suggested_asset_type_감독') != user_asset_type_choice and user_asset_type_choice != "Auto (Director's Choice)":
401
+ logger.info(f"Scene {scene_n}: User asset choice '{user_asset_type_choice}' overrides Gemini suggestion '{updated_sc_data.get('suggested_asset_type_감독')}'.")
402
 
 
 
 
 
403
 
404
  st.session_state.story_treatment_scenes[i_main] = updated_sc_data
405
+ s_treat_regen.update(label="Treatment updated! Regenerating asset...", state="running")
406
 
 
407
  v_num = 1
408
+ if current_asset_data and current_asset_data.get('path') and os.path.exists(current_asset_data['path']):
409
+ try: b,_=os.path.splitext(os.path.basename(current_asset_data['path'])); v_num = int(b.split('_v')[-1])+1 if '_v' in b else 2
410
+ except: v_num = 2
411
  else: v_num = 1
412
+ # <<< MODIFIED START >>> : Call new function, pass user_selected_asset_type
413
+ if generate_asset_for_scene_core(i_main, updated_sc_data, version=v_num, user_selected_asset_type=user_asset_type_choice):
414
+ s_treat_regen.update(label="Treatment & Asset Updated! πŸŽ‰", state="complete", expanded=False)
415
+ else: s_treat_regen.update(label="Treatment updated, asset failed.", state="complete", expanded=False)
416
+ # <<< MODIFIED END >>>
417
  st.rerun()
418
  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)
419
  else: st.warning("Please provide feedback.")
420
 
421
  with st.popover(f"🎨 Edit Scene {scene_n} Visual Prompt"):
422
+ prompt_to_edit = st.session_state.scene_prompts[i_main] if i_main < len(st.session_state.scene_prompts) else "No prompt generated yet."
423
+ st.caption("Current Asset Generation Prompt:"); st.code(prompt_to_edit, language='text')
424
+ fb_visual = st.text_area("Changes for asset generation prompt:", key=f"visual_fb_{key_base}", height=150)
425
+ if st.button(f"πŸ”„ Update Scene {scene_n} Asset", key=f"regen_visual_btn_{key_base}"):
426
  if fb_visual:
427
+ with st.status(f"Refining prompt & asset for Scene {scene_n}...", expanded=True) as s_visual_regen:
428
+ user_asset_type_choice = st.session_state.story_treatment_scenes[i_main]['user_selected_asset_type']
429
+ is_video_prompt = (user_asset_type_choice == "Video Clip") or \
430
+ (user_asset_type_choice == "Auto (Director's Choice)" and \
431
+ scene_content_display.get('suggested_asset_type_감독') == 'video_clip')
432
+
433
+ # Note: Visual regeneration prompt is primarily for DALL-E (images).
434
+ # For video, we might need a different refinement strategy or just regenerate with the same prompt construction.
435
+ # For simplicity here, if it's a video, we'll regenerate the prompt using standard construction.
436
+ # If it's an image, we use Gemini to refine the DALL-E prompt.
437
+ new_asset_gen_prompt = ""
438
+ if not is_video_prompt : # Refining an image prompt
439
+ ref_req_prompt_for_gemini = create_visual_regeneration_prompt(prompt_to_edit, fb_visual, scene_content_display,
440
+ st.session_state.character_definitions, st.session_state.global_style_additions)
441
+ try:
442
+ new_asset_gen_prompt = st.session_state.gemini_handler.refine_image_prompt_from_feedback(ref_req_prompt_for_gemini)
443
+ st.session_state.scene_prompts[i_main] = new_asset_gen_prompt
444
+ s_visual_regen.update(label="Image prompt refined by Gemini! Regenerating asset...", state="running")
445
+ except Exception as e_gemini_refine:
446
+ s_visual_regen.update(label=f"Error refining prompt: {e_gemini_refine}", state="error");
447
+ logger.error(f"Visual prompt refinement error: {e_gemini_refine}", exc_info=True)
448
+ continue # Skip asset generation if prompt refinement failed
449
+ else: # For video, or auto choosing video, reconstruct the prompt
450
+ new_asset_gen_prompt = construct_text_to_video_prompt(scene_content_display, st.session_state.character_definitions, st.session_state.global_style_additions)
451
+ st.session_state.scene_prompts[i_main] = new_asset_gen_prompt
452
+ s_visual_regen.update(label="Video prompt reconstructed! Regenerating asset...", state="running")
453
+
454
+
455
+ v_num = 1
456
+ if current_asset_data and current_asset_data.get('path') and os.path.exists(current_asset_data['path']):
457
+ try: b,_=os.path.splitext(os.path.basename(current_asset_data['path'])); v_num = int(b.split('_v')[-1])+1 if '_v' in b else 2
458
+ except: v_num=2
459
+ else: v_num = 1
460
+
461
+ # <<< MODIFIED START >>> : Call new function
462
+ # Pass the current scene_content_display as its prompt might have changed.
463
+ # User asset type choice from the scene data for consistency
464
+ if generate_asset_for_scene_core(i_main, st.session_state.story_treatment_scenes[i_main], version=v_num, user_selected_asset_type=user_asset_type_choice):
465
+ s_visual_regen.update(label="Asset Updated! πŸŽ‰", state="complete", expanded=False)
466
+ else: s_visual_regen.update(label="Prompt updated, asset regeneration failed.", state="complete", expanded=False)
467
+ # <<< MODIFIED END >>>
468
+ st.rerun()
469
  else: st.warning("Please provide feedback.")
470
  st.markdown("---")
471
 
472
+ # Video Assembly Button
473
+ # <<< MODIFIED START >>> : Check generated_scene_assets and use its data
474
+ if st.session_state.story_treatment_scenes and any(asset_info and not asset_info.get('error') and asset_info.get('path') for asset_info in st.session_state.generated_scene_assets if asset_info is not None):
475
  if st.button("🎬 Assemble Narrated Cinematic Animatic", key="assemble_ultra_video_btn_v5", type="primary", use_container_width=True):
476
  with st.status("Assembling Ultra Animatic...", expanded=True) as status_vid:
477
+ assets_for_video_assembly = []
478
  for i_v, sc_c in enumerate(st.session_state.story_treatment_scenes):
479
+ asset_info = st.session_state.generated_scene_assets[i_v] if i_v < len(st.session_state.generated_scene_assets) else None
480
+ if asset_info and not asset_info.get('error') and asset_info.get('path') and os.path.exists(asset_info['path']):
481
+ assets_for_video_assembly.append({
482
+ 'path': asset_info['path'],
483
+ 'type': asset_info.get('type', 'image'), # Default to image if type missing
 
484
  'scene_num': sc_c.get('scene_number', i_v + 1),
485
  'key_action': sc_c.get('key_plot_beat', ''),
486
+ 'duration': sc_c.get('user_scene_duration_secs', DEFAULT_SCENE_DURATION_SECS) # Use user-set duration
487
  })
488
+ status_vid.write(f"Adding Scene {sc_c.get('scene_number', i_v + 1)} ({asset_info.get('type')}).")
 
489
  else:
490
+ logger.warning(f"Skipping Scene {sc_c.get('scene_number', i_v+1)} for video: No valid asset.")
 
491
 
492
+ if assets_for_video_assembly:
493
  status_vid.write("Calling video engine...");
494
+ st.session_state.video_path = st.session_state.visual_engine.assemble_animatic_from_assets( # Changed method name
495
+ asset_data_list=assets_for_video_assembly, # Pass the list of asset dicts
 
 
 
496
  overall_narration_path=st.session_state.overall_narration_audio_path,
497
  output_filename="cinegen_ultra_animatic.mp4",
498
+ fps=24
499
  )
500
+ if st.session_state.video_path and os.path.exists(st.session_state.video_path):
501
+ status_vid.update(label="Ultra animatic assembled! πŸŽ‰", state="complete", expanded=False); st.balloons()
502
+ else:
503
+ status_vid.update(label="Video assembly failed. Check logs.", state="error", expanded=False); logger.error("Video assembly returned None or file does not exist.")
504
+ else:
505
+ status_vid.update(label="No valid assets for video assembly.", state="error", expanded=False); logger.warning("No valid assets found for video assembly.")
506
+ elif st.session_state.story_treatment_scenes: st.info("Generate visual assets before assembling video.")
507
+ # <<< MODIFIED END >>>
508
 
509
  if st.session_state.video_path and os.path.exists(st.session_state.video_path):
510
  st.header("🎬 Generated Cinematic Animatic");
511
  try:
512
  with open(st.session_state.video_path, 'rb') as vf_obj: video_bytes = vf_obj.read()
513
  st.video(video_bytes, format="video/mp4")
514
+ st.download_button(label="Download Ultra Animatic", data=video_bytes, file_name=os.path.basename(st.session_state.video_path), mime="video/mp4", use_container_width=True, key="download_ultra_video_btn_v5" )
 
515
  except Exception as e: st.error(f"Error displaying video: {e}"); logger.error(f"Error displaying video: {e}", exc_info=True)
516
 
517
  # --- Footer ---