ismdrobiul489 commited on
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
- clip = ImageClip(image_path).set_duration(duration)
570
-
571
- # Resize to portrait (1080x1920)
572
- clip = clip.resize(height=TARGET_HEIGHT)
573
-
574
- # Scene position-based effects
575
- # Hook (first 2 clips): Zoom OUT (start big, end normal) - grabs attention
576
- # Middle clips: Subtle zoom IN (Ken Burns)
577
- # Outro (last clip): Static with fade out
578
-
579
- if i < 2:
580
- # HOOK: Zoom OUT effect (1.1 → 1.0) - dynamic attention grabber
581
- def make_zoom_out(t, clip_duration=duration):
582
- zoom = 1.1 - (0.1 * (t / clip_duration)) # 1.1 to 1.0
583
- return zoom
584
- clip = clip.resize(lambda t: make_zoom_out(t))
585
-
586
- elif i < total_scenes - 1:
587
- # MIDDLE: Ken Burns zoom IN (1.0 → 1.05)
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
- # Last clip stays static (no zoom)
594
-
595
- # Center crop after zoom (to maintain 1080x1920)
596
- clip = clip.resize(width=TARGET_WIDTH, height=TARGET_HEIGHT)
 
 
 
597
 
598
- # Transitions: crossfade for smooth scene changes (NOT on first 2 clips)
599
- # Hook clips: NO crossfade, clean direct cut
600
- if i >= 2 and duration > CROSSFADE_DURATION:
601
- clip = clip.crossfadein(CROSSFADE_DURATION)
602
 
603
- # NO fade in for Hook (first 2 clips) - start immediately visible!
604
- # Only fade out at the very end
605
- if i == total_scenes - 1:
606
- clip = clip.fadeout(FADE_DURATION)
 
 
 
 
 
 
607
 
608
  clips.append(clip)
609
  total_video_duration += duration
610
 
611
- # Concatenate with crossfade transitions
612
- if len(clips) > 1:
613
- video = concatenate_videoclips(clips, method="compose", padding=-CROSSFADE_DURATION)
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