Spaces:
Sleeping
Sleeping
modified video_editor, html/css/js
Browse files- endpoints/video_editor_cut.py +30 -45
- static/index.html +31 -29
- static/scripts.js +32 -49
- static/styles.css +28 -0
endpoints/video_editor_cut.py
CHANGED
@@ -1,70 +1,55 @@
|
|
1 |
-
import json
|
2 |
import shutil
|
3 |
import subprocess
|
4 |
-
from typing import List
|
5 |
from fastapi import UploadFile, File, Form
|
6 |
from fastapi.responses import FileResponse
|
7 |
from fastapi import APIRouter, BackgroundTasks
|
8 |
import os
|
|
|
|
|
9 |
|
10 |
router = APIRouter()
|
11 |
|
|
|
12 |
@router.post("/cut")
|
13 |
async def cut_videos(
|
14 |
-
|
15 |
-
|
16 |
-
|
|
|
17 |
):
|
18 |
-
timestamps_dict = json.loads(timestamps)
|
19 |
temp_dir = "./data/video_editor/videos"
|
20 |
if not os.path.exists(temp_dir):
|
21 |
os.makedirs(temp_dir)
|
22 |
|
23 |
-
save_and_cut(
|
24 |
-
|
25 |
-
clean_temp_dir = remove_temp_files(temp_dir)
|
26 |
-
|
27 |
-
zip_filename = shutil.make_archive(f"{clean_temp_dir}", 'zip', "./data/video_editor/")
|
28 |
-
|
29 |
-
background_tasks.add_task(remove_zip_folder, zip_filename)
|
30 |
|
31 |
-
|
|
|
|
|
32 |
|
|
|
33 |
|
34 |
-
def save_and_cut(videos, temp_dir, timestamps_dict):
|
35 |
-
for index, video in enumerate(videos):
|
36 |
-
temp_video_path = f"{temp_dir}/{video.filename}-{index}.mp4"
|
37 |
-
with open(temp_video_path, "wb") as f:
|
38 |
-
shutil.copyfileobj(video.file, f)
|
39 |
|
40 |
-
|
41 |
-
|
|
|
|
|
|
|
42 |
|
43 |
-
|
44 |
-
|
45 |
-
filename_without_index = filename_without_extension.rsplit("-", 1)[0]
|
46 |
-
output_video_path = f"{filename_without_index}_cut.mp4"
|
47 |
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
|
|
55 |
|
56 |
-
|
57 |
-
|
58 |
|
59 |
-
|
60 |
-
for filename in os.listdir(temp_dir):
|
61 |
-
if "cut" not in filename:
|
62 |
-
os.remove(f"{temp_dir}/{filename}")
|
63 |
-
if "cut" in filename:
|
64 |
-
os.rename(f"{temp_dir}/{filename}", f"{temp_dir}/{filename.replace('_cut.mp4', '')}")
|
65 |
-
return temp_dir
|
66 |
|
67 |
-
def remove_zip_folder(zip_filename):
|
68 |
-
if os.path.exists(zip_filename):
|
69 |
-
shutil.rmtree(zip_filename[:-4])
|
70 |
-
os.remove(zip_filename)
|
|
|
|
|
1 |
import shutil
|
2 |
import subprocess
|
|
|
3 |
from fastapi import UploadFile, File, Form
|
4 |
from fastapi.responses import FileResponse
|
5 |
from fastapi import APIRouter, BackgroundTasks
|
6 |
import os
|
7 |
+
from endpoints.utils import get_file_name
|
8 |
+
from endpoints.utils import remove_content_from_dir
|
9 |
|
10 |
router = APIRouter()
|
11 |
|
12 |
+
|
13 |
@router.post("/cut")
|
14 |
async def cut_videos(
|
15 |
+
video: UploadFile = File(...),
|
16 |
+
start_time: str = Form(...),
|
17 |
+
end_time: str = Form(...),
|
18 |
+
background_tasks: BackgroundTasks = BackgroundTasks()
|
19 |
):
|
|
|
20 |
temp_dir = "./data/video_editor/videos"
|
21 |
if not os.path.exists(temp_dir):
|
22 |
os.makedirs(temp_dir)
|
23 |
|
24 |
+
output_video_path = save_and_cut(video, temp_dir, start_time, end_time)
|
|
|
|
|
|
|
|
|
|
|
|
|
25 |
|
26 |
+
# send video back
|
27 |
+
video_name = get_file_name("video_cut")
|
28 |
+
# background_tasks.add_task(remove_content_from_dir, temp_dir)
|
29 |
|
30 |
+
return FileResponse(output_video_path, media_type="video/mp4", filename=video_name)
|
31 |
|
|
|
|
|
|
|
|
|
|
|
32 |
|
33 |
+
def save_and_cut(video: UploadFile, temp_dir, start_time, end_time):
|
34 |
+
# Save uploaded video to temporary directory
|
35 |
+
video_path = os.path.join(temp_dir, video.filename)
|
36 |
+
with open(video_path, 'wb') as buffer:
|
37 |
+
shutil.copyfileobj(video.file, buffer)
|
38 |
|
39 |
+
# Output video path
|
40 |
+
output_video_path = os.path.join(temp_dir, "output.mp4")
|
|
|
|
|
41 |
|
42 |
+
# Command to cut video using ffmpeg
|
43 |
+
ffmpeg_command = [
|
44 |
+
"ffmpeg",
|
45 |
+
"-i", video_path,
|
46 |
+
"-ss", start_time,
|
47 |
+
"-to", end_time,
|
48 |
+
"-y", output_video_path
|
49 |
+
]
|
50 |
|
51 |
+
# Execute ffmpeg command
|
52 |
+
subprocess.run(ffmpeg_command, check=True)
|
53 |
|
54 |
+
return output_video_path
|
|
|
|
|
|
|
|
|
|
|
|
|
55 |
|
|
|
|
|
|
|
|
static/index.html
CHANGED
@@ -11,6 +11,9 @@
|
|
11 |
</head>
|
12 |
<body>
|
13 |
<h1>BrainRotTok</h1>
|
|
|
|
|
|
|
14 |
<div class="form-buttons">
|
15 |
<ul>
|
16 |
<li>
|
@@ -32,7 +35,6 @@
|
|
32 |
<button
|
33 |
class="show-form"
|
34 |
data-form-id="video-cutter-form"
|
35 |
-
disabled
|
36 |
>
|
37 |
VIDEO CUTTER
|
38 |
</button>
|
@@ -51,7 +53,7 @@
|
|
51 |
<div id="forms-container">
|
52 |
<form
|
53 |
id="basic-form"
|
54 |
-
class="
|
55 |
action="/generate-subtitles/basic"
|
56 |
method="post"
|
57 |
enctype="multipart/form-data"
|
@@ -59,17 +61,17 @@
|
|
59 |
<h2 class="form-title">BASIC</h2>
|
60 |
<p>Generate subtitles for a video.</p>
|
61 |
<label for="video">Video:</label>
|
62 |
-
<input type="file" id="video" name="video" />
|
63 |
<label for="color">Color:</label>
|
64 |
-
<input type="text" id="color" name="color" data-coloris />
|
65 |
<label for="size">Size:</label>
|
66 |
-
<input type="number" id="size" name="size" min="0"/>
|
67 |
<label for="font">Font:</label>
|
68 |
<select id="font" name="font"></select>
|
69 |
<label for="credit">Credit:</label>
|
70 |
-
<input type="text" id="credit" name="credit" />
|
71 |
<label for="credit_size">Credit Size:</label>
|
72 |
-
<input type="number" id="credit_size" name="credit_size" min="0" />
|
73 |
<button type="submit">Submit</button>
|
74 |
</form>
|
75 |
<form
|
@@ -86,19 +88,19 @@
|
|
86 |
have its subtitles generated.
|
87 |
</p>
|
88 |
<label for="top_video">Top Video:</label>
|
89 |
-
<input type="file" id="top_video" name="top_video" />
|
90 |
<label for="bottom_video">Bottom Video:</label>
|
91 |
-
<input type="file" id="bottom_video" name="bottom_video" />
|
92 |
<label for="color">Color:</label>
|
93 |
-
<input type="text" id="color" name="color" data-coloris />
|
94 |
<label for="size">Size:</label>
|
95 |
-
<input type="number" id="size" name="size" min="0"/>
|
96 |
<label for="font">Font:</label>
|
97 |
<select id="font" name="font"></select>
|
98 |
<label for="credit">Credit:</label>
|
99 |
-
<input type="text" id="credit" name="credit" />
|
100 |
<label for="credit_size">Credit Size:</label>
|
101 |
-
<input type="number" id="credit_size" name="credit_size" min="0" />
|
102 |
<button type="submit">Submit</button>
|
103 |
</form>
|
104 |
<form
|
@@ -114,19 +116,19 @@
|
|
114 |
an AI voice with subtitles.
|
115 |
</p>
|
116 |
<label for="video">Video:</label>
|
117 |
-
<input type="file" id="video" name="video" />
|
118 |
<label for="subtitles">Subtitles:</label>
|
119 |
-
<input type="text" id="subtitles" name="subtitles" />
|
120 |
<label for="color">Color:</label>
|
121 |
-
<input type="text" id="color" name="color" data-coloris />
|
122 |
<label for="size">Size:</label>
|
123 |
-
<input type="number" id="size" name="size" min="0"/>
|
124 |
<label for="font">Font:</label>
|
125 |
<select id="font" name="font"></select>
|
126 |
<label for="credit">Credit:</label>
|
127 |
-
<input type="text" id="credit" name="credit" />
|
128 |
<label for="credit_size">Credit Size:</label>
|
129 |
-
<input type="number" id="credit_size" name="credit_size" min="0" />
|
130 |
<button type="submit">Submit</button>
|
131 |
</form>
|
132 |
<form
|
@@ -139,17 +141,17 @@
|
|
139 |
<h2 class="form-title">RUMBLE</h2>
|
140 |
<p>Generates subtitles for Rumble videos.</p>
|
141 |
<label for="video_url">Video Url:</label>
|
142 |
-
<input type="text" id="video_url" name="video_url" />
|
143 |
<label for="color">Color:</label>
|
144 |
-
<input type="text" id="color" name="color" data-coloris />
|
145 |
<label for="size">Size:</label>
|
146 |
-
<input type="number" id="size" name="size" min="0"/>
|
147 |
<label for="font">Font:</label>
|
148 |
<select id="font" name="font"></select>
|
149 |
<label for="credit">Credit:</label>
|
150 |
-
<input type="text" id="credit" name="credit" />
|
151 |
<label for="credit_size">Credit Size:</label>
|
152 |
-
<input type="number" id="credit_size" name="credit_size" min="0" />
|
153 |
<button type="submit">Submit</button>
|
154 |
</form>
|
155 |
<form
|
@@ -162,11 +164,11 @@
|
|
162 |
<h2 class="form-title">VIDEO CUTTER</h2>
|
163 |
<p>Cut a video from a start time to an end time.</p>
|
164 |
<label for="video">Video:</label>
|
165 |
-
<input type="file" id="video" name="video" />
|
166 |
-
<label for="start_time">Start Time:</label>
|
167 |
-
<input type="text" id="start_time" name="start_time" />
|
168 |
-
<label for="end_time">End Time:</label>
|
169 |
-
<input type="text" id="end_time" name="end_time" />
|
170 |
<button type="submit">Submit</button>
|
171 |
</form>
|
172 |
</div>
|
|
|
11 |
</head>
|
12 |
<body>
|
13 |
<h1>BrainRotTok</h1>
|
14 |
+
<div class="reference">
|
15 |
+
<a href="https://github.com/Detopall/BrainRotTok" target="_blank">GitHub</a>
|
16 |
+
</div>
|
17 |
<div class="form-buttons">
|
18 |
<ul>
|
19 |
<li>
|
|
|
35 |
<button
|
36 |
class="show-form"
|
37 |
data-form-id="video-cutter-form"
|
|
|
38 |
>
|
39 |
VIDEO CUTTER
|
40 |
</button>
|
|
|
53 |
<div id="forms-container">
|
54 |
<form
|
55 |
id="basic-form"
|
56 |
+
class="form-container"
|
57 |
action="/generate-subtitles/basic"
|
58 |
method="post"
|
59 |
enctype="multipart/form-data"
|
|
|
61 |
<h2 class="form-title">BASIC</h2>
|
62 |
<p>Generate subtitles for a video.</p>
|
63 |
<label for="video">Video:</label>
|
64 |
+
<input required type="file" id="video" name="video" />
|
65 |
<label for="color">Color:</label>
|
66 |
+
<input required type="text" id="color" name="color" data-coloris />
|
67 |
<label for="size">Size:</label>
|
68 |
+
<input required type="number" id="size" name="size" min="0"/>
|
69 |
<label for="font">Font:</label>
|
70 |
<select id="font" name="font"></select>
|
71 |
<label for="credit">Credit:</label>
|
72 |
+
<input required type="text" id="credit" name="credit" />
|
73 |
<label for="credit_size">Credit Size:</label>
|
74 |
+
<input required type="number" id="credit_size" name="credit_size" min="0" />
|
75 |
<button type="submit">Submit</button>
|
76 |
</form>
|
77 |
<form
|
|
|
88 |
have its subtitles generated.
|
89 |
</p>
|
90 |
<label for="top_video">Top Video:</label>
|
91 |
+
<input required type="file" id="top_video" name="top_video" />
|
92 |
<label for="bottom_video">Bottom Video:</label>
|
93 |
+
<input required type="file" id="bottom_video" name="bottom_video" />
|
94 |
<label for="color">Color:</label>
|
95 |
+
<input required type="text" id="color" name="color" data-coloris />
|
96 |
<label for="size">Size:</label>
|
97 |
+
<input required type="number" id="size" name="size" min="0"/>
|
98 |
<label for="font">Font:</label>
|
99 |
<select id="font" name="font"></select>
|
100 |
<label for="credit">Credit:</label>
|
101 |
+
<input required type="text" id="credit" name="credit" />
|
102 |
<label for="credit_size">Credit Size:</label>
|
103 |
+
<input required type="number" id="credit_size" name="credit_size" min="0" />
|
104 |
<button type="submit">Submit</button>
|
105 |
</form>
|
106 |
<form
|
|
|
116 |
an AI voice with subtitles.
|
117 |
</p>
|
118 |
<label for="video">Video:</label>
|
119 |
+
<input required type="file" id="video" name="video" />
|
120 |
<label for="subtitles">Subtitles:</label>
|
121 |
+
<input required type="text" id="subtitles" name="subtitles" />
|
122 |
<label for="color">Color:</label>
|
123 |
+
<input required type="text" id="color" name="color" data-coloris />
|
124 |
<label for="size">Size:</label>
|
125 |
+
<input required type="number" id="size" name="size" min="0"/>
|
126 |
<label for="font">Font:</label>
|
127 |
<select id="font" name="font"></select>
|
128 |
<label for="credit">Credit:</label>
|
129 |
+
<input required type="text" id="credit" name="credit" />
|
130 |
<label for="credit_size">Credit Size:</label>
|
131 |
+
<input required type="number" id="credit_size" name="credit_size" min="0" />
|
132 |
<button type="submit">Submit</button>
|
133 |
</form>
|
134 |
<form
|
|
|
141 |
<h2 class="form-title">RUMBLE</h2>
|
142 |
<p>Generates subtitles for Rumble videos.</p>
|
143 |
<label for="video_url">Video Url:</label>
|
144 |
+
<input required type="text" id="video_url" name="video_url" />
|
145 |
<label for="color">Color:</label>
|
146 |
+
<input required type="text" id="color" name="color" data-coloris />
|
147 |
<label for="size">Size:</label>
|
148 |
+
<input required type="number" id="size" name="size" min="0"/>
|
149 |
<label for="font">Font:</label>
|
150 |
<select id="font" name="font"></select>
|
151 |
<label for="credit">Credit:</label>
|
152 |
+
<input required type="text" id="credit" name="credit" />
|
153 |
<label for="credit_size">Credit Size:</label>
|
154 |
+
<input required type="number" id="credit_size" name="credit_size" min="0" />
|
155 |
<button type="submit">Submit</button>
|
156 |
</form>
|
157 |
<form
|
|
|
164 |
<h2 class="form-title">VIDEO CUTTER</h2>
|
165 |
<p>Cut a video from a start time to an end time.</p>
|
166 |
<label for="video">Video:</label>
|
167 |
+
<input required type="file" id="video" name="video" />
|
168 |
+
<label for="start_time">Start Time (HH:MM:SS):</label>
|
169 |
+
<input required type="text" id="start_time" name="start_time" placeholder="00:00:00"/>
|
170 |
+
<label for="end_time">End Time (HH:MM:SS):</label>
|
171 |
+
<input required type="text" id="end_time" name="end_time" placeholder="00:00:00"/>
|
172 |
<button type="submit">Submit</button>
|
173 |
</form>
|
174 |
</div>
|
static/scripts.js
CHANGED
@@ -1,65 +1,48 @@
|
|
1 |
-
"use strict"
|
2 |
|
3 |
const common_fonts = [
|
4 |
-
"Arial",
|
5 |
-
"
|
6 |
-
"
|
7 |
-
"
|
8 |
-
"Georgia",
|
9 |
-
"Palatino",
|
10 |
-
"Bookman Old Style",
|
11 |
-
"Tahoma",
|
12 |
-
"Century Gothic",
|
13 |
-
"Lucida Console",
|
14 |
-
"Monaco",
|
15 |
-
"Bradley Hand",
|
16 |
-
"Calibri",
|
17 |
-
"Cambria",
|
18 |
-
"Candara",
|
19 |
-
"Consolas",
|
20 |
-
"Constantia",
|
21 |
-
"Corbel",
|
22 |
-
"Franklin Gothic Medium",
|
23 |
-
"Gabriola"
|
24 |
];
|
25 |
|
26 |
document.addEventListener("DOMContentLoaded", () => {
|
27 |
-
const
|
28 |
-
|
29 |
-
|
|
|
|
|
30 |
});
|
31 |
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
});
|
38 |
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
fontSelect.add(option);
|
45 |
});
|
46 |
-
});
|
47 |
|
48 |
-
document.querySelectorAll(".show-form").forEach(
|
49 |
-
|
50 |
-
showForm(event.target.dataset.formId);
|
51 |
});
|
52 |
});
|
53 |
|
54 |
function showForm(formId) {
|
55 |
-
|
56 |
-
|
57 |
-
if (form.id === formId) {
|
58 |
-
form.style.display = "block";
|
59 |
-
const noChosenForm = document.querySelector(".error");
|
60 |
-
noChosenForm.style.display = "none";
|
61 |
-
} else {
|
62 |
-
form.style.display = "none";
|
63 |
-
}
|
64 |
});
|
65 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"use strict";
|
2 |
|
3 |
const common_fonts = [
|
4 |
+
"Arial", "Times New Roman", "Courier New", "Verdana", "Georgia", "Palatino",
|
5 |
+
"Bookman Old Style", "Tahoma", "Century Gothic", "Lucida Console", "Monaco",
|
6 |
+
"Bradley Hand", "Calibri", "Cambria", "Candara", "Consolas", "Constantia",
|
7 |
+
"Corbel", "Franklin Gothic Medium", "Gabriola"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
8 |
];
|
9 |
|
10 |
document.addEventListener("DOMContentLoaded", () => {
|
11 |
+
const startTimeInput = document.querySelector("#start_time");
|
12 |
+
const endTimeInput = document.querySelector("#end_time");
|
13 |
+
|
14 |
+
[startTimeInput, endTimeInput].forEach(input => {
|
15 |
+
input.addEventListener("input", () => validateTimeFormat(input));
|
16 |
});
|
17 |
|
18 |
+
document.querySelectorAll(".show-form").forEach(option => {
|
19 |
+
option.addEventListener("click", () => {
|
20 |
+
document.querySelectorAll(".show-form").forEach(opt => opt.classList.remove("active-option"));
|
21 |
+
option.classList.add("active-option");
|
22 |
+
});
|
23 |
+
});
|
24 |
|
25 |
+
document.querySelectorAll("#font").forEach(fontSelect => {
|
26 |
+
common_fonts.forEach(font => {
|
27 |
+
const option = new Option(font, font);
|
28 |
+
fontSelect.add(option);
|
29 |
+
});
|
|
|
30 |
});
|
|
|
31 |
|
32 |
+
document.querySelectorAll(".show-form").forEach(button => {
|
33 |
+
button.addEventListener("click", event => showForm(event.target.dataset.formId));
|
|
|
34 |
});
|
35 |
});
|
36 |
|
37 |
function showForm(formId) {
|
38 |
+
document.querySelectorAll("#forms-container form").forEach(form => {
|
39 |
+
form.style.display = form.id === formId ? "block" : "none";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
40 |
});
|
41 |
}
|
42 |
+
|
43 |
+
function validateTimeFormat(input) {
|
44 |
+
const regex = /^(0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]$/;
|
45 |
+
const isValid = regex.test(input.value);
|
46 |
+
input.setCustomValidity(isValid ? "" : "Invalid time format. Please use HH:MM:SS");
|
47 |
+
input.style.borderColor = isValid ? "#ccc" : "red";
|
48 |
+
}
|
static/styles.css
CHANGED
@@ -177,3 +177,31 @@ option:checked {
|
|
177 |
background-color: #007bff;
|
178 |
color: #fff;
|
179 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
177 |
background-color: #007bff;
|
178 |
color: #fff;
|
179 |
}
|
180 |
+
|
181 |
+
.active-option {
|
182 |
+
background-color: #1f5792;
|
183 |
+
color: #fff;
|
184 |
+
}
|
185 |
+
|
186 |
+
.clr-field {
|
187 |
+
margin-bottom: 15px;
|
188 |
+
}
|
189 |
+
|
190 |
+
.reference {
|
191 |
+
margin-top: 20px;
|
192 |
+
text-align: center;
|
193 |
+
}
|
194 |
+
|
195 |
+
.reference a {
|
196 |
+
color: #007bff;
|
197 |
+
font-weight: bold;
|
198 |
+
transition: color 0.3s ease;
|
199 |
+
}
|
200 |
+
|
201 |
+
.reference a:hover {
|
202 |
+
color: #0056b3;
|
203 |
+
}
|
204 |
+
|
205 |
+
.hidden {
|
206 |
+
display: none;
|
207 |
+
}
|