devendergarg14 commited on
Commit
f99f3d5
·
verified ·
1 Parent(s): ecb1111

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +142 -167
app.py CHANGED
@@ -75,62 +75,60 @@ def save_frames(v, t, n):
75
  return [], None, str(e)
76
 
77
 
78
- def extract_last(v, n):
79
- v = p(v)
80
- if not v:
81
- return [], None, "Upload a video."
82
- try:
83
- n = int(n)
84
- _, _, fps, dur = meta(v)
85
- if n < 1:
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 the playback speed of the complete compiled video and its audio."""
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
- # Detect whether the compiled video has an audio stream.
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
- # setpts makes the video faster/slower; atempo applies the same factor to audio.
120
- cmd = common + [
121
  "-filter_complex",
122
- f"[0:v:0]setpts=PTS/{speed_factor}[v];[0:a:0]atempo={speed_factor}[a]",
 
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 = common + [
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 finish_video(out, msg, speed_factor):
143
- """Apply the selected speed to the full result and update the status message."""
144
- speed_factor = float(speed_factor)
145
- if abs(speed_factor - 1.0) < 1e-9:
146
- msg += " Playback speed: normal (1.0x)."
147
- else:
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 extracted frames before the source video, optionally skipping its first N frames."""
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 the initial-frame trimming feature."
174
-
175
- if not has_frames and not audio_path:
176
- return None, "Extract frames first OR upload an audio file to replace."
177
-
178
- out = os.path.join(tempfile.mkdtemp(), f"final_{int(time.time())}.mp4")
179
-
180
- if has_frames:
181
- d_s, v, fs, fps, dur = s["d"], s["v"], s["fs"], s["fps"], s["dur"]
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
- cmd = (
238
- ["ffmpeg", "-y", "-i", v]
239
- + loop_args
240
- + [
241
- "-i", audio_path,
242
- "-map", "0:v", "-map", "1:a",
243
- "-t", str(total_vid_dur),
244
- "-c:v", "copy", "-c:a", "aac", "-b:a", "192k",
245
- "-movflags", "+faststart", out,
246
- ]
247
- )
248
-
249
- r = subprocess.run(cmd, capture_output=True, text=True)
250
- if r.returncode:
251
- return None, r.stderr
252
-
253
- msg = "✅ Final video created."
254
- if has_frames:
255
- msg += f" Added {len(fs)} frame(s)."
256
- if trim_frames:
257
- msg += f" Removed the first {trim_frames} source frame(s) ({trim_seconds:.3f}s)."
258
- if loop_args:
259
- msg += f" Audio looped (was {total_vid_dur - a_dur:.1f}s short)."
260
- else:
261
- msg += " Audio matched to video length."
262
- return finish_video(out, msg, speed_factor)
263
-
264
- # Keep the source video's audio if no replacement audio was uploaded.
265
- cmd = [
266
- "ffmpeg", "-y", "-i", intro, "-i", v,
267
- "-filter_complex", concat_filter,
268
- "-map", "[v]", "-map", "1:a?",
269
- "-t", str(total_vid_dur),
270
- "-c:v", "libx264", "-crf", "18", "-preset", "medium",
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
- msg = f"✅ Added {len(fs)} frame(s) to the beginning (original audio kept)."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
278
  if trim_frames:
279
  msg += f" Removed the first {trim_frames} source frame(s) ({trim_seconds:.3f}s)."
280
- return finish_video(out, msg, speed_factor)
 
 
 
 
 
 
 
 
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. This applies when extracted frames are added first.",
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
- last_btn = gr.Button("⏮️ Extract From Last")
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
- last_btn.click(
332
- fn=extract_last,
333
- inputs=[video, count],
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],