File size: 4,553 Bytes
96558cb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
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,
    )