Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -76,12 +76,11 @@ def save_frames(v, t, n):
|
|
| 76 |
|
| 77 |
|
| 78 |
def clear_extracted_frames():
|
| 79 |
-
"""Clear the
|
| 80 |
return (
|
| 81 |
[],
|
| 82 |
None,
|
| 83 |
-
"✅ Extracted frames cleared. The final video will not have a frame intro.",
|
| 84 |
-
0,
|
| 85 |
)
|
| 86 |
|
| 87 |
|
|
@@ -164,11 +163,9 @@ def make_video(s, video_in, audio_in, trim_frames=0, speed_factor=1.0):
|
|
| 164 |
return None, "Initial frames to remove must be 0 or greater."
|
| 165 |
if not 0.5 <= speed_factor <= 1.5:
|
| 166 |
return None, "Playback speed must be between 0.5x and 1.5x."
|
| 167 |
-
|
| 168 |
-
|
| 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:
|
|
@@ -178,27 +175,80 @@ def make_video(s, video_in, audio_in, trim_frames=0, speed_factor=1.0):
|
|
| 178 |
)
|
| 179 |
|
| 180 |
if not has_frames:
|
| 181 |
-
#
|
| 182 |
-
#
|
| 183 |
-
|
| 184 |
-
return None, "Extract frames first to use initial-frame trimming."
|
| 185 |
base_out = v_path
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
"
|
| 196 |
-
|
| 197 |
-
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
result = subprocess.run(cmd, capture_output=True, text=True)
|
| 199 |
if result.returncode:
|
| 200 |
-
return None, result.stderr or "Could not
|
| 201 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 202 |
final_out = apply_speed_to_video(base_out, speed_factor)
|
| 203 |
return final_out, add_speed_message(msg, speed_factor)
|
| 204 |
|
|
@@ -345,7 +395,7 @@ with gr.Blocks(title="Video Frame Editor") as demo:
|
|
| 345 |
clear_btn.click(
|
| 346 |
fn=clear_extracted_frames,
|
| 347 |
inputs=[],
|
| 348 |
-
outputs=[gallery, state, status
|
| 349 |
)
|
| 350 |
copy_btn.click(
|
| 351 |
fn=make_video,
|
|
|
|
| 76 |
|
| 77 |
|
| 78 |
def clear_extracted_frames():
|
| 79 |
+
"""Clear only the extracted-frame sequence; keep the independent trim setting unchanged."""
|
| 80 |
return (
|
| 81 |
[],
|
| 82 |
None,
|
| 83 |
+
"✅ Extracted frames cleared. The final video will not have a frame intro. Initial-frame trimming is unchanged.",
|
|
|
|
| 84 |
)
|
| 85 |
|
| 86 |
|
|
|
|
| 163 |
return None, "Initial frames to remove must be 0 or greater."
|
| 164 |
if not 0.5 <= speed_factor <= 1.5:
|
| 165 |
return None, "Playback speed must be between 0.5x and 1.5x."
|
| 166 |
+
# Always use the currently selected input as the original-video segment. Extracted-frame
|
| 167 |
+
# state is used only to build the optional intro. Source-video trimming works independently,
|
| 168 |
+
# whether or not any frames have been extracted.
|
|
|
|
|
|
|
| 169 |
_, _, source_fps, source_duration = meta(v_path)
|
| 170 |
trim_seconds = trim_frames / source_fps
|
| 171 |
if trim_frames and trim_seconds >= source_duration:
|
|
|
|
| 175 |
)
|
| 176 |
|
| 177 |
if not has_frames:
|
| 178 |
+
# No intro frames: create an output from the original video alone, applying any
|
| 179 |
+
# requested frame trim and/or replacement audio. This is intentionally independent
|
| 180 |
+
# of the extraction controls.
|
|
|
|
| 181 |
base_out = v_path
|
| 182 |
+
body_duration = max(0.0, source_duration - trim_seconds)
|
| 183 |
+
has_source_audio = stream_has_audio(v_path)
|
| 184 |
+
needs_processing = bool(trim_frames or audio_path)
|
| 185 |
+
|
| 186 |
+
if needs_processing:
|
| 187 |
+
base_out = os.path.join(
|
| 188 |
+
tempfile.mkdtemp(), f"source_only_{int(time.time() * 1000)}.mp4"
|
| 189 |
+
)
|
| 190 |
+
video_filter = (
|
| 191 |
+
f"[0:v:0]trim=start_frame={trim_frames},setpts=PTS-STARTPTS[vout]"
|
| 192 |
+
)
|
| 193 |
+
cmd = ["ffmpeg", "-y", "-i", v_path]
|
| 194 |
+
|
| 195 |
+
if audio_path:
|
| 196 |
+
replacement_duration = audio_meta(audio_path)
|
| 197 |
+
loop_args = (
|
| 198 |
+
["-stream_loop", "-1"]
|
| 199 |
+
if replacement_duration > 0 and (body_duration - replacement_duration) > 1.0
|
| 200 |
+
else []
|
| 201 |
+
)
|
| 202 |
+
cmd += loop_args + ["-i", audio_path]
|
| 203 |
+
cmd += [
|
| 204 |
+
"-filter_complex", video_filter,
|
| 205 |
+
"-map", "[vout]", "-map", "1:a:0",
|
| 206 |
+
"-t", str(body_duration),
|
| 207 |
+
"-c:v", "libx264", "-crf", "18", "-preset", "medium",
|
| 208 |
+
"-pix_fmt", "yuv420p", "-c:a", "aac", "-b:a", "192k",
|
| 209 |
+
"-movflags", "+faststart", base_out,
|
| 210 |
+
]
|
| 211 |
+
elif has_source_audio:
|
| 212 |
+
audio_filter = (
|
| 213 |
+
f"{video_filter};[0:a:0]atrim=start={trim_seconds:.8f},"
|
| 214 |
+
"asetpts=PTS-STARTPTS[aout]"
|
| 215 |
+
)
|
| 216 |
+
cmd += [
|
| 217 |
+
"-filter_complex", audio_filter,
|
| 218 |
+
"-map", "[vout]", "-map", "[aout]",
|
| 219 |
+
"-t", str(body_duration),
|
| 220 |
+
"-c:v", "libx264", "-crf", "18", "-preset", "medium",
|
| 221 |
+
"-pix_fmt", "yuv420p", "-c:a", "aac", "-b:a", "192k",
|
| 222 |
+
"-movflags", "+faststart", base_out,
|
| 223 |
+
]
|
| 224 |
+
else:
|
| 225 |
+
cmd += [
|
| 226 |
+
"-filter_complex", video_filter,
|
| 227 |
+
"-map", "[vout]", "-an",
|
| 228 |
+
"-t", str(body_duration),
|
| 229 |
+
"-c:v", "libx264", "-crf", "18", "-preset", "medium",
|
| 230 |
+
"-pix_fmt", "yuv420p", "-movflags", "+faststart", base_out,
|
| 231 |
+
]
|
| 232 |
+
|
| 233 |
result = subprocess.run(cmd, capture_output=True, text=True)
|
| 234 |
if result.returncode:
|
| 235 |
+
return None, result.stderr or "Could not process the original video."
|
| 236 |
+
|
| 237 |
+
if trim_frames:
|
| 238 |
+
msg = (
|
| 239 |
+
"✅ Final video created from the original video with no extracted-frame intro. "
|
| 240 |
+
f"Skipped the first {trim_frames} frame(s) ({trim_seconds:.3f}s at "
|
| 241 |
+
f"{source_fps:.3f} FPS)."
|
| 242 |
+
)
|
| 243 |
+
else:
|
| 244 |
+
msg = "✅ Final video created from the original video. No extracted-frame intro was added."
|
| 245 |
+
if audio_path:
|
| 246 |
+
msg = "✅ Final video created from the original video with replacement audio and no extracted-frame intro."
|
| 247 |
+
elif has_source_audio and trim_frames:
|
| 248 |
+
msg += " Original audio was trimmed to stay aligned with the video."
|
| 249 |
+
elif not has_source_audio:
|
| 250 |
+
msg += " The source video has no audio track."
|
| 251 |
+
|
| 252 |
final_out = apply_speed_to_video(base_out, speed_factor)
|
| 253 |
return final_out, add_speed_message(msg, speed_factor)
|
| 254 |
|
|
|
|
| 395 |
clear_btn.click(
|
| 396 |
fn=clear_extracted_frames,
|
| 397 |
inputs=[],
|
| 398 |
+
outputs=[gallery, state, status],
|
| 399 |
)
|
| 400 |
copy_btn.click(
|
| 401 |
fn=make_video,
|