|
|
import gradio as gr |
|
|
import tempfile, os, subprocess, shutil |
|
|
from typing import List, Tuple |
|
|
from mutagen.id3 import ID3, ID3NoHeaderError, GEOB, TSSE |
|
|
from mutagen.mp3 import MP3 |
|
|
|
|
|
TARGET_SR = 44100 |
|
|
TARGET_CH = 1 |
|
|
TARGET_BR = "128k" |
|
|
|
|
|
def standardize_one(in_path: str, tsse_text: str, geob_bytes: bytes, geob_mime: str, geob_desc: str) -> str: |
|
|
base = os.path.splitext(os.path.basename(in_path))[0] |
|
|
out_path = os.path.join(tempfile.gettempdir(), f"{base}_standardized.mp3") |
|
|
|
|
|
|
|
|
cmd = [ |
|
|
"ffmpeg", "-y", |
|
|
"-i", in_path, |
|
|
"-ar", str(TARGET_SR), |
|
|
"-ac", str(TARGET_CH), |
|
|
"-c:a", "libmp3lame", |
|
|
"-b:a", TARGET_BR, |
|
|
"-write_xing", "0", |
|
|
"-id3v2_version", "4", |
|
|
"-write_id3v1", "0", |
|
|
"-metadata", f"TSSE={tsse_text or 'HF-Space'}", |
|
|
out_path |
|
|
] |
|
|
subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
|
|
|
|
|
|
|
|
try: |
|
|
tags = ID3(out_path) |
|
|
except ID3NoHeaderError: |
|
|
tags = ID3() |
|
|
|
|
|
|
|
|
tags.setall("TSSE", [TSSE(encoding=3, text=[tsse_text or "HF-Space"])]) |
|
|
|
|
|
if geob_bytes is None or len(geob_bytes) == 0: |
|
|
geob_bytes = b"HF-Space GEOB placeholder" |
|
|
geob_mime = geob_mime or "application/octet-stream" |
|
|
geob_desc = geob_desc or "asset" |
|
|
tags.add(GEOB(encoding=3, mime=geob_mime or "application/octet-stream", desc=geob_desc or "asset", data=geob_bytes)) |
|
|
|
|
|
|
|
|
tags.save(out_path, v2_version=4) |
|
|
|
|
|
|
|
|
_ = MP3(out_path) |
|
|
return out_path |
|
|
|
|
|
def standardize_many(files: List[gr.File], tsse_text: str, geob_file: gr.File, geob_mime: str, geob_desc: str): |
|
|
outputs = [] |
|
|
geob_bytes = None |
|
|
if geob_file is not None and hasattr(geob_file, "name") and geob_file.name: |
|
|
with open(geob_file.name, "rb") as f: |
|
|
geob_bytes = f.read() |
|
|
|
|
|
for f in files or []: |
|
|
out = standardize_one(f.name, tsse_text, geob_bytes, geob_mime, geob_desc) |
|
|
outputs.append(out) |
|
|
|
|
|
|
|
|
if len(outputs) > 1: |
|
|
zip_path = os.path.join(tempfile.gettempdir(), "standardized_mp3s.zip") |
|
|
if os.path.exists(zip_path): |
|
|
os.remove(zip_path) |
|
|
with tempfile.TemporaryDirectory() as tmpd: |
|
|
paths = [] |
|
|
for p in outputs: |
|
|
dst = os.path.join(tmpd, os.path.basename(p)) |
|
|
shutil.copy2(p, dst) |
|
|
paths.append(dst) |
|
|
shutil.make_archive(zip_path[:-4], 'zip', tmpd) |
|
|
return zip_path |
|
|
elif len(outputs) == 1: |
|
|
return outputs[0] |
|
|
else: |
|
|
return None |
|
|
|
|
|
with gr.Blocks(title="MP3 Standardizer (44.1k/mono/128k CBR, no Xing, ID3v2.4, TSSE+GEOB)") as demo: |
|
|
gr.Markdown("Upload MP3s to standardize them to 44.1 kHz, mono, 128 kbps CBR, no Xing/Info, ID3v2.4 only, plus TSSE and GEOB tags.") |
|
|
|
|
|
with gr.Row(): |
|
|
files = gr.File(label="MP3 files", file_types=[".mp3"], file_count="multiple") |
|
|
with gr.Accordion("Metadata options", open=False): |
|
|
tsse = gr.Textbox(label="TSSE (encoder/software)", value="HF-Space", placeholder="e.g., Lavf60.16.100") |
|
|
geob = gr.File(label="GEOB payload (optional)", file_types=[".bin", ".txt", ".json", ".png", ".jpg", ".mp3", ".mp4"], file_count="single") |
|
|
geob_mime = gr.Textbox(label="GEOB MIME type", value="application/octet-stream") |
|
|
geob_desc = gr.Textbox(label="GEOB description", value="asset") |
|
|
|
|
|
run = gr.Button("Standardize") |
|
|
result = gr.File(label="Download standardized file(s)") |
|
|
run.click(standardize_many, inputs=[files, tsse, geob, geob_mime, geob_desc], outputs=[result]) |
|
|
|
|
|
if __name__ == "__main__": |
|
|
demo.launch() |
|
|
|