update
Browse files- modal_video_processing.py +38 -11
modal_video_processing.py
CHANGED
|
@@ -41,7 +41,14 @@ def process_quote_video(
|
|
| 41 |
- Overlays `quote_text` using a chosen `text_style`.
|
| 42 |
- If `audio_b64` is provided, decodes it and:
|
| 43 |
* sets it as the audio track
|
| 44 |
-
*
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
|
| 46 |
Returns:
|
| 47 |
Raw bytes of the final MP4 video.
|
|
@@ -77,13 +84,17 @@ def process_quote_video(
|
|
| 77 |
# 2. Load video
|
| 78 |
# ---------------------------
|
| 79 |
video = VideoFileClip(temp_video.name)
|
|
|
|
| 80 |
|
| 81 |
# ---------------------------
|
| 82 |
-
# 3.
|
| 83 |
# ---------------------------
|
| 84 |
audio_clip = None
|
| 85 |
temp_audio_path = None
|
| 86 |
|
|
|
|
|
|
|
|
|
|
| 87 |
if audio_b64:
|
| 88 |
try:
|
| 89 |
temp_audio = tempfile.NamedTemporaryFile(delete=False, suffix=".mp3")
|
|
@@ -97,20 +108,36 @@ def process_quote_video(
|
|
| 97 |
audio_clip = AudioFileClip(temp_audio_path)
|
| 98 |
audio_duration = audio_clip.duration
|
| 99 |
|
| 100 |
-
#
|
| 101 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
|
| 103 |
-
#
|
| 104 |
if target_duration > video.duration:
|
| 105 |
video = vfx_loop(video, duration=target_duration)
|
|
|
|
|
|
|
| 106 |
|
| 107 |
-
# IMPORTANT: we do NOT crop video when audio is shorter;
|
| 108 |
-
# it's fine if video runs a bit longer than the narration.
|
| 109 |
except Exception as e:
|
| 110 |
print(f"⚠️ Audio handling error: {e}")
|
| 111 |
audio_clip = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
|
| 113 |
-
#
|
| 114 |
w, h = video.size
|
| 115 |
|
| 116 |
# ---------------------------
|
|
@@ -193,11 +220,10 @@ def process_quote_video(
|
|
| 193 |
# ---------------------------
|
| 194 |
final_video = CompositeVideoClip([video, text_clip])
|
| 195 |
|
| 196 |
-
#
|
| 197 |
if audio_clip is not None:
|
| 198 |
try:
|
| 199 |
final_video = final_video.set_audio(audio_clip)
|
| 200 |
-
# Do NOT call set_duration() again: video is already long enough.
|
| 201 |
except Exception as e:
|
| 202 |
print(f"⚠️ Could not attach audio: {e}")
|
| 203 |
|
|
@@ -254,7 +280,8 @@ def process_quote_video(
|
|
| 254 |
|
| 255 |
total_time = time.time() - start_time
|
| 256 |
print(
|
| 257 |
-
f"🎉 Total: {total_time:.1f}s, Size: {len(video_bytes) / 1024 / 1024:.2f}MB,
|
|
|
|
| 258 |
)
|
| 259 |
|
| 260 |
return video_bytes
|
|
|
|
| 41 |
- Overlays `quote_text` using a chosen `text_style`.
|
| 42 |
- If `audio_b64` is provided, decodes it and:
|
| 43 |
* sets it as the audio track
|
| 44 |
+
* makes video duration roughly match audio (with min/max bounds).
|
| 45 |
+
|
| 46 |
+
Duration rules:
|
| 47 |
+
- With audio:
|
| 48 |
+
target = audio_duration + 0.5s
|
| 49 |
+
MIN = 7s, MAX = 20s
|
| 50 |
+
- Without audio:
|
| 51 |
+
target = min(original_video_duration, 15s)
|
| 52 |
|
| 53 |
Returns:
|
| 54 |
Raw bytes of the final MP4 video.
|
|
|
|
| 84 |
# 2. Load video
|
| 85 |
# ---------------------------
|
| 86 |
video = VideoFileClip(temp_video.name)
|
| 87 |
+
orig_duration = video.duration
|
| 88 |
|
| 89 |
# ---------------------------
|
| 90 |
+
# 3. Duration logic + optional audio
|
| 91 |
# ---------------------------
|
| 92 |
audio_clip = None
|
| 93 |
temp_audio_path = None
|
| 94 |
|
| 95 |
+
# Default target when no audio
|
| 96 |
+
target_duration = orig_duration
|
| 97 |
+
|
| 98 |
if audio_b64:
|
| 99 |
try:
|
| 100 |
temp_audio = tempfile.NamedTemporaryFile(delete=False, suffix=".mp3")
|
|
|
|
| 108 |
audio_clip = AudioFileClip(temp_audio_path)
|
| 109 |
audio_duration = audio_clip.duration
|
| 110 |
|
| 111 |
+
# Proportional rules with audio
|
| 112 |
+
MIN_DUR = 7.0
|
| 113 |
+
MAX_DUR = 20.0
|
| 114 |
+
target_duration = audio_duration + 0.5 # small buffer
|
| 115 |
+
if target_duration < MIN_DUR:
|
| 116 |
+
target_duration = MIN_DUR
|
| 117 |
+
if target_duration > MAX_DUR:
|
| 118 |
+
target_duration = MAX_DUR
|
| 119 |
|
| 120 |
+
# Adjust video to target_duration
|
| 121 |
if target_duration > video.duration:
|
| 122 |
video = vfx_loop(video, duration=target_duration)
|
| 123 |
+
elif target_duration < video.duration:
|
| 124 |
+
video = video.subclip(0, target_duration)
|
| 125 |
|
|
|
|
|
|
|
| 126 |
except Exception as e:
|
| 127 |
print(f"⚠️ Audio handling error: {e}")
|
| 128 |
audio_clip = None
|
| 129 |
+
# Fall back to no-audio behavior below
|
| 130 |
+
|
| 131 |
+
if audio_clip is None:
|
| 132 |
+
# No audio path: clamp to reasonable length
|
| 133 |
+
MAX_NO_AUDIO = 15.0
|
| 134 |
+
if orig_duration > MAX_NO_AUDIO:
|
| 135 |
+
target_duration = MAX_NO_AUDIO
|
| 136 |
+
video = video.subclip(0, target_duration)
|
| 137 |
+
else:
|
| 138 |
+
target_duration = orig_duration
|
| 139 |
|
| 140 |
+
# At this point, video.duration ≈ target_duration
|
| 141 |
w, h = video.size
|
| 142 |
|
| 143 |
# ---------------------------
|
|
|
|
| 220 |
# ---------------------------
|
| 221 |
final_video = CompositeVideoClip([video, text_clip])
|
| 222 |
|
| 223 |
+
# Attach audio if available (no extra duration forcing)
|
| 224 |
if audio_clip is not None:
|
| 225 |
try:
|
| 226 |
final_video = final_video.set_audio(audio_clip)
|
|
|
|
| 227 |
except Exception as e:
|
| 228 |
print(f"⚠️ Could not attach audio: {e}")
|
| 229 |
|
|
|
|
| 280 |
|
| 281 |
total_time = time.time() - start_time
|
| 282 |
print(
|
| 283 |
+
f"🎉 Total: {total_time:.1f}s, Size: {len(video_bytes) / 1024 / 1024:.2f}MB, "
|
| 284 |
+
f"text_style={text_style}, target_duration≈{target_duration:.1f}s"
|
| 285 |
)
|
| 286 |
|
| 287 |
return video_bytes
|