Spaces:
Running
Running
ziqiangao
commited on
Commit
·
e50325c
1
Parent(s):
6ecdd65
add progress tracking for ffmpeg
Browse files
app.py
CHANGED
@@ -16,43 +16,51 @@ import traceback
|
|
16 |
import shutil
|
17 |
import LRC2SRT
|
18 |
import sys
|
|
|
19 |
|
20 |
-
flag = 1
|
21 |
|
22 |
path = "" # Update with your path
|
23 |
|
|
|
24 |
def safe_read(i: int, a: list):
|
25 |
if i >= len(a):
|
26 |
return 128
|
27 |
else:
|
28 |
return a[i]
|
29 |
|
|
|
30 |
def getRenderCords(ta: list, idx: int, res: int = 1024, size: tuple = (1280, 720)) -> list:
|
31 |
i = idx - res // 2
|
32 |
-
x, y = size[0] * .9 / -2, (safe_read(i,ta) - 128) *
|
|
|
33 |
c = []
|
34 |
while i < idx + (res // 2):
|
35 |
c.append((x, y))
|
36 |
i += 1
|
37 |
-
y = (safe_read(i,ta) - 128) * (size[1] / 2000) + (size[1] * .7 / -2)
|
38 |
x += (size[0] * .9) / res
|
39 |
return c
|
40 |
|
|
|
41 |
def center_to_top_left(coords, width=1280, height=720):
|
42 |
new_coords = []
|
43 |
for x, y in coords:
|
44 |
new_coords.append(totopleft((x, y), width=width, height=height))
|
45 |
return new_coords
|
46 |
|
|
|
47 |
def totopleft(coord, width=1280, height=720):
|
48 |
return coord[0] + width / 2, height / 2 - coord[1]
|
49 |
|
|
|
50 |
def getTrigger(ad: int, a: list, max: int = 1024) -> int:
|
51 |
i = ad
|
52 |
-
while not (safe_read(i,a) <
|
53 |
i += 1
|
54 |
return i
|
55 |
|
|
|
56 |
def extract_cover_image(mp3_file):
|
57 |
audio = MP3(mp3_file, ID3=ID3)
|
58 |
if audio.tags == None:
|
@@ -65,12 +73,14 @@ def extract_cover_image(mp3_file):
|
|
65 |
print("No cover image found in the MP3 file.")
|
66 |
return None
|
67 |
|
|
|
68 |
def getTitleAndArtist(mp3_file):
|
69 |
audio = MP3(mp3_file, ID3=ID3)
|
70 |
title = audio.get('TIT2', TIT2(encoding=3, text='')).text[0]
|
71 |
artist = audio.get('TPE1', TPE1(encoding=3, text='')).text[0]
|
72 |
return title, artist
|
73 |
|
|
|
74 |
def getColour(img):
|
75 |
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmpfile:
|
76 |
img.save(tmpfile.name, format="PNG")
|
@@ -79,19 +89,23 @@ def getColour(img):
|
|
79 |
os.remove(tmpfile.name)
|
80 |
return dominant_color
|
81 |
|
|
|
82 |
def clamp(number):
|
83 |
return max(0, min(number, 1))
|
84 |
|
85 |
-
|
|
|
86 |
cc = colorsys.rgb_to_hsv(C[0] / 255, C[1] / 255, C[2] / 255)
|
87 |
ccc = colorsys.hsv_to_rgb(cc[0], clamp(1.3 * cc[1]), .8)
|
88 |
return math.floor(ccc[0] * 255), math.floor(ccc[1] * 255), math.floor(ccc[2] * 255)
|
89 |
|
90 |
-
|
|
|
91 |
cc = colorsys.rgb_to_hsv(C[0] / 255, C[1] / 255, C[2] / 255)
|
92 |
ccc = colorsys.hsv_to_rgb(cc[0], clamp(1.4 * cc[1]), .6)
|
93 |
return math.floor(ccc[0] * 255), math.floor(ccc[1] * 255), math.floor(ccc[2] * 255)
|
94 |
|
|
|
95 |
def stamp_text(draw, text, font, position, align='left'):
|
96 |
text_bbox = draw.textbbox((0, 0), text, font=font)
|
97 |
text_width = text_bbox[2] - text_bbox[0]
|
@@ -105,14 +119,17 @@ def stamp_text(draw, text, font, position, align='left'):
|
|
105 |
|
106 |
draw.text((x, y), text, font=font, fill="#fff")
|
107 |
|
|
|
108 |
def linear_interpolate(start, stop, progress):
|
109 |
return start + progress * (stop - start)
|
110 |
|
|
|
111 |
def filecount(p):
|
112 |
files = os.listdir()
|
113 |
file_count = len(files)
|
114 |
return file_count
|
115 |
|
|
|
116 |
def render_frame(params):
|
117 |
n, samples_array, cover_img, title, artist, dominant_color, width, height, fps, name, oscres, sr = params
|
118 |
num_frames = len(samples_array) // (sr // fps)
|
@@ -120,71 +137,89 @@ def render_frame(params):
|
|
120 |
d = ImageDraw.Draw(img)
|
121 |
|
122 |
s = math.floor((sr / fps) * n)
|
123 |
-
e = center_to_top_left(getRenderCords(samples_array, getTrigger(
|
124 |
-
|
|
|
125 |
|
126 |
cs = math.floor(min(width, height) / 2)
|
127 |
cov = cover_img.resize((cs, cs))
|
128 |
img.paste(cov, (((width // 2) - cs // 2), math.floor(height * .1)))
|
129 |
|
130 |
-
fontT = ImageFont.truetype(
|
131 |
-
|
132 |
-
|
|
|
|
|
|
|
133 |
|
134 |
-
stamp_text(d, title, fontT, totopleft(
|
135 |
-
|
|
|
|
|
136 |
|
137 |
d.line(center_to_top_left([(width * .96 // -2, height * .95 // -2), (width * .96 // 2, height * .95 // -2)], width=width, height=height),
|
138 |
fill=normalizeColourBar(dominant_color), width=15 * height // 360)
|
139 |
d.line(center_to_top_left([(width * .95 // -2, height * .95 // -2),
|
140 |
(linear_interpolate(width * .95 // -2, width * .95 // 2, s / len(samples_array)),
|
141 |
-
height * .95 // -2)],width=width, height=height), fill='#fff', width=10 * height // 360)
|
142 |
|
143 |
img.save(path+f'out/{name}/{str(n)}.png', 'PNG')
|
144 |
|
145 |
return 1 # Indicate one frame processed
|
146 |
|
|
|
147 |
def RenderVid(af, n, fps=30):
|
148 |
-
(ffmpeg
|
149 |
-
.input(path+f'out/{n}/%d.png', framerate=fps)
|
150 |
-
.input(af)
|
151 |
-
.output(n + '.mp4', vcodec='libx264', r=fps, pix_fmt='yuv420p', acodec='aac', shortest=None)
|
152 |
.run()
|
153 |
)
|
154 |
gr.Interface.download(f"{n}.mp4")
|
155 |
|
|
|
156 |
invisible_chars = ["\u200B", "\uFEFF"]
|
157 |
|
|
|
158 |
def remove_bom(data: str) -> str:
|
159 |
BOM = '\ufeff'
|
160 |
return data.lstrip(BOM)
|
161 |
|
|
|
162 |
def stripinvisibles(s):
|
163 |
e = remove_bom(s)
|
164 |
for i in invisible_chars:
|
165 |
-
e.replace(i,"")
|
166 |
return e
|
167 |
|
168 |
def start_progress(title):
|
169 |
global progress_x
|
170 |
-
sys.stdout.write(title + ": [" + "-"*40 + "]"
|
|
|
171 |
sys.stdout.flush()
|
172 |
progress_x = 0
|
173 |
|
174 |
def progress(x):
|
175 |
global progress_x
|
176 |
x = int(x * 40 // 100)
|
|
|
177 |
sys.stdout.write("#" * (x - progress_x))
|
|
|
|
|
178 |
sys.stdout.flush()
|
179 |
progress_x = x
|
180 |
|
181 |
def end_progress():
|
182 |
-
|
|
|
183 |
sys.stdout.flush()
|
184 |
|
|
|
|
|
185 |
haslyrics = False
|
186 |
|
187 |
-
|
|
|
188 |
global flag
|
189 |
p = gr.Progress()
|
190 |
LRC2SRT.clear()
|
@@ -210,8 +245,9 @@ def main(file, name, fps=30, res: tuple=(1280,720), oscres=512, sr=11025, lyrics
|
|
210 |
gr.Warning("Lyrics file is invalid, skipping")
|
211 |
except Exception as e:
|
212 |
print(traceback.format_exc())
|
213 |
-
gr.Warning(
|
214 |
-
|
|
|
215 |
os.makedirs(path + f'out/{name}/', exist_ok=True)
|
216 |
global iii
|
217 |
iii = 0
|
@@ -233,11 +269,13 @@ def main(file, name, fps=30, res: tuple=(1280,720), oscres=512, sr=11025, lyrics
|
|
233 |
if img:
|
234 |
cover_img = cover_file
|
235 |
if cover_img is None:
|
236 |
-
raise gr.Error(
|
|
|
237 |
elif cover_img == -1 and not (tit or ast or img):
|
238 |
-
raise gr.Error(
|
239 |
-
|
240 |
-
|
|
|
241 |
if tit and ast:
|
242 |
title, artist = tit, ast
|
243 |
if title == '' or artist == '':
|
@@ -254,9 +292,10 @@ def main(file, name, fps=30, res: tuple=(1280,720), oscres=512, sr=11025, lyrics
|
|
254 |
num_frames = len(samples_array) // (sr // fps)
|
255 |
|
256 |
# Prepare parameters for each frame
|
257 |
-
params = [(n, samples_array, cover_img, title, artist, dominant_color,
|
|
|
258 |
print('---------------------------------------------------------')
|
259 |
-
print('Info:')
|
260 |
print("Title: " + title)
|
261 |
print("Artist: " + artist)
|
262 |
print(f'Resolution: {str(width)}x{str(height)}')
|
@@ -265,19 +304,23 @@ def main(file, name, fps=30, res: tuple=(1280,720), oscres=512, sr=11025, lyrics
|
|
265 |
print('Frame Count: ' + str(num_frames))
|
266 |
print('Segments per frame: ' + str(oscres))
|
267 |
print('---------------------------------------------------------')
|
|
|
268 |
try:
|
269 |
-
with Pool(cpu_count()) as pool:
|
270 |
num_frames = len(samples_array) // (sr // fps)
|
271 |
# Use imap to get progress updates
|
272 |
for _ in pool.imap_unordered(render_frame, params):
|
273 |
iii += 1 # Increment frame count for progress
|
274 |
p((iii, num_frames), desc="Rendering Frames")
|
275 |
-
|
|
|
276 |
except Exception as e:
|
277 |
raise gr.Error("Something went wrong whilst rendering")
|
278 |
-
|
|
|
|
|
279 |
p = gr.Progress()
|
280 |
-
p(0
|
281 |
print('---------------------------------------------------------')
|
282 |
print('FFMPEG')
|
283 |
if haslyrics:
|
@@ -304,14 +347,32 @@ def main(file, name, fps=30, res: tuple=(1280,720), oscres=512, sr=11025, lyrics
|
|
304 |
'-c:v', 'libx264',
|
305 |
'-r', str(fps),
|
306 |
'-pix_fmt', 'yuv420p',
|
307 |
-
'-c:a', 'aac',
|
308 |
'-y',
|
309 |
path + f'{name}.mp4' # Output MP4 filename
|
310 |
]
|
311 |
-
subprocess.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
312 |
print('---------------------------------------------------------')
|
313 |
return f"{name}.mp4", haslyrics
|
314 |
|
|
|
315 |
def gradio_interface(audio_file, lyrics, output_name, fps=30, vidwidth=1280, vidheight=720, oscres=512, img=None, tit=None, ast=None):
|
316 |
if audio_file is None:
|
317 |
raise gr.Error("Please Provide an Audio File")
|
@@ -320,48 +381,59 @@ def gradio_interface(audio_file, lyrics, output_name, fps=30, vidwidth=1280, vid
|
|
320 |
|
321 |
resolution = f"{vidwidth}x{vidheight}"
|
322 |
res = tuple(map(int, resolution.split('x')))
|
323 |
-
video_file, haslyrics = main(audio_file, output_name, fps=fps,
|
324 |
-
|
|
|
325 |
# Clean up the temporary file
|
326 |
-
os.remove(audio_file)
|
327 |
shutil.rmtree("out")
|
328 |
|
329 |
srt_output = "out.srt" if haslyrics else None
|
330 |
return video_file, srt_output, haslyrics
|
331 |
|
|
|
332 |
def update_srt_output_visibility(haslyrics):
|
333 |
return gr.update(visible=haslyrics)
|
334 |
|
|
|
335 |
with gr.Blocks() as demo:
|
336 |
-
gr.Markdown(
|
337 |
-
|
|
|
|
|
338 |
|
339 |
with gr.Row():
|
340 |
# Inputs on the left
|
341 |
with gr.Column():
|
342 |
with gr.Accordion(label="Audio Settings", open=True):
|
343 |
gr.Markdown('## Load your mp3 file here')
|
344 |
-
audio_file = gr.File(
|
345 |
-
|
|
|
346 |
with gr.Accordion(label="Mp3 Metadata", open=False):
|
347 |
-
gr.Markdown(
|
|
|
348 |
cover_img = gr.Image(label='Cover Art', type="filepath")
|
349 |
title_input = gr.Textbox(label='Title')
|
350 |
artist_input = gr.Textbox(label='Artists')
|
351 |
|
352 |
with gr.Accordion(label="Video Output Settings", open=False):
|
353 |
gr.Markdown('## Configure Video Output Here')
|
354 |
-
output_name = gr.Textbox(
|
355 |
-
|
356 |
-
|
357 |
-
|
358 |
-
|
|
|
|
|
|
|
|
|
359 |
with gr.Accordion(label="Advanced Options", open=False):
|
360 |
-
oscres_slider = gr.Slider(
|
361 |
-
|
362 |
-
|
363 |
-
|
364 |
-
|
|
|
365 |
|
366 |
# Add a submit button
|
367 |
submit_btn = gr.Button("Generate Video")
|
@@ -375,10 +447,11 @@ with gr.Blocks() as demo:
|
|
375 |
# Bind the button to the function
|
376 |
submit_btn.click(
|
377 |
fn=gradio_interface,
|
378 |
-
inputs=[audio_file, lyrics_file, output_name, fps_slider, vidwidth_slider,
|
|
|
379 |
outputs=[output_video, srt_output]
|
380 |
)
|
381 |
|
382 |
# Launch Gradio interface
|
383 |
if __name__ == '__main__':
|
384 |
-
demo.launch()
|
|
|
16 |
import shutil
|
17 |
import LRC2SRT
|
18 |
import sys
|
19 |
+
import re
|
20 |
|
21 |
+
flag = 1
|
22 |
|
23 |
path = "" # Update with your path
|
24 |
|
25 |
+
|
26 |
def safe_read(i: int, a: list):
|
27 |
if i >= len(a):
|
28 |
return 128
|
29 |
else:
|
30 |
return a[i]
|
31 |
|
32 |
+
|
33 |
def getRenderCords(ta: list, idx: int, res: int = 1024, size: tuple = (1280, 720)) -> list:
|
34 |
i = idx - res // 2
|
35 |
+
x, y = size[0] * .9 / -2, (safe_read(i, ta) - 128) * \
|
36 |
+
(size[1] / 2000) + (size[1] * .7 / -2)
|
37 |
c = []
|
38 |
while i < idx + (res // 2):
|
39 |
c.append((x, y))
|
40 |
i += 1
|
41 |
+
y = (safe_read(i, ta) - 128) * (size[1] / 2000) + (size[1] * .7 / -2)
|
42 |
x += (size[0] * .9) / res
|
43 |
return c
|
44 |
|
45 |
+
|
46 |
def center_to_top_left(coords, width=1280, height=720):
|
47 |
new_coords = []
|
48 |
for x, y in coords:
|
49 |
new_coords.append(totopleft((x, y), width=width, height=height))
|
50 |
return new_coords
|
51 |
|
52 |
+
|
53 |
def totopleft(coord, width=1280, height=720):
|
54 |
return coord[0] + width / 2, height / 2 - coord[1]
|
55 |
|
56 |
+
|
57 |
def getTrigger(ad: int, a: list, max: int = 1024) -> int:
|
58 |
i = ad
|
59 |
+
while not (safe_read(i, a) < 126 and safe_read(i+10, a) < 130 or i - ad > max):
|
60 |
i += 1
|
61 |
return i
|
62 |
|
63 |
+
|
64 |
def extract_cover_image(mp3_file):
|
65 |
audio = MP3(mp3_file, ID3=ID3)
|
66 |
if audio.tags == None:
|
|
|
73 |
print("No cover image found in the MP3 file.")
|
74 |
return None
|
75 |
|
76 |
+
|
77 |
def getTitleAndArtist(mp3_file):
|
78 |
audio = MP3(mp3_file, ID3=ID3)
|
79 |
title = audio.get('TIT2', TIT2(encoding=3, text='')).text[0]
|
80 |
artist = audio.get('TPE1', TPE1(encoding=3, text='')).text[0]
|
81 |
return title, artist
|
82 |
|
83 |
+
|
84 |
def getColour(img):
|
85 |
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmpfile:
|
86 |
img.save(tmpfile.name, format="PNG")
|
|
|
89 |
os.remove(tmpfile.name)
|
90 |
return dominant_color
|
91 |
|
92 |
+
|
93 |
def clamp(number):
|
94 |
return max(0, min(number, 1))
|
95 |
|
96 |
+
|
97 |
+
def normalizeColour(C):
|
98 |
cc = colorsys.rgb_to_hsv(C[0] / 255, C[1] / 255, C[2] / 255)
|
99 |
ccc = colorsys.hsv_to_rgb(cc[0], clamp(1.3 * cc[1]), .8)
|
100 |
return math.floor(ccc[0] * 255), math.floor(ccc[1] * 255), math.floor(ccc[2] * 255)
|
101 |
|
102 |
+
|
103 |
+
def normalizeColourBar(C):
|
104 |
cc = colorsys.rgb_to_hsv(C[0] / 255, C[1] / 255, C[2] / 255)
|
105 |
ccc = colorsys.hsv_to_rgb(cc[0], clamp(1.4 * cc[1]), .6)
|
106 |
return math.floor(ccc[0] * 255), math.floor(ccc[1] * 255), math.floor(ccc[2] * 255)
|
107 |
|
108 |
+
|
109 |
def stamp_text(draw, text, font, position, align='left'):
|
110 |
text_bbox = draw.textbbox((0, 0), text, font=font)
|
111 |
text_width = text_bbox[2] - text_bbox[0]
|
|
|
119 |
|
120 |
draw.text((x, y), text, font=font, fill="#fff")
|
121 |
|
122 |
+
|
123 |
def linear_interpolate(start, stop, progress):
|
124 |
return start + progress * (stop - start)
|
125 |
|
126 |
+
|
127 |
def filecount(p):
|
128 |
files = os.listdir()
|
129 |
file_count = len(files)
|
130 |
return file_count
|
131 |
|
132 |
+
|
133 |
def render_frame(params):
|
134 |
n, samples_array, cover_img, title, artist, dominant_color, width, height, fps, name, oscres, sr = params
|
135 |
num_frames = len(samples_array) // (sr // fps)
|
|
|
137 |
d = ImageDraw.Draw(img)
|
138 |
|
139 |
s = math.floor((sr / fps) * n)
|
140 |
+
e = center_to_top_left(getRenderCords(samples_array, getTrigger(
|
141 |
+
s, samples_array, max=oscres), res=oscres, size=(width, height)), width=width, height=height)
|
142 |
+
d.line(e, fill='#fff', width=round(min(2*height/720, 2*width/1280)))
|
143 |
|
144 |
cs = math.floor(min(width, height) / 2)
|
145 |
cov = cover_img.resize((cs, cs))
|
146 |
img.paste(cov, (((width // 2) - cs // 2), math.floor(height * .1)))
|
147 |
|
148 |
+
fontT = ImageFont.truetype(
|
149 |
+
path+'Lexend-Bold.ttf', 50*(min(width, height)/720)//1)
|
150 |
+
fontA = ImageFont.truetype(
|
151 |
+
path+'Lexend-Bold.ttf', 40*(min(width, height)/720)//1)
|
152 |
+
fontD = ImageFont.truetype(
|
153 |
+
path+'SpaceMono-Bold.ttf', 30*(min(width, height)/720)//1)
|
154 |
|
155 |
+
stamp_text(d, title, fontT, totopleft(
|
156 |
+
(0, min(width, height) * .3 // -2), width=width, height=height), 'center')
|
157 |
+
stamp_text(d, artist, fontA, totopleft(
|
158 |
+
(0, min(width, height) * .44 // -2), width=width, height=height), 'center')
|
159 |
|
160 |
d.line(center_to_top_left([(width * .96 // -2, height * .95 // -2), (width * .96 // 2, height * .95 // -2)], width=width, height=height),
|
161 |
fill=normalizeColourBar(dominant_color), width=15 * height // 360)
|
162 |
d.line(center_to_top_left([(width * .95 // -2, height * .95 // -2),
|
163 |
(linear_interpolate(width * .95 // -2, width * .95 // 2, s / len(samples_array)),
|
164 |
+
height * .95 // -2)], width=width, height=height), fill='#fff', width=10 * height // 360)
|
165 |
|
166 |
img.save(path+f'out/{name}/{str(n)}.png', 'PNG')
|
167 |
|
168 |
return 1 # Indicate one frame processed
|
169 |
|
170 |
+
|
171 |
def RenderVid(af, n, fps=30):
|
172 |
+
(ffmpeg
|
173 |
+
.input(path+f'out/{n}/%d.png', framerate=fps)
|
174 |
+
.input(af)
|
175 |
+
.output(n + '.mp4', vcodec='libx264', r=fps, pix_fmt='yuv420p', acodec='aac', shortest=None)
|
176 |
.run()
|
177 |
)
|
178 |
gr.Interface.download(f"{n}.mp4")
|
179 |
|
180 |
+
|
181 |
invisible_chars = ["\u200B", "\uFEFF"]
|
182 |
|
183 |
+
|
184 |
def remove_bom(data: str) -> str:
|
185 |
BOM = '\ufeff'
|
186 |
return data.lstrip(BOM)
|
187 |
|
188 |
+
|
189 |
def stripinvisibles(s):
|
190 |
e = remove_bom(s)
|
191 |
for i in invisible_chars:
|
192 |
+
e.replace(i, "")
|
193 |
return e
|
194 |
|
195 |
def start_progress(title):
|
196 |
global progress_x
|
197 |
+
sys.stdout.write(title + ": [" + "-"*40 + "] 0%")
|
198 |
+
sys.stdout.write(chr(8) * 44) # Move the cursor back to the start of the progress bar
|
199 |
sys.stdout.flush()
|
200 |
progress_x = 0
|
201 |
|
202 |
def progress(x):
|
203 |
global progress_x
|
204 |
x = int(x * 40 // 100)
|
205 |
+
percent_complete = int(x / 40 * 100)
|
206 |
sys.stdout.write("#" * (x - progress_x))
|
207 |
+
sys.stdout.write(" " * (40 - x) + "] " + f"{percent_complete}%")
|
208 |
+
sys.stdout.write(chr(8) * (44 + len(str(percent_complete)) + 1)) # Move the cursor back to the start of the progress bar
|
209 |
sys.stdout.flush()
|
210 |
progress_x = x
|
211 |
|
212 |
def end_progress():
|
213 |
+
global progress_x
|
214 |
+
sys.stdout.write("#" * (40 - progress_x) + "] 100%\n")
|
215 |
sys.stdout.flush()
|
216 |
|
217 |
+
|
218 |
+
|
219 |
haslyrics = False
|
220 |
|
221 |
+
|
222 |
+
def main(file, name, fps=30, res: tuple = (1280, 720), oscres=512, sr=11025, lyrics=None, img=None, tit=None, ast=None):
|
223 |
global flag
|
224 |
p = gr.Progress()
|
225 |
LRC2SRT.clear()
|
|
|
245 |
gr.Warning("Lyrics file is invalid, skipping")
|
246 |
except Exception as e:
|
247 |
print(traceback.format_exc())
|
248 |
+
gr.Warning(
|
249 |
+
"Failed to parse lyrics, ensure there are no blank lines in between, you may use Lyrics Editor to ensure compatability")
|
250 |
+
|
251 |
os.makedirs(path + f'out/{name}/', exist_ok=True)
|
252 |
global iii
|
253 |
iii = 0
|
|
|
269 |
if img:
|
270 |
cover_img = cover_file
|
271 |
if cover_img is None:
|
272 |
+
raise gr.Error(
|
273 |
+
"Mp3 must have a cover image, upload the image under the 'Metadata' section", duration=None)
|
274 |
elif cover_img == -1 and not (tit or ast or img):
|
275 |
+
raise gr.Error(
|
276 |
+
"Mp3 is missing tags, add the info under the 'Metadata' section", duration=None)
|
277 |
+
|
278 |
+
title, artist = getTitleAndArtist(audio_path)
|
279 |
if tit and ast:
|
280 |
title, artist = tit, ast
|
281 |
if title == '' or artist == '':
|
|
|
292 |
num_frames = len(samples_array) // (sr // fps)
|
293 |
|
294 |
# Prepare parameters for each frame
|
295 |
+
params = [(n, samples_array, cover_img, title, artist, dominant_color,
|
296 |
+
width, height, fps, name, oscres, sr) for n in range(num_frames)]
|
297 |
print('---------------------------------------------------------')
|
298 |
+
print('Info:' + "External" if img else "ID3")
|
299 |
print("Title: " + title)
|
300 |
print("Artist: " + artist)
|
301 |
print(f'Resolution: {str(width)}x{str(height)}')
|
|
|
304 |
print('Frame Count: ' + str(num_frames))
|
305 |
print('Segments per frame: ' + str(oscres))
|
306 |
print('---------------------------------------------------------')
|
307 |
+
start_progress("Rendering:")
|
308 |
try:
|
309 |
+
with Pool(cpu_count()-1) as pool:
|
310 |
num_frames = len(samples_array) // (sr // fps)
|
311 |
# Use imap to get progress updates
|
312 |
for _ in pool.imap_unordered(render_frame, params):
|
313 |
iii += 1 # Increment frame count for progress
|
314 |
p((iii, num_frames), desc="Rendering Frames")
|
315 |
+
progress(iii/num_frames*100)
|
316 |
+
|
317 |
except Exception as e:
|
318 |
raise gr.Error("Something went wrong whilst rendering")
|
319 |
+
finally:
|
320 |
+
end_progress()
|
321 |
+
|
322 |
p = gr.Progress()
|
323 |
+
p(0, desc="Compiling video")
|
324 |
print('---------------------------------------------------------')
|
325 |
print('FFMPEG')
|
326 |
if haslyrics:
|
|
|
347 |
'-c:v', 'libx264',
|
348 |
'-r', str(fps),
|
349 |
'-pix_fmt', 'yuv420p',
|
350 |
+
'-c:a', 'aac',
|
351 |
'-y',
|
352 |
path + f'{name}.mp4' # Output MP4 filename
|
353 |
]
|
354 |
+
process = subprocess.Popen(ffmpeg_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
|
355 |
+
# Regular expression to match frame information
|
356 |
+
frame_re = re.compile(r"frame=\s*(\d+)")
|
357 |
+
p = gr.Progress()
|
358 |
+
while True:
|
359 |
+
output = process.stderr.readline()
|
360 |
+
if output == '' and process.poll() is not None:
|
361 |
+
break
|
362 |
+
if output:
|
363 |
+
# Check if the output line contains frame information
|
364 |
+
match = frame_re.search(output)
|
365 |
+
if match:
|
366 |
+
frame = match.group(1)
|
367 |
+
p((int(frame), num_frames), desc="Compiling Video")
|
368 |
+
print(f"Frame: {frame}")
|
369 |
+
|
370 |
+
# Wait for the process to complete
|
371 |
+
process.wait()
|
372 |
print('---------------------------------------------------------')
|
373 |
return f"{name}.mp4", haslyrics
|
374 |
|
375 |
+
|
376 |
def gradio_interface(audio_file, lyrics, output_name, fps=30, vidwidth=1280, vidheight=720, oscres=512, img=None, tit=None, ast=None):
|
377 |
if audio_file is None:
|
378 |
raise gr.Error("Please Provide an Audio File")
|
|
|
381 |
|
382 |
resolution = f"{vidwidth}x{vidheight}"
|
383 |
res = tuple(map(int, resolution.split('x')))
|
384 |
+
video_file, haslyrics = main(audio_file, output_name, fps=fps,
|
385 |
+
res=res, oscres=oscres, lyrics=lyrics, img=img, tit=tit, ast=ast)
|
386 |
+
|
387 |
# Clean up the temporary file
|
|
|
388 |
shutil.rmtree("out")
|
389 |
|
390 |
srt_output = "out.srt" if haslyrics else None
|
391 |
return video_file, srt_output, haslyrics
|
392 |
|
393 |
+
|
394 |
def update_srt_output_visibility(haslyrics):
|
395 |
return gr.update(visible=haslyrics)
|
396 |
|
397 |
+
|
398 |
with gr.Blocks() as demo:
|
399 |
+
gr.Markdown(
|
400 |
+
'Upload an MP3 file and configure parameters to create a visualization video.')
|
401 |
+
gr.Markdown(
|
402 |
+
'Optionally upload a word or line synced lyric file in the advanced section.')
|
403 |
|
404 |
with gr.Row():
|
405 |
# Inputs on the left
|
406 |
with gr.Column():
|
407 |
with gr.Accordion(label="Audio Settings", open=True):
|
408 |
gr.Markdown('## Load your mp3 file here')
|
409 |
+
audio_file = gr.File(
|
410 |
+
label="Upload your MP3 file", file_count='single', file_types=['mp3'])
|
411 |
+
|
412 |
with gr.Accordion(label="Mp3 Metadata", open=False):
|
413 |
+
gr.Markdown(
|
414 |
+
'## Add Metadata here if your mp3 does not have one')
|
415 |
cover_img = gr.Image(label='Cover Art', type="filepath")
|
416 |
title_input = gr.Textbox(label='Title')
|
417 |
artist_input = gr.Textbox(label='Artists')
|
418 |
|
419 |
with gr.Accordion(label="Video Output Settings", open=False):
|
420 |
gr.Markdown('## Configure Video Output Here')
|
421 |
+
output_name = gr.Textbox(
|
422 |
+
label="Output Video Name", value='Output')
|
423 |
+
fps_slider = gr.Slider(
|
424 |
+
label="Frames per Second", minimum=20, maximum=60, step=1, value=30)
|
425 |
+
vidwidth_slider = gr.Slider(
|
426 |
+
label="Output Video Width", minimum=100, maximum=2000, value=1280, step=2)
|
427 |
+
vidheight_slider = gr.Slider(
|
428 |
+
label="Output Video Height", minimum=100, maximum=2000, value=720, step=2)
|
429 |
+
|
430 |
with gr.Accordion(label="Advanced Options", open=False):
|
431 |
+
oscres_slider = gr.Slider(
|
432 |
+
label="Number of Visualization Segments", minimum=256, maximum=2048, step=2, value=1024)
|
433 |
+
gr.Markdown(
|
434 |
+
'If uploading LRC, ensure a blank timed line at the end to avoid conversion errors')
|
435 |
+
lyrics_file = gr.File(label="(Optional) Upload Lyrics as LRC or SRT",
|
436 |
+
file_count='single', file_types=['lrc', 'srt'])
|
437 |
|
438 |
# Add a submit button
|
439 |
submit_btn = gr.Button("Generate Video")
|
|
|
447 |
# Bind the button to the function
|
448 |
submit_btn.click(
|
449 |
fn=gradio_interface,
|
450 |
+
inputs=[audio_file, lyrics_file, output_name, fps_slider, vidwidth_slider,
|
451 |
+
vidheight_slider, oscres_slider, cover_img, title_input, artist_input],
|
452 |
outputs=[output_video, srt_output]
|
453 |
)
|
454 |
|
455 |
# Launch Gradio interface
|
456 |
if __name__ == '__main__':
|
457 |
+
demo.launch()
|