File size: 3,398 Bytes
f330184
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15b272c
f330184
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15b272c
 
f506945
fa73991
 
 
ce70fdf
f330184
 
 
f506945
 
f330184
 
 
 
 
 
f506945
f330184
 
 
 
f506945
fa73991
 
 
 
 
 
 
 
 
 
f506945
 
f330184
 
 
 
 
 
 
f506945
 
 
f330184
fa73991
f330184
 
 
 
 
 
 
 
 
 
 
 
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
import sys

sys.stdout.reconfigure(line_buffering=True)

try:
    import spaces
except ImportError:
    # keep @spaces.GPU usable as a no-op; ZeroGPU requires this exact name.
    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)
                func = args[0]
                return func


from pyharp import ModelCard, build_endpoint
from pyharp.labels import LabelList, AudioLabel

import gradio as gr
import torch

from beat_this.inference import File2Beats

DEVICE = "cuda" if torch.cuda.is_available() else "cpu"

# built on CPU at import time so a broken checkpoint fails fast in the logs;
# moved to DEVICE lazily on first request, since ZeroGPU only allows CUDA
# calls made inside an @spaces.GPU-decorated call.
model = File2Beats(checkpoint_path="final0.ckpt", device="cpu", dbn=False)
model_ready = DEVICE == "cpu"

model_card = ModelCard(
    name="Beat This!",
    description="Detects the beat and downbeat (start-of-bar) positions in a piece of music.",
    author="Francesco Foscarin, Jan Schlüter",
    tags=["beat tracking", "rhythm"],
)

BEAT_COLOR = AudioLabel.rgb_color_to_int(120, 170, 255)  # blue
DOWNBEAT_COLOR = AudioLabel.rgb_color_to_int(255, 130, 40)  # orange

# downbeats stay in the overhead row (no amplitude); beats get an amplitude
# near the top of the waveform, so they show up as a row just below downbeats
BEAT_AMPLITUDE = 0.9


@spaces.GPU
@torch.inference_mode()
def process_fn(input_audio_path: str) -> tuple[str, LabelList]:
    """Finds beat and downbeat times in the input audio and returns them as labeled markers on the audio."""
    global model, model_ready
    if not model_ready:
        model = File2Beats(checkpoint_path="final0.ckpt", device=DEVICE, dbn=False)
        model_ready = True

    beats, downbeats = model(input_audio_path)
    # downbeats are a subset of beats with matching float values
    downbeat_times = set(downbeats.tolist())

    output_labels = LabelList()
    for t in beats:
        is_downbeat = float(t) in downbeat_times
        if is_downbeat:
            output_labels.append(
                AudioLabel(t=float(t), label="downbeat", color=DOWNBEAT_COLOR)
            )
        else:
            output_labels.append(
                AudioLabel(
                    t=float(t), label="beat", color=BEAT_COLOR, amplitude=BEAT_AMPLITUDE
                )
            )
    # HARP only attaches labels to an output track, so pass the audio through unchanged
    return input_audio_path, output_labels


with gr.Blocks() as demo:
    input_components = [
        gr.Audio(type="filepath", label="Input Audio").harp_required(True),
    ]
    output_components = [
        gr.Audio(type="filepath", label="Output Audio").set_info(
            "Input audio, unchanged, with detected beats and downbeats marked."
        ),
        gr.JSON(label="Beats").set_info(
            "Detected beat and downbeat times, labeled \"beat\" or \"downbeat\"."
        ),
    ]

    build_endpoint(
        model_card=model_card,
        input_components=input_components,
        output_components=output_components,
        process_fn=process_fn,
    )

if __name__ == "__main__":
    demo.queue().launch(pwa=True)