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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +280 -115
app.py CHANGED
@@ -1,179 +1,344 @@
1
- import gradio as gr, subprocess, os, tempfile, glob, time
 
 
 
 
 
 
2
 
3
  def p(x):
4
- return x if isinstance(x,str) else (x.get("path") or x.get("name") if isinstance(x,dict) else None)
 
5
 
6
  def meta(v):
7
- r=subprocess.run(["ffprobe","-v","error","-select_streams","v:0",
8
- "-show_entries","stream=width,height,r_frame_rate,duration",
9
- "-of","default=noprint_wrappers=1",v],capture_output=True,text=True,check=True)
10
- d=dict(x.split("=",1) for x in r.stdout.strip().splitlines())
11
- a=d["r_frame_rate"].split("/")
12
- return int(d["width"]),int(d["height"]),float(a[0])/float(a[1]),float(d["duration"])
 
 
 
 
 
 
13
 
14
  def audio_meta(a):
15
- """Fetches the duration of the audio file in seconds."""
16
  try:
17
- r = subprocess.run(["ffprobe","-v","error","-show_entries",
18
- "format=duration","-of","default=noprint_wrappers=1:nokey=1",a],
19
- capture_output=True,text=True,check=True)
 
 
 
 
20
  return float(r.stdout.strip())
21
  except Exception:
22
  return 0.0
23
 
24
- def save_frames(v,t,n):
25
- v=p(v)
26
- if not v:return [],None,"Upload a video."
 
 
27
  try:
28
- t=float(t); n=int(n); w,h,fps,dur=meta(v)
29
- if n<1:return [],None,"Number of frames must be at least 1."
30
- if t<0 or t>=dur:return [],None,f"Time must be between 0 and {dur:.3f}s."
31
-
32
- d=tempfile.mkdtemp(); pat=os.path.join(d,"f_%06d.png")
33
- r=subprocess.run(["ffmpeg","-y","-ss",str(t),"-i",v,
34
- "-frames:v",str(n),"-vsync","0",pat],capture_output=True,text=True)
35
- if r.returncode:return [],None,r.stderr
36
-
37
- fs=sorted(glob.glob(os.path.join(d,"f_*.png")))
38
- if not fs:return [],None,"No frames found."
39
-
40
- return [(f,f"Frame {i+1}") for i,f in enumerate(fs)],\
41
- {"v":v,"d":d,"fs":fs,"fps":fps,"w":w,"h":h, "dur":dur},\
42
- f"✅ Extracted {len(fs)} frame(s) from {t:.3f}s"
43
- except Exception as e:return [],None,str(e)
44
-
45
- def extract_last(v,n):
46
- v=p(v)
47
- if not v:return [],None,"Upload a video."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  try:
49
- n=int(n); w,h,fps,dur=meta(v)
50
- if n<1:return [],None,"Number of frames must be at least 1."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
 
52
- t=max(0,dur-(n/fps))
53
- return save_frames(v,t,n)
54
- except Exception as e:return [],None,str(e)
55
 
56
- def make_video(s, video_in, audio_in):
 
57
  v_path = p(video_in)
58
  audio_path = p(audio_in)
59
 
60
  if not v_path:
61
  return None, "Upload a video first."
62
-
63
- # If no frames extracted AND no audio uploaded
64
- if not s and not audio_path:
65
- return None, "Extract frames first OR upload an audio file to replace."
66
 
 
67
  try:
 
 
 
 
 
 
 
 
 
 
 
 
68
  out = os.path.join(tempfile.mkdtemp(), f"final_{int(time.time())}.mp4")
69
-
70
- # Check if we have extracted frames
71
- has_frames = bool(s and s.get("fs"))
72
-
73
  if has_frames:
74
  d_s, v, fs, fps, dur = s["d"], s["v"], s["fs"], s["fps"], s["dur"]
75
- total_vid_dur = dur + (len(fs) / fps)
 
 
 
 
 
76
  intro = os.path.join(d_s, "intro.mp4")
77
-
78
- # 1. Create intro block
79
- r = subprocess.run(["ffmpeg","-y","-framerate",str(fps),
80
- "-i",os.path.join(d_s,"f_%06d.png"),
81
- "-c:v","libx264","-crf","18","-preset","medium",
82
- "-pix_fmt","yuv420p",intro], capture_output=True, text=True)
83
- if r.returncode: return None, r.stderr
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  else:
85
- # Fallback if no frames were extracted
86
  _, _, _, dur = meta(v_path)
87
  total_vid_dur = dur
88
- v = v_path # Just use the original video
89
  fs = []
 
 
 
90
 
91
- # 2. Combine Logic
92
  if audio_path:
93
  a_dur = audio_meta(audio_path)
94
  loop_args = ["-stream_loop", "-1"] if (total_vid_dur - a_dur) > 1.0 else []
95
-
96
  if has_frames:
97
- # Scenario A: Frames + Audio Replacement
98
- cmd = ["ffmpeg", "-y", "-i", intro, "-i", v] + loop_args + ["-i", audio_path,
99
- "-filter_complex", "[0:v][1:v]concat=n=2:v=1:a=0[v]",
100
- "-map", "[v]", "-map", "2:a",
101
- "-t", str(total_vid_dur),
102
- "-c:v", "libx264", "-crf", "18", "-preset", "medium",
103
- "-c:a", "aac", "-b:a", "192k", "-movflags", "+faststart", out]
 
 
 
 
 
104
  else:
105
- # Scenario B: NO Frames + Audio Replacement ONLY
106
- # We use "-c:v copy" here which is instantaneous since we aren't merging videos
107
- cmd = ["ffmpeg", "-y", "-i", v] + loop_args + ["-i", audio_path,
108
- "-map", "0:v", "-map", "1:a",
109
- "-t", str(total_vid_dur),
110
- "-c:v", "copy",
111
- "-c:a", "aac", "-b:a", "192k", "-movflags", "+faststart", out]
112
-
113
- r = subprocess.run(cmd, capture_output=True, text=True)
114
- if r.returncode: return None, r.stderr
115
-
116
- msg = f"✅ Final video created."
117
- if has_frames: msg += f" Added {len(fs)} frame(s)."
118
- if loop_args: msg += f" Audio looped (was {total_vid_dur - a_dur:.1f}s short)."
119
- else: msg += " Audio matched to video length."
120
- return out, msg
121
-
122
- else:
123
- # Scenario C: Frames added + Keep Original Audio
124
- cmd = ["ffmpeg","-y","-i",intro,"-i",v,
125
- "-filter_complex","[0:v][1:v]concat=n=2:v=1:a=0[v]",
126
- "-map","[v]","-map","1:a?",
127
- "-c:v","libx264","-crf","18","-preset","medium",
128
- "-c:a","copy","-movflags","+faststart",out]
129
-
130
  r = subprocess.run(cmd, capture_output=True, text=True)
131
- if r.returncode: return None, r.stderr
132
- return out, f"✅ Added {len(fs)} frame(s) to beginning (original audio kept)."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
 
134
  except Exception as e:
135
  return None, str(e)
136
 
 
137
  with gr.Blocks(title="Video Frame Editor") as demo:
138
  gr.Markdown("# 🎬 Video Frame Editor")
139
 
140
  with gr.Row():
141
- video=gr.Video(label="Input Video")
142
- audio_in=gr.Audio(type="filepath", label="Upload New Final Audio (Optional)")
143
 
144
  with gr.Row():
145
- start=gr.Number(value=0,label="Start Time (seconds)")
146
- count=gr.Number(value=1,minimum=1,precision=0,label="Number of Frames")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
 
148
  with gr.Row():
149
- extract_btn=gr.Button("🎞️ Extract From Time",variant="primary")
150
- last_btn=gr.Button("⏮️ Extract From Last")
151
 
152
- gallery=gr.Gallery(label="Extracted Frames",columns=5,object_fit="contain")
153
- state=gr.State()
154
- status=gr.Textbox(label="Status")
155
 
156
- copy_btn=gr.Button("📋 Compile Final Video",variant="primary")
157
- output=gr.Video(label="Final Video")
158
- output_status=gr.Textbox(label="Output Status")
159
 
160
  extract_btn.click(
161
  fn=save_frames,
162
- inputs=[video,start,count],
163
- outputs=[gallery,state,status]
164
  )
165
 
166
  last_btn.click(
167
  fn=extract_last,
168
- inputs=[video,count],
169
- outputs=[gallery,state,status]
170
  )
171
 
172
- # We now pass the original 'video' variable directly alongside state and audio
173
  copy_btn.click(
174
  fn=make_video,
175
- inputs=[state, video, audio_in],
176
- outputs=[output,output_status]
177
  )
178
 
179
- demo.launch(server_name="0.0.0.0",server_port=7860)
 
 
1
+ import gradio as gr
2
+ import subprocess
3
+ import os
4
+ import tempfile
5
+ import glob
6
+ import time
7
+
8
 
9
  def p(x):
10
+ return x if isinstance(x, str) else (x.get("path") or x.get("name") if isinstance(x, dict) else None)
11
+
12
 
13
  def meta(v):
14
+ r = subprocess.run(
15
+ [
16
+ "ffprobe", "-v", "error", "-select_streams", "v:0",
17
+ "-show_entries", "stream=width,height,r_frame_rate,duration",
18
+ "-of", "default=noprint_wrappers=1", v,
19
+ ],
20
+ capture_output=True, text=True, check=True,
21
+ )
22
+ d = dict(x.split("=", 1) for x in r.stdout.strip().splitlines())
23
+ a = d["r_frame_rate"].split("/")
24
+ return int(d["width"]), int(d["height"]), float(a[0]) / float(a[1]), float(d["duration"])
25
+
26
 
27
  def audio_meta(a):
28
+ """Fetch the duration of an audio file in seconds."""
29
  try:
30
+ r = subprocess.run(
31
+ [
32
+ "ffprobe", "-v", "error", "-show_entries", "format=duration",
33
+ "-of", "default=noprint_wrappers=1:nokey=1", a,
34
+ ],
35
+ capture_output=True, text=True, check=True,
36
+ )
37
  return float(r.stdout.strip())
38
  except Exception:
39
  return 0.0
40
 
41
+
42
+ def save_frames(v, t, n):
43
+ v = p(v)
44
+ if not v:
45
+ return [], None, "Upload a video."
46
  try:
47
+ t = float(t)
48
+ n = int(n)
49
+ w, h, fps, dur = meta(v)
50
+ if n < 1:
51
+ return [], None, "Number of frames must be at least 1."
52
+ if t < 0 or t >= dur:
53
+ return [], None, f"Time must be between 0 and {dur:.3f}s."
54
+
55
+ d = tempfile.mkdtemp()
56
+ pat = os.path.join(d, "f_%06d.png")
57
+ r = subprocess.run(
58
+ ["ffmpeg", "-y", "-ss", str(t), "-i", v,
59
+ "-frames:v", str(n), "-vsync", "0", pat],
60
+ capture_output=True, text=True,
61
+ )
62
+ if r.returncode:
63
+ return [], None, r.stderr
64
+
65
+ fs = sorted(glob.glob(os.path.join(d, "f_*.png")))
66
+ if not fs:
67
+ return [], None, "No frames found."
68
+
69
+ return (
70
+ [(f, f"Frame {i + 1}") for i, f in enumerate(fs)],
71
+ {"v": v, "d": d, "fs": fs, "fps": fps, "w": w, "h": h, "dur": dur},
72
+ f"✅ Extracted {len(fs)} frame(s) from {t:.3f}s",
73
+ )
74
+ except Exception as e:
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)
137
+ if result.returncode:
138
+ raise RuntimeError(result.stderr or "Could not change playback speed.")
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
 
164
+ has_frames = bool(s and s.get("fs"))
165
  try:
166
+ trim_frames = int(trim_frames or 0)
167
+ speed_factor = float(speed_factor if speed_factor is not None else 1.0)
168
+ if trim_frames < 0:
169
+ return None, "Initial frames to remove must be 0 or greater."
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)
284
 
285
+
286
  with gr.Blocks(title="Video Frame Editor") as demo:
287
  gr.Markdown("# 🎬 Video Frame Editor")
288
 
289
  with gr.Row():
290
+ video = gr.Video(label="Input Video")
291
+ audio_in = gr.Audio(type="filepath", label="Upload New Final Audio (Optional)")
292
 
293
  with gr.Row():
294
+ start = gr.Number(value=0, label="Start Time (seconds)")
295
+ count = gr.Number(value=1, minimum=1, precision=0, label="Number of Frames to Extract")
296
+ trim_count = gr.Number(
297
+ value=0,
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")
324
 
325
  extract_btn.click(
326
  fn=save_frames,
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],
340
+ outputs=[output, output_status],
341
  )
342
 
343
+
344
+ demo.launch(server_name="0.0.0.0", server_port=7860)