Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -146,7 +146,11 @@ def add_speed_message(msg, speed_factor):
|
|
| 146 |
|
| 147 |
|
| 148 |
def make_video(s, video_in, audio_in, trim_frames=0, speed_factor=1.0):
|
| 149 |
-
"""Compile
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
v_path = p(video_in)
|
| 151 |
audio_path = p(audio_in)
|
| 152 |
if not v_path:
|
|
@@ -163,42 +167,49 @@ def make_video(s, video_in, audio_in, trim_frames=0, speed_factor=1.0):
|
|
| 163 |
if trim_frames > 0 and not has_frames:
|
| 164 |
return None, "Extract frames first to use initial-frame trimming."
|
| 165 |
|
| 166 |
-
#
|
| 167 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 168 |
if not has_frames:
|
| 169 |
-
|
| 170 |
-
if
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
|
|
|
|
|
|
| 175 |
replacement_duration = audio_meta(audio_path)
|
| 176 |
-
loop_args = ["-stream_loop", "-1"] if replacement_duration > 0 and (
|
| 177 |
cmd = ["ffmpeg", "-y", "-i", v_path] + loop_args + [
|
| 178 |
"-i", audio_path,
|
| 179 |
"-map", "0:v:0", "-map", "1:a:0",
|
| 180 |
-
"-t", str(
|
| 181 |
"-c:v", "copy", "-c:a", "aac", "-b:a", "192k",
|
| 182 |
-
"-movflags", "+faststart",
|
| 183 |
]
|
| 184 |
result = subprocess.run(cmd, capture_output=True, text=True)
|
| 185 |
if result.returncode:
|
| 186 |
return None, result.stderr or "Could not replace the audio."
|
| 187 |
msg = "✅ Final video created using the original video and replacement audio."
|
| 188 |
-
|
| 189 |
-
return
|
| 190 |
-
|
| 191 |
-
# Use the video associated with the extracted frames and its original frame rate.
|
| 192 |
-
frame_state_video = s["v"]
|
| 193 |
-
frame_dir, frames, fps, source_duration = s["d"], s["fs"], s["fps"], s["dur"]
|
| 194 |
-
trim_seconds = trim_frames / fps
|
| 195 |
-
if trim_seconds >= source_duration:
|
| 196 |
-
return None, f"Cannot remove {trim_frames} frame(s): the video is only {source_duration:.3f}s long."
|
| 197 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
intro = os.path.join(frame_dir, "intro.mp4")
|
| 199 |
r = subprocess.run(
|
| 200 |
[
|
| 201 |
-
"ffmpeg", "-y", "-framerate", str(
|
| 202 |
"-i", os.path.join(frame_dir, "f_%06d.png"),
|
| 203 |
"-c:v", "libx264", "-crf", "18", "-preset", "medium",
|
| 204 |
"-pix_fmt", "yuv420p", intro,
|
|
@@ -208,36 +219,57 @@ def make_video(s, video_in, audio_in, trim_frames=0, speed_factor=1.0):
|
|
| 208 |
if r.returncode:
|
| 209 |
return None, r.stderr or "Could not compile extracted frames."
|
| 210 |
|
| 211 |
-
intro_duration = len(frames) /
|
| 212 |
-
|
|
|
|
| 213 |
out = os.path.join(tempfile.mkdtemp(), f"final_{int(time.time() * 1000)}.mp4")
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
)
|
| 218 |
|
|
|
|
| 219 |
if audio_path:
|
| 220 |
replacement_duration = audio_meta(audio_path)
|
| 221 |
loop_args = ["-stream_loop", "-1"] if replacement_duration > 0 and (total_duration - replacement_duration) > 1.0 else []
|
| 222 |
-
cmd =
|
| 223 |
-
"-
|
| 224 |
-
"-filter_complex", video_concat,
|
| 225 |
"-map", "[vout]", "-map", "2:a:0",
|
| 226 |
"-t", str(total_duration),
|
| 227 |
"-c:v", "libx264", "-crf", "18", "-preset", "medium",
|
| 228 |
"-pix_fmt", "yuv420p", "-c:a", "aac", "-b:a", "192k",
|
| 229 |
-
"-movflags", "+faststart", out
|
| 230 |
-
]
|
| 231 |
else:
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 241 |
|
| 242 |
result = subprocess.run(cmd, capture_output=True, text=True)
|
| 243 |
if result.returncode:
|
|
@@ -245,16 +277,22 @@ def make_video(s, video_in, audio_in, trim_frames=0, speed_factor=1.0):
|
|
| 245 |
|
| 246 |
msg = f"✅ Final video created. Added {len(frames)} extracted frame(s)."
|
| 247 |
if trim_frames:
|
| 248 |
-
msg += f"
|
|
|
|
|
|
|
| 249 |
if audio_path:
|
| 250 |
if loop_args:
|
| 251 |
msg += f" Replacement audio looped (it was {total_duration - replacement_duration:.1f}s short)."
|
| 252 |
else:
|
| 253 |
msg += " Replacement audio added."
|
|
|
|
|
|
|
| 254 |
else:
|
| 255 |
-
msg += "
|
| 256 |
-
|
| 257 |
-
|
|
|
|
|
|
|
| 258 |
|
| 259 |
except Exception as e:
|
| 260 |
return None, str(e)
|
|
@@ -275,7 +313,7 @@ with gr.Blocks(title="Video Frame Editor") as demo:
|
|
| 275 |
minimum=0,
|
| 276 |
precision=0,
|
| 277 |
label="Initial Frames to Remove from Original Video",
|
| 278 |
-
info="
|
| 279 |
)
|
| 280 |
|
| 281 |
with gr.Row():
|
|
|
|
| 146 |
|
| 147 |
|
| 148 |
def make_video(s, video_in, audio_in, trim_frames=0, speed_factor=1.0):
|
| 149 |
+
"""Compile the extracted-frame intro with the currently selected, independently trimmed source video.
|
| 150 |
+
|
| 151 |
+
Extracted frames keep their existing extraction timing. Trimming affects only the source-video
|
| 152 |
+
section, then the whole compiled output is sped up/slowed down as the final operation.
|
| 153 |
+
"""
|
| 154 |
v_path = p(video_in)
|
| 155 |
audio_path = p(audio_in)
|
| 156 |
if not v_path:
|
|
|
|
| 167 |
if trim_frames > 0 and not has_frames:
|
| 168 |
return None, "Extract frames first to use initial-frame trimming."
|
| 169 |
|
| 170 |
+
# Always use the currently selected input as the original-video segment. The extracted
|
| 171 |
+
# frame state is used only to build the intro, so those two parts stay independent.
|
| 172 |
+
_, _, source_fps, source_duration = meta(v_path)
|
| 173 |
+
trim_seconds = trim_frames / source_fps
|
| 174 |
+
if trim_frames and trim_seconds >= source_duration:
|
| 175 |
+
return None, (
|
| 176 |
+
f"Cannot remove {trim_frames} source frame(s): the selected video is only "
|
| 177 |
+
f"{source_duration:.3f}s long at {source_fps:.3f} FPS."
|
| 178 |
+
)
|
| 179 |
+
|
| 180 |
if not has_frames:
|
| 181 |
+
# With no intro frames, trim the source video if requested (the UI normally expects
|
| 182 |
+
# a frame intro for trimming, but this branch remains safe if called directly).
|
| 183 |
+
if trim_frames:
|
| 184 |
+
return None, "Extract frames first to use initial-frame trimming."
|
| 185 |
+
base_out = v_path
|
| 186 |
+
msg = "✅ Final video created from the original video. No extracted-frame intro was added."
|
| 187 |
+
if audio_path:
|
| 188 |
+
base_out = os.path.join(tempfile.mkdtemp(), f"final_audio_{int(time.time() * 1000)}.mp4")
|
| 189 |
replacement_duration = audio_meta(audio_path)
|
| 190 |
+
loop_args = ["-stream_loop", "-1"] if replacement_duration > 0 and (source_duration - replacement_duration) > 1.0 else []
|
| 191 |
cmd = ["ffmpeg", "-y", "-i", v_path] + loop_args + [
|
| 192 |
"-i", audio_path,
|
| 193 |
"-map", "0:v:0", "-map", "1:a:0",
|
| 194 |
+
"-t", str(source_duration),
|
| 195 |
"-c:v", "copy", "-c:a", "aac", "-b:a", "192k",
|
| 196 |
+
"-movflags", "+faststart", base_out,
|
| 197 |
]
|
| 198 |
result = subprocess.run(cmd, capture_output=True, text=True)
|
| 199 |
if result.returncode:
|
| 200 |
return None, result.stderr or "Could not replace the audio."
|
| 201 |
msg = "✅ Final video created using the original video and replacement audio."
|
| 202 |
+
final_out = apply_speed_to_video(base_out, speed_factor)
|
| 203 |
+
return final_out, add_speed_message(msg, speed_factor)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
|
| 205 |
+
# Build the intro from the extracted frames with the same frame rate as extraction,
|
| 206 |
+
# preserving the existing behavior. Trimming below is calculated from the current source.
|
| 207 |
+
frame_dir, frames = s["d"], s["fs"]
|
| 208 |
+
intro_fps = float(s["fps"])
|
| 209 |
intro = os.path.join(frame_dir, "intro.mp4")
|
| 210 |
r = subprocess.run(
|
| 211 |
[
|
| 212 |
+
"ffmpeg", "-y", "-framerate", str(intro_fps),
|
| 213 |
"-i", os.path.join(frame_dir, "f_%06d.png"),
|
| 214 |
"-c:v", "libx264", "-crf", "18", "-preset", "medium",
|
| 215 |
"-pix_fmt", "yuv420p", intro,
|
|
|
|
| 219 |
if r.returncode:
|
| 220 |
return None, r.stderr or "Could not compile extracted frames."
|
| 221 |
|
| 222 |
+
intro_duration = len(frames) / intro_fps
|
| 223 |
+
body_duration = source_duration - trim_seconds
|
| 224 |
+
total_duration = intro_duration + body_duration
|
| 225 |
out = os.path.join(tempfile.mkdtemp(), f"final_{int(time.time() * 1000)}.mp4")
|
| 226 |
+
|
| 227 |
+
# Normalize the source segment after trimming, then concatenate it after the intro.
|
| 228 |
+
# Scaling/padding the intro to source dimensions avoids concat failures if the selected
|
| 229 |
+
# source video differs from the video used to extract the frames.
|
| 230 |
+
width, height, _, _ = meta(v_path)
|
| 231 |
+
video_filter = (
|
| 232 |
+
f"[0:v:0]scale={width}:{height}:force_original_aspect_ratio=decrease,"
|
| 233 |
+
f"pad={width}:{height}:(ow-iw)/2:(oh-ih)/2,setsar=1,fps={source_fps:.8f}[intro];"
|
| 234 |
+
f"[1:v:0]trim=start_frame={trim_frames},setpts=PTS-STARTPTS,"
|
| 235 |
+
f"fps={source_fps:.8f},setsar=1[body];"
|
| 236 |
+
"[intro][body]concat=n=2:v=1:a=0[vout]"
|
| 237 |
)
|
| 238 |
|
| 239 |
+
cmd = ["ffmpeg", "-y", "-i", intro, "-i", v_path]
|
| 240 |
if audio_path:
|
| 241 |
replacement_duration = audio_meta(audio_path)
|
| 242 |
loop_args = ["-stream_loop", "-1"] if replacement_duration > 0 and (total_duration - replacement_duration) > 1.0 else []
|
| 243 |
+
cmd += loop_args + ["-i", audio_path,
|
| 244 |
+
"-filter_complex", video_filter,
|
|
|
|
| 245 |
"-map", "[vout]", "-map", "2:a:0",
|
| 246 |
"-t", str(total_duration),
|
| 247 |
"-c:v", "libx264", "-crf", "18", "-preset", "medium",
|
| 248 |
"-pix_fmt", "yuv420p", "-c:a", "aac", "-b:a", "192k",
|
| 249 |
+
"-movflags", "+faststart", out]
|
|
|
|
| 250 |
else:
|
| 251 |
+
if stream_has_audio(v_path):
|
| 252 |
+
# Keep source audio aligned with the trimmed video section. The intro is silent.
|
| 253 |
+
audio_filter = (
|
| 254 |
+
f"{video_filter};[1:a:0]atrim=start={trim_seconds:.8f},"
|
| 255 |
+
f"asetpts=PTS-STARTPTS,adelay={int(round(intro_duration * 1000))}:all=1[aout]"
|
| 256 |
+
)
|
| 257 |
+
cmd += [
|
| 258 |
+
"-filter_complex", audio_filter,
|
| 259 |
+
"-map", "[vout]", "-map", "[aout]",
|
| 260 |
+
"-t", str(total_duration),
|
| 261 |
+
"-c:v", "libx264", "-crf", "18", "-preset", "medium",
|
| 262 |
+
"-pix_fmt", "yuv420p", "-c:a", "aac", "-b:a", "192k",
|
| 263 |
+
"-movflags", "+faststart", out,
|
| 264 |
+
]
|
| 265 |
+
else:
|
| 266 |
+
cmd += [
|
| 267 |
+
"-filter_complex", video_filter,
|
| 268 |
+
"-map", "[vout]", "-an",
|
| 269 |
+
"-t", str(total_duration),
|
| 270 |
+
"-c:v", "libx264", "-crf", "18", "-preset", "medium",
|
| 271 |
+
"-pix_fmt", "yuv420p", "-movflags", "+faststart", out,
|
| 272 |
+
]
|
| 273 |
|
| 274 |
result = subprocess.run(cmd, capture_output=True, text=True)
|
| 275 |
if result.returncode:
|
|
|
|
| 277 |
|
| 278 |
msg = f"✅ Final video created. Added {len(frames)} extracted frame(s)."
|
| 279 |
if trim_frames:
|
| 280 |
+
msg += f" Original video starts after skipping {trim_frames} frame(s) ({trim_seconds:.3f}s at {source_fps:.3f} FPS)."
|
| 281 |
+
else:
|
| 282 |
+
msg += " Original video starts from its beginning."
|
| 283 |
if audio_path:
|
| 284 |
if loop_args:
|
| 285 |
msg += f" Replacement audio looped (it was {total_duration - replacement_duration:.1f}s short)."
|
| 286 |
else:
|
| 287 |
msg += " Replacement audio added."
|
| 288 |
+
elif stream_has_audio(v_path):
|
| 289 |
+
msg += " Original audio is trimmed to match the original-video section; the intro is silent."
|
| 290 |
else:
|
| 291 |
+
msg += " The source video has no audio track."
|
| 292 |
+
|
| 293 |
+
# Speed is deliberately the final step, so it affects the intro and original video together.
|
| 294 |
+
final_out = apply_speed_to_video(out, speed_factor)
|
| 295 |
+
return final_out, add_speed_message(msg, speed_factor)
|
| 296 |
|
| 297 |
except Exception as e:
|
| 298 |
return None, str(e)
|
|
|
|
| 313 |
minimum=0,
|
| 314 |
precision=0,
|
| 315 |
label="Initial Frames to Remove from Original Video",
|
| 316 |
+
info="Affects only the original-video section, not the extracted-frame intro. Frame count is based on the currently selected original video FPS.",
|
| 317 |
)
|
| 318 |
|
| 319 |
with gr.Row():
|