Spaces:
Runtime error
Runtime error
Upload app.py with huggingface_hub
Browse files
app.py
ADDED
@@ -0,0 +1,137 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""
|
2 |
+
A minimal, self-contained Gradio video editor that allows:
|
3 |
+
1. Uploading one or more videos.
|
4 |
+
2. Trimming each video to a start/end time.
|
5 |
+
3. Concatenating the trimmed clips in the order supplied.
|
6 |
+
4. Downloading the final edited video.
|
7 |
+
|
8 |
+
Requirements (install once):
|
9 |
+
pip install gradio moviepy==1.0.3
|
10 |
+
|
11 |
+
Run:
|
12 |
+
python video_editor.py
|
13 |
+
"""
|
14 |
+
|
15 |
+
import tempfile
|
16 |
+
import os
|
17 |
+
from typing import List, Tuple
|
18 |
+
|
19 |
+
import gradio as gr
|
20 |
+
from moviepy.editor import VideoFileClip, concatenate_videoclips
|
21 |
+
|
22 |
+
|
23 |
+
def _time_to_seconds(time_str: str) -> float:
|
24 |
+
"""Convert HH:MM:SS[.ms] → seconds (float)."""
|
25 |
+
parts = list(map(float, reversed(time_str.split(":"))))
|
26 |
+
return sum(v * 60 ** idx for idx, v in enumerate(parts))
|
27 |
+
|
28 |
+
|
29 |
+
def edit_videos(
|
30 |
+
videos: List[str],
|
31 |
+
trims: List[Tuple[str, str]],
|
32 |
+
) -> str:
|
33 |
+
"""
|
34 |
+
Create a single edited video from the list of uploaded videos
|
35 |
+
and corresponding (start, end) times.
|
36 |
+
|
37 |
+
Returns the file path of the final video.
|
38 |
+
"""
|
39 |
+
|
40 |
+
if not videos:
|
41 |
+
raise ValueError("At least one video is required.")
|
42 |
+
|
43 |
+
if len(videos) != len(trims):
|
44 |
+
raise ValueError("Each video must have a start/end time pair.")
|
45 |
+
|
46 |
+
clips = []
|
47 |
+
for vid_path, (start_str, end_str) in zip(videos, trims):
|
48 |
+
start = _time_to_seconds(start_str)
|
49 |
+
end = _time_to_seconds(end_str)
|
50 |
+
clip = VideoFileClip(vid_path).subclip(start, end)
|
51 |
+
clips.append(clip)
|
52 |
+
|
53 |
+
final = concatenate_videoclips(clips)
|
54 |
+
|
55 |
+
# Save to a temporary file with a unique name
|
56 |
+
out_path = os.path.join(
|
57 |
+
tempfile.gettempdir(),
|
58 |
+
"edited_video.mp4"
|
59 |
+
)
|
60 |
+
final.write_videofile(out_path, codec="libx264", audio_codec="aac")
|
61 |
+
|
62 |
+
# Close clips to release file handles
|
63 |
+
for c in clips:
|
64 |
+
c.close()
|
65 |
+
final.close()
|
66 |
+
|
67 |
+
return out_path
|
68 |
+
|
69 |
+
|
70 |
+
def gradio_interface():
|
71 |
+
with gr.Blocks(title="Video Editor") as demo:
|
72 |
+
gr.Markdown("## Simple Gradio Video Editor")
|
73 |
+
gr.Markdown(
|
74 |
+
"Upload one or more videos. \n"
|
75 |
+
"Provide the **start** and **end** time (HH:MM:SS) for each clip. \n"
|
76 |
+
"The clips will be concatenated in the order given."
|
77 |
+
)
|
78 |
+
|
79 |
+
with gr.Row():
|
80 |
+
with gr.Column(scale=2):
|
81 |
+
video_files = gr.File(
|
82 |
+
label="Upload videos",
|
83 |
+
file_count="multiple",
|
84 |
+
file_types=["video"],
|
85 |
+
type="filepath",
|
86 |
+
)
|
87 |
+
|
88 |
+
# Dynamic UI for trims
|
89 |
+
trim_rows = gr.State([])
|
90 |
+
|
91 |
+
def _update_trim_inputs(videos):
|
92 |
+
rows = []
|
93 |
+
for idx, v in enumerate(videos or []):
|
94 |
+
with gr.Row():
|
95 |
+
start = gr.Textbox(
|
96 |
+
label=f"Start time #{idx+1}",
|
97 |
+
value="00:00:00",
|
98 |
+
scale=1,
|
99 |
+
)
|
100 |
+
end = gr.Textbox(
|
101 |
+
label=f"End time #{idx+1}",
|
102 |
+
value="00:00:05",
|
103 |
+
scale=1,
|
104 |
+
)
|
105 |
+
rows.append((start, end))
|
106 |
+
return rows
|
107 |
+
|
108 |
+
trim_inputs_placeholder = gr.HTML() # dummy; we will render rows
|
109 |
+
|
110 |
+
# Render trim inputs after file upload
|
111 |
+
video_files.change(
|
112 |
+
fn=_update_trim_inputs,
|
113 |
+
inputs=video_files,
|
114 |
+
outputs=trim_inputs_placeholder,
|
115 |
+
)
|
116 |
+
|
117 |
+
with gr.Column(scale=1):
|
118 |
+
output_video = gr.Video(label="Edited Video")
|
119 |
+
edit_btn = gr.Button("Edit & Download", variant="primary")
|
120 |
+
|
121 |
+
# Submit
|
122 |
+
def _on_edit(videos):
|
123 |
+
# Re-create trim tuples based on current videos
|
124 |
+
rows = _update_trim_inputs(videos)
|
125 |
+
trims = [
|
126 |
+
(start.value, end.value) for start, end in rows
|
127 |
+
]
|
128 |
+
out_path = edit_videos(videos, trims)
|
129 |
+
return out_path
|
130 |
+
|
131 |
+
edit_btn.click(_on_edit, inputs=[video_files], outputs=output_video)
|
132 |
+
|
133 |
+
demo.launch()
|
134 |
+
|
135 |
+
|
136 |
+
if __name__ == "__main__":
|
137 |
+
gradio_interface()
|