devendergarg14 commited on
Commit
fca8c0f
·
verified ·
1 Parent(s): 4544301

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +91 -584
app.py CHANGED
@@ -1,599 +1,106 @@
1
- import gradio as gr
2
- import os
3
- import subprocess
4
- import tempfile
5
- import shutil
6
- import time
7
- import glob
8
-
9
-
10
- # =========================================================
11
- # Helpers
12
- # =========================================================
13
-
14
- def run_cmd(cmd):
15
- result = subprocess.run(
16
- cmd,
17
- stdout=subprocess.PIPE,
18
- stderr=subprocess.PIPE,
19
- text=True,
20
- check=False
21
- )
22
- return result
23
-
24
-
25
- def get_video_info(video_path):
26
- """
27
- Get original video properties.
28
- """
29
- if not video_path or not os.path.exists(video_path):
30
- raise ValueError("Video file not found.")
31
-
32
- # Resolution
33
- cmd = [
34
- "ffprobe",
35
- "-v", "error",
36
- "-select_streams", "v:0",
37
- "-show_entries", "stream=width,height",
38
- "-of", "csv=p=0:s=x",
39
- video_path
40
- ]
41
-
42
- result = run_cmd(cmd)
43
-
44
- if result.returncode != 0:
45
- raise RuntimeError(result.stderr)
46
-
47
- width, height = map(int, result.stdout.strip().split("x"))
48
-
49
- # FPS
50
- cmd = [
51
- "ffprobe",
52
- "-v", "error",
53
- "-select_streams", "v:0",
54
- "-show_entries", "stream=r_frame_rate",
55
- "-of", "default=noprint_wrappers=1:nokey=1",
56
- video_path
57
- ]
58
-
59
- result = run_cmd(cmd)
60
-
61
- if result.returncode != 0:
62
- raise RuntimeError(result.stderr)
63
-
64
- fps_text = result.stdout.strip()
65
-
66
- if "/" in fps_text:
67
- num, den = fps_text.split("/")
68
- fps = float(num) / float(den)
69
- else:
70
- fps = float(fps_text)
71
-
72
- # Duration
73
- cmd = [
74
- "ffprobe",
75
- "-v", "error",
76
- "-show_entries", "format=duration",
77
- "-of", "default=noprint_wrappers=1:nokey=1",
78
- video_path
79
- ]
80
-
81
- result = run_cmd(cmd)
82
-
83
- if result.returncode != 0:
84
- raise RuntimeError(result.stderr)
85
-
86
- duration = float(result.stdout.strip())
87
-
88
- # Audio presence
89
- cmd = [
90
- "ffprobe",
91
- "-v", "error",
92
- "-select_streams", "a:0",
93
- "-show_entries", "stream=index",
94
- "-of", "csv=p=0",
95
- video_path
96
- ]
97
-
98
- result = run_cmd(cmd)
99
-
100
- has_audio = bool(result.stdout.strip())
101
-
102
- return {
103
- "width": width,
104
- "height": height,
105
- "fps": fps,
106
- "duration": duration,
107
- "has_audio": has_audio
108
- }
109
-
110
-
111
- def format_time(seconds):
112
- seconds = float(seconds)
113
-
114
- hours = int(seconds // 3600)
115
- minutes = int((seconds % 3600) // 60)
116
- secs = seconds % 60
117
-
118
- if hours > 0:
119
- return f"{hours:02d}:{minutes:02d}:{secs:06.3f}"
120
-
121
- return f"{minutes:02d}:{secs:06.3f}"
122
-
123
-
124
- # =========================================================
125
- # Extract frames
126
- # =========================================================
127
-
128
- def extract_frames(video_path, start_time, frame_count):
129
- """
130
- Extract consecutive frames beginning at start_time.
131
- """
132
-
133
- if not video_path:
134
- return [], None, "Please upload a video first."
135
-
136
  try:
137
- start_time = float(start_time)
138
- frame_count = int(frame_count)
139
-
140
- if start_time < 0:
141
- return [], None, "Start time cannot be negative."
142
-
143
- if frame_count < 1:
144
- return [], None, "Number of frames must be at least 1."
145
-
146
- info = get_video_info(video_path)
147
-
148
- if start_time >= info["duration"]:
149
- return [], None, (
150
- f"Start time is beyond the video duration "
151
- f"({format_time(info['duration'])})."
152
- )
153
-
154
- max_possible = int(
155
- (info["duration"] - start_time) * info["fps"]
156
- )
157
-
158
- if frame_count > max_possible:
159
- frame_count = max_possible
160
-
161
- if frame_count < 1:
162
- return [], None, "No frames are available at that time."
163
-
164
- work_dir = tempfile.mkdtemp(prefix="frame_editor_")
165
- frame_pattern = os.path.join(
166
- work_dir,
167
- "frame_%06d.png"
168
- )
169
-
170
- # Exact consecutive frames from selected point
171
- cmd = [
172
- "ffmpeg",
173
- "-y",
174
- "-ss", str(start_time),
175
- "-i", video_path,
176
- "-frames:v", str(frame_count),
177
- "-vsync", "0",
178
- frame_pattern
179
- ]
180
-
181
- result = run_cmd(cmd)
182
-
183
- if result.returncode != 0:
184
- shutil.rmtree(work_dir, ignore_errors=True)
185
-
186
- return [], None, (
187
- "Frame extraction failed:\n\n"
188
- + result.stderr
189
- )
190
-
191
- frames = sorted(
192
- glob.glob(
193
- os.path.join(work_dir, "frame_*.png")
194
- )
195
- )
196
-
197
- if not frames:
198
- shutil.rmtree(work_dir, ignore_errors=True)
199
-
200
- return [], None, "No frames could be extracted."
201
-
202
- # Gallery captions
203
- gallery_items = []
204
-
205
- for i, frame_path in enumerate(frames):
206
- frame_time = start_time + (i / info["fps"])
207
-
208
- gallery_items.append(
209
- (
210
- frame_path,
211
- f"Frame {i + 1} • {format_time(frame_time)}"
212
- )
213
- )
214
-
215
- state = {
216
- "work_dir": work_dir,
217
- "frames": frames,
218
- "video_path": video_path,
219
- "start_time": start_time,
220
- "frame_count": len(frames),
221
- "info": info
222
- }
223
-
224
- message = (
225
- f"Extracted {len(frames)} frame(s)\n"
226
- f"Starting at {format_time(start_time)}\n"
227
- f"Original FPS: {info['fps']:.3f}\n"
228
- f"Resolution: {info['width']} × {info['height']}"
229
- )
230
-
231
- return gallery_items, state, message
232
-
233
  except Exception as e:
234
- return [], None, f"Error: {str(e)}"
235
-
236
-
237
- # =========================================================
238
- # Prepend frames
239
- # =========================================================
240
-
241
- def prepend_frames(state):
242
- """
243
- Put the extracted frames at the beginning of the video.
244
- Original audio stream is preserved with -c:a copy.
245
- """
246
-
247
- if not state:
248
- return None, "Please extract frames first."
249
 
 
 
250
  try:
251
- work_dir = state["work_dir"]
252
- frames = state["frames"]
253
- video_path = state["video_path"]
254
- info = state["info"]
255
-
256
- if not frames:
257
- return None, "No extracted frames found."
258
-
259
- fps = info["fps"]
260
- width = info["width"]
261
- height = info["height"]
262
-
263
- # -------------------------------------------------
264
- # Create intro video from extracted PNG frames
265
- # -------------------------------------------------
266
-
267
- intro_video = os.path.join(
268
- work_dir,
269
- "intro.mp4"
270
- )
271
-
272
- # Use the exact source dimensions.
273
- # High-quality H.264 encoding.
274
- cmd = [
275
- "ffmpeg",
276
- "-y",
277
- "-framerate", str(fps),
278
- "-i", os.path.join(
279
- work_dir,
280
- "frame_%06d.png"
281
- ),
282
- "-vf",
283
- f"scale={width}:{height}:flags=lanczos,"
284
- f"format=yuv420p",
285
- "-c:v", "libx264",
286
- "-preset", "medium",
287
- "-crf", "18",
288
- "-movflags", "+faststart",
289
- intro_video
290
  ]
291
 
292
- result = run_cmd(cmd)
293
-
294
- if result.returncode != 0:
295
- return None, (
296
- "Could not create intro video:\n\n"
297
- + result.stderr
298
- )
299
-
300
- # -------------------------------------------------
301
- # Concatenate intro + original video
302
- #
303
- # Video must be rebuilt because new frames were
304
- # inserted. Audio is taken directly from original.
305
- # -------------------------------------------------
306
-
307
- output_dir = "edited_videos"
308
- os.makedirs(output_dir, exist_ok=True)
309
-
310
- output_path = os.path.join(
311
- output_dir,
312
- f"video_with_intro_{int(time.time())}.mp4"
313
- )
314
-
315
- concat_video = os.path.join(
316
- work_dir,
317
- "combined_video.mp4"
318
- )
319
-
320
- # Normalize both video sections to the same format
321
- # and concatenate them without touching the audio.
322
- filter_complex = (
323
- "[0:v]"
324
- f"scale={width}:{height}:flags=lanczos,"
325
- "format=yuv420p,"
326
- "setpts=PTS-STARTPTS[v0];"
327
-
328
- "[1:v]"
329
- f"scale={width}:{height}:flags=lanczos,"
330
- "format=yuv420p,"
331
- "setpts=PTS-STARTPTS[v1];"
332
-
333
- "[v0][v1]"
334
- "concat=n=2:v=1:a=0[outv]"
335
- )
336
-
337
- if info["has_audio"]:
338
-
339
- cmd = [
340
- "ffmpeg",
341
- "-y",
342
-
343
- "-i", intro_video,
344
- "-i", video_path,
345
-
346
- "-filter_complex", filter_complex,
347
-
348
- "-map", "[outv]",
349
- "-map", "1:a:0",
350
-
351
- # High visual quality
352
- "-c:v", "libx264",
353
- "-preset", "medium",
354
- "-crf", "18",
355
-
356
- # IMPORTANT:
357
- # original audio copied without re-encoding
358
- "-c:a", "copy",
359
-
360
- "-movflags", "+faststart",
361
-
362
- "-shortest",
363
-
364
- output_path
365
- ]
366
-
367
- else:
368
-
369
- cmd = [
370
- "ffmpeg",
371
- "-y",
372
-
373
- "-i", intro_video,
374
- "-i", video_path,
375
-
376
- "-filter_complex", filter_complex,
377
-
378
- "-map", "[outv]",
379
-
380
- "-c:v", "libx264",
381
- "-preset", "medium",
382
- "-crf", "18",
383
-
384
- "-movflags", "+faststart",
385
-
386
- output_path
387
- ]
388
-
389
- result = run_cmd(cmd)
390
-
391
- if result.returncode != 0:
392
- return None, (
393
- "Could not create final video:\n\n"
394
- + result.stderr
395
- )
396
-
397
- # -------------------------------------------------
398
- # Verify output
399
- # -------------------------------------------------
400
-
401
- if not os.path.exists(output_path):
402
- return None, "Final video was not created."
403
-
404
- original_duration = info["duration"]
405
- intro_duration = len(frames) / fps
406
- final_duration = original_duration + intro_duration
407
-
408
- audio_text = (
409
- "Original audio copied unchanged."
410
- if info["has_audio"]
411
- else "No audio track detected."
412
- )
413
-
414
- status = (
415
- "✅ Frames copied to the beginning.\n\n"
416
- f"Frames added: {len(frames)}\n"
417
- f"Added duration: {intro_duration:.3f} s\n"
418
- f"Original duration: {original_duration:.3f} s\n"
419
- f"Final duration: {final_duration:.3f} s\n"
420
- f"Resolution: {width} × {height}\n"
421
- f"FPS: {fps:.3f}\n\n"
422
- f"{audio_text}"
423
- )
424
-
425
- return output_path, status
426
 
 
 
427
  except Exception as e:
428
- return None, f"Error: {str(e)}"
429
-
430
-
431
- # =========================================================
432
- # Clear / reset
433
- # =========================================================
434
-
435
- def reset_editor():
436
- return (
437
- None,
438
- None,
439
- None,
440
- [],
441
- None,
442
- "Ready."
443
- )
444
 
 
 
445
 
446
- # =========================================================
447
- # Gradio UI
448
- # =========================================================
449
-
450
- with gr.Blocks(
451
- title="Frame Intro Video Editor"
452
- ) as demo:
453
-
454
- gr.Markdown(
455
- """
456
- # 🎬 Frame Intro Video Editor
457
-
458
- Extract frames from any point in a video, preview them,
459
- then copy those frames to the beginning of the video.
460
- """
461
- )
462
-
463
- # -----------------------------------------------------
464
- # Upload
465
- # -----------------------------------------------------
466
-
467
- video_input = gr.Video(
468
- label="Input Video",
469
- type="filepath"
470
- )
471
-
472
- # -----------------------------------------------------
473
- # Controls
474
- # -----------------------------------------------------
475
-
476
  with gr.Row():
477
-
478
- start_time = gr.Number(
479
- value=0,
480
- label="Start Time (seconds)",
481
- precision=3
482
- )
483
-
484
- frame_count = gr.Number(
485
- value=1,
486
- minimum=1,
487
- precision=0,
488
- label="Number of Frames"
489
- )
490
-
491
- extract_btn = gr.Button(
492
- "🎞️ Extract Frames",
493
- variant="primary"
494
- )
495
-
496
- # -----------------------------------------------------
497
- # Extracted frames
498
- # -----------------------------------------------------
499
-
500
- frames_output = gr.Gallery(
501
- label="Extracted Frames",
502
- columns=4,
503
- rows=1,
504
- height="auto",
505
- object_fit="contain",
506
- preview=True
507
- )
508
-
509
- frame_state = gr.State(None)
510
-
511
- extraction_status = gr.Textbox(
512
- label="Extraction Status",
513
- interactive=False
514
  )
515
 
516
- # -----------------------------------------------------
517
- # Copy to beginning
518
- # -----------------------------------------------------
519
-
520
- copy_btn = gr.Button(
521
- "📋 Copy Frames to Intro",
522
- variant="primary"
523
- )
524
-
525
- # -----------------------------------------------------
526
- # Final output
527
- # -----------------------------------------------------
528
-
529
- final_video = gr.Video(
530
- label="Final Video",
531
- autoplay=False,
532
- interactive=False
533
  )
534
 
535
- final_status = gr.Textbox(
536
- label="Final Status",
537
- interactive=False
538
- )
539
-
540
- # -----------------------------------------------------
541
- # Reset
542
- # -----------------------------------------------------
543
-
544
- reset_btn = gr.Button(
545
- "Reset"
546
- )
547
-
548
- # =====================================================
549
- # Events
550
- # =====================================================
551
-
552
- extract_btn.click(
553
- fn=extract_frames,
554
- inputs=[
555
- video_input,
556
- start_time,
557
- frame_count
558
- ],
559
- outputs=[
560
- frames_output,
561
- frame_state,
562
- extraction_status
563
- ]
564
- )
565
-
566
- copy_btn.click(
567
- fn=prepend_frames,
568
- inputs=[
569
- frame_state
570
- ],
571
- outputs=[
572
- final_video,
573
- final_status
574
- ]
575
- )
576
-
577
- reset_btn.click(
578
- fn=reset_editor,
579
- inputs=[],
580
- outputs=[
581
- video_input,
582
- start_time,
583
- frame_count,
584
- frames_output,
585
- final_video,
586
- final_status
587
- ]
588
- )
589
-
590
-
591
- # =========================================================
592
- # Launch
593
- # =========================================================
594
-
595
- if __name__ == "__main__":
596
- demo.launch(
597
- server_name="0.0.0.0",
598
- server_port=7860
599
- )
 
1
+ import gradio as gr, subprocess, os, tempfile, glob, time
2
+
3
+ def path(x):
4
+ if isinstance(x,str): return x
5
+ if isinstance(x,dict): return x.get("path") or x.get("name")
6
+ return None
7
+
8
+ def info(v):
9
+ r=subprocess.run([
10
+ "ffprobe","-v","error","-select_streams","v:0",
11
+ "-show_entries","stream=width,height,r_frame_rate",
12
+ "-of","default=noprint_wrappers=1",v
13
+ ],capture_output=True,text=True,check=True)
14
+ d={}
15
+ for x in r.stdout.strip().splitlines():
16
+ if "=" in x:
17
+ k,val=x.split("=",1); d[k]=val
18
+ a= d["r_frame_rate"].split("/")
19
+ return int(d["width"]),int(d["height"]),float(a[0])/float(a[1])
20
+
21
+ def extract(v,t,n):
22
+ v=path(v)
23
+ if not v: return [],None,"Upload a video."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  try:
25
+ t=float(t); n=int(n)
26
+ w,h,fps=info(v)
27
+ d=tempfile.mkdtemp()
28
+ pat=os.path.join(d,"f_%05d.png")
29
+ r=subprocess.run([
30
+ "ffmpeg","-y","-ss",str(t),"-i",v,
31
+ "-frames:v",str(n),"-vsync","0",pat
32
+ ],capture_output=True,text=True)
33
+ if r.returncode: return [],None,r.stderr
34
+ fs=sorted(glob.glob(os.path.join(d,"f_*.png")))
35
+ if not fs: return [],None,"No frames found."
36
+ return [(f,f"Frame {i+1}") for i,f in enumerate(fs)],{
37
+ "video":v,"dir":d,"frames":fs,"fps":fps,"w":w,"h":h
38
+ },f"Extracted {len(fs)} frame(s) at {t:.3f}s"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  except Exception as e:
40
+ return [],None,str(e)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
 
42
+ def prepend(s):
43
+ if not s: return None,"Extract frames first."
44
  try:
45
+ d,fs,v,fps,w,h=s["dir"],s["frames"],s["video"],s["fps"],s["w"],s["h"]
46
+ intro=os.path.join(d,"intro.mp4")
47
+ out=f"edited_{int(time.time())}.mp4"
48
+
49
+ r=subprocess.run([
50
+ "ffmpeg","-y","-framerate",str(fps),
51
+ "-i",os.path.join(d,"f_%05d.png"),
52
+ "-c:v","libx264","-crf","18","-preset","medium",
53
+ "-pix_fmt","yuv420p",intro
54
+ ],capture_output=True,text=True)
55
+ if r.returncode: return None,r.stderr
56
+
57
+ cmd=[
58
+ "ffmpeg","-y","-i",intro,"-i",v,
59
+ "-filter_complex",
60
+ f"[0:v]scale={w}:{h},setsar=1[v0];"
61
+ f"[1:v]scale={w}:{h},setsar=1[v1];"
62
+ "[v0][v1]concat=n=2:v=1:a=0[v]",
63
+ "-map","[v]"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  ]
65
 
66
+ # Keep original audio exactly as it is
67
+ r=subprocess.run([
68
+ *cmd,"-map","1:a?", "-c:v","libx264","-crf","18",
69
+ "-preset","medium","-c:a","copy","-movflags","+faststart",out
70
+ ],capture_output=True,text=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
 
72
+ if r.returncode: return None,r.stderr
73
+ return out,f"✅ Added {len(fs)} frame(s) to the beginning."
74
  except Exception as e:
75
+ return None,str(e)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
 
77
+ with gr.Blocks(title="Video Frame Editor") as demo:
78
+ gr.Markdown("# 🎬 Video Frame Editor")
79
 
80
+ video=gr.Video(label="Video")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  with gr.Row():
82
+ start=gr.Number(0,label="Start Time (seconds)")
83
+ count=gr.Number(1,label="Number of Frames",precision=0)
84
+
85
+ extract=gr.Button("Extract Frames",variant="primary")
86
+ gallery=gr.Gallery(label="Extracted Frames",columns=5,object_fit="contain")
87
+ state=gr.State()
88
+ status=gr.Textbox(label="Status")
89
+
90
+ copy=gr.Button("Copy Frames to Beginning",variant="primary")
91
+ output=gr.Video(label="Final Video")
92
+ outstatus=gr.Textbox(label="Output")
93
+
94
+ extract.click(
95
+ extract,
96
+ [video,start,count],
97
+ [gallery,state,status]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  )
99
 
100
+ copy.click(
101
+ prepend,
102
+ state,
103
+ [output,outstatus]
 
 
 
 
 
 
 
 
 
 
 
 
 
104
  )
105
 
106
+ demo.launch(server_name="0.0.0.0",server_port=7860)