Create app.py
Browse files
app.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
import tempfile, os, subprocess, shutil
|
| 3 |
+
from typing import List, Tuple
|
| 4 |
+
from mutagen.id3 import ID3, ID3NoHeaderError, GEOB, TSSE
|
| 5 |
+
from mutagen.mp3 import MP3
|
| 6 |
+
|
| 7 |
+
TARGET_SR = 44100
|
| 8 |
+
TARGET_CH = 1
|
| 9 |
+
TARGET_BR = "128k"
|
| 10 |
+
|
| 11 |
+
def standardize_one(in_path: str, tsse_text: str, geob_bytes: bytes, geob_mime: str, geob_desc: str) -> str:
|
| 12 |
+
base = os.path.splitext(os.path.basename(in_path))[0]
|
| 13 |
+
out_path = os.path.join(tempfile.gettempdir(), f"{base}_standardized.mp3")
|
| 14 |
+
|
| 15 |
+
# 1) Re-encode audio with FFmpeg and enforce container/tag options
|
| 16 |
+
cmd = [
|
| 17 |
+
"ffmpeg", "-y",
|
| 18 |
+
"-i", in_path,
|
| 19 |
+
"-ar", str(TARGET_SR),
|
| 20 |
+
"-ac", str(TARGET_CH),
|
| 21 |
+
"-c:a", "libmp3lame",
|
| 22 |
+
"-b:a", TARGET_BR,
|
| 23 |
+
"-write_xing", "0",
|
| 24 |
+
"-id3v2_version", "4",
|
| 25 |
+
"-write_id3v1", "0",
|
| 26 |
+
"-metadata", f"TSSE={tsse_text or 'HF-Space'}",
|
| 27 |
+
out_path
|
| 28 |
+
]
|
| 29 |
+
subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
| 30 |
+
|
| 31 |
+
# 2) Ensure ID3v2.4 and add GEOB + TSSE with Mutagen
|
| 32 |
+
try:
|
| 33 |
+
tags = ID3(out_path)
|
| 34 |
+
except ID3NoHeaderError:
|
| 35 |
+
tags = ID3()
|
| 36 |
+
|
| 37 |
+
# Enforce TSSE (software/encoder)
|
| 38 |
+
tags.setall("TSSE", [TSSE(encoding=3, text=[tsse_text or "HF-Space"])])
|
| 39 |
+
# Always add a GEOB frame (either from upload or a tiny placeholder)
|
| 40 |
+
if geob_bytes is None or len(geob_bytes) == 0:
|
| 41 |
+
geob_bytes = b"HF-Space GEOB placeholder"
|
| 42 |
+
geob_mime = geob_mime or "application/octet-stream"
|
| 43 |
+
geob_desc = geob_desc or "asset"
|
| 44 |
+
tags.add(GEOB(encoding=3, mime=geob_mime or "application/octet-stream", desc=geob_desc or "asset", data=geob_bytes))
|
| 45 |
+
|
| 46 |
+
# Save as ID3v2.4 explicitly
|
| 47 |
+
tags.save(out_path, v2_version=4)
|
| 48 |
+
|
| 49 |
+
# Sanity check via Mutagen
|
| 50 |
+
_ = MP3(out_path) # will raise if corrupt
|
| 51 |
+
return out_path
|
| 52 |
+
|
| 53 |
+
def standardize_many(files: List[gr.File], tsse_text: str, geob_file: gr.File, geob_mime: str, geob_desc: str):
|
| 54 |
+
outputs = []
|
| 55 |
+
geob_bytes = None
|
| 56 |
+
if geob_file is not None and hasattr(geob_file, "name") and geob_file.name:
|
| 57 |
+
with open(geob_file.name, "rb") as f:
|
| 58 |
+
geob_bytes = f.read()
|
| 59 |
+
|
| 60 |
+
for f in files or []:
|
| 61 |
+
out = standardize_one(f.name, tsse_text, geob_bytes, geob_mime, geob_desc)
|
| 62 |
+
outputs.append(out)
|
| 63 |
+
|
| 64 |
+
# Optionally zip if multiple
|
| 65 |
+
if len(outputs) > 1:
|
| 66 |
+
zip_path = os.path.join(tempfile.gettempdir(), "standardized_mp3s.zip")
|
| 67 |
+
if os.path.exists(zip_path):
|
| 68 |
+
os.remove(zip_path)
|
| 69 |
+
with tempfile.TemporaryDirectory() as tmpd:
|
| 70 |
+
paths = []
|
| 71 |
+
for p in outputs:
|
| 72 |
+
dst = os.path.join(tmpd, os.path.basename(p))
|
| 73 |
+
shutil.copy2(p, dst)
|
| 74 |
+
paths.append(dst)
|
| 75 |
+
shutil.make_archive(zip_path[:-4], 'zip', tmpd)
|
| 76 |
+
return zip_path
|
| 77 |
+
elif len(outputs) == 1:
|
| 78 |
+
return outputs[0]
|
| 79 |
+
else:
|
| 80 |
+
return None
|
| 81 |
+
|
| 82 |
+
with gr.Blocks(title="MP3 Standardizer (44.1k/mono/128k CBR, no Xing, ID3v2.4, TSSE+GEOB)") as demo:
|
| 83 |
+
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.")
|
| 84 |
+
|
| 85 |
+
with gr.Row():
|
| 86 |
+
files = gr.File(label="MP3 files", file_types=[".mp3"], file_count="multiple")
|
| 87 |
+
with gr.Accordion("Metadata options", open=False):
|
| 88 |
+
tsse = gr.Textbox(label="TSSE (encoder/software)", value="HF-Space", placeholder="e.g., Lavf60.16.100")
|
| 89 |
+
geob = gr.File(label="GEOB payload (optional)", file_types=[".bin", ".txt", ".json", ".png", ".jpg", ".mp3", ".mp4"], file_count="single")
|
| 90 |
+
geob_mime = gr.Textbox(label="GEOB MIME type", value="application/octet-stream")
|
| 91 |
+
geob_desc = gr.Textbox(label="GEOB description", value="asset")
|
| 92 |
+
|
| 93 |
+
run = gr.Button("Standardize")
|
| 94 |
+
result = gr.File(label="Download standardized file(s)")
|
| 95 |
+
run.click(standardize_many, inputs=[files, tsse, geob, geob_mime, geob_desc], outputs=[result])
|
| 96 |
+
|
| 97 |
+
if __name__ == "__main__":
|
| 98 |
+
demo.launch()
|