Commit ·
d4de697
1
Parent(s): 16ae8b2
Fix video playback: Add ffmpeg params (yuv420p/faststart), cleanup image composition, add imageio-ffmpeg
Browse files
modules/story_reels/services/story_creator.py
CHANGED
|
@@ -566,53 +566,54 @@ class StoryCreator:
|
|
| 566 |
duration = remaining
|
| 567 |
|
| 568 |
# Create image clip
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
#
|
| 575 |
-
|
| 576 |
-
#
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
|
| 588 |
-
def make_zoom_in(t, clip_duration=duration):
|
| 589 |
-
zoom = 1.0 + (ZOOM_FACTOR - 1.0) * (t / clip_duration)
|
| 590 |
-
return zoom
|
| 591 |
-
clip = clip.resize(lambda t: make_zoom_in(t))
|
| 592 |
|
| 593 |
-
#
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
|
|
|
|
|
|
|
|
|
|
| 597 |
|
| 598 |
-
#
|
| 599 |
-
|
| 600 |
-
if i >= 2 and duration > CROSSFADE_DURATION:
|
| 601 |
-
clip = clip.crossfadein(CROSSFADE_DURATION)
|
| 602 |
|
| 603 |
-
#
|
| 604 |
-
#
|
| 605 |
-
if
|
| 606 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 607 |
|
| 608 |
clips.append(clip)
|
| 609 |
total_video_duration += duration
|
| 610 |
|
| 611 |
-
# Concatenate
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
else:
|
| 615 |
-
video = clips[0]
|
| 616 |
|
| 617 |
# Final safety: match video length to audio exactly
|
| 618 |
if abs(video.duration - audio_duration) > 0.1:
|
|
@@ -677,7 +678,7 @@ class StoryCreator:
|
|
| 677 |
video = CompositeVideoClip([video] + caption_clips)
|
| 678 |
logger.info(f"Added {len(caption_clips)} caption clips")
|
| 679 |
|
| 680 |
-
# Write final video
|
| 681 |
logger.info(f"Writing video with effects: crossfade={CROSSFADE_DURATION}s, zoom={ZOOM_FACTOR}x")
|
| 682 |
video.write_videofile(
|
| 683 |
str(output_path),
|
|
@@ -685,7 +686,13 @@ class StoryCreator:
|
|
| 685 |
codec='libx264',
|
| 686 |
audio_codec='aac',
|
| 687 |
threads=4,
|
| 688 |
-
preset='medium'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 689 |
)
|
| 690 |
|
| 691 |
# Cleanup
|
|
|
|
| 566 |
duration = remaining
|
| 567 |
|
| 568 |
# Create image clip
|
| 569 |
+
# Create video clips from images
|
| 570 |
+
img_clip = ImageClip(str(image_path))
|
| 571 |
+
|
| 572 |
+
# Resize logic:
|
| 573 |
+
# 1. Resize to cover screen height (maintaining aspect ratio)
|
| 574 |
+
# 2. Center crop to target width
|
| 575 |
+
|
| 576 |
+
# Calculate resize factor to cover height
|
| 577 |
+
w, h = img_clip.size
|
| 578 |
+
ratio = TARGET_HEIGHT / h
|
| 579 |
+
new_width = int(w * ratio)
|
| 580 |
+
|
| 581 |
+
if new_width < TARGET_WIDTH:
|
| 582 |
+
# If width is still too small, resize by width
|
| 583 |
+
ratio = TARGET_WIDTH / w
|
| 584 |
+
# new_height = int(h * ratio)
|
| 585 |
+
clip = img_clip.resize(width=TARGET_WIDTH)
|
| 586 |
+
else:
|
| 587 |
+
clip = img_clip.resize(height=TARGET_HEIGHT)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 588 |
|
| 589 |
+
# Center Crop
|
| 590 |
+
clip = clip.crop(
|
| 591 |
+
x1=(clip.w - TARGET_WIDTH) // 2,
|
| 592 |
+
y1=0,
|
| 593 |
+
width=TARGET_WIDTH,
|
| 594 |
+
height=TARGET_HEIGHT
|
| 595 |
+
)
|
| 596 |
|
| 597 |
+
# Set duration
|
| 598 |
+
clip = clip.set_duration(duration)
|
|
|
|
|
|
|
| 599 |
|
| 600 |
+
# Simple Zoom Effect (Ken Burns)
|
| 601 |
+
# We apply a slight zoom (1.0 -> 1.05) to all clips for movement
|
| 602 |
+
if duration > 0:
|
| 603 |
+
clip = clip.resize(lambda t: 1 + 0.04 * t / duration)
|
| 604 |
+
# Re-crop to ensure no borders appear during zoom
|
| 605 |
+
clip = clip.crop(x_center=clip.w/2, y_center=clip.h/2, width=TARGET_WIDTH, height=TARGET_HEIGHT)
|
| 606 |
+
|
| 607 |
+
# Transitions
|
| 608 |
+
if i > 0:
|
| 609 |
+
clip = clip.crossfadein(crossfade_duration)
|
| 610 |
|
| 611 |
clips.append(clip)
|
| 612 |
total_video_duration += duration
|
| 613 |
|
| 614 |
+
# Concatenate clips
|
| 615 |
+
# method="compose" is safer for crossfades
|
| 616 |
+
video = concatenate_videoclips(clips, method="compose", padding=-crossfade_duration)
|
|
|
|
|
|
|
| 617 |
|
| 618 |
# Final safety: match video length to audio exactly
|
| 619 |
if abs(video.duration - audio_duration) > 0.1:
|
|
|
|
| 678 |
video = CompositeVideoClip([video] + caption_clips)
|
| 679 |
logger.info(f"Added {len(caption_clips)} caption clips")
|
| 680 |
|
| 681 |
+
# Write final video (with browser-compatible settings)
|
| 682 |
logger.info(f"Writing video with effects: crossfade={CROSSFADE_DURATION}s, zoom={ZOOM_FACTOR}x")
|
| 683 |
video.write_videofile(
|
| 684 |
str(output_path),
|
|
|
|
| 686 |
codec='libx264',
|
| 687 |
audio_codec='aac',
|
| 688 |
threads=4,
|
| 689 |
+
preset='medium',
|
| 690 |
+
ffmpeg_params=[
|
| 691 |
+
'-pix_fmt', 'yuv420p', # Browser compatible pixel format
|
| 692 |
+
'-movflags', '+faststart', # Enable streaming/progressive download
|
| 693 |
+
'-profile:v', 'baseline', # Maximum compatibility
|
| 694 |
+
'-level', '3.0' # Compatible with most devices
|
| 695 |
+
]
|
| 696 |
)
|
| 697 |
|
| 698 |
# Cleanup
|
requirements.txt
CHANGED
|
@@ -24,3 +24,4 @@ groq
|
|
| 24 |
# Utilities
|
| 25 |
python-multipart
|
| 26 |
huggingface_hub
|
|
|
|
|
|
| 24 |
# Utilities
|
| 25 |
python-multipart
|
| 26 |
huggingface_hub
|
| 27 |
+
imageio-ffmpeg>=0.4.9
|