Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -75,62 +75,60 @@ def save_frames(v, t, n):
|
|
| 75 |
return [], None, str(e)
|
| 76 |
|
| 77 |
|
| 78 |
-
def
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
return [], None, "Number of frames must be at least 1."
|
| 87 |
-
t = max(0, dur - (n / fps))
|
| 88 |
-
return save_frames(v, t, n)
|
| 89 |
-
except Exception as e:
|
| 90 |
-
return [], None, str(e)
|
| 91 |
|
| 92 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
|
| 94 |
def apply_speed_to_video(input_path, speed_factor):
|
| 95 |
-
"""Change
|
| 96 |
speed_factor = float(speed_factor)
|
| 97 |
if not 0.5 <= speed_factor <= 1.5:
|
| 98 |
raise ValueError("Playback speed must be between 0.5x and 1.5x.")
|
| 99 |
if abs(speed_factor - 1.0) < 1e-9:
|
| 100 |
return input_path
|
| 101 |
|
| 102 |
-
|
| 103 |
-
probe = subprocess.run(
|
| 104 |
-
[
|
| 105 |
-
"ffprobe", "-v", "error", "-select_streams", "a:0",
|
| 106 |
-
"-show_entries", "stream=index", "-of", "csv=p=0", input_path,
|
| 107 |
-
],
|
| 108 |
-
capture_output=True, text=True,
|
| 109 |
-
)
|
| 110 |
-
has_audio = probe.returncode == 0 and bool(probe.stdout.strip())
|
| 111 |
sped_path = os.path.join(
|
| 112 |
tempfile.mkdtemp(), f"speed_{int(time.time() * 1000)}.mp4"
|
| 113 |
)
|
| 114 |
-
|
| 115 |
-
common = [
|
| 116 |
-
"ffmpeg", "-y", "-i", input_path,
|
| 117 |
-
]
|
| 118 |
if has_audio:
|
| 119 |
-
|
| 120 |
-
cmd = common + [
|
| 121 |
"-filter_complex",
|
| 122 |
-
f"[0:v:0]setpts=PTS/{speed_factor}[v];
|
|
|
|
| 123 |
"-map", "[v]", "-map", "[a]",
|
| 124 |
"-c:v", "libx264", "-crf", "18", "-preset", "medium",
|
|
|
|
| 125 |
"-c:a", "aac", "-b:a", "192k",
|
| 126 |
"-movflags", "+faststart", sped_path,
|
| 127 |
]
|
| 128 |
else:
|
| 129 |
-
cmd =
|
| 130 |
-
"-vf", f"setpts=PTS/{speed_factor}",
|
| 131 |
"-map", "0:v:0", "-an",
|
| 132 |
"-c:v", "libx264", "-crf", "18", "-preset", "medium",
|
| 133 |
-
"-movflags", "+faststart", sped_path,
|
| 134 |
]
|
| 135 |
|
| 136 |
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
@@ -139,25 +137,18 @@ def apply_speed_to_video(input_path, speed_factor):
|
|
| 139 |
return sped_path
|
| 140 |
|
| 141 |
|
| 142 |
-
def
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
if
|
| 146 |
-
msg +
|
| 147 |
-
|
| 148 |
-
out = apply_speed_to_video(out, speed_factor)
|
| 149 |
-
if speed_factor < 1.0:
|
| 150 |
-
msg += f" Playback slowed to {speed_factor:.1f}x."
|
| 151 |
-
else:
|
| 152 |
-
msg += f" Playback sped up to {speed_factor:.1f}x."
|
| 153 |
-
return out, msg
|
| 154 |
|
| 155 |
|
| 156 |
def make_video(s, video_in, audio_in, trim_frames=0, speed_factor=1.0):
|
| 157 |
-
"""Compile
|
| 158 |
v_path = p(video_in)
|
| 159 |
audio_path = p(audio_in)
|
| 160 |
-
|
| 161 |
if not v_path:
|
| 162 |
return None, "Upload a video first."
|
| 163 |
|
|
@@ -170,114 +161,100 @@ def make_video(s, video_in, audio_in, trim_frames=0, speed_factor=1.0):
|
|
| 170 |
if not 0.5 <= speed_factor <= 1.5:
|
| 171 |
return None, "Playback speed must be between 0.5x and 1.5x."
|
| 172 |
if trim_frames > 0 and not has_frames:
|
| 173 |
-
return None, "Extract frames first to use
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
trim_seconds = trim_frames / fps
|
| 183 |
-
if trim_seconds >= dur:
|
| 184 |
-
return None, f"Cannot remove {trim_frames} frame(s): the video is only {dur:.3f}s long."
|
| 185 |
-
|
| 186 |
-
intro_duration = len(fs) / fps
|
| 187 |
-
total_vid_dur = (dur - trim_seconds) + intro_duration
|
| 188 |
-
intro = os.path.join(d_s, "intro.mp4")
|
| 189 |
-
|
| 190 |
-
# Turn the extracted PNG sequence into a short video.
|
| 191 |
-
r = subprocess.run(
|
| 192 |
-
[
|
| 193 |
-
"ffmpeg", "-y", "-framerate", str(fps),
|
| 194 |
-
"-i", os.path.join(d_s, "f_%06d.png"),
|
| 195 |
-
"-c:v", "libx264", "-crf", "18", "-preset", "medium",
|
| 196 |
-
"-pix_fmt", "yuv420p", intro,
|
| 197 |
-
],
|
| 198 |
-
capture_output=True, text=True,
|
| 199 |
-
)
|
| 200 |
-
if r.returncode:
|
| 201 |
-
return None, r.stderr
|
| 202 |
-
|
| 203 |
-
# Drop the specified number of initial source-video frames, reset timestamps,
|
| 204 |
-
# and place the extracted frame sequence before the remaining source video.
|
| 205 |
-
concat_filter = (
|
| 206 |
-
f"[1:v]trim=start_frame={trim_frames},setpts=PTS-STARTPTS[vtrim];"
|
| 207 |
-
"[0:v][vtrim]concat=n=2:v=1:a=0[v]"
|
| 208 |
-
)
|
| 209 |
-
else:
|
| 210 |
-
# Audio replacement without extracted frames retains the original video.
|
| 211 |
-
_, _, _, dur = meta(v_path)
|
| 212 |
-
total_vid_dur = dur
|
| 213 |
-
v = v_path
|
| 214 |
-
fs = []
|
| 215 |
-
trim_seconds = 0.0
|
| 216 |
-
intro = None
|
| 217 |
-
concat_filter = None
|
| 218 |
-
|
| 219 |
-
if audio_path:
|
| 220 |
-
a_dur = audio_meta(audio_path)
|
| 221 |
-
loop_args = ["-stream_loop", "-1"] if (total_vid_dur - a_dur) > 1.0 else []
|
| 222 |
-
|
| 223 |
-
if has_frames:
|
| 224 |
-
cmd = (
|
| 225 |
-
["ffmpeg", "-y", "-i", intro, "-i", v]
|
| 226 |
-
+ loop_args
|
| 227 |
-
+ [
|
| 228 |
-
"-i", audio_path,
|
| 229 |
-
"-filter_complex", concat_filter,
|
| 230 |
-
"-map", "[v]", "-map", "2:a",
|
| 231 |
-
"-t", str(total_vid_dur),
|
| 232 |
-
"-c:v", "libx264", "-crf", "18", "-preset", "medium",
|
| 233 |
-
"-c:a", "aac", "-b:a", "192k", "-movflags", "+faststart", out,
|
| 234 |
-
]
|
| 235 |
-
)
|
| 236 |
else:
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
)
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
"-c:a", "copy", "-movflags", "+faststart", out,
|
| 272 |
-
]
|
| 273 |
-
r = subprocess.run(cmd, capture_output=True, text=True)
|
| 274 |
if r.returncode:
|
| 275 |
-
return None, r.stderr
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 276 |
|
| 277 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 278 |
if trim_frames:
|
| 279 |
msg += f" Removed the first {trim_frames} source frame(s) ({trim_seconds:.3f}s)."
|
| 280 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 281 |
|
| 282 |
except Exception as e:
|
| 283 |
return None, str(e)
|
|
@@ -298,26 +275,26 @@ with gr.Blocks(title="Video Frame Editor") as demo:
|
|
| 298 |
minimum=0,
|
| 299 |
precision=0,
|
| 300 |
label="Initial Frames to Remove from Original Video",
|
| 301 |
-
info="Set to 0 to keep the original beginning.
|
| 302 |
)
|
| 303 |
|
| 304 |
-
speed_factor = gr.Slider(
|
| 305 |
-
minimum=0.5,
|
| 306 |
-
maximum=1.5,
|
| 307 |
-
value=1.0,
|
| 308 |
-
step=0.1,
|
| 309 |
-
label="Whole Video Playback Speed",
|
| 310 |
-
info="0.5x = half speed, 1.0x = normal speed, 1.5x = 50% faster. Audio speed changes too.",
|
| 311 |
-
)
|
| 312 |
-
|
| 313 |
with gr.Row():
|
| 314 |
extract_btn = gr.Button("🎞️ Extract From Time", variant="primary")
|
| 315 |
-
|
| 316 |
|
| 317 |
gallery = gr.Gallery(label="Extracted Frames", columns=5, object_fit="contain")
|
| 318 |
state = gr.State()
|
| 319 |
status = gr.Textbox(label="Status")
|
| 320 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 321 |
copy_btn = gr.Button("📋 Compile Final Video", variant="primary")
|
| 322 |
output = gr.Video(label="Final Video")
|
| 323 |
output_status = gr.Textbox(label="Output Status")
|
|
@@ -327,13 +304,11 @@ with gr.Blocks(title="Video Frame Editor") as demo:
|
|
| 327 |
inputs=[video, start, count],
|
| 328 |
outputs=[gallery, state, status],
|
| 329 |
)
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
outputs=[gallery, state, status],
|
| 335 |
)
|
| 336 |
-
|
| 337 |
copy_btn.click(
|
| 338 |
fn=make_video,
|
| 339 |
inputs=[state, video, audio_in, trim_count, speed_factor],
|
|
|
|
| 75 |
return [], None, str(e)
|
| 76 |
|
| 77 |
|
| 78 |
+
def clear_extracted_frames():
|
| 79 |
+
"""Clear the selected frame sequence and reset trimming tied to that sequence."""
|
| 80 |
+
return (
|
| 81 |
+
[],
|
| 82 |
+
None,
|
| 83 |
+
"✅ Extracted frames cleared. The final video will not have a frame intro.",
|
| 84 |
+
0,
|
| 85 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
|
| 87 |
|
| 88 |
+
def stream_has_audio(path):
|
| 89 |
+
"""Return True when a media file contains an audio stream."""
|
| 90 |
+
if not path:
|
| 91 |
+
return False
|
| 92 |
+
probe = subprocess.run(
|
| 93 |
+
[
|
| 94 |
+
"ffprobe", "-v", "error", "-select_streams", "a:0",
|
| 95 |
+
"-show_entries", "stream=index", "-of", "csv=p=0", path,
|
| 96 |
+
],
|
| 97 |
+
capture_output=True, text=True,
|
| 98 |
+
)
|
| 99 |
+
return probe.returncode == 0 and bool(probe.stdout.strip())
|
| 100 |
+
|
| 101 |
|
| 102 |
def apply_speed_to_video(input_path, speed_factor):
|
| 103 |
+
"""Change playback speed of the complete video and its audio together."""
|
| 104 |
speed_factor = float(speed_factor)
|
| 105 |
if not 0.5 <= speed_factor <= 1.5:
|
| 106 |
raise ValueError("Playback speed must be between 0.5x and 1.5x.")
|
| 107 |
if abs(speed_factor - 1.0) < 1e-9:
|
| 108 |
return input_path
|
| 109 |
|
| 110 |
+
has_audio = stream_has_audio(input_path)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
sped_path = os.path.join(
|
| 112 |
tempfile.mkdtemp(), f"speed_{int(time.time() * 1000)}.mp4"
|
| 113 |
)
|
| 114 |
+
cmd = ["ffmpeg", "-y", "-i", input_path]
|
|
|
|
|
|
|
|
|
|
| 115 |
if has_audio:
|
| 116 |
+
cmd += [
|
|
|
|
| 117 |
"-filter_complex",
|
| 118 |
+
f"[0:v:0]setpts=PTS/{speed_factor:.6f}[v];"
|
| 119 |
+
f"[0:a:0]atempo={speed_factor:.6f}[a]",
|
| 120 |
"-map", "[v]", "-map", "[a]",
|
| 121 |
"-c:v", "libx264", "-crf", "18", "-preset", "medium",
|
| 122 |
+
"-pix_fmt", "yuv420p",
|
| 123 |
"-c:a", "aac", "-b:a", "192k",
|
| 124 |
"-movflags", "+faststart", sped_path,
|
| 125 |
]
|
| 126 |
else:
|
| 127 |
+
cmd += [
|
| 128 |
+
"-vf", f"setpts=PTS/{speed_factor:.6f}",
|
| 129 |
"-map", "0:v:0", "-an",
|
| 130 |
"-c:v", "libx264", "-crf", "18", "-preset", "medium",
|
| 131 |
+
"-pix_fmt", "yuv420p", "-movflags", "+faststart", sped_path,
|
| 132 |
]
|
| 133 |
|
| 134 |
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
|
|
| 137 |
return sped_path
|
| 138 |
|
| 139 |
|
| 140 |
+
def add_speed_message(msg, speed_factor):
|
| 141 |
+
if abs(float(speed_factor) - 1.0) < 1e-9:
|
| 142 |
+
return msg + " Playback speed: normal (1.0x)."
|
| 143 |
+
if speed_factor < 1.0:
|
| 144 |
+
return msg + f" Entire video slowed to {speed_factor:.1f}x."
|
| 145 |
+
return msg + f" Entire video sped up to {speed_factor:.1f}x."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
|
| 147 |
|
| 148 |
def make_video(s, video_in, audio_in, trim_frames=0, speed_factor=1.0):
|
| 149 |
+
"""Compile frames before the source video, then apply speed to the complete result."""
|
| 150 |
v_path = p(video_in)
|
| 151 |
audio_path = p(audio_in)
|
|
|
|
| 152 |
if not v_path:
|
| 153 |
return None, "Upload a video first."
|
| 154 |
|
|
|
|
| 161 |
if not 0.5 <= speed_factor <= 1.5:
|
| 162 |
return None, "Playback speed must be between 0.5x and 1.5x."
|
| 163 |
if trim_frames > 0 and not has_frames:
|
| 164 |
+
return None, "Extract frames first to use initial-frame trimming."
|
| 165 |
+
|
| 166 |
+
# With no extracted frames, keep the original video (and its original audio)
|
| 167 |
+
# unless the user supplies replacement audio.
|
| 168 |
+
if not has_frames:
|
| 169 |
+
_, _, _, duration = meta(v_path)
|
| 170 |
+
if not audio_path:
|
| 171 |
+
out = v_path
|
| 172 |
+
msg = "✅ Final video created from the original video. No extracted-frame intro was added."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 173 |
else:
|
| 174 |
+
out = os.path.join(tempfile.mkdtemp(), f"final_audio_{int(time.time() * 1000)}.mp4")
|
| 175 |
+
replacement_duration = audio_meta(audio_path)
|
| 176 |
+
loop_args = ["-stream_loop", "-1"] if replacement_duration > 0 and (duration - replacement_duration) > 1.0 else []
|
| 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(duration),
|
| 181 |
+
"-c:v", "copy", "-c:a", "aac", "-b:a", "192k",
|
| 182 |
+
"-movflags", "+faststart", out,
|
| 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 |
+
out = apply_speed_to_video(out, speed_factor)
|
| 189 |
+
return out, add_speed_message(msg, speed_factor)
|
| 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(fps),
|
| 202 |
+
"-i", os.path.join(frame_dir, "f_%06d.png"),
|
| 203 |
+
"-c:v", "libx264", "-crf", "18", "-preset", "medium",
|
| 204 |
+
"-pix_fmt", "yuv420p", intro,
|
| 205 |
+
],
|
| 206 |
+
capture_output=True, text=True,
|
| 207 |
+
)
|
|
|
|
|
|
|
|
|
|
| 208 |
if r.returncode:
|
| 209 |
+
return None, r.stderr or "Could not compile extracted frames."
|
| 210 |
+
|
| 211 |
+
intro_duration = len(frames) / fps
|
| 212 |
+
total_duration = intro_duration + (source_duration - trim_seconds)
|
| 213 |
+
out = os.path.join(tempfile.mkdtemp(), f"final_{int(time.time() * 1000)}.mp4")
|
| 214 |
+
video_concat = (
|
| 215 |
+
f"[1:v:0]trim=start_frame={trim_frames},setpts=PTS-STARTPTS[bodyv];"
|
| 216 |
+
"[0:v:0][bodyv]concat=n=2:v=1:a=0[vout]"
|
| 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 = ["ffmpeg", "-y", "-i", intro, "-i", frame_state_video] + loop_args + [
|
| 223 |
+
"-i", audio_path,
|
| 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 |
+
cmd = [
|
| 233 |
+
"ffmpeg", "-y", "-i", intro, "-i", frame_state_video,
|
| 234 |
+
"-filter_complex", video_concat,
|
| 235 |
+
"-map", "[vout]", "-map", "1:a?",
|
| 236 |
+
"-t", str(total_duration),
|
| 237 |
+
"-c:v", "libx264", "-crf", "18", "-preset", "medium",
|
| 238 |
+
"-pix_fmt", "yuv420p", "-c:a", "aac", "-b:a", "192k",
|
| 239 |
+
"-movflags", "+faststart", out,
|
| 240 |
+
]
|
| 241 |
+
|
| 242 |
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
| 243 |
+
if result.returncode:
|
| 244 |
+
return None, result.stderr or "Could not compile the final video."
|
| 245 |
+
|
| 246 |
+
msg = f"✅ Final video created. Added {len(frames)} extracted frame(s)."
|
| 247 |
if trim_frames:
|
| 248 |
msg += f" Removed the first {trim_frames} source frame(s) ({trim_seconds:.3f}s)."
|
| 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 += " Original video audio retained when available."
|
| 256 |
+
out = apply_speed_to_video(out, speed_factor)
|
| 257 |
+
return out, add_speed_message(msg, speed_factor)
|
| 258 |
|
| 259 |
except Exception as e:
|
| 260 |
return None, str(e)
|
|
|
|
| 275 |
minimum=0,
|
| 276 |
precision=0,
|
| 277 |
label="Initial Frames to Remove from Original Video",
|
| 278 |
+
info="Set to 0 to keep the original beginning. Applies when extracted frames are added first.",
|
| 279 |
)
|
| 280 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 281 |
with gr.Row():
|
| 282 |
extract_btn = gr.Button("🎞️ Extract From Time", variant="primary")
|
| 283 |
+
clear_btn = gr.Button("🧹 Clear Extracted Frames")
|
| 284 |
|
| 285 |
gallery = gr.Gallery(label="Extracted Frames", columns=5, object_fit="contain")
|
| 286 |
state = gr.State()
|
| 287 |
status = gr.Textbox(label="Status")
|
| 288 |
|
| 289 |
+
# Keep the whole-video speed control immediately above the compile button.
|
| 290 |
+
speed_factor = gr.Slider(
|
| 291 |
+
minimum=0.5,
|
| 292 |
+
maximum=1.5,
|
| 293 |
+
value=1.0,
|
| 294 |
+
step=0.1,
|
| 295 |
+
label="Whole Video Playback Speed",
|
| 296 |
+
info="0.5x = half speed, 1.0x = normal speed, 1.5x = 50% faster. Applies to the entire final video and its audio.",
|
| 297 |
+
)
|
| 298 |
copy_btn = gr.Button("📋 Compile Final Video", variant="primary")
|
| 299 |
output = gr.Video(label="Final Video")
|
| 300 |
output_status = gr.Textbox(label="Output Status")
|
|
|
|
| 304 |
inputs=[video, start, count],
|
| 305 |
outputs=[gallery, state, status],
|
| 306 |
)
|
| 307 |
+
clear_btn.click(
|
| 308 |
+
fn=clear_extracted_frames,
|
| 309 |
+
inputs=[],
|
| 310 |
+
outputs=[gallery, state, status, trim_count],
|
|
|
|
| 311 |
)
|
|
|
|
| 312 |
copy_btn.click(
|
| 313 |
fn=make_video,
|
| 314 |
inputs=[state, video, audio_in, trim_count, speed_factor],
|