MuQ-MuLan / app.py
imChuling's picture
Use English-only description prompts
b420842
Raw
History Blame Contribute Delete
4.55 kB
from __future__ import annotations
import json
import tempfile
import uuid
from pathlib import Path
import gradio as gr
import soundfile as sf
try:
import spaces
except ImportError:
class spaces:
class GPU:
def __init__(self, func=None, duration=60):
self.func = func
def __call__(self, *args, **kwargs):
if self.func is not None:
return self.func(*args, **kwargs)
return args[0]
from pyharp import ModelCard, build_endpoint
from muq_mulan_runtime import rank_descriptions
MIN_AUDIO_SECONDS = 10
MAX_AUDIO_SECONDS = 60
MAX_DESCRIPTIONS = 8
MAX_DESCRIPTION_LENGTH = 300
OUTPUT_ROOT = Path(tempfile.gettempdir()) / "muq_mulan_outputs"
model_card = ModelCard(
name="MuQ-MuLan",
description=(
"Rank English music descriptions by their similarity to an "
"uploaded music clip."
),
author="Tencent AI Lab",
tags=[
"music-information-retrieval",
"music-text-retrieval",
"music-tagging",
"audio-analysis",
],
)
def _validate_audio(path: str | None) -> str:
if not path:
raise gr.Error("Please upload a music clip.")
try:
duration = sf.info(path).duration
except Exception as exc:
raise gr.Error(f"Could not read the audio file: {exc}") from exc
if duration < MIN_AUDIO_SECONDS:
raise gr.Error(
f"Audio must be at least {MIN_AUDIO_SECONDS} seconds long. "
f"Received {duration:.1f} seconds."
)
if duration > MAX_AUDIO_SECONDS:
raise gr.Error(
f"Audio must be no longer than {MAX_AUDIO_SECONDS} seconds. "
f"Received {duration:.1f} seconds."
)
return path
def _parse_descriptions(value: str | None) -> list[str]:
descriptions = [
line.strip()
for line in (value or "").splitlines()
if line.strip()
]
if not descriptions:
raise gr.Error("Enter at least one music description.")
if len(descriptions) > MAX_DESCRIPTIONS:
raise gr.Error(
f"Enter no more than {MAX_DESCRIPTIONS} descriptions."
)
if any(len(description) > MAX_DESCRIPTION_LENGTH for description in descriptions):
raise gr.Error(
"Each description must be no more than "
f"{MAX_DESCRIPTION_LENGTH} characters."
)
return descriptions
@spaces.GPU(duration=240)
def process_fn(
input_audio: str | None,
candidate_descriptions: str | None,
) -> str:
input_audio = _validate_audio(input_audio)
descriptions = _parse_descriptions(candidate_descriptions)
try:
results = rank_descriptions(input_audio, descriptions)
except Exception as exc:
raise gr.Error(f"MuQ-MuLan inference failed: {exc}") from exc
output_dir = OUTPUT_ROOT / uuid.uuid4().hex
output_dir.mkdir(parents=True, exist_ok=True)
output_path = output_dir / "muq_mulan_similarity.json"
output_path.write_text(
json.dumps(
{
"model": "OpenMuQ/MuQ-MuLan-large",
"score_type": "cosine_similarity",
"score_range": [-1.0, 1.0],
"results": results,
},
ensure_ascii=False,
indent=2,
)
+ "\n",
encoding="utf-8",
)
return str(output_path)
with gr.Blocks(title="MuQ-MuLan Music-Text Similarity") as demo:
input_components = [
gr.Audio(
type="filepath",
label="Music Audio",
)
.harp_required(True)
.set_info("Music clip between 10 and 60 seconds long."),
gr.Textbox(
lines=5,
label="Candidate Descriptions",
placeholder=(
"upbeat electronic dance music\n"
"slow acoustic ballad\n"
"bright piano melody"
),
)
.harp_required(True)
.set_info("Enter one English description per line."),
]
output_components = [
gr.File(
type="filepath",
file_types=[".json"],
label="Similarity Ranking",
).set_info("Descriptions ranked by cosine similarity."),
]
build_endpoint(
model_card=model_card,
input_components=input_components,
output_components=output_components,
process_fn=process_fn,
)
if __name__ == "__main__":
demo.queue(default_concurrency_limit=1).launch(
show_error=True,
pwa=True,
)