Spaces:
Sleeping
Sleeping
Update app.py
Browse files
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 |
-
|
|
|
|
|
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
|
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
|
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
|
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)
|
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()
|
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
|
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
|
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
|
|
|
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}. "
|
348 |
-
f"
|
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 |
-
|
369 |
-
|
370 |
-
|
371 |
-
|
372 |
-
|
373 |
-
|
374 |
-
|
375 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
376 |
|
377 |
if image_bytes:
|
378 |
try:
|
379 |
image = Image.open(BytesIO(image_bytes))
|
380 |
logger.info(f" β
[{task_id}] Image generated successfully.")
|
381 |
-
#
|
382 |
-
|
383 |
-
if
|
384 |
-
|
385 |
-
|
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 |
-
#
|
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 |
-
|
406 |
-
|
407 |
-
|
408 |
-
|
|
|
|
|
|
|
|
|
|
|
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="π¨")
|
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 |
-
|
477 |
-
|
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}"
|
491 |
st.subheader(f"Processing {timeline_label}: {divergence}")
|
492 |
logger.info(f"--- Processing {timeline_label} (Index: {timeline_index}) ---")
|
493 |
-
generation_errors[timeline_id] = []
|
494 |
|
495 |
-
temp_image_files = {}
|
496 |
-
temp_audio_files = {}
|
497 |
-
video_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}"
|
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
|
518 |
with st.spinner(f"[{task_id}] Generating image... π¨"):
|
519 |
-
|
|
|
|
|
|
|
520 |
if segment.timeline_visual_modifier:
|
521 |
-
combined_prompt += f"
|
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
|
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
|
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
|
589 |
-
image_clip_instance = None
|
590 |
-
composite_clip = None
|
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)
|
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
|
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
|
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:
|
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
|
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:
|
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:
|
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]
|
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}"
|
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
|
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 |
|