File size: 3,551 Bytes
f55264b
 
4bf5da8
 
f55264b
fb903a9
f55264b
 
105e04d
 
f55264b
459123a
f55264b
 
 
3cdf6fd
 
 
 
 
 
ae41f86
f55264b
 
 
459123a
 
 
 
 
 
 
 
f55264b
d4636df
 
f55264b
 
d4636df
 
 
 
 
 
fb903a9
 
 
 
 
 
 
 
 
 
 
 
f55264b
 
 
5074382
 
 
 
 
154d1e1
 
 
 
 
 
 
 
 
 
5074382
ae41f86
199d6c3
 
 
5074382
 
 
 
 
 
 
 
ce8de21
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
from __future__ import annotations

import spaces  # imported first, as ZeroGPU requires

from pathlib import Path
from tempfile import mkdtemp

import gradio as gr
from pyharp import ModelCard, build_endpoint
from pyharp.tags import Subcategory

from runtime import EXAMPLE, load_model, render

model_card = ModelCard(
    name="DExter",
    description=(
        "Render an expressive piano MIDI performance from a MusicXML score. "
        "Repository: https://github.com/anusfoil/DExter Paper: "
        "https://arxiv.org/abs/2406.14850"
    ),
    author="Huan Zhang, Shreyan Chowdhury, Carlos Eduardo Cancino-Chacón, et al.",
    tags=[Subcategory.PERFORMANCE_RENDERING, 'piano'],
)


# pyharp runs process_fn in a worker, which imports this file as __mp_main__ and starts with the
# server. The model is built and placed there alone, where ZeroGPU's emulation records the CUDA
# placement. ZeroGPU runs each request in a process of its own, so a model first built inside the
# GPU call was built again, inside the reservation, every time.
if __name__ != "__main__":
    load_model()


def process_fn(xml_path: str | None, use_example: bool, seed: int) -> str:
    # Checked before the GPU is reserved, so a request without a score spends none of the
    # caller's ZeroGPU allowance
    if not xml_path and not use_example:
        raise gr.Error("Upload a MusicXML score or select the example.")

    return perform(xml_path or str(EXAMPLE), int(seed))


@spaces.GPU(duration=75)
def perform(source: str, seed: int) -> str:
    # mkdtemp, not a context manager: that would delete the directory on exit,
    # taking the rendered file with it before the caller can read it, which is
    # what the block-cache call here was working around. That also reached for
    # output_components, which exists only under __main__ and so is undefined
    # in the worker process that imports this module.
    output = Path(mkdtemp(prefix="dexter-")) / "performance.mid"
    try:
        render(source, output, int(seed))
    except Exception as exc:
        raise gr.Error(f"DExter inference failed: {exc}") from exc

    return str(output)


if __name__ == "__main__":
    with gr.Blocks(title="DExter", delete_cache=(3600, 86400)) as demo:
        inputs = [
            gr.File(type="filepath", file_types=[".xml", ".musicxml"], label="Piano MusicXML score").set_info(
                "Uncompressed MusicXML, up to 2 MB and 400 notes. Upload takes priority over the example."
            ),
            gr.Checkbox(value=True, label="Use bundled Schubert example if no file is uploaded",
            info=(
                "Runs on a bundled Schubert excerpt when you have not uploaded a "
                "score."
            )),
            gr.Number(value=13, precision=0, minimum=0, maximum=2147483647, label="Random seed",
            info=(
                "Changes the performance the model samples. The same seed and score "
                "give the same performance."
            )),
        ]
        output_components = [gr.File(type="filepath", label="Expressive MIDI performance", file_types=[".mid", ".midi"]).set_info((
            "The score played with timing, dynamics and pedalling the model "
            "chose."
        ))]
        build_endpoint(
            model_card=model_card,
            input_components=inputs,
            output_components=output_components,
            process_fn=process_fn,
        )


    demo.queue(default_concurrency_limit=1, max_size=4).launch(show_error=True, pwa=True, max_file_size="2mb")