Commit ·
73b4be4
1
Parent(s): b7c85d5
fix pgmusic with ffmgep
Browse files- ffmpeg_utils.py +55 -17
- hybrid_processor.py +7 -17
- routers/video.py +1 -1
- schemas.py +6 -3
- video_processor.py +102 -23
ffmpeg_utils.py
CHANGED
|
@@ -124,7 +124,11 @@ def extract_clip_ffmpeg(
|
|
| 124 |
target_height: Optional[int] = None,
|
| 125 |
include_audio: bool = True,
|
| 126 |
style: Optional[ShortsStyle] = None,
|
| 127 |
-
aspect_ratio: Optional[AspectRatio] = None
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
) -> str:
|
| 129 |
"""
|
| 130 |
Extract video clip using FFmpeg - high speed with advanced filters
|
|
@@ -133,9 +137,19 @@ def extract_clip_ffmpeg(
|
|
| 133 |
duration = end_time - start_time
|
| 134 |
ffmpeg_exe = imageio_ffmpeg.get_ffmpeg_exe()
|
| 135 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
# Build FFmpeg command
|
| 137 |
-
|
| 138 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
|
| 140 |
# Resolution defaults for Shorts if not provided
|
| 141 |
if not target_width or not target_height:
|
|
@@ -143,13 +157,17 @@ def extract_clip_ffmpeg(
|
|
| 143 |
|
| 144 |
filter_complex = ""
|
| 145 |
|
| 146 |
-
if style == ShortsStyle.
|
|
|
|
|
|
|
|
|
|
| 147 |
# Cinematic Blur: BG (Blurred & Cropped) + FG (Scaled to fit width)
|
|
|
|
| 148 |
filter_complex = (
|
| 149 |
f"[0:v]split=2[bg_raw][fg_raw];"
|
| 150 |
-
f"[bg_raw]scale={target_width}:{target_height}:force_original_aspect_ratio=increase,crop={target_width}:{target_height},boxblur=20:
|
| 151 |
-
f"[fg_raw]scale={target_width}:-1[fg];"
|
| 152 |
-
f"[bg][fg]overlay=(W-w)/2:(H-h)/2[v]"
|
| 153 |
)
|
| 154 |
elif style == ShortsStyle.SPLIT_SCREEN:
|
| 155 |
# Split Screen: Top half and Bottom half
|
|
@@ -166,22 +184,42 @@ def extract_clip_ffmpeg(
|
|
| 166 |
elif style == ShortsStyle.FIT_BARS:
|
| 167 |
# Fit with black bars
|
| 168 |
filter_complex = f"scale={target_width}:{target_height}:force_original_aspect_ratio=decrease,pad={target_width}:{target_height}:(ow-iw)/2:(oh-ih)/2"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
else:
|
| 170 |
-
|
| 171 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 172 |
|
| 173 |
if filter_complex:
|
| 174 |
-
|
| 175 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 176 |
cmd.extend(['-filter_complex', filter_complex, '-map', '[v]'])
|
| 177 |
if include_audio:
|
| 178 |
-
# Always try to map audio if it exists
|
| 179 |
cmd.extend(['-map', '0:a?'])
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
if include_audio:
|
| 184 |
-
cmd.extend(['-map', '0:v:0', '-map', '0:a?'])
|
| 185 |
|
| 186 |
if not include_audio:
|
| 187 |
cmd.extend(['-an'])
|
|
|
|
| 124 |
target_height: Optional[int] = None,
|
| 125 |
include_audio: bool = True,
|
| 126 |
style: Optional[ShortsStyle] = None,
|
| 127 |
+
aspect_ratio: Optional[AspectRatio] = None,
|
| 128 |
+
bg_music_path: Optional[str] = None,
|
| 129 |
+
video_volume: float = 1.0,
|
| 130 |
+
music_volume: float = 0.2,
|
| 131 |
+
loop_music: bool = True
|
| 132 |
) -> str:
|
| 133 |
"""
|
| 134 |
Extract video clip using FFmpeg - high speed with advanced filters
|
|
|
|
| 137 |
duration = end_time - start_time
|
| 138 |
ffmpeg_exe = imageio_ffmpeg.get_ffmpeg_exe()
|
| 139 |
|
| 140 |
+
# Get video info to check for audio stream
|
| 141 |
+
video_info = get_video_info_ffmpeg(video_path)
|
| 142 |
+
has_orig_audio = video_info.get('has_audio', False) if video_info else False
|
| 143 |
+
|
| 144 |
# Build FFmpeg command
|
| 145 |
+
inputs = ['-i', video_path]
|
| 146 |
+
if bg_music_path and os.path.exists(bg_music_path):
|
| 147 |
+
if loop_music:
|
| 148 |
+
inputs.extend(['-stream_loop', '-1', '-i', bg_music_path])
|
| 149 |
+
else:
|
| 150 |
+
inputs.extend(['-i', bg_music_path])
|
| 151 |
+
|
| 152 |
+
cmd = [ffmpeg_exe] + inputs + ['-ss', str(start_time), '-t', str(duration)]
|
| 153 |
|
| 154 |
# Resolution defaults for Shorts if not provided
|
| 155 |
if not target_width or not target_height:
|
|
|
|
| 157 |
|
| 158 |
filter_complex = ""
|
| 159 |
|
| 160 |
+
if aspect_ratio == AspectRatio.ORIGINAL or style == ShortsStyle.ORIGINAL:
|
| 161 |
+
# No filters, just copy/passthrough
|
| 162 |
+
filter_complex = ""
|
| 163 |
+
elif style == ShortsStyle.CINEMATIC:
|
| 164 |
# Cinematic Blur: BG (Blurred & Cropped) + FG (Scaled to fit width)
|
| 165 |
+
# Use -2 to ensure height is divisible by 2 for yuv420p
|
| 166 |
filter_complex = (
|
| 167 |
f"[0:v]split=2[bg_raw][fg_raw];"
|
| 168 |
+
f"[bg_raw]scale={target_width}:{target_height}:force_original_aspect_ratio=increase,crop={target_width}:{target_height},boxblur=20:2[bg];"
|
| 169 |
+
f"[fg_raw]scale={target_width}:-2,setsar=1[fg];"
|
| 170 |
+
f"[bg][fg]overlay=(W-w)/2:(H-h)/2,setsar=1[v]"
|
| 171 |
)
|
| 172 |
elif style == ShortsStyle.SPLIT_SCREEN:
|
| 173 |
# Split Screen: Top half and Bottom half
|
|
|
|
| 184 |
elif style == ShortsStyle.FIT_BARS:
|
| 185 |
# Fit with black bars
|
| 186 |
filter_complex = f"scale={target_width}:{target_height}:force_original_aspect_ratio=decrease,pad={target_width}:{target_height}:(ow-iw)/2:(oh-ih)/2"
|
| 187 |
+
elif aspect_ratio != AspectRatio.ORIGINAL:
|
| 188 |
+
# Default for non-original ratios: Scale to fit width
|
| 189 |
+
# Use -2 to ensure dimensions are divisible by 2 for yuv420p
|
| 190 |
+
filter_complex = f"scale={target_width}:-2,pad={target_width}:{target_height}:(ow-iw)/2:(oh-ih)/2,setsar=1[v]"
|
| 191 |
else:
|
| 192 |
+
filter_complex = ""
|
| 193 |
+
|
| 194 |
+
# Audio Filter logic
|
| 195 |
+
audio_filter = ""
|
| 196 |
+
if include_audio:
|
| 197 |
+
if bg_music_path and os.path.exists(bg_music_path):
|
| 198 |
+
if has_orig_audio:
|
| 199 |
+
# Mix original audio with background music
|
| 200 |
+
# Use unique names [a_orig] [a_bg] to avoid collisions with [v]
|
| 201 |
+
audio_filter = f"[0:a]volume={video_volume}[a_orig];[1:a]volume={music_volume}[a_bg];[a_orig][a_bg]amix=inputs=2:duration=first[aout]"
|
| 202 |
+
else:
|
| 203 |
+
# Only background music (original has no audio)
|
| 204 |
+
audio_filter = f"[1:a]volume={music_volume}[aout]"
|
| 205 |
+
elif has_orig_audio and video_volume != 1.0:
|
| 206 |
+
audio_filter = f"[0:a]volume={video_volume}[aout]"
|
| 207 |
|
| 208 |
if filter_complex:
|
| 209 |
+
# If [v] is not the final tag, ensure it is
|
| 210 |
+
if "[v]" not in filter_complex:
|
| 211 |
+
filter_complex += "[v]"
|
| 212 |
+
|
| 213 |
+
if audio_filter:
|
| 214 |
+
combined_filter = f"{filter_complex};{audio_filter}"
|
| 215 |
+
cmd.extend(['-filter_complex', combined_filter, '-map', '[v]', '-map', '[aout]'])
|
| 216 |
+
else:
|
| 217 |
cmd.extend(['-filter_complex', filter_complex, '-map', '[v]'])
|
| 218 |
if include_audio:
|
|
|
|
| 219 |
cmd.extend(['-map', '0:a?'])
|
| 220 |
+
elif audio_filter:
|
| 221 |
+
# Only audio filter
|
| 222 |
+
cmd.extend(['-filter_complex', audio_filter, '-map', '0:v:0', '-map', '[aout]'])
|
|
|
|
|
|
|
| 223 |
|
| 224 |
if not include_audio:
|
| 225 |
cmd.extend(['-an'])
|
hybrid_processor.py
CHANGED
|
@@ -8,22 +8,8 @@ from typing import List, Optional, Tuple, Any
|
|
| 8 |
from ffmpeg_utils import extract_clip_ffmpeg, get_video_info_ffmpeg
|
| 9 |
|
| 10 |
def should_use_ffmpeg_processing(timestamps, custom_dims, export_audio, bg_music, output_format) -> bool:
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
# If background music is requested, need MoviePy for audio mixing
|
| 14 |
-
if bg_music:
|
| 15 |
-
return False
|
| 16 |
-
|
| 17 |
-
# We can now handle almost all ShortsStyle in FFmpeg!
|
| 18 |
-
# These styles are now optimized in ffmpeg_utils.py
|
| 19 |
-
if output_format and isinstance(output_format, ShortsStyle):
|
| 20 |
-
return True
|
| 21 |
-
|
| 22 |
-
# If a specific LayoutType is requested
|
| 23 |
-
if custom_dims and hasattr(custom_dims, 'layout_type'):
|
| 24 |
-
return True
|
| 25 |
-
|
| 26 |
-
# Default fallback
|
| 27 |
return True
|
| 28 |
|
| 29 |
def process_single_clip_hybrid(video_path: str, start_time: float, end_time: float, clip_id: str,
|
|
@@ -59,7 +45,11 @@ def process_single_clip_hybrid(video_path: str, start_time: float, end_time: flo
|
|
| 59 |
target_height=custom_dims.height if hasattr(custom_dims, 'height') else None,
|
| 60 |
include_audio=True, # Video should always have audio if it exists
|
| 61 |
style=style,
|
| 62 |
-
aspect_ratio=aspect_ratio
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
)
|
| 64 |
else:
|
| 65 |
# Use MoviePy for features (fallback)
|
|
|
|
| 8 |
from ffmpeg_utils import extract_clip_ffmpeg, get_video_info_ffmpeg
|
| 9 |
|
| 10 |
def should_use_ffmpeg_processing(timestamps, custom_dims, export_audio, bg_music, output_format) -> bool:
|
| 11 |
+
# FFmpeg can now handle background music mixing and all ShortsStyle!
|
| 12 |
+
# Default to True to use FFmpeg optimization for most cases
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
return True
|
| 14 |
|
| 15 |
def process_single_clip_hybrid(video_path: str, start_time: float, end_time: float, clip_id: str,
|
|
|
|
| 45 |
target_height=custom_dims.height if hasattr(custom_dims, 'height') else None,
|
| 46 |
include_audio=True, # Video should always have audio if it exists
|
| 47 |
style=style,
|
| 48 |
+
aspect_ratio=aspect_ratio,
|
| 49 |
+
bg_music_path=bg_music,
|
| 50 |
+
video_volume=custom_dims.video_volume if hasattr(custom_dims, 'video_volume') else 1.0,
|
| 51 |
+
music_volume=custom_dims.music_volume if hasattr(custom_dims, 'music_volume') else 0.2,
|
| 52 |
+
loop_music=custom_dims.loop_music if hasattr(custom_dims, 'loop_music') else True
|
| 53 |
)
|
| 54 |
else:
|
| 55 |
# Use MoviePy for features (fallback)
|
routers/video.py
CHANGED
|
@@ -219,7 +219,7 @@ async def process_video(
|
|
| 219 |
background_tasks: BackgroundTasks,
|
| 220 |
video: UploadFile = File(...),
|
| 221 |
aspect_ratio: AspectRatio = Form(AspectRatio.RATIO_9_16, description="Select target aspect ratio"),
|
| 222 |
-
style: ShortsStyle = Form(ShortsStyle.
|
| 223 |
background_music: UploadFile = File(None, description="Upload an audio file for background music"),
|
| 224 |
video_volume: float = Form(1.0, description="Volume of original video (0.0 to 1.0+)"),
|
| 225 |
music_volume: float = Form(0.2, description="Volume of background music (0.0 to 1.0+)"),
|
|
|
|
| 219 |
background_tasks: BackgroundTasks,
|
| 220 |
video: UploadFile = File(...),
|
| 221 |
aspect_ratio: AspectRatio = Form(AspectRatio.RATIO_9_16, description="Select target aspect ratio"),
|
| 222 |
+
style: ShortsStyle = Form(ShortsStyle.ORIGINAL, description="Select the visual style"),
|
| 223 |
background_music: UploadFile = File(None, description="Upload an audio file for background music"),
|
| 224 |
video_volume: float = Form(1.0, description="Volume of original video (0.0 to 1.0+)"),
|
| 225 |
music_volume: float = Form(0.2, description="Volume of background music (0.0 to 1.0+)"),
|
schemas.py
CHANGED
|
@@ -19,11 +19,11 @@ class AspectRatio(str, Enum):
|
|
| 19 |
ORIGINAL = "original" # Keep source ratio
|
| 20 |
|
| 21 |
class ShortsStyle(str, Enum):
|
|
|
|
| 22 |
CINEMATIC = "cinematic" # Blurred background
|
| 23 |
CROP_FILL = "crop_fill" # Crop to fill target ratio
|
| 24 |
SPLIT_SCREEN = "split" # Split screen
|
| 25 |
FIT_BARS = "fit_bars" # Letterboxing
|
| 26 |
-
ORIGINAL = "original" # No changes
|
| 27 |
|
| 28 |
class ProcessingType(str, Enum):
|
| 29 |
TRANSCRIPT = "transcript"
|
|
@@ -48,7 +48,7 @@ class Dimensions(BaseModel):
|
|
| 48 |
width: Optional[int] = 0
|
| 49 |
height: Optional[int] = 0
|
| 50 |
target_ratio: AspectRatio = AspectRatio.RATIO_9_16
|
| 51 |
-
style: ShortsStyle = ShortsStyle.
|
| 52 |
video_scale: float = 1.0
|
| 53 |
vertical_shift: float = 0.0
|
| 54 |
blur_intensity: int = 20
|
|
@@ -62,7 +62,10 @@ class Dimensions(BaseModel):
|
|
| 62 |
class ClipRequest(BaseModel):
|
| 63 |
video_url: Optional[str] = None
|
| 64 |
aspect_ratio: AspectRatio = AspectRatio.RATIO_9_16
|
| 65 |
-
style: ShortsStyle = ShortsStyle.
|
|
|
|
|
|
|
|
|
|
| 66 |
custom_dimensions: Optional[Dimensions] = None
|
| 67 |
timestamps: Optional[List[Timestamp]] = None
|
| 68 |
|
|
|
|
| 19 |
ORIGINAL = "original" # Keep source ratio
|
| 20 |
|
| 21 |
class ShortsStyle(str, Enum):
|
| 22 |
+
ORIGINAL = "original" # Keep original style
|
| 23 |
CINEMATIC = "cinematic" # Blurred background
|
| 24 |
CROP_FILL = "crop_fill" # Crop to fill target ratio
|
| 25 |
SPLIT_SCREEN = "split" # Split screen
|
| 26 |
FIT_BARS = "fit_bars" # Letterboxing
|
|
|
|
| 27 |
|
| 28 |
class ProcessingType(str, Enum):
|
| 29 |
TRANSCRIPT = "transcript"
|
|
|
|
| 48 |
width: Optional[int] = 0
|
| 49 |
height: Optional[int] = 0
|
| 50 |
target_ratio: AspectRatio = AspectRatio.RATIO_9_16
|
| 51 |
+
style: ShortsStyle = ShortsStyle.ORIGINAL
|
| 52 |
video_scale: float = 1.0
|
| 53 |
vertical_shift: float = 0.0
|
| 54 |
blur_intensity: int = 20
|
|
|
|
| 62 |
class ClipRequest(BaseModel):
|
| 63 |
video_url: Optional[str] = None
|
| 64 |
aspect_ratio: AspectRatio = AspectRatio.RATIO_9_16
|
| 65 |
+
style: ShortsStyle = ShortsStyle.ORIGINAL
|
| 66 |
+
video_volume: float = 1.0
|
| 67 |
+
music_volume: float = 0.2
|
| 68 |
+
loop_music: bool = True
|
| 69 |
custom_dimensions: Optional[Dimensions] = None
|
| 70 |
timestamps: Optional[List[Timestamp]] = None
|
| 71 |
|
video_processor.py
CHANGED
|
@@ -19,7 +19,7 @@ def get_canvas_dimensions(ratio: AspectRatio) -> tuple:
|
|
| 19 |
return 1080, 1350
|
| 20 |
return None, None # For ORIGINAL
|
| 21 |
|
| 22 |
-
def process_video_clips(video_path: str, timestamps, aspect_ratio: AspectRatio = AspectRatio.RATIO_9_16, style: ShortsStyle = ShortsStyle.
|
| 23 |
"""
|
| 24 |
Processes a video file into multiple clips based on timestamps and style.
|
| 25 |
If export_audio is True, also saves the original audio track of each clip.
|
|
@@ -29,6 +29,18 @@ def process_video_clips(video_path: str, timestamps, aspect_ratio: AspectRatio =
|
|
| 29 |
clip_paths = []
|
| 30 |
audio_paths = []
|
| 31 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
# Try FFmpeg optimization first for simple cases
|
| 33 |
if use_ffmpeg_optimization:
|
| 34 |
try:
|
|
@@ -39,7 +51,8 @@ def process_video_clips(video_path: str, timestamps, aspect_ratio: AspectRatio =
|
|
| 39 |
output_format=style,
|
| 40 |
custom_dims=custom_dims,
|
| 41 |
export_audio=export_audio,
|
| 42 |
-
aspect_ratio=aspect_ratio
|
|
|
|
| 43 |
)
|
| 44 |
if clip_paths: # If FFmpeg worked, return results
|
| 45 |
print(f"✅ FFmpeg optimization successful! Processed {len(clip_paths)} clips")
|
|
@@ -49,13 +62,12 @@ def process_video_clips(video_path: str, timestamps, aspect_ratio: AspectRatio =
|
|
| 49 |
print("🎬 Falling back to MoviePy...")
|
| 50 |
|
| 51 |
try:
|
| 52 |
-
# Load background music if provided
|
| 53 |
bg_music = None
|
| 54 |
-
if
|
| 55 |
from moviepy import AudioFileClip, CompositeAudioClip
|
| 56 |
import moviepy.audio.fx as afx
|
| 57 |
-
|
| 58 |
-
bg_music = AudioFileClip(custom_dims.audio_path)
|
| 59 |
|
| 60 |
if use_parallel and len(timestamps) > 1:
|
| 61 |
# Process clips in parallel for better performance
|
|
@@ -69,7 +81,10 @@ def process_video_clips(video_path: str, timestamps, aspect_ratio: AspectRatio =
|
|
| 69 |
future = executor.submit(
|
| 70 |
process_single_clip,
|
| 71 |
ts, video_path, aspect_ratio, style, custom_dims,
|
| 72 |
-
export_audio, bg_music, clip_id
|
|
|
|
|
|
|
|
|
|
| 73 |
)
|
| 74 |
futures.append((future, clip_id))
|
| 75 |
|
|
@@ -114,14 +129,40 @@ def process_video_clips(video_path: str, timestamps, aspect_ratio: AspectRatio =
|
|
| 114 |
|
| 115 |
# Extract subclip and process it
|
| 116 |
with video.subclipped(ts.start_time, end) as subclip:
|
| 117 |
-
# (Removed automatic audio extraction to mp3)
|
| 118 |
-
|
| 119 |
# Apply background music if available
|
| 120 |
if bg_music:
|
| 121 |
-
#
|
| 122 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
|
| 124 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
|
| 126 |
# Write file - optimized settings for compatibility and speed
|
| 127 |
temp_audio = os.path.join(TEMP_DIR, f"temp-audio-{clip_id}.m4a")
|
|
@@ -299,13 +340,23 @@ def create_zip_archive(file_paths: list, output_filename: str):
|
|
| 299 |
|
| 300 |
return zip_path
|
| 301 |
|
| 302 |
-
def process_single_clip(ts, video_path, style, custom_dims, export_audio, bg_music, clip_id):
|
| 303 |
"""
|
| 304 |
Process a single clip - for parallel processing.
|
| 305 |
"""
|
| 306 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 307 |
from moviepy import VideoFileClip, CompositeAudioClip
|
| 308 |
import moviepy.audio.fx as afx
|
|
|
|
| 309 |
|
| 310 |
# Open video for this clip (parallel processing)
|
| 311 |
with VideoFileClip(video_path) as video:
|
|
@@ -330,7 +381,41 @@ def process_single_clip(ts, video_path, style, custom_dims, export_audio, bg_mus
|
|
| 330 |
# (Removed automatic audio extraction to mp3)
|
| 331 |
audio_output_path = None
|
| 332 |
|
| 333 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 334 |
|
| 335 |
# Write file - optimized settings for speed
|
| 336 |
temp_audio = os.path.join(TEMP_DIR, f"temp-audio-{clip_id}.m4a")
|
|
@@ -359,21 +444,15 @@ def process_single_clip(ts, video_path, style, custom_dims, export_audio, bg_mus
|
|
| 359 |
def extract_audio_from_video(video_path: str, output_format: str = "mp3"):
|
| 360 |
"""
|
| 361 |
Extract audio from a video file and save it as an audio file.
|
| 362 |
-
|
| 363 |
-
Args:
|
| 364 |
-
video_path: Path to the input video file
|
| 365 |
-
output_format: Output audio format (mp3, wav, etc.)
|
| 366 |
-
|
| 367 |
-
Returns:
|
| 368 |
-
Path to the extracted audio file
|
| 369 |
"""
|
| 370 |
try:
|
|
|
|
| 371 |
# Generate output filename
|
| 372 |
base_name = os.path.splitext(os.path.basename(video_path))[0]
|
| 373 |
audio_filename = f"{base_name}_audio.{output_format}"
|
| 374 |
-
output_path = os.path.join(
|
| 375 |
|
| 376 |
-
# Try FFmpeg first (fastest method)
|
| 377 |
try:
|
| 378 |
import subprocess
|
| 379 |
import imageio_ffmpeg
|
|
|
|
| 19 |
return 1080, 1350
|
| 20 |
return None, None # For ORIGINAL
|
| 21 |
|
| 22 |
+
def process_video_clips(video_path: str, timestamps, aspect_ratio: AspectRatio = AspectRatio.RATIO_9_16, style: ShortsStyle = ShortsStyle.ORIGINAL, custom_dims: Dimensions = None, export_audio: bool = False, use_parallel: bool = True, use_ffmpeg_optimization: bool = True, video_volume: float = 1.0, music_volume: float = 0.2, loop_music: bool = True):
|
| 23 |
"""
|
| 24 |
Processes a video file into multiple clips based on timestamps and style.
|
| 25 |
If export_audio is True, also saves the original audio track of each clip.
|
|
|
|
| 29 |
clip_paths = []
|
| 30 |
audio_paths = []
|
| 31 |
|
| 32 |
+
# Extract background music path if available
|
| 33 |
+
bg_music_path = None
|
| 34 |
+
if custom_dims and hasattr(custom_dims, 'audio_path') and custom_dims.audio_path:
|
| 35 |
+
if os.path.exists(custom_dims.audio_path):
|
| 36 |
+
bg_music_path = custom_dims.audio_path
|
| 37 |
+
|
| 38 |
+
# Ensure custom_dims has the volume info for hybrid processing
|
| 39 |
+
if custom_dims:
|
| 40 |
+
custom_dims.video_volume = video_volume
|
| 41 |
+
custom_dims.music_volume = music_volume
|
| 42 |
+
custom_dims.loop_music = loop_music
|
| 43 |
+
|
| 44 |
# Try FFmpeg optimization first for simple cases
|
| 45 |
if use_ffmpeg_optimization:
|
| 46 |
try:
|
|
|
|
| 51 |
output_format=style,
|
| 52 |
custom_dims=custom_dims,
|
| 53 |
export_audio=export_audio,
|
| 54 |
+
aspect_ratio=aspect_ratio,
|
| 55 |
+
bg_music=bg_music_path
|
| 56 |
)
|
| 57 |
if clip_paths: # If FFmpeg worked, return results
|
| 58 |
print(f"✅ FFmpeg optimization successful! Processed {len(clip_paths)} clips")
|
|
|
|
| 62 |
print("🎬 Falling back to MoviePy...")
|
| 63 |
|
| 64 |
try:
|
| 65 |
+
# Load background music if provided for MoviePy fallback
|
| 66 |
bg_music = None
|
| 67 |
+
if bg_music_path:
|
| 68 |
from moviepy import AudioFileClip, CompositeAudioClip
|
| 69 |
import moviepy.audio.fx as afx
|
| 70 |
+
bg_music = AudioFileClip(bg_music_path)
|
|
|
|
| 71 |
|
| 72 |
if use_parallel and len(timestamps) > 1:
|
| 73 |
# Process clips in parallel for better performance
|
|
|
|
| 81 |
future = executor.submit(
|
| 82 |
process_single_clip,
|
| 83 |
ts, video_path, aspect_ratio, style, custom_dims,
|
| 84 |
+
export_audio, bg_music, clip_id,
|
| 85 |
+
video_volume=video_volume,
|
| 86 |
+
music_volume=music_volume,
|
| 87 |
+
loop_music=loop_music
|
| 88 |
)
|
| 89 |
futures.append((future, clip_id))
|
| 90 |
|
|
|
|
| 129 |
|
| 130 |
# Extract subclip and process it
|
| 131 |
with video.subclipped(ts.start_time, end) as subclip:
|
|
|
|
|
|
|
| 132 |
# Apply background music if available
|
| 133 |
if bg_music:
|
| 134 |
+
# Setup background music
|
| 135 |
+
bg_music_clip = bg_music.with_duration(subclip.duration)
|
| 136 |
+
if loop_music:
|
| 137 |
+
bg_music_clip = bg_music_clip.fx(afx.AudioLoop, duration=subclip.duration)
|
| 138 |
+
|
| 139 |
+
# Apply volumes
|
| 140 |
+
if subclip.audio:
|
| 141 |
+
original_audio = subclip.audio.with_volume_scaled(video_volume)
|
| 142 |
+
bg_music_clip = bg_music_clip.with_volume_scaled(music_volume)
|
| 143 |
+
# Combine audio
|
| 144 |
+
subclip.audio = CompositeAudioClip([original_audio, bg_music_clip])
|
| 145 |
+
else:
|
| 146 |
+
subclip.audio = bg_music_clip.with_volume_scaled(music_volume)
|
| 147 |
|
| 148 |
+
# Apply formatting
|
| 149 |
+
if aspect_ratio == AspectRatio.ORIGINAL or style == ShortsStyle.ORIGINAL:
|
| 150 |
+
pass # Skip resizing, keep original dimensions
|
| 151 |
+
else:
|
| 152 |
+
# Map style to layout
|
| 153 |
+
layout_map = {
|
| 154 |
+
ShortsStyle.CINEMATIC: LayoutType.CINEMATIC_BLUR,
|
| 155 |
+
ShortsStyle.CROP_FILL: LayoutType.CROP_CENTER,
|
| 156 |
+
ShortsStyle.FIT_BARS: LayoutType.FIT_CENTER,
|
| 157 |
+
ShortsStyle.SPLIT_SCREEN: LayoutType.SPLIT_SCREEN
|
| 158 |
+
}
|
| 159 |
+
layout = layout_map.get(style, LayoutType.CROP_CENTER)
|
| 160 |
+
|
| 161 |
+
# Get target dimensions
|
| 162 |
+
target_w, target_h = get_canvas_dimensions(aspect_ratio)
|
| 163 |
+
|
| 164 |
+
if target_w and target_h:
|
| 165 |
+
subclip = apply_layout_factory(subclip, layout, target_w, target_h, custom_dims)
|
| 166 |
|
| 167 |
# Write file - optimized settings for compatibility and speed
|
| 168 |
temp_audio = os.path.join(TEMP_DIR, f"temp-audio-{clip_id}.m4a")
|
|
|
|
| 340 |
|
| 341 |
return zip_path
|
| 342 |
|
| 343 |
+
def process_single_clip(ts, video_path, aspect_ratio, style, custom_dims, export_audio, bg_music, clip_id, video_volume=1.0, music_volume=0.2, loop_music=True):
|
| 344 |
"""
|
| 345 |
Process a single clip - for parallel processing.
|
| 346 |
"""
|
| 347 |
try:
|
| 348 |
+
# Ensure custom_dims has the volume info
|
| 349 |
+
if not custom_dims:
|
| 350 |
+
from schemas import Dimensions
|
| 351 |
+
custom_dims = Dimensions()
|
| 352 |
+
|
| 353 |
+
custom_dims.video_volume = video_volume
|
| 354 |
+
custom_dims.music_volume = music_volume
|
| 355 |
+
custom_dims.loop_music = loop_music
|
| 356 |
+
|
| 357 |
from moviepy import VideoFileClip, CompositeAudioClip
|
| 358 |
import moviepy.audio.fx as afx
|
| 359 |
+
from schemas import AspectRatio, ShortsStyle, LayoutType
|
| 360 |
|
| 361 |
# Open video for this clip (parallel processing)
|
| 362 |
with VideoFileClip(video_path) as video:
|
|
|
|
| 381 |
# (Removed automatic audio extraction to mp3)
|
| 382 |
audio_output_path = None
|
| 383 |
|
| 384 |
+
# Apply background music if available
|
| 385 |
+
if bg_music:
|
| 386 |
+
from moviepy import CompositeAudioClip
|
| 387 |
+
import moviepy.audio.fx as afx
|
| 388 |
+
|
| 389 |
+
# Setup background music
|
| 390 |
+
bg_music_clip = bg_music.with_duration(subclip.duration)
|
| 391 |
+
if loop_music:
|
| 392 |
+
bg_music_clip = bg_music_clip.fx(afx.AudioLoop, duration=subclip.duration)
|
| 393 |
+
|
| 394 |
+
# Apply volumes
|
| 395 |
+
original_audio = subclip.audio.with_volume_scaled(video_volume)
|
| 396 |
+
bg_music_clip = bg_music_clip.with_volume_scaled(music_volume)
|
| 397 |
+
|
| 398 |
+
# Combine audio
|
| 399 |
+
subclip.audio = CompositeAudioClip([original_audio, bg_music_clip])
|
| 400 |
+
|
| 401 |
+
# Apply formatting
|
| 402 |
+
if aspect_ratio == AspectRatio.ORIGINAL or style == ShortsStyle.ORIGINAL:
|
| 403 |
+
pass # Skip resizing, keep original dimensions
|
| 404 |
+
else:
|
| 405 |
+
# Map style to layout
|
| 406 |
+
layout_map = {
|
| 407 |
+
ShortsStyle.CINEMATIC: LayoutType.CINEMATIC_BLUR,
|
| 408 |
+
ShortsStyle.CROP_FILL: LayoutType.CROP_CENTER,
|
| 409 |
+
ShortsStyle.FIT_BARS: LayoutType.FIT_CENTER,
|
| 410 |
+
ShortsStyle.SPLIT_SCREEN: LayoutType.SPLIT_SCREEN
|
| 411 |
+
}
|
| 412 |
+
layout = layout_map.get(style, LayoutType.CROP_CENTER)
|
| 413 |
+
|
| 414 |
+
# Get target dimensions
|
| 415 |
+
target_w, target_h = get_canvas_dimensions(aspect_ratio)
|
| 416 |
+
|
| 417 |
+
if target_w and target_h:
|
| 418 |
+
subclip = apply_layout_factory(subclip, layout, target_w, target_h, custom_dims)
|
| 419 |
|
| 420 |
# Write file - optimized settings for speed
|
| 421 |
temp_audio = os.path.join(TEMP_DIR, f"temp-audio-{clip_id}.m4a")
|
|
|
|
| 444 |
def extract_audio_from_video(video_path: str, output_format: str = "mp3"):
|
| 445 |
"""
|
| 446 |
Extract audio from a video file and save it as an audio file.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 447 |
"""
|
| 448 |
try:
|
| 449 |
+
from routers.video import AUDIO_DIR
|
| 450 |
# Generate output filename
|
| 451 |
base_name = os.path.splitext(os.path.basename(video_path))[0]
|
| 452 |
audio_filename = f"{base_name}_audio.{output_format}"
|
| 453 |
+
output_path = os.path.join(AUDIO_DIR, audio_filename)
|
| 454 |
|
| 455 |
+
# Try FFmpeg first (fastest method)
|
| 456 |
try:
|
| 457 |
import subprocess
|
| 458 |
import imageio_ffmpeg
|