mgbam commited on
Commit
6c3bf7e
Β·
verified Β·
1 Parent(s): b0ee9ad

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +86 -111
app.py CHANGED
@@ -99,14 +99,10 @@ try:
99
 
100
  # Model Handle for Audio Generation
101
  # Use the standard GenerativeModel initialization.
102
- # The necessary methods (like .connect) are part of this object.
103
  live_model = genai.GenerativeModel(AUDIO_MODEL_ID) # Use GenerativeModel here
104
  logger.info(f"Initialized GenerativeModel handle for audio ({AUDIO_MODEL_ID}).")
105
- # We no longer use or need 'client_live' or explicit endpoint setting here.
106
- # The audio config is handled within the generate_audio_live_async function.
107
 
108
  except AttributeError as ae:
109
- # Keep this specific error catch just in case library structure is very old/unexpected
110
  logger.exception("AttributeError during Google AI Client Initialization.")
111
  st.error(f"🚨 Failed to initialize Google AI Clients due to an unexpected library structure error: {ae}. Please ensure 'google-generativeai' is up-to-date.", icon="🚨")
112
  st.stop()
@@ -119,7 +115,9 @@ except Exception as e:
119
  # --- Define Pydantic Schemas for Robust Validation ---
120
  class StorySegment(BaseModel):
121
  scene_id: int = Field(..., ge=0, description="Scene number within the timeline, starting from 0.")
122
- image_prompt: str = Field(..., min_length=10, max_length=150, description="Concise visual description for image generation (15-35 words). Focus on non-human characters, setting, action, style.")
 
 
123
  audio_text: str = Field(..., min_length=5, max_length=150, description="Single sentence of narration/dialogue for the scene (max 30 words).")
124
  character_description: str = Field(..., max_length=100, description="Brief description of key non-human characters/objects in *this* scene's prompt for consistency.")
125
  timeline_visual_modifier: Optional[str] = Field(None, max_length=50, description="Optional subtle visual style hint (e.g., 'slightly darker', 'more vibrant colors').")
@@ -142,6 +140,7 @@ class ChronoWeaveResponse(BaseModel):
142
 
143
  @validator('timelines')
144
  def check_timeline_segment_count(cls, timelines, values):
 
145
  if 'total_scenes_per_timeline' in values:
146
  expected_scenes = values['total_scenes_per_timeline']
147
  for i, timeline in enumerate(timelines):
@@ -188,7 +187,7 @@ async def generate_audio_live_async(api_text: str, output_filename: str, voice:
188
  "audio_config": {
189
  "audio_encoding": "LINEAR16", # Required format for WAV output
190
  "sample_rate_hertz": AUDIO_SAMPLING_RATE,
191
- # "voice": voice if voice else "aura-asteria-en" # Optional: Specify voice if needed and available
192
  }
193
  }
194
 
@@ -206,18 +205,17 @@ async def generate_audio_live_async(api_text: str, output_filename: str, voice:
206
  async for response in session.stream_content():
207
  if response.audio_chunk and response.audio_chunk.data:
208
  collected_audio.extend(response.audio_chunk.data)
209
- # Handle potential errors within the stream if the API provides them
210
  if hasattr(response, 'error') and response.error:
211
  logger.error(f" ❌ [{task_id}] Error during audio stream: {response.error}")
212
  st.error(f"Audio stream error for scene {task_id}: {response.error}", icon="πŸ”Š")
213
- return None # Stop processing this audio request
214
 
215
  if not collected_audio:
216
  logger.warning(f"⚠️ [{task_id}] No audio data received for: '{api_text[:60]}...'")
217
  st.warning(f"No audio data generated for scene {task_id}.", icon="πŸ”Š")
218
  return None
219
 
220
- # Write the collected audio bytes into a WAV file using the context manager.
221
  with wave_file_writer(output_filename, rate=AUDIO_SAMPLING_RATE) as wf:
222
  wf.writeframes(bytes(collected_audio))
223
  logger.info(f" βœ… [{task_id}] Audio saved: {os.path.basename(output_filename)} ({len(collected_audio)} bytes)")
@@ -228,7 +226,6 @@ async def generate_audio_live_async(api_text: str, output_filename: str, voice:
228
  st.error(f"Audio generation blocked for scene {task_id} due to safety settings.", icon="πŸ”‡")
229
  return None
230
  except Exception as e:
231
- # Catch other potential errors during connect/send/stream
232
  logger.exception(f" ❌ [{task_id}] Audio generation failed unexpectedly for '{api_text[:60]}...': {e}")
233
  st.error(f"Audio generation failed for scene {task_id}: {e}", icon="πŸ”Š")
234
  return None
@@ -253,6 +250,7 @@ def generate_story_sequence_chrono(
253
  f"Clearly state the divergence reason for each timeline (except potentially the first)."
254
  )
255
 
 
256
  prompt = f"""
257
  Act as an expert narrative designer specializing in short, visual, branching stories for children.
258
  Create a story based on the core theme: "{theme}".
@@ -264,7 +262,7 @@ def generate_story_sequence_chrono(
264
  4. {divergence_instruction}
265
  5. Maintain a consistent visual style across all scenes and timelines: **'Simple, friendly kids animation style with bright colors and rounded shapes'**, unless a `timeline_visual_modifier` subtly alters it.
266
  6. Each scene's narration (`audio_text`) should be a single, concise sentence (approx. 5-10 seconds spoken length, max 30 words).
267
- 7. Image prompts (`image_prompt`) should be descriptive (15-35 words), focusing on the non-human character(s), setting, action, and visual style. Explicitly mention the main character(s) for consistency.
268
  8. `character_description` should briefly describe recurring non-human characters mentioned *in the specific scene's image prompt* (name, key visual features). Keep consistent within a timeline.
269
 
270
  **Output Format:**
@@ -274,7 +272,7 @@ def generate_story_sequence_chrono(
274
  ```json
275
  {json.dumps(ChronoWeaveResponse.schema(), indent=2)}
276
  ```
277
- """ # Using .schema() which is the Pydantic v1 way, adjust if using v2 (.model_json_schema())
278
 
279
  try:
280
  # Use the standard client (GenerativeModel instance) for text generation
@@ -282,16 +280,12 @@ def generate_story_sequence_chrono(
282
  contents=prompt,
283
  generation_config=genai.types.GenerationConfig(
284
  response_mime_type="application/json",
285
- temperature=0.7 # Add some creativity
286
  )
287
  )
288
 
289
- # Debugging: Log raw response
290
- # logger.debug(f"Raw Gemini Response Text:\n{response.text}")
291
-
292
  # Attempt to parse the JSON
293
  try:
294
- # Use response.text which should contain the JSON string
295
  raw_data = json.loads(response.text)
296
  except json.JSONDecodeError as json_err:
297
  logger.error(f"Failed to decode JSON response: {json_err}")
@@ -302,14 +296,11 @@ def generate_story_sequence_chrono(
302
  except Exception as e:
303
  logger.error(f"Error accessing or decoding response text: {e}")
304
  st.error(f"🚨 Error processing AI response: {e}", icon="πŸ“„")
305
- # Log the response object itself if possible
306
- # logger.debug(f"Response object: {response}")
307
  return None
308
 
309
-
310
  # Validate the parsed data using Pydantic
311
  try:
312
- # Use parse_obj for Pydantic v1, or YourModel.model_validate(raw_data) for v2
313
  validated_data = ChronoWeaveResponse.parse_obj(raw_data)
314
  logger.info("βœ… Story structure generated and validated successfully!")
315
  st.success("βœ… Story structure generated and validated!")
@@ -318,7 +309,7 @@ def generate_story_sequence_chrono(
318
  logger.error(f"JSON structure validation failed: {val_err}")
319
  logger.error(f"Received Data:\n{json.dumps(raw_data, indent=2)}")
320
  st.error(f"🚨 The generated story structure is invalid: {val_err}", icon="🧬")
321
- st.json(raw_data) # Show the invalid structure
322
  return None
323
 
324
  except genai.types.generation_types.BlockedPromptException as bpe:
@@ -328,8 +319,6 @@ def generate_story_sequence_chrono(
328
  except Exception as e:
329
  logger.exception("Error during story sequence generation:")
330
  st.error(f"🚨 An unexpected error occurred during story generation: {e}", icon="πŸ’₯")
331
- # Optional: Show the prompt that failed (be mindful of length/PII)
332
- # st.text_area("Failed Prompt (excerpt):", prompt[:500]+"...", height=150)
333
  return None
334
 
335
 
@@ -340,76 +329,80 @@ def generate_image_imagen(prompt: str, aspect_ratio: str = "1:1", task_id: str =
340
  """
341
  logger.info(f"πŸ–ΌοΈ [{task_id}] Requesting image for: '{prompt[:70]}...' (Aspect: {aspect_ratio})")
342
 
343
- # Refined prompt incorporating negative constraints and style guidance
 
344
  full_prompt = (
345
  f"Generate an image in a child-friendly, simple animation style with bright colors and rounded shapes. "
346
  f"Ensure absolutely NO humans or human-like figures are present. Focus on animals or objects. "
347
- f"Aspect ratio should be {aspect_ratio}. " # Explicitly state aspect ratio in prompt too
348
- f"Prompt: {prompt}"
349
  )
350
 
351
  try:
352
- # Use the standard client's generate_content method.
353
  response = client_standard.generate_content(
354
  full_prompt,
355
  generation_config=genai.types.GenerationConfig(
356
  candidate_count=1,
357
- # Add other config like temperature if desired
358
  ),
359
- # Safety settings can be adjusted here if necessary and permitted
360
- # safety_settings={'HARM_CATEGORY_DANGEROUS_CONTENT': 'BLOCK_NONE'} # Use cautiously
361
  )
362
 
363
- # Check for valid response and image data
364
- # Accessing image data might depend slightly on the exact API response structure
365
- # common pattern is response.candidates[0].content.parts[0].inline_data.data
366
- # or directly response.parts if simpler structure
367
  image_bytes = None
368
- if response.candidates and response.candidates[0].content and response.candidates[0].content.parts:
369
- part = response.candidates[0].content.parts[0]
370
- if hasattr(part, 'inline_data') and part.inline_data and hasattr(part.inline_data,'data'):
371
- image_bytes = part.inline_data.data
372
- elif hasattr(part, 'file_data') and part.file_data: # Handle potential file URIs if API changes
373
- logger.warning(f" ⚠️ [{task_id}] Received file URI instead of inline data. Handling not implemented.")
374
- # Potentially download from part.file_data.file_uri here
375
- return None # Or implement download
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
376
 
377
  if image_bytes:
378
  try:
379
  image = Image.open(BytesIO(image_bytes))
380
  logger.info(f" βœ… [{task_id}] Image generated successfully.")
381
- # Check safety feedback even on success
382
- safety_ratings = getattr(response.candidates[0], 'safety_ratings', [])
383
- if safety_ratings:
384
- filtered_ratings = [f"{r.category.name}: {r.probability.name}" for r in safety_ratings if r.probability.name != 'NEGLIGIBLE']
385
- if filtered_ratings:
386
- logger.warning(f" ⚠️ [{task_id}] Image generated but flagged by safety filters: {', '.join(filtered_ratings)}.")
387
- st.warning(f"Image for scene {task_id} flagged by safety filters: {', '.join(filtered_ratings)}", icon="⚠️")
388
-
389
  return image
390
  except Exception as img_err:
391
  logger.error(f" ❌ [{task_id}] Failed to decode generated image data: {img_err}")
392
  st.warning(f"Failed to decode image data for scene {task_id}.", icon="πŸ–ΌοΈ")
393
  return None
394
  else:
395
- # Check for blocking or other issues
396
- block_reason = None
397
- prompt_feedback = getattr(response, 'prompt_feedback', None)
398
- if prompt_feedback:
399
- block_reason = getattr(prompt_feedback, 'block_reason', None)
400
-
401
  if block_reason:
402
  logger.warning(f" ⚠️ [{task_id}] Image generation blocked. Reason: {block_reason}. Prompt: '{prompt[:70]}...'")
403
  st.warning(f"Image generation blocked for scene {task_id}. Reason: {block_reason}", icon="🚫")
404
  else:
405
- logger.warning(f" ⚠️ [{task_id}] No image data received, unknown reason. Prompt: '{prompt[:70]}...'")
406
- st.warning(f"No image data received for scene {task_id}, reason unclear.", icon="πŸ–ΌοΈ")
407
- # Log the full response for debugging
408
- # logger.debug(f"Full Imagen response object: {response}")
 
 
 
 
 
409
  return None
410
 
411
  except genai.types.generation_types.BlockedPromptException as bpe:
412
- # This might be caught by the block_reason check above, but good to have explicit catch
413
  logger.error(f" ❌ [{task_id}] Image generation blocked (exception): {bpe}")
414
  st.error(f"Image generation blocked for scene {task_id} due to safety settings.", icon="🚫")
415
  return None
@@ -426,7 +419,7 @@ st.sidebar.header("βš™οΈ Configuration")
426
  if GOOGLE_API_KEY:
427
  st.sidebar.success("Google API Key Loaded", icon="βœ…")
428
  else:
429
- st.sidebar.error("Google API Key Missing!", icon="🚨") # Should not be reached if st.stop() works
430
 
431
  # Story Parameters
432
  theme = st.sidebar.text_input("πŸ“– Story Theme:", "A curious squirrel finds a mysterious, glowing acorn")
@@ -437,9 +430,6 @@ divergence_prompt = st.sidebar.text_input("↔️ Divergence Hint (Optional):",
437
  # Generation Settings
438
  st.sidebar.subheader("🎨 Visual & Audio Settings")
439
  aspect_ratio = st.sidebar.selectbox("πŸ–ΌοΈ Image Aspect Ratio:", ["1:1", "16:9", "9:16"], index=0, help="Aspect ratio for generated images.")
440
- # Add audio voice selection if API supports it and voices are known
441
- # available_voices = ["aura-asteria-en", "aura-luna-en", "aura-stella-en"] # Example
442
- # audio_voice = st.sidebar.selectbox("πŸ—£οΈ Narration Voice:", available_voices, index=0)
443
  audio_voice = None # Placeholder
444
 
445
  generate_button = st.sidebar.button("✨ Generate ChronoWeave ✨", type="primary", disabled=(not GOOGLE_API_KEY), use_container_width=True)
@@ -473,35 +463,33 @@ if generate_button:
473
  chrono_response = generate_story_sequence_chrono(theme, num_scenes, num_timelines, divergence_prompt)
474
 
475
  if chrono_response:
476
- st.success(f"Narrative structure received for {len(chrono_response.timelines)} timelines.")
477
- logger.info(f"Successfully generated structure for {len(chrono_response.timelines)} timelines.")
478
 
479
  # --- 2. Process Each Timeline ---
480
  overall_start_time = time.time()
481
  all_timelines_successful = True # Assume success initially
482
 
483
- # Use st.status for collapsible progress updates
484
  with st.status("Generating assets and composing videos...", expanded=True) as status:
485
 
486
  for timeline_index, timeline in enumerate(chrono_response.timelines):
487
  timeline_id = timeline.timeline_id
488
  divergence = timeline.divergence_reason
489
  segments = timeline.segments
490
- timeline_label = f"Timeline {timeline_id}" # Consistent label
491
  st.subheader(f"Processing {timeline_label}: {divergence}")
492
  logger.info(f"--- Processing {timeline_label} (Index: {timeline_index}) ---")
493
- generation_errors[timeline_id] = [] # Initialize error list
494
 
495
- temp_image_files = {} # {scene_id: path}
496
- temp_audio_files = {} # {scene_id: path}
497
- video_clips = [] # List of moviepy clips
498
  timeline_start_time = time.time()
499
  scene_success_count = 0
500
 
501
-
502
  for scene_index, segment in enumerate(segments):
503
  scene_id = segment.scene_id
504
- task_id = f"T{timeline_id}_S{scene_id}" # Unique ID
505
  status_message = f"Processing {timeline_label}, Scene {scene_id + 1}/{len(segments)}..."
506
  status.update(label=status_message)
507
  st.markdown(f"--- **Scene {scene_id + 1} ({task_id})** ---")
@@ -509,16 +497,18 @@ if generate_button:
509
 
510
  scene_has_error = False
511
 
512
- # Log scene details
513
  st.write(f" *Image Prompt:* {segment.image_prompt}" + (f" *(Modifier: {segment.timeline_visual_modifier})*" if segment.timeline_visual_modifier else ""))
514
  st.write(f" *Audio Text:* {segment.audio_text}")
515
 
516
  # --- 2a. Image Generation ---
517
- generated_image: Optional[Image.Image] = None # Define before spinner
518
  with st.spinner(f"[{task_id}] Generating image... 🎨"):
519
- combined_prompt = f"{segment.image_prompt}. {segment.character_description}"
 
 
 
520
  if segment.timeline_visual_modifier:
521
- combined_prompt += f" Visual style hint: {segment.timeline_visual_modifier}."
522
  generated_image = generate_image_imagen(combined_prompt, aspect_ratio, task_id)
523
 
524
  if generated_image:
@@ -536,7 +526,7 @@ if generate_button:
536
  st.warning(f"Image generation failed for scene {task_id}. Skipping scene.", icon="πŸ–ΌοΈ")
537
  scene_has_error = True
538
  generation_errors[timeline_id].append(f"Scene {scene_id+1}: Image generation failed.")
539
- continue # Skip audio/video for this scene
540
 
541
  # --- 2b. Audio Generation ---
542
  generated_audio_path: Optional[str] = None
@@ -544,7 +534,6 @@ if generate_button:
544
  with st.spinner(f"[{task_id}] Generating audio... πŸ”Š"):
545
  audio_path_temp = os.path.join(temp_dir, f"{task_id}_audio.wav")
546
  try:
547
- # Run the async function using asyncio.run()
548
  generated_audio_path = asyncio.run(
549
  generate_audio_live_async(segment.audio_text, audio_path_temp, audio_voice)
550
  )
@@ -570,7 +559,6 @@ if generate_button:
570
  st.warning(f"Audio generation failed for {task_id}. Skipping video clip.", icon="πŸ”Š")
571
  scene_has_error = True
572
  generation_errors[timeline_id].append(f"Scene {scene_id+1}: Audio generation failed.")
573
- # Clean up image if audio fails
574
  if scene_id in temp_image_files and os.path.exists(temp_image_files[scene_id]):
575
  try:
576
  os.remove(temp_image_files[scene_id])
@@ -578,16 +566,16 @@ if generate_button:
578
  del temp_image_files[scene_id]
579
  except OSError as e:
580
  logger.warning(f" ⚠️ [{task_id}] Could not remove image file {temp_image_files[scene_id]} after audio failure: {e}")
581
- continue # Skip video clip creation
582
 
583
  # --- 2c. Create Video Clip ---
584
  if not scene_has_error and scene_id in temp_image_files and scene_id in temp_audio_files:
585
  st.write(f" 🎬 Creating video clip for Scene {scene_id+1}...")
586
  img_path = temp_image_files[scene_id]
587
  aud_path = temp_audio_files[scene_id]
588
- audio_clip_instance = None # Define before try
589
- image_clip_instance = None # Define before try
590
- composite_clip = None # Define before try
591
  try:
592
  if not os.path.exists(img_path): raise FileNotFoundError(f"Image file not found: {img_path}")
593
  if not os.path.exists(aud_path): raise FileNotFoundError(f"Audio file not found: {aud_path}")
@@ -597,21 +585,18 @@ if generate_button:
597
  image_clip_instance = ImageClip(np_image).set_duration(audio_clip_instance.duration)
598
 
599
  composite_clip = image_clip_instance.set_audio(audio_clip_instance)
600
- video_clips.append(composite_clip) # Add the clip to be concatenated later
601
  logger.info(f" βœ… [{task_id}] Video clip created (Duration: {audio_clip_instance.duration:.2f}s).")
602
  st.write(f" βœ… Clip created (Duration: {audio_clip_instance.duration:.2f}s).")
603
  scene_success_count += 1
604
- # Don't close individual clips here yet, needed for concatenation
605
 
606
  except Exception as e:
607
  logger.exception(f" ❌ [{task_id}] Failed to create video clip for scene {scene_id+1}: {e}")
608
  st.error(f"Failed to create video clip for {task_id}: {e}", icon="🎬")
609
  scene_has_error = True
610
  generation_errors[timeline_id].append(f"Scene {scene_id+1}: Video clip creation failed.")
611
- # Cleanup resources if clip creation failed for *this* scene
612
  if audio_clip_instance: audio_clip_instance.close()
613
  if image_clip_instance: image_clip_instance.close()
614
- # Attempt cleanup of related files
615
  if os.path.exists(img_path): os.remove(img_path)
616
  if os.path.exists(aud_path): os.remove(aud_path)
617
 
@@ -619,23 +604,20 @@ if generate_button:
619
 
620
  # --- 2d. Assemble Timeline Video ---
621
  timeline_duration = time.time() - timeline_start_time
622
- # Only assemble if clips were created and no *fatal* errors occurred during scene processing
623
- # (We check scene_success_count against expected number)
624
  if video_clips and scene_success_count == len(segments):
625
  status.update(label=f"Composing final video for {timeline_label}...")
626
  st.write(f"🎞️ Assembling final video for {timeline_label}...")
627
  logger.info(f"🎞️ Assembling final video for {timeline_label} ({len(video_clips)} clips)...")
628
  output_filename = os.path.join(temp_dir, f"timeline_{timeline_id}_final.mp4")
629
- final_timeline_video = None # Define before try block
630
  try:
631
- # Concatenate the collected clips
632
  final_timeline_video = concatenate_videoclips(video_clips, method="compose")
633
  final_timeline_video.write_videofile(
634
  output_filename,
635
  fps=VIDEO_FPS,
636
  codec=VIDEO_CODEC,
637
  audio_codec=AUDIO_CODEC,
638
- logger=None # Suppress moviepy console spam
639
  )
640
  final_video_paths[timeline_id] = output_filename
641
  logger.info(f" βœ… [{timeline_label}] Final video saved: {os.path.basename(output_filename)}")
@@ -647,11 +629,10 @@ if generate_button:
647
  all_timelines_successful = False
648
  generation_errors[timeline_id].append(f"Timeline {timeline_id}: Final video assembly failed.")
649
  finally:
650
- # Now close all individual clips and the final concatenated clip
651
  logger.debug(f"[{timeline_label}] Closing {len(video_clips)} source clips...")
652
  for i, clip in enumerate(video_clips):
653
  try:
654
- if clip: # Check if clip object exists
655
  if clip.audio: clip.audio.close()
656
  clip.close()
657
  except Exception as e_close:
@@ -668,29 +649,27 @@ if generate_button:
668
  logger.warning(f"[{timeline_label}] No video clips successfully generated. Skipping final assembly.")
669
  st.warning(f"No scenes were successfully processed for {timeline_label}. Video cannot be created.", icon="🚫")
670
  all_timelines_successful = False
671
- else: # Some scenes failed, so scene_success_count < len(segments)
672
  error_count = len(segments) - scene_success_count
673
  logger.warning(f"[{timeline_label}] Encountered errors in {error_count} scene(s). Skipping final video assembly.")
674
  st.warning(f"{timeline_label} had errors in {error_count} scene(s). Final video not assembled.", icon="⚠️")
675
  all_timelines_successful = False
676
 
677
- # Log accumulated errors for the timeline if any occurred
678
  if generation_errors[timeline_id]:
679
  logger.error(f"Summary of errors in {timeline_label}: {generation_errors[timeline_id]}")
680
 
681
  # --- End of Timelines Loop ---
682
 
683
- # Final status update
684
  overall_duration = time.time() - overall_start_time
685
  if all_timelines_successful and final_video_paths:
686
  status_msg = f"ChronoWeave Generation Complete! ({len(final_video_paths)} videos in {overall_duration:.2f}s)"
687
  status.update(label=status_msg, state="complete", expanded=False)
688
  logger.info(status_msg)
689
- elif final_video_paths: # Some videos made, but errors occurred
690
  status_msg = f"ChronoWeave Partially Complete ({len(final_video_paths)} videos, some errors occurred). Total time: {overall_duration:.2f}s"
691
  status.update(label=status_msg, state="warning", expanded=True)
692
  logger.warning(status_msg)
693
- else: # No videos made
694
  status_msg = f"ChronoWeave Generation Failed. No videos produced. Total time: {overall_duration:.2f}s"
695
  status.update(label=status_msg, state="error", expanded=True)
696
  logger.error(status_msg)
@@ -699,12 +678,11 @@ if generate_button:
699
  st.header("🎬 Generated Timelines")
700
  if final_video_paths:
701
  sorted_timeline_ids = sorted(final_video_paths.keys())
702
- # Adjust column count based on number of videos, max 3-4 wide?
703
  num_cols = min(len(sorted_timeline_ids), 3)
704
  cols = st.columns(num_cols)
705
 
706
  for idx, timeline_id in enumerate(sorted_timeline_ids):
707
- col = cols[idx % num_cols] # Cycle through columns
708
  video_path = final_video_paths[timeline_id]
709
  timeline_data = next((t for t in chrono_response.timelines if t.timeline_id == timeline_id), None)
710
  reason = timeline_data.divergence_reason if timeline_data else "Unknown Divergence"
@@ -722,9 +700,8 @@ if generate_button:
722
  data=video_bytes,
723
  file_name=f"chronoweave_timeline_{timeline_id}.mp4",
724
  mime="video/mp4",
725
- key=f"download_btn_{timeline_id}" # Unique key for download button
726
  )
727
- # Display errors for this timeline if any occurred
728
  if generation_errors.get(timeline_id):
729
  with st.expander(f"⚠️ View {len(generation_errors[timeline_id])} Generation Issues"):
730
  for error_msg in generation_errors[timeline_id]:
@@ -738,7 +715,6 @@ if generate_button:
738
  st.error(f"Error displaying video for Timeline {timeline_id}: {e}", icon="🚨")
739
  else:
740
  st.warning("No final videos were successfully generated in this run.")
741
- # Display summary of all errors if no videos were made
742
  all_errors = [msg for err_list in generation_errors.values() for msg in err_list]
743
  if all_errors:
744
  st.subheader("Summary of Generation Issues")
@@ -760,10 +736,9 @@ if generate_button:
760
  st.warning(f"Could not automatically remove temporary files: {temp_dir}. Please remove it manually if needed.", icon="⚠️")
761
 
762
  elif not chrono_response:
763
- # Error message likely already shown by generate_story_sequence_chrono
764
- logger.error("Story generation failed, cannot proceed.")
765
  else:
766
- # Fallback for unexpected state
767
  st.error("An unexpected issue occurred after story generation. Cannot proceed.", icon="πŸ›‘")
768
  logger.error("Chrono_response existed but was falsy in the main logic block.")
769
 
 
99
 
100
  # Model Handle for Audio Generation
101
  # Use the standard GenerativeModel initialization.
 
102
  live_model = genai.GenerativeModel(AUDIO_MODEL_ID) # Use GenerativeModel here
103
  logger.info(f"Initialized GenerativeModel handle for audio ({AUDIO_MODEL_ID}).")
 
 
104
 
105
  except AttributeError as ae:
 
106
  logger.exception("AttributeError during Google AI Client Initialization.")
107
  st.error(f"🚨 Failed to initialize Google AI Clients due to an unexpected library structure error: {ae}. Please ensure 'google-generativeai' is up-to-date.", icon="🚨")
108
  st.stop()
 
115
  # --- Define Pydantic Schemas for Robust Validation ---
116
  class StorySegment(BaseModel):
117
  scene_id: int = Field(..., ge=0, description="Scene number within the timeline, starting from 0.")
118
+ # Increased max_length for image_prompt
119
+ image_prompt: str = Field(..., min_length=10, max_length=250, # <-- Increased from 150
120
+ description="Concise visual description for image generation (target 15-35 words). Focus on non-human characters, setting, action, style.")
121
  audio_text: str = Field(..., min_length=5, max_length=150, description="Single sentence of narration/dialogue for the scene (max 30 words).")
122
  character_description: str = Field(..., max_length=100, description="Brief description of key non-human characters/objects in *this* scene's prompt for consistency.")
123
  timeline_visual_modifier: Optional[str] = Field(None, max_length=50, description="Optional subtle visual style hint (e.g., 'slightly darker', 'more vibrant colors').")
 
140
 
141
  @validator('timelines')
142
  def check_timeline_segment_count(cls, timelines, values):
143
+ # Pydantic v1 style validation. For v2, use model_validator(mode='before') or similar
144
  if 'total_scenes_per_timeline' in values:
145
  expected_scenes = values['total_scenes_per_timeline']
146
  for i, timeline in enumerate(timelines):
 
187
  "audio_config": {
188
  "audio_encoding": "LINEAR16", # Required format for WAV output
189
  "sample_rate_hertz": AUDIO_SAMPLING_RATE,
190
+ # "voice": voice if voice else "aura-asteria-en" # Optional
191
  }
192
  }
193
 
 
205
  async for response in session.stream_content():
206
  if response.audio_chunk and response.audio_chunk.data:
207
  collected_audio.extend(response.audio_chunk.data)
 
208
  if hasattr(response, 'error') and response.error:
209
  logger.error(f" ❌ [{task_id}] Error during audio stream: {response.error}")
210
  st.error(f"Audio stream error for scene {task_id}: {response.error}", icon="πŸ”Š")
211
+ return None
212
 
213
  if not collected_audio:
214
  logger.warning(f"⚠️ [{task_id}] No audio data received for: '{api_text[:60]}...'")
215
  st.warning(f"No audio data generated for scene {task_id}.", icon="πŸ”Š")
216
  return None
217
 
218
+ # Write the collected audio bytes into a WAV file.
219
  with wave_file_writer(output_filename, rate=AUDIO_SAMPLING_RATE) as wf:
220
  wf.writeframes(bytes(collected_audio))
221
  logger.info(f" βœ… [{task_id}] Audio saved: {os.path.basename(output_filename)} ({len(collected_audio)} bytes)")
 
226
  st.error(f"Audio generation blocked for scene {task_id} due to safety settings.", icon="πŸ”‡")
227
  return None
228
  except Exception as e:
 
229
  logger.exception(f" ❌ [{task_id}] Audio generation failed unexpectedly for '{api_text[:60]}...': {e}")
230
  st.error(f"Audio generation failed for scene {task_id}: {e}", icon="πŸ”Š")
231
  return None
 
250
  f"Clearly state the divergence reason for each timeline (except potentially the first)."
251
  )
252
 
253
+ # Updated prompt with stricter image_prompt length guidance
254
  prompt = f"""
255
  Act as an expert narrative designer specializing in short, visual, branching stories for children.
256
  Create a story based on the core theme: "{theme}".
 
262
  4. {divergence_instruction}
263
  5. Maintain a consistent visual style across all scenes and timelines: **'Simple, friendly kids animation style with bright colors and rounded shapes'**, unless a `timeline_visual_modifier` subtly alters it.
264
  6. Each scene's narration (`audio_text`) should be a single, concise sentence (approx. 5-10 seconds spoken length, max 30 words).
265
+ 7. Image prompts (`image_prompt`) should be descriptive **and concise (target 15-35 words MAXIMUM)**, focusing only on the non-human character(s), setting, action, and essential visual style elements for *this specific scene*. Explicitly mention the main character(s) for consistency. **Do NOT repeat the general 'Simple, friendly kids animation style...' description in every image prompt unless it is essential for a specific visual change**; rely on the overall style instruction and the optional `timeline_visual_modifier`.
266
  8. `character_description` should briefly describe recurring non-human characters mentioned *in the specific scene's image prompt* (name, key visual features). Keep consistent within a timeline.
267
 
268
  **Output Format:**
 
272
  ```json
273
  {json.dumps(ChronoWeaveResponse.schema(), indent=2)}
274
  ```
275
+ """ # Using .schema() for Pydantic v1. Use .model_json_schema() for v2.
276
 
277
  try:
278
  # Use the standard client (GenerativeModel instance) for text generation
 
280
  contents=prompt,
281
  generation_config=genai.types.GenerationConfig(
282
  response_mime_type="application/json",
283
+ temperature=0.7
284
  )
285
  )
286
 
 
 
 
287
  # Attempt to parse the JSON
288
  try:
 
289
  raw_data = json.loads(response.text)
290
  except json.JSONDecodeError as json_err:
291
  logger.error(f"Failed to decode JSON response: {json_err}")
 
296
  except Exception as e:
297
  logger.error(f"Error accessing or decoding response text: {e}")
298
  st.error(f"🚨 Error processing AI response: {e}", icon="πŸ“„")
 
 
299
  return None
300
 
 
301
  # Validate the parsed data using Pydantic
302
  try:
303
+ # Use parse_obj for Pydantic v1, or model_validate(raw_data) for v2
304
  validated_data = ChronoWeaveResponse.parse_obj(raw_data)
305
  logger.info("βœ… Story structure generated and validated successfully!")
306
  st.success("βœ… Story structure generated and validated!")
 
309
  logger.error(f"JSON structure validation failed: {val_err}")
310
  logger.error(f"Received Data:\n{json.dumps(raw_data, indent=2)}")
311
  st.error(f"🚨 The generated story structure is invalid: {val_err}", icon="🧬")
312
+ st.json(raw_data) # Show the invalid structure that failed validation
313
  return None
314
 
315
  except genai.types.generation_types.BlockedPromptException as bpe:
 
319
  except Exception as e:
320
  logger.exception("Error during story sequence generation:")
321
  st.error(f"🚨 An unexpected error occurred during story generation: {e}", icon="πŸ’₯")
 
 
322
  return None
323
 
324
 
 
329
  """
330
  logger.info(f"πŸ–ΌοΈ [{task_id}] Requesting image for: '{prompt[:70]}...' (Aspect: {aspect_ratio})")
331
 
332
+ # Refined prompt - relies on the story generator to provide concise prompts now
333
+ # Still includes base style and negative constraints as reinforcement
334
  full_prompt = (
335
  f"Generate an image in a child-friendly, simple animation style with bright colors and rounded shapes. "
336
  f"Ensure absolutely NO humans or human-like figures are present. Focus on animals or objects. "
337
+ f"Aspect ratio should be {aspect_ratio}. "
338
+ f"Scene Description: {prompt}" # Use the potentially shorter prompt from story gen
339
  )
340
 
341
  try:
 
342
  response = client_standard.generate_content(
343
  full_prompt,
344
  generation_config=genai.types.GenerationConfig(
345
  candidate_count=1,
 
346
  ),
 
 
347
  )
348
 
 
 
 
 
349
  image_bytes = None
350
+ safety_ratings = []
351
+ block_reason = None
352
+
353
+ # Check response structure - adjust based on actual API behavior
354
+ if hasattr(response, 'candidates') and response.candidates:
355
+ candidate = response.candidates[0]
356
+ if hasattr(candidate, 'content') and candidate.content and hasattr(candidate.content, 'parts') and candidate.content.parts:
357
+ part = candidate.content.parts[0]
358
+ if hasattr(part, 'inline_data') and part.inline_data and hasattr(part.inline_data, 'data'):
359
+ image_bytes = part.inline_data.data
360
+ if hasattr(candidate, 'safety_ratings'):
361
+ safety_ratings = candidate.safety_ratings
362
+ # Finish reason might also indicate issues (e.g., SAFETY)
363
+ # if hasattr(candidate, 'finish_reason') and candidate.finish_reason != 'STOP': ...
364
+
365
+ # Check prompt feedback for blocking outside of candidates
366
+ if hasattr(response, 'prompt_feedback') and response.prompt_feedback:
367
+ if hasattr(response.prompt_feedback, 'block_reason') and response.prompt_feedback.block_reason != 'BLOCK_REASON_UNSPECIFIED':
368
+ block_reason = response.prompt_feedback.block_reason.name # Get the name of the enum
369
+ if hasattr(response.prompt_feedback, 'safety_ratings'):
370
+ # Combine prompt feedback ratings with candidate ratings if necessary
371
+ safety_ratings.extend(response.prompt_feedback.safety_ratings)
372
+
373
 
374
  if image_bytes:
375
  try:
376
  image = Image.open(BytesIO(image_bytes))
377
  logger.info(f" βœ… [{task_id}] Image generated successfully.")
378
+ # Log safety flags if present
379
+ filtered_ratings = [f"{r.category.name}: {r.probability.name}" for r in safety_ratings if hasattr(r,'probability') and r.probability.name != 'NEGLIGIBLE']
380
+ if filtered_ratings:
381
+ logger.warning(f" ⚠️ [{task_id}] Image generated but flagged by safety filters: {', '.join(filtered_ratings)}.")
382
+ st.warning(f"Image for scene {task_id} flagged by safety filters: {', '.join(filtered_ratings)}", icon="⚠️")
 
 
 
383
  return image
384
  except Exception as img_err:
385
  logger.error(f" ❌ [{task_id}] Failed to decode generated image data: {img_err}")
386
  st.warning(f"Failed to decode image data for scene {task_id}.", icon="πŸ–ΌοΈ")
387
  return None
388
  else:
389
+ # If no image bytes, determine why
 
 
 
 
 
390
  if block_reason:
391
  logger.warning(f" ⚠️ [{task_id}] Image generation blocked. Reason: {block_reason}. Prompt: '{prompt[:70]}...'")
392
  st.warning(f"Image generation blocked for scene {task_id}. Reason: {block_reason}", icon="🚫")
393
  else:
394
+ # Check for safety flags even if no block reason explicitly given
395
+ filtered_ratings = [f"{r.category.name}: {r.probability.name}" for r in safety_ratings if hasattr(r,'probability') and r.probability.name != 'NEGLIGIBLE']
396
+ if filtered_ratings:
397
+ logger.warning(f" ⚠️ [{task_id}] Image generation failed, safety filters triggered: {', '.join(filtered_ratings)}. Prompt: '{prompt[:70]}...'")
398
+ st.warning(f"Image generation failed for scene {task_id}, safety filters triggered: {', '.join(filtered_ratings)}", icon="⚠️")
399
+ else:
400
+ logger.warning(f" ⚠️ [{task_id}] No image data received, unknown reason. Prompt: '{prompt[:70]}...'")
401
+ st.warning(f"No image data received for scene {task_id}, reason unclear.", icon="πŸ–ΌοΈ")
402
+ # logger.debug(f"Full Imagen response object: {response}")
403
  return None
404
 
405
  except genai.types.generation_types.BlockedPromptException as bpe:
 
406
  logger.error(f" ❌ [{task_id}] Image generation blocked (exception): {bpe}")
407
  st.error(f"Image generation blocked for scene {task_id} due to safety settings.", icon="🚫")
408
  return None
 
419
  if GOOGLE_API_KEY:
420
  st.sidebar.success("Google API Key Loaded", icon="βœ…")
421
  else:
422
+ st.sidebar.error("Google API Key Missing!", icon="🚨")
423
 
424
  # Story Parameters
425
  theme = st.sidebar.text_input("πŸ“– Story Theme:", "A curious squirrel finds a mysterious, glowing acorn")
 
430
  # Generation Settings
431
  st.sidebar.subheader("🎨 Visual & Audio Settings")
432
  aspect_ratio = st.sidebar.selectbox("πŸ–ΌοΈ Image Aspect Ratio:", ["1:1", "16:9", "9:16"], index=0, help="Aspect ratio for generated images.")
 
 
 
433
  audio_voice = None # Placeholder
434
 
435
  generate_button = st.sidebar.button("✨ Generate ChronoWeave ✨", type="primary", disabled=(not GOOGLE_API_KEY), use_container_width=True)
 
463
  chrono_response = generate_story_sequence_chrono(theme, num_scenes, num_timelines, divergence_prompt)
464
 
465
  if chrono_response:
466
+ # Structure generated and validated successfully by the function
467
+ # st.success(...) is now inside generate_story_sequence_chrono on success
468
 
469
  # --- 2. Process Each Timeline ---
470
  overall_start_time = time.time()
471
  all_timelines_successful = True # Assume success initially
472
 
 
473
  with st.status("Generating assets and composing videos...", expanded=True) as status:
474
 
475
  for timeline_index, timeline in enumerate(chrono_response.timelines):
476
  timeline_id = timeline.timeline_id
477
  divergence = timeline.divergence_reason
478
  segments = timeline.segments
479
+ timeline_label = f"Timeline {timeline_id}"
480
  st.subheader(f"Processing {timeline_label}: {divergence}")
481
  logger.info(f"--- Processing {timeline_label} (Index: {timeline_index}) ---")
482
+ generation_errors[timeline_id] = []
483
 
484
+ temp_image_files = {}
485
+ temp_audio_files = {}
486
+ video_clips = []
487
  timeline_start_time = time.time()
488
  scene_success_count = 0
489
 
 
490
  for scene_index, segment in enumerate(segments):
491
  scene_id = segment.scene_id
492
+ task_id = f"T{timeline_id}_S{scene_id}"
493
  status_message = f"Processing {timeline_label}, Scene {scene_id + 1}/{len(segments)}..."
494
  status.update(label=status_message)
495
  st.markdown(f"--- **Scene {scene_id + 1} ({task_id})** ---")
 
497
 
498
  scene_has_error = False
499
 
 
500
  st.write(f" *Image Prompt:* {segment.image_prompt}" + (f" *(Modifier: {segment.timeline_visual_modifier})*" if segment.timeline_visual_modifier else ""))
501
  st.write(f" *Audio Text:* {segment.audio_text}")
502
 
503
  # --- 2a. Image Generation ---
504
+ generated_image: Optional[Image.Image] = None
505
  with st.spinner(f"[{task_id}] Generating image... 🎨"):
506
+ # Combine prompt using the (hopefully shorter) prompt from the structure
507
+ combined_prompt = segment.image_prompt # Use directly
508
+ if segment.character_description: # Add character desc if present
509
+ combined_prompt += f" Featuring: {segment.character_description}"
510
  if segment.timeline_visual_modifier:
511
+ combined_prompt += f" Style hint: {segment.timeline_visual_modifier}."
512
  generated_image = generate_image_imagen(combined_prompt, aspect_ratio, task_id)
513
 
514
  if generated_image:
 
526
  st.warning(f"Image generation failed for scene {task_id}. Skipping scene.", icon="πŸ–ΌοΈ")
527
  scene_has_error = True
528
  generation_errors[timeline_id].append(f"Scene {scene_id+1}: Image generation failed.")
529
+ continue
530
 
531
  # --- 2b. Audio Generation ---
532
  generated_audio_path: Optional[str] = None
 
534
  with st.spinner(f"[{task_id}] Generating audio... πŸ”Š"):
535
  audio_path_temp = os.path.join(temp_dir, f"{task_id}_audio.wav")
536
  try:
 
537
  generated_audio_path = asyncio.run(
538
  generate_audio_live_async(segment.audio_text, audio_path_temp, audio_voice)
539
  )
 
559
  st.warning(f"Audio generation failed for {task_id}. Skipping video clip.", icon="πŸ”Š")
560
  scene_has_error = True
561
  generation_errors[timeline_id].append(f"Scene {scene_id+1}: Audio generation failed.")
 
562
  if scene_id in temp_image_files and os.path.exists(temp_image_files[scene_id]):
563
  try:
564
  os.remove(temp_image_files[scene_id])
 
566
  del temp_image_files[scene_id]
567
  except OSError as e:
568
  logger.warning(f" ⚠️ [{task_id}] Could not remove image file {temp_image_files[scene_id]} after audio failure: {e}")
569
+ continue
570
 
571
  # --- 2c. Create Video Clip ---
572
  if not scene_has_error and scene_id in temp_image_files and scene_id in temp_audio_files:
573
  st.write(f" 🎬 Creating video clip for Scene {scene_id+1}...")
574
  img_path = temp_image_files[scene_id]
575
  aud_path = temp_audio_files[scene_id]
576
+ audio_clip_instance = None
577
+ image_clip_instance = None
578
+ composite_clip = None
579
  try:
580
  if not os.path.exists(img_path): raise FileNotFoundError(f"Image file not found: {img_path}")
581
  if not os.path.exists(aud_path): raise FileNotFoundError(f"Audio file not found: {aud_path}")
 
585
  image_clip_instance = ImageClip(np_image).set_duration(audio_clip_instance.duration)
586
 
587
  composite_clip = image_clip_instance.set_audio(audio_clip_instance)
588
+ video_clips.append(composite_clip)
589
  logger.info(f" βœ… [{task_id}] Video clip created (Duration: {audio_clip_instance.duration:.2f}s).")
590
  st.write(f" βœ… Clip created (Duration: {audio_clip_instance.duration:.2f}s).")
591
  scene_success_count += 1
 
592
 
593
  except Exception as e:
594
  logger.exception(f" ❌ [{task_id}] Failed to create video clip for scene {scene_id+1}: {e}")
595
  st.error(f"Failed to create video clip for {task_id}: {e}", icon="🎬")
596
  scene_has_error = True
597
  generation_errors[timeline_id].append(f"Scene {scene_id+1}: Video clip creation failed.")
 
598
  if audio_clip_instance: audio_clip_instance.close()
599
  if image_clip_instance: image_clip_instance.close()
 
600
  if os.path.exists(img_path): os.remove(img_path)
601
  if os.path.exists(aud_path): os.remove(aud_path)
602
 
 
604
 
605
  # --- 2d. Assemble Timeline Video ---
606
  timeline_duration = time.time() - timeline_start_time
 
 
607
  if video_clips and scene_success_count == len(segments):
608
  status.update(label=f"Composing final video for {timeline_label}...")
609
  st.write(f"🎞️ Assembling final video for {timeline_label}...")
610
  logger.info(f"🎞️ Assembling final video for {timeline_label} ({len(video_clips)} clips)...")
611
  output_filename = os.path.join(temp_dir, f"timeline_{timeline_id}_final.mp4")
612
+ final_timeline_video = None
613
  try:
 
614
  final_timeline_video = concatenate_videoclips(video_clips, method="compose")
615
  final_timeline_video.write_videofile(
616
  output_filename,
617
  fps=VIDEO_FPS,
618
  codec=VIDEO_CODEC,
619
  audio_codec=AUDIO_CODEC,
620
+ logger=None
621
  )
622
  final_video_paths[timeline_id] = output_filename
623
  logger.info(f" βœ… [{timeline_label}] Final video saved: {os.path.basename(output_filename)}")
 
629
  all_timelines_successful = False
630
  generation_errors[timeline_id].append(f"Timeline {timeline_id}: Final video assembly failed.")
631
  finally:
 
632
  logger.debug(f"[{timeline_label}] Closing {len(video_clips)} source clips...")
633
  for i, clip in enumerate(video_clips):
634
  try:
635
+ if clip:
636
  if clip.audio: clip.audio.close()
637
  clip.close()
638
  except Exception as e_close:
 
649
  logger.warning(f"[{timeline_label}] No video clips successfully generated. Skipping final assembly.")
650
  st.warning(f"No scenes were successfully processed for {timeline_label}. Video cannot be created.", icon="🚫")
651
  all_timelines_successful = False
652
+ else: # Some scenes failed
653
  error_count = len(segments) - scene_success_count
654
  logger.warning(f"[{timeline_label}] Encountered errors in {error_count} scene(s). Skipping final video assembly.")
655
  st.warning(f"{timeline_label} had errors in {error_count} scene(s). Final video not assembled.", icon="⚠️")
656
  all_timelines_successful = False
657
 
 
658
  if generation_errors[timeline_id]:
659
  logger.error(f"Summary of errors in {timeline_label}: {generation_errors[timeline_id]}")
660
 
661
  # --- End of Timelines Loop ---
662
 
 
663
  overall_duration = time.time() - overall_start_time
664
  if all_timelines_successful and final_video_paths:
665
  status_msg = f"ChronoWeave Generation Complete! ({len(final_video_paths)} videos in {overall_duration:.2f}s)"
666
  status.update(label=status_msg, state="complete", expanded=False)
667
  logger.info(status_msg)
668
+ elif final_video_paths:
669
  status_msg = f"ChronoWeave Partially Complete ({len(final_video_paths)} videos, some errors occurred). Total time: {overall_duration:.2f}s"
670
  status.update(label=status_msg, state="warning", expanded=True)
671
  logger.warning(status_msg)
672
+ else:
673
  status_msg = f"ChronoWeave Generation Failed. No videos produced. Total time: {overall_duration:.2f}s"
674
  status.update(label=status_msg, state="error", expanded=True)
675
  logger.error(status_msg)
 
678
  st.header("🎬 Generated Timelines")
679
  if final_video_paths:
680
  sorted_timeline_ids = sorted(final_video_paths.keys())
 
681
  num_cols = min(len(sorted_timeline_ids), 3)
682
  cols = st.columns(num_cols)
683
 
684
  for idx, timeline_id in enumerate(sorted_timeline_ids):
685
+ col = cols[idx % num_cols]
686
  video_path = final_video_paths[timeline_id]
687
  timeline_data = next((t for t in chrono_response.timelines if t.timeline_id == timeline_id), None)
688
  reason = timeline_data.divergence_reason if timeline_data else "Unknown Divergence"
 
700
  data=video_bytes,
701
  file_name=f"chronoweave_timeline_{timeline_id}.mp4",
702
  mime="video/mp4",
703
+ key=f"download_btn_{timeline_id}"
704
  )
 
705
  if generation_errors.get(timeline_id):
706
  with st.expander(f"⚠️ View {len(generation_errors[timeline_id])} Generation Issues"):
707
  for error_msg in generation_errors[timeline_id]:
 
715
  st.error(f"Error displaying video for Timeline {timeline_id}: {e}", icon="🚨")
716
  else:
717
  st.warning("No final videos were successfully generated in this run.")
 
718
  all_errors = [msg for err_list in generation_errors.values() for msg in err_list]
719
  if all_errors:
720
  st.subheader("Summary of Generation Issues")
 
736
  st.warning(f"Could not automatically remove temporary files: {temp_dir}. Please remove it manually if needed.", icon="⚠️")
737
 
738
  elif not chrono_response:
739
+ # Error message already shown by generate_story_sequence_chrono or validation
740
+ logger.error("Story generation or validation failed, cannot proceed.")
741
  else:
 
742
  st.error("An unexpected issue occurred after story generation. Cannot proceed.", icon="πŸ›‘")
743
  logger.error("Chrono_response existed but was falsy in the main logic block.")
744