Vansh Chugh commited on
Commit
fe7e262
·
1 Parent(s): bf6c893

initial deploy

Browse files
.gitignore ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ .DS_Store
5
+ PiCoGenv2-repo/
README.md CHANGED
@@ -1,11 +1,11 @@
1
  ---
2
  title: PiCoGen
3
- emoji: 🏆
4
  colorFrom: pink
5
  colorTo: red
6
  sdk: gradio
7
- sdk_version: 6.20.0
8
- python_version: '3.12'
9
  app_file: app.py
10
  pinned: false
11
  license: mit
 
1
  ---
2
  title: PiCoGen
3
+ emoji: 🎹
4
  colorFrom: pink
5
  colorTo: red
6
  sdk: gradio
7
+ sdk_version: 5.28.0
8
+ python_version: '3.10'
9
  app_file: app.py
10
  pinned: false
11
  license: mit
app.py ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+
3
+ sys.stdout.reconfigure(line_buffering=True)
4
+
5
+ try:
6
+ import spaces
7
+ except ImportError:
8
+ # keep @spaces.GPU usable as a no-op; ZeroGPU requires this exact name.
9
+ class spaces:
10
+ class GPU:
11
+ def __init__(self, func=None, duration=60):
12
+ self.func = func
13
+
14
+ def __call__(self, *args, **kwargs):
15
+ if self.func is not None:
16
+ return self.func(*args, **kwargs)
17
+ func = args[0]
18
+ return func
19
+
20
+ import tempfile
21
+ import threading
22
+
23
+ import gradio as gr
24
+ import soundfile as sf
25
+ import torch
26
+ from pyharp import ModelCard, build_endpoint
27
+
28
+ import picogen2
29
+ from picogen2.mirtoolkit.beat_this import BeatThis
30
+ from picogen2.mirtoolkit.sheetsage import SheetSage
31
+
32
+ # SheetSage (this model's audio feature extractor) was trained on ~24s segments and is
33
+ # most accurate on short clips; longer songs also risk exceeding the GPU time budget below.
34
+ MAX_INPUT_SECONDS = 30.0
35
+
36
+ DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
37
+
38
+ decoder = None
39
+ decoder_ready = False # has the decoder been moved onto the GPU yet?
40
+ tokenizer = None
41
+ beat_detector = None
42
+ sheetsage_model = None
43
+
44
+ model_loading = True
45
+ model_error = None
46
+
47
+
48
+ def load_assets():
49
+ """Downloads PiCoGen2's checkpoint, pre-fetches SheetSage's (large) asset cache, and
50
+ builds the beat tracker, all on CPU so the app can start serving while this runs."""
51
+ global decoder, tokenizer, beat_detector, model_loading, model_error
52
+ try:
53
+ tokenizer = picogen2.Tokenizer()
54
+ decoder = picogen2.PiCoGenDecoder.from_pretrained(device="cpu")
55
+
56
+ try:
57
+ import sheetsage.assets as sheetsage_assets
58
+
59
+ for tag in sheetsage_assets.get_asset_tags():
60
+ sheetsage_assets.retrieve_asset(tag)
61
+ except Exception as e:
62
+ # SheetSage lazily re-downloads whatever's missing on first use, so this is
63
+ # a warm-up, not a hard requirement.
64
+ print(f"SheetSage asset pre-fetch incomplete, will retry on first request: {e}")
65
+
66
+ beat_detector = BeatThis(cuda=False)
67
+ print("Models loaded (CPU).")
68
+ except Exception as e:
69
+ model_error = str(e)
70
+ print(f"Load error: {e}")
71
+ finally:
72
+ model_loading = False
73
+
74
+
75
+ threading.Thread(target=load_assets, daemon=True).start()
76
+
77
+
78
+ model_card = ModelCard(
79
+ name="PiCoGen2",
80
+ description=(
81
+ "Generates a piano cover from a short pop song clip: SheetSage extracts melody/"
82
+ "harmony audio features, and a GPT-NeoX decoder turns them into piano notes."
83
+ ),
84
+ author="Chih-Pin Tan, Hsin Ai, Yi-Hsin Chang, Shuen-Huei Guan, Yi-Hsuan Yang",
85
+ tags=["music generation", "piano cover", "midi"],
86
+ )
87
+
88
+
89
+ @spaces.GPU(duration=300)
90
+ @torch.inference_mode()
91
+ def process_fn(input_audio_path: str, temperature: float) -> str:
92
+ """Detects beats, extracts SheetSage audio features, and generates a piano cover."""
93
+ global decoder, sheetsage_model, decoder_ready
94
+
95
+ if model_loading:
96
+ raise gr.Error("Model is still loading, please wait a moment and try again.")
97
+ if decoder is None:
98
+ raise gr.Error(f"Model failed to load: {model_error}")
99
+ if DEVICE != "cuda":
100
+ raise gr.Error("This model requires a GPU; none is available.")
101
+
102
+ duration = sf.info(input_audio_path).duration
103
+ if duration > MAX_INPUT_SECONDS:
104
+ raise gr.Error(
105
+ f"Input is {duration:.1f}s long; please trim it to {MAX_INPUT_SECONDS:.0f}s "
106
+ "or shorter."
107
+ )
108
+
109
+ if not decoder_ready:
110
+ decoder = decoder.to(DEVICE) # only safe here, inside @spaces.GPU
111
+ decoder_ready = True
112
+ if sheetsage_model is None:
113
+ sheetsage_model = SheetSage() # constructs Jukebox on the GPU internally
114
+
115
+ beats, downbeats = beat_detector(input_audio_path)
116
+ beat_information = {"beats": beats.tolist(), "downbeats": downbeats.tolist()}
117
+
118
+ sheetsage_output = sheetsage_model(
119
+ audio_path=input_audio_path, beat_information=beat_information
120
+ )
121
+
122
+ out_events = picogen2.decode(
123
+ model=decoder,
124
+ tokenizer=tokenizer,
125
+ beat_information=beat_information,
126
+ melody_last_embs=sheetsage_output["melody_last_hidden_state"],
127
+ harmony_last_embs=sheetsage_output["harmony_last_hidden_state"],
128
+ temperature=temperature,
129
+ device=DEVICE,
130
+ )
131
+
132
+ with tempfile.NamedTemporaryFile(suffix=".mid", delete=False) as f:
133
+ output_midi_path = f.name
134
+ tokenizer.events_to_midi(out_events).dump(output_midi_path)
135
+ return output_midi_path
136
+
137
+
138
+ with gr.Blocks() as demo:
139
+ input_components = [
140
+ gr.Audio(
141
+ type="filepath",
142
+ label="Input Audio",
143
+ info=f"Short song clip, up to {MAX_INPUT_SECONDS:.0f}s",
144
+ ).harp_required(True),
145
+ gr.Slider(
146
+ minimum=0.1,
147
+ maximum=2.0,
148
+ step=0.1,
149
+ value=1.0,
150
+ label="Temperature",
151
+ info="Sampling temperature for the piano decoder (default: 1.0, per repo config)",
152
+ ),
153
+ ]
154
+ output_components = [
155
+ gr.File(type="filepath", label="Piano Cover", file_types=[".mid", ".midi"]).set_info(
156
+ "Generated piano cover, as a MIDI file."
157
+ ),
158
+ ]
159
+
160
+ build_endpoint(
161
+ model_card=model_card,
162
+ input_components=input_components,
163
+ output_components=output_components,
164
+ process_fn=process_fn,
165
+ )
166
+
167
+ demo.queue().launch(pwa=True)
packages.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ ffmpeg
2
+ libopenmpi-dev
3
+ wget
picogen2/__init__.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ from .infer import decode
2
+ from .model import PiCoGenDecoder
3
+ from .repr import Tokenizer
4
+ from .version import VERSION, VERSION_SHORT
5
+
6
+ __all__ = ["decode", "PiCoGenDecoder", "Tokenizer", "VERSION", "VERSION_SHORT"]
picogen2/assets.py ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import shutil
2
+ import subprocess
3
+ from pathlib import Path
4
+
5
+ from .utils import logger
6
+
7
+ CACHE_DIR = Path.home() / ".cache" / "picogen2"
8
+
9
+ URL_MODEL = "https://zenodo.org/records/13380452/files/model_ft_00070000?download=1"
10
+ URL_VOCAB = "https://raw.githubusercontent.com/tanchihpin0517/PiCoGen/v2/assets/vocab.json"
11
+ URL_CONFIG = "https://raw.githubusercontent.com/tanchihpin0517/PiCoGen/v2/assets/config.json"
12
+ URL_TEST_SONG = "https://www.dropbox.com/scl/fi/zj68yghtn0cwtwnqj7vrx/pop.00000.wav?rlkey=bejuh89wehbc8psl9ujmqa73u&st=kb265uvz&dl=0"
13
+
14
+
15
+ def default_cache_dir_decorator(func):
16
+ def wrapper(*args, **kwargs):
17
+ CACHE_DIR.mkdir(parents=True, exist_ok=True)
18
+ return func(*args, **kwargs)
19
+
20
+ return wrapper
21
+
22
+
23
+ @default_cache_dir_decorator
24
+ def checkpoint_file():
25
+ default_ckpt_file = CACHE_DIR / "model_ft_00070000"
26
+ if not default_ckpt_file.exists():
27
+ logger.warning("Download default model from {}".format(URL_MODEL))
28
+ logger.warning("Save to {}".format(default_ckpt_file))
29
+
30
+ _download(URL_MODEL, default_ckpt_file)
31
+
32
+ return default_ckpt_file
33
+
34
+
35
+ @default_cache_dir_decorator
36
+ def vocab_file():
37
+ default_vocab_file = CACHE_DIR / "vocab.json"
38
+ if not default_vocab_file.exists():
39
+ logger.warning("Download default vocab from {}".format(URL_VOCAB))
40
+ logger.warning("Save to {}".format(default_vocab_file))
41
+
42
+ _download(URL_VOCAB, default_vocab_file)
43
+
44
+ return default_vocab_file
45
+
46
+
47
+ @default_cache_dir_decorator
48
+ def config_file():
49
+ default_config_file = CACHE_DIR / "config.json"
50
+ if not default_config_file.exists():
51
+ logger.warning("Download default config from {}".format(URL_CONFIG))
52
+ logger.warning("Save to {}".format(default_config_file))
53
+
54
+ _download(URL_CONFIG, default_config_file)
55
+
56
+ return default_config_file
57
+
58
+
59
+ @default_cache_dir_decorator
60
+ def test_song():
61
+ default_test_song = CACHE_DIR / "pop.00000.wav"
62
+ if not default_test_song.exists():
63
+ logger.warning("Download default test song from {}".format(URL_TEST_SONG))
64
+ logger.warning("Save to {}".format(default_test_song))
65
+
66
+ _download(URL_TEST_SONG, default_test_song)
67
+
68
+ return default_test_song
69
+
70
+
71
+ def _download(url, output_file_path, verbose=True):
72
+ if verbose:
73
+ logger.info(f"Downloading {url} to {output_file_path}")
74
+
75
+ if shutil.which("wget") is None:
76
+ logger.error("wget is not installed. Please install wget to download the model.")
77
+ raise FileNotFoundError("`wget` is not installed")
78
+
79
+ try:
80
+ subprocess.run(["wget", url, "-O", str(output_file_path)], check=True)
81
+ except subprocess.CalledProcessError as e:
82
+ logger.error(f"Failed to download file from {url}: {e}")
83
+ if output_file_path.exists():
84
+ output_file_path.unlink()
85
+ raise e
picogen2/infer.py ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import torch
3
+ from tqdm import tqdm
4
+
5
+ from .model import PiCoGenDecoder
6
+ from .repr import Event
7
+ from .utils import downbeat_time_to_index
8
+
9
+
10
+ @torch.no_grad()
11
+ def decode(
12
+ model,
13
+ tokenizer,
14
+ beat_information,
15
+ melody_last_embs,
16
+ harmony_last_embs,
17
+ max_bar_num=None,
18
+ max_token_num=None,
19
+ temperature=1.0,
20
+ device=None,
21
+ ):
22
+ if device is None:
23
+ device = model.parameters().__next__().device
24
+
25
+ starting_bpm = 60 / np.diff(np.array(beat_information["beats"])[:2]).mean()
26
+
27
+ TARGET = PiCoGenDecoder.InputClass.TARGET.value
28
+ CONDITION = PiCoGenDecoder.InputClass.CONDITION.value
29
+
30
+ out_events = [
31
+ Event(etype="spec", value="spec_ss"),
32
+ tokenizer.get_tempo_event(starting_bpm),
33
+ ]
34
+
35
+ input_seg = [tokenizer.e2i(Event(etype="spec", value="spec_bos"))]
36
+ input_seg.extend([tokenizer.e2i(e) for e in out_events]) # song start, global tempo
37
+ need_encode_seg = [0] * len(input_seg)
38
+ input_cls_seg = [TARGET] * len(input_seg)
39
+
40
+ end_event = Event(etype="spec", value="spec_se")
41
+ bar_start_event = Event(etype="bar", value="bar_start")
42
+ bar_end_event = Event(etype="bar", value="bar_end")
43
+
44
+ total_beats = len(beat_information["beats"])
45
+ downbeats = downbeat_time_to_index(beat_information["beats"], beat_information["downbeats"])
46
+ if downbeats[-1] < total_beats:
47
+ downbeats.append(total_beats - 1)
48
+ if max_bar_num is not None:
49
+ downbeats = downbeats[: max_bar_num + 1]
50
+
51
+ pbar = tqdm(total=len(downbeats) - 1)
52
+ for bar_i, b in enumerate(range(len(downbeats) - 1)):
53
+ last_past_kv = None
54
+
55
+ # NOTE: upbeat is handled by SheetSage
56
+ downbeat_start, downbeat_end = downbeats[b], downbeats[b + 1]
57
+ # downbeat_start, downbeat_end = downbeats[b]-downbeats[0], downbeats[b+1]-downbeats[0]
58
+ for j in range(downbeat_start * tokenizer.beat_div, downbeat_end * tokenizer.beat_div):
59
+ input_seg.append((melody_last_embs[j], harmony_last_embs[j]))
60
+ input_cls_seg.append(CONDITION)
61
+ need_encode_seg.append(1)
62
+ if b == len(downbeats) - 2: # NOTE: add song_end to the last bar condition
63
+ input_seg.append(tokenizer.e2i(Event(etype="spec", value="spec_se")))
64
+ input_cls_seg.append(CONDITION)
65
+ need_encode_seg.append(0)
66
+
67
+ input_seg.append(tokenizer.e2i(bar_start_event))
68
+ need_encode_seg.append(0)
69
+ input_cls_seg.append(TARGET)
70
+ out_events.append(bar_start_event)
71
+
72
+ while True: # generate one bar
73
+ if len(input_seg) > model.hp.max_seq_len:
74
+ input_seg = input_seg[-model.hp.max_seq_len // 2 :]
75
+ input_seg[0] = tokenizer.e2i(Event(etype="spec", value="spec_bos"))
76
+ need_encode_seg = need_encode_seg[-model.hp.max_seq_len // 2 :]
77
+ need_encode_seg[0] = 0
78
+ input_cls_seg = input_cls_seg[-model.hp.max_seq_len // 2 :]
79
+ input_cls_seg[0] = TARGET
80
+ last_past_kv = None
81
+
82
+ input_cls_ids = torch.LongTensor(input_cls_seg)[None, :].to(device)
83
+ need_encode = torch.BoolTensor(need_encode_seg)[None, :].to(device)
84
+
85
+ output_ids, past_kv = model.generate(
86
+ input_seg=[input_seg],
87
+ input_cls_ids=input_cls_ids,
88
+ need_encode=need_encode,
89
+ kv_cache=last_past_kv,
90
+ temperature=temperature,
91
+ )
92
+ out_id = output_ids[0][-1].item()
93
+ out_event = tokenizer.i2e(out_id)
94
+
95
+ out_events.append(out_event)
96
+ input_seg.append(out_id)
97
+ input_cls_seg.append(TARGET)
98
+ need_encode_seg.append(0)
99
+
100
+ if out_event in (bar_end_event, end_event):
101
+ break
102
+ last_past_kv = past_kv
103
+
104
+ pbar.set_description(f"length: {len(out_events)}({len(input_seg)})")
105
+
106
+ pbar.update(1)
107
+
108
+ if max_token_num is not None and len(input_seg) > max_token_num:
109
+ break
110
+
111
+ pbar.close()
112
+ return out_events
picogen2/mirtoolkit/__init__.py ADDED
File without changes
picogen2/mirtoolkit/beat_this.py ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ CPJKU's beat_this
3
+ - reference: https://github.com/CPJKU/beat_this
4
+ """
5
+
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ import torch
10
+
11
+ from .utils import load_audio
12
+
13
+ if not sys.version_info < (3, 10):
14
+ from beat_this.inference import Audio2Beats
15
+
16
+
17
+ class BeatThis:
18
+ def __init__(self, cuda=None, dbn=True):
19
+ if cuda is None:
20
+ cuda = torch.cuda.is_available()
21
+
22
+ self.audio2beats = Audio2Beats(
23
+ device="cuda" if cuda else "cpu",
24
+ dbn=dbn,
25
+ )
26
+ self.use_dbn = dbn
27
+
28
+ @torch.no_grad()
29
+ def __call__(
30
+ self,
31
+ file_or_array,
32
+ sr=None,
33
+ beats_per_bar=[3, 4],
34
+ min_bpm=55.0,
35
+ max_bpm=215.0,
36
+ fps=50,
37
+ transition_lambda=100,
38
+ ):
39
+ """
40
+ Function for extracting beat and downbeat positions (in seconds) from a file or a data array.
41
+
42
+ Args:
43
+ file_or_array (str or Path or ndarray): Path to the audio file or numpy array containing the audio data.
44
+ sr (int, optional): Sample rate of the audio file. Required if `file_or_array` is a numpy array. Defaults to None.
45
+ if dbn is True:
46
+ beats_per_bar (list, optional): List of possible beats per bar. Defaults to [3, 4].
47
+ min_bpm (float, optional): Minimum tempo in BPM. Defaults to 55.0.
48
+ max_bpm (float, optional): Maximum tempo in BPM. Defaults to 215.0.
49
+ fps (int, optional): Frames per second. Defaults to 50.
50
+
51
+ Returns:
52
+ beats (ndarray): Array of beat positions in seconds.
53
+ downbeats (ndarray): Array of downbeat positions in seconds.
54
+ """
55
+ if sys.version_info < (3, 10):
56
+ raise ImportError("Python 3.10 or higher is required to use this function.")
57
+
58
+ if isinstance(file_or_array, (str, Path)):
59
+ audio, sr = load_audio(file_or_array, dtype="float64")
60
+ else:
61
+ audio = file_or_array
62
+
63
+ if self.use_dbn:
64
+ from madmom.features.downbeats import DBNDownBeatTrackingProcessor
65
+
66
+ # WARN: This is a hacky way to set the DBN parameters
67
+ dbn = DBNDownBeatTrackingProcessor(
68
+ beats_per_bar=beats_per_bar,
69
+ min_bpm=min_bpm,
70
+ max_bpm=max_bpm,
71
+ fps=fps,
72
+ transition_lambda=transition_lambda,
73
+ )
74
+ self.audio2beats.frames2beats.dbn = dbn
75
+
76
+ beats, downbeats = self.audio2beats(audio, sr)
77
+
78
+ return beats, downbeats
picogen2/mirtoolkit/sheetsage.py ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ SheetSage: lead sheet transcription model
3
+ - reference: https://github.com/chrisdonahue/sheetsage
4
+ """
5
+
6
+ import logging
7
+ import shutil
8
+ import subprocess
9
+ import tempfile
10
+ from pathlib import Path
11
+ from typing import Union
12
+
13
+ from sheetsage.infer import sheetsage as sheetsage_infer
14
+ from tqdm import tqdm as tqdm_fn
15
+
16
+
17
+ class SheetSage:
18
+ def __init__(self):
19
+ pass
20
+
21
+ def __call__(
22
+ self,
23
+ audio_path: Union[str, Path] = None,
24
+ audio_url: str = None,
25
+ segment_start_hint=None,
26
+ segment_end_hint=None,
27
+ use_jukebox=True,
28
+ measures_per_chunk=8,
29
+ dynamic_chunking=True,
30
+ segment_hints_are_downbeats=False,
31
+ beat_information=None,
32
+ beats_per_measure_hint=None,
33
+ beats_per_minute_hint=None,
34
+ detect_melody=True,
35
+ detect_harmony=True,
36
+ melody_threshold=None,
37
+ harmony_threshold=None,
38
+ beat_detection_padding=15.0,
39
+ avoid_chunking_if_possible=True,
40
+ legacy_behavior=False,
41
+ status_change_callback=lambda s: logging.info(s.name),
42
+ return_intermediaries=False,
43
+ tqdm=tqdm_fn,
44
+ return_dict=True,
45
+ ):
46
+ # assert audio_path or audio_url should be provided but not both
47
+ assert (audio_path and not audio_url) or (audio_url and not audio_path), (
48
+ f"One of audio_path or audio_url should be provided but not both: {audio_path}, {audio_url}"
49
+ )
50
+
51
+ assert shutil.which("ffmpeg") is not None, "ffmpeg not found. Please install ffmpeg."
52
+
53
+ if audio_path: # if audio_path is provided
54
+ ext = "flac"
55
+ tmp_audio_file = tempfile.NamedTemporaryFile(suffix=f".{ext}")
56
+ subprocess.run(
57
+ [
58
+ "ffmpeg",
59
+ "-i",
60
+ str(audio_path),
61
+ "-vn",
62
+ "-f",
63
+ ext,
64
+ "-y",
65
+ tmp_audio_file.name,
66
+ ]
67
+ )
68
+ audio_path = Path(tmp_audio_file.name)
69
+
70
+ assert audio_path.exists(), f"File not found: {audio_path}"
71
+ else: # if audio_url is provided
72
+ ext = "flac"
73
+ tmp_dir = tempfile.TemporaryDirectory()
74
+ tmp_audio_file = tmp_dir.name + f"/audio.{ext}"
75
+ subprocess.run(
76
+ [
77
+ "yt-dlp",
78
+ "-x",
79
+ "--audio-format",
80
+ ext,
81
+ "--audio-quality",
82
+ "0",
83
+ "-o",
84
+ tmp_dir.name + "/audio.%(ext)s",
85
+ audio_url,
86
+ ],
87
+ check=True,
88
+ )
89
+ audio_path = Path(tmp_audio_file)
90
+
91
+ sheetsage_output = sheetsage_infer(
92
+ audio_path_bytes_or_url=audio_path,
93
+ segment_start_hint=segment_start_hint,
94
+ segment_end_hint=segment_end_hint,
95
+ use_jukebox=use_jukebox,
96
+ measures_per_chunk=measures_per_chunk,
97
+ dynamic_chunking=dynamic_chunking,
98
+ segment_hints_are_downbeats=segment_hints_are_downbeats,
99
+ beat_information=beat_information,
100
+ beats_per_measure_hint=beats_per_measure_hint,
101
+ beats_per_minute_hint=beats_per_minute_hint,
102
+ detect_melody=detect_melody,
103
+ detect_harmony=detect_harmony,
104
+ melody_threshold=melody_threshold,
105
+ harmony_threshold=harmony_threshold,
106
+ beat_detection_padding=beat_detection_padding,
107
+ avoid_chunking_if_possible=avoid_chunking_if_possible,
108
+ legacy_behavior=legacy_behavior,
109
+ status_change_callback=status_change_callback,
110
+ return_intermediaries=return_intermediaries,
111
+ tqdm=tqdm,
112
+ )
113
+
114
+ if return_dict:
115
+ return sheetsage_output
116
+ else:
117
+ return (
118
+ sheetsage_output["lead_sheet"],
119
+ sheetsage_output["segment_beats"],
120
+ sheetsage_output["segment_beats_times"],
121
+ sheetsage_output["chunks_tertiaries"],
122
+ sheetsage_output["melody_logits"],
123
+ sheetsage_output["harmony_logits"],
124
+ sheetsage_output["melody_last_hidden_state"],
125
+ sheetsage_output["harmony_last_hidden_state"],
126
+ )
picogen2/mirtoolkit/utils.py ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import shutil
2
+ import subprocess
3
+ import tempfile
4
+ from pathlib import Path
5
+
6
+ import torch
7
+ import torchaudio
8
+
9
+
10
+ def download(url, file):
11
+ assert isinstance(url, str)
12
+ assert isinstance(file, (str, Path))
13
+ if isinstance(file, str):
14
+ file = Path(file)
15
+
16
+ if shutil.which("wget") is None:
17
+ raise FileNotFoundError("wget not found. Please install wget.")
18
+ # Download the file using wget
19
+ tmp_dir = tempfile.TemporaryDirectory()
20
+ tmp_file = tmp_dir.name + "/" + file.name
21
+ subprocess.run(["wget", "-O", tmp_file, url], check=True)
22
+ shutil.move(tmp_file, file)
23
+
24
+ # try:
25
+ # # Send a GET request to the URL
26
+ # response = requests.get(url, stream=True)
27
+ # response.raise_for_status() # Raise an error for bad status codes
28
+
29
+ # tmp_dir = tempfile.TemporaryDirectory()
30
+ # tmp_file = tmp_dir.name + "/" + file.name
31
+ # with open(tmp_file, "wb") as f:
32
+ # for chunk in response.iter_content(chunk_size=8192):
33
+ # f.write(chunk)
34
+ # shutil.move(tmp_file, file)
35
+ # except requests.RequestException as e:
36
+ # print(f"An error occurred: {e}")
37
+
38
+
39
+ def load_audio(
40
+ path, dtype="float32", return_tensor=False, channels_first=False, mono=False, sr=None
41
+ ):
42
+ """
43
+ Load an audio file from the given path.
44
+ Args:
45
+ path (str): The path to the audio file.
46
+ dtype (str, optional): The desired data type of the audio waveform. Defaults to "float64".
47
+ return_tensor (bool, optional): Whether to return the audio waveform as a tensor. Defaults to False.
48
+ channels_first (bool, optional): Whether to return the audio waveform with channels as the first dimension. Defaults to False.
49
+ Returns:
50
+ tuple or ndarray: If `return_tensor` is False, returns a tuple containing the audio waveform as a numpy ndarray and the sample rate as an integer. If `return_tensor` is True, returns a tuple containing the audio waveform as a PyTorch tensor and the sample rate as an integer.
51
+ """
52
+ assert dtype in ["float32", "float64"]
53
+
54
+ try:
55
+ waveform, sample_rate = torchaudio.load(path, channels_first=channels_first)
56
+ if mono and waveform.shape[0] > 1:
57
+ if channels_first:
58
+ waveform = waveform.mean(0)
59
+ else:
60
+ waveform = waveform.mean(-1)
61
+
62
+ if dtype == "float64":
63
+ waveform = waveform.to(dtype=torch.float64)
64
+
65
+ except Exception:
66
+ # in case torchaudio fails, try soundfile
67
+ import soundfile as sf
68
+
69
+ waveform, sample_rate = sf.read(path, dtype=dtype)
70
+ waveform = torch.from_numpy(waveform)
71
+
72
+ if sr is not None:
73
+ # resample the audio to the given sample rate
74
+ waveform = torchaudio.transforms.Resample(sample_rate, sr)(waveform)
75
+ sample_rate = sr
76
+
77
+ if not return_tensor:
78
+ waveform = waveform.squeeze().numpy()
79
+
80
+ return waveform, sample_rate
picogen2/model.py ADDED
@@ -0,0 +1,230 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from enum import Enum
2
+
3
+ import numpy as np
4
+ import torch
5
+ import torch.nn.functional as F
6
+ from torch import nn
7
+ from transformers import GPTNeoXConfig, GPTNeoXModel
8
+
9
+ from . import assets
10
+ from .utils import load_checkpoint, load_config, top_p
11
+
12
+
13
+ def _get_device(module):
14
+ return next(module.parameters()).device
15
+
16
+
17
+ class ConditionEncoder(nn.Module):
18
+ def __init__(self, hp):
19
+ super().__init__()
20
+ self.l1_encoder = nn.TransformerEncoder(
21
+ nn.TransformerEncoderLayer(
22
+ d_model=hp.d_model,
23
+ nhead=hp.num_heads,
24
+ dim_feedforward=hp.d_model * 4,
25
+ dropout=hp.dropout,
26
+ activation=hp.activation,
27
+ batch_first=True,
28
+ ),
29
+ hp.num_layers_encoder,
30
+ )
31
+ self.pos_emb = nn.Embedding(hp.condition_class, hp.d_model)
32
+ self.bottlenect = nn.Sequential(
33
+ nn.Linear(hp.d_model, hp.d_bottleneck),
34
+ nn.ReLU(),
35
+ nn.Linear(hp.d_bottleneck, hp.d_model),
36
+ )
37
+
38
+ def forward(self, input_embs):
39
+ B, L, N, D = input_embs.shape
40
+ pos = torch.arange(N).to(input_embs.device)
41
+ pos = self.pos_emb(pos)[None, None, :, :].expand(B, L, N, D)
42
+ input_embs = input_embs + pos
43
+ out = self.l1_encoder(input_embs.view(B * L, N, D)).view(B, L, N, D)
44
+ out = out[:, :, 0, :]
45
+ assert out.shape == (B, L, D)
46
+ out = self.bottlenect(out)
47
+ return out
48
+
49
+
50
+ class PiCoGenDecoder(nn.Module):
51
+ class InputClass(Enum):
52
+ TARGET = 0
53
+ CONDITION = 1
54
+
55
+ def __init__(self, hp):
56
+ super().__init__()
57
+ self.hp = hp
58
+ config = GPTNeoXConfig(
59
+ vocab_size=hp.vocab_size,
60
+ hidden_size=hp.d_model,
61
+ num_hidden_layers=hp.num_layers,
62
+ num_attention_heads=hp.num_heads,
63
+ intermediate_size=hp.d_model * 4,
64
+ hidden_act=hp.activation,
65
+ hidden_dropout=hp.dropout,
66
+ max_position_embeddings=hp.max_position_embeddings,
67
+ )
68
+ self.model = GPTNeoXModel(config)
69
+ self.word_emb = nn.Embedding(hp.vocab_size, hp.d_model, padding_idx=0)
70
+ self.cond_encoder = ConditionEncoder(hp)
71
+ self.cls_emb = nn.Embedding(
72
+ hp.token_class, hp.d_model, padding_idx=0
73
+ ) # 0: target, 1: condition
74
+ self.lm_head = nn.Linear(hp.d_model, hp.vocab_size)
75
+
76
+ @staticmethod
77
+ def from_pretrained(
78
+ ckpt_file=None,
79
+ config_file=None,
80
+ device="cpu",
81
+ ):
82
+ ckpt_file = ckpt_file if ckpt_file is not None else assets.checkpoint_file()
83
+ config_file = config_file if config_file is not None else assets.config_file()
84
+ hp = load_config(config_file)
85
+ model = PiCoGenDecoder(hp)
86
+ state_dict = load_checkpoint(ckpt_file, device)
87
+ model.load_state_dict(state_dict["model"])
88
+ model.to(device)
89
+ model.eval()
90
+ return model
91
+
92
+ def generate(
93
+ self, input_seg, input_cls_ids, need_encode, kv_cache=None, temperature=1.0, thres=0.9
94
+ ):
95
+ B, L = input_cls_ids.shape
96
+
97
+ if kv_cache is None:
98
+ input_ids = torch.zeros(B, L, device=_get_device(self.word_emb)).long()
99
+ input_cond_embs = torch.zeros(
100
+ B,
101
+ L,
102
+ self.hp.condition_class,
103
+ self.hp.d_model,
104
+ device=_get_device(self.cond_encoder),
105
+ ).float()
106
+ for b in range(B):
107
+ for ll in range(L):
108
+ if need_encode[b, ll]:
109
+ emb = torch.FloatTensor(np.array(input_seg[b][ll])).to(
110
+ _get_device(self.cond_encoder)
111
+ )
112
+ input_cond_embs[b, ll] = emb
113
+ else:
114
+ input_ids[b, ll] = input_seg[b][ll]
115
+ else: # NOTE: only use the last token as input
116
+ input_ids = torch.zeros(B, 1, device=_get_device(self.word_emb)).long()
117
+ input_cond_embs = torch.zeros(
118
+ B,
119
+ 1,
120
+ self.hp.condition_class,
121
+ self.hp.d_model,
122
+ device=_get_device(self.cond_encoder),
123
+ ).float()
124
+ for b in range(B):
125
+ if need_encode[b, -1]:
126
+ emb = torch.FloatTensor(np.array(input_seg[b][-1])).to(
127
+ _get_device(self.cond_encoder)
128
+ )
129
+ input_cond_embs[b, -1] = emb
130
+ else:
131
+ input_ids[b, -1] = input_seg[b][-1]
132
+ input_cls_ids = input_cls_ids[:, -1:]
133
+ assert input_ids.shape == input_cls_ids.shape
134
+
135
+ input_embs = self.word_emb(input_ids)
136
+ input_cond_embs = self.cond_encoder(input_cond_embs)
137
+ input_cls_embs = self.cls_emb(input_cls_ids)
138
+
139
+ if kv_cache is None:
140
+ mask = (input_embs.sum(dim=-1, keepdim=True) != 0).expand(B, L, self.hp.d_model)
141
+ else:
142
+ mask = (input_embs.sum(dim=-1, keepdim=True) != 0).expand(B, 1, self.hp.d_model)
143
+ input_cond_embs[mask] = 0 # NOTE: where input_embs is not zero
144
+
145
+ input_embs = input_embs + input_cond_embs + input_cls_embs
146
+
147
+ model_out = self.model(
148
+ inputs_embeds=input_embs,
149
+ past_key_values=kv_cache,
150
+ )
151
+
152
+ logits = self.lm_head(model_out.last_hidden_state)[:, -1, :]
153
+ assert logits.shape == (B, self.hp.vocab_size)
154
+ probs = F.softmax(top_p(logits, thres=thres, temperature=temperature), dim=-1)
155
+ output_ids = torch.multinomial(probs, num_samples=1)
156
+ assert output_ids.shape == (B, 1)
157
+
158
+ return output_ids, model_out.past_key_values
159
+
160
+ def forward(
161
+ self,
162
+ input_seqs,
163
+ input_cls_ids,
164
+ need_encode,
165
+ input_ids=None,
166
+ input_cond_embs=None,
167
+ labels=None,
168
+ kv_cache=None,
169
+ ):
170
+ B, L = input_cls_ids.shape
171
+ input_cls_ids = input_cls_ids.to(_get_device(self.cls_emb))
172
+
173
+ if input_seqs is not None:
174
+ assert input_ids is None and input_cond_embs is None
175
+ input_ids = torch.zeros(B, L, device=_get_device(self.word_emb)).long()
176
+ input_cond_embs = torch.zeros(
177
+ B,
178
+ L,
179
+ self.hp.condition_class,
180
+ self.hp.d_model,
181
+ device=_get_device(self.cond_encoder),
182
+ ).float()
183
+ for b in range(B):
184
+ for ll in range(L):
185
+ if need_encode[b, ll]:
186
+ emb = torch.FloatTensor(np.array(input_seqs[b][ll])).to(
187
+ _get_device(self.cond_encoder)
188
+ )
189
+ input_cond_embs[b, ll] = emb
190
+ else:
191
+ input_ids[b, ll] = input_seqs[b][ll]
192
+ else:
193
+ assert input_ids is not None and input_cond_embs is not None
194
+ input_ids = input_ids.to(_get_device(self.word_emb))
195
+ input_cond_embs = input_cond_embs.to(_get_device(self.cond_encoder))
196
+
197
+ input_embs = self.word_emb(input_ids)
198
+ input_cond_embs = self.cond_encoder(input_cond_embs)
199
+ input_cls_embs = self.cls_emb(input_cls_ids)
200
+
201
+ mask = (input_embs.sum(dim=-1, keepdim=True) != 0).expand(B, L, self.hp.d_model)
202
+ input_cond_embs[mask] = 0 # NOTE: where input_embs is not zero
203
+
204
+ input_embs = input_embs + input_cond_embs + input_cls_embs
205
+
206
+ model_out = self.model(
207
+ inputs_embeds=input_embs,
208
+ past_key_values=kv_cache,
209
+ )
210
+
211
+ logits = self.lm_head(model_out.last_hidden_state)
212
+ assert logits.shape == (B, L, self.hp.vocab_size)
213
+
214
+ lm_loss = None
215
+ if labels is not None:
216
+ assert labels.shape == (B, L)
217
+ labels = labels.to(logits.device)
218
+
219
+ loss_fct = F.cross_entropy
220
+ lm_loss = loss_fct(logits.view(-1, self.hp.vocab_size), labels.view(-1))
221
+
222
+ out = {
223
+ "loss": lm_loss,
224
+ "logits": logits,
225
+ "past_key_values": model_out.past_key_values,
226
+ "hidden_states": model_out.hidden_states,
227
+ "attentions": model_out.attentions,
228
+ }
229
+
230
+ return out
picogen2/repr.py ADDED
@@ -0,0 +1,530 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import collections
2
+ import json
3
+ from itertools import chain
4
+ from pathlib import Path
5
+
6
+ import miditoolkit
7
+ import numpy as np
8
+
9
+ from . import assets
10
+ from .utils import load_config
11
+
12
+ DEFAULT_SUBBEAT_RANGE = np.arange(0, 64, dtype=int)
13
+ DEFAULT_PIANO_RANGE = np.arange(21, 109, dtype=int)
14
+ DEFAULT_VELOCITY_BINS = np.linspace(0, 124, 31 + 1, dtype=int) # midi velocity: 0~127
15
+ LS_DEFAULT_VELOCITY = 80
16
+ DEFAULT_BPM_BINS = np.linspace(32, 224, 64 + 1, dtype=int)
17
+ DEFAULT_DURATION_RANGE = np.arange(1, 1 + 32, dtype=int)
18
+ DEFAULT_CHORD_ROOTS = [
19
+ "A",
20
+ "A#",
21
+ "B",
22
+ "C",
23
+ "C#",
24
+ "D",
25
+ "D#",
26
+ "E",
27
+ "F",
28
+ "F#",
29
+ "G",
30
+ "G#",
31
+ ]
32
+ DEFAULT_CHORD_QUALITY = [
33
+ "+",
34
+ "/o7",
35
+ "7",
36
+ "M",
37
+ "M7",
38
+ "m",
39
+ "m7",
40
+ "o",
41
+ "o7",
42
+ "sus2",
43
+ "sus4",
44
+ ]
45
+ VOCAB_SIZE = 500
46
+
47
+
48
+ def gen_vocab():
49
+ spec = [f"spec_{t}" for t in ["pad", "bos", "eos", "unk", "mask", "ss", "se"]]
50
+ bar = ["bar_start", "bar_end"] + [f"bar_{i}" for i in range(1, 5)] + ["bar_N"]
51
+ position = [f"position_{i}" for i in DEFAULT_SUBBEAT_RANGE]
52
+ chord = ["chord_N_N"]
53
+ for root in DEFAULT_CHORD_ROOTS:
54
+ for quality in DEFAULT_CHORD_QUALITY:
55
+ chord.append(f"chord_{root}_{quality}")
56
+ tempo = [f"tempo_{i}" for i in DEFAULT_BPM_BINS]
57
+ pitch = [f"pitch_{i}" for i in DEFAULT_PIANO_RANGE]
58
+ duration = [f"duration_{i}" for i in DEFAULT_DURATION_RANGE]
59
+ velocity = [f"velocity_{i}" for i in DEFAULT_VELOCITY_BINS]
60
+ vocab = spec + bar + position + chord + tempo + pitch + duration + velocity
61
+ vocab = vocab + ["reserved"] * (VOCAB_SIZE - len(vocab))
62
+ return vocab
63
+
64
+
65
+ class Event:
66
+ def __init__(
67
+ self,
68
+ etype,
69
+ value,
70
+ ):
71
+ self.etype = etype
72
+ self.value = value
73
+
74
+ def init_check(self):
75
+ if self.etype == "spec":
76
+ assert self.value.split("_")[0] in ["spec"], f"{self.etype}: {self.value}"
77
+
78
+ elif self.etype == "bar":
79
+ assert self.value.split("_")[0] in ["bar"], f"{self.etype}: {self.value}"
80
+
81
+ elif self.etype == "metric":
82
+ assert self.value.split("_")[0] in [
83
+ "position",
84
+ "chord",
85
+ "tempo",
86
+ ], f"{self.etype}: {self.value}"
87
+
88
+ elif self.etype == "note":
89
+ assert self.value.split("_")[0] in [
90
+ "pitch",
91
+ "duration",
92
+ "velocity",
93
+ ], f"{self.etype}: {self.value}"
94
+
95
+ else:
96
+ raise ValueError(f"Unknown etype: {self.etype}")
97
+
98
+ def unwrap(self, vtype):
99
+ return vtype(self.value.split("_")[1])
100
+
101
+ def __repr__(self):
102
+ return f"Event({self.etype}: {self.value})"
103
+
104
+ def __eq__(self, other):
105
+ if not isinstance(other, Event):
106
+ return False
107
+ return self.etype == other.etype and self.value == other.value
108
+
109
+
110
+ class Tokenizer:
111
+ def __init__(self, vocab_file=None, beat_div=None, ticks_per_beat=None):
112
+ vocab_file = assets.vocab_file() if vocab_file is None else vocab_file
113
+
114
+ if beat_div is None or ticks_per_beat is None:
115
+ config_file = assets.config_file()
116
+ hp = load_config(config_file)
117
+ beat_div = hp.beat_div
118
+ ticks_per_beat = hp.ticks_per_beat
119
+
120
+ self.vocab = Vocab(vocab_file)
121
+ self.beat_div = beat_div
122
+ self.ticks_per_beat = ticks_per_beat
123
+
124
+ def get_song_from_midi(self, midi):
125
+ song = midi_to_song(midi, self.beat_div)
126
+ song["events"] = song_to_events(song)
127
+
128
+ song["ls_events"] = extract_leadsheet_from_events(
129
+ song["events"], song["metadata"]["beat_per_bar"], self.beat_div
130
+ )
131
+
132
+ return song
133
+
134
+ def events_to_midi(self, events):
135
+ midi = events_to_midi(events, self.ticks_per_beat, self.beat_div)
136
+ return midi
137
+
138
+ def e2i(self, e):
139
+ return self.vocab.t2i[e.value]
140
+
141
+ def i2e(self, eid):
142
+ token = self.vocab.i2t[eid]
143
+ if token.startswith("spec"):
144
+ return Event("spec", token)
145
+ elif token.startswith("bar"):
146
+ return Event("bar", token)
147
+ elif token.split("_")[0] in ["position", "chord", "tempo"]:
148
+ return Event("metric", token)
149
+ elif token.split("_")[0] in ["pitch", "duration", "velocity"]:
150
+ return Event("note", token)
151
+ else:
152
+ raise ValueError(f"Unknown token: {token}")
153
+
154
+ def get_bar_ranges(self, events, from_start=True):
155
+ return get_bar_ranges(events, from_start)
156
+
157
+ @staticmethod
158
+ def get_tempo_event(bpm):
159
+ tempo = DEFAULT_BPM_BINS[np.argmin(abs(DEFAULT_BPM_BINS - bpm))]
160
+ return Event("metric", f"tempo_{tempo}")
161
+
162
+ @staticmethod
163
+ def get_duration_event(duration):
164
+ duration = DEFAULT_DURATION_RANGE[np.argmin(abs(DEFAULT_DURATION_RANGE - duration))]
165
+ return Event("note", f"duration_{duration}")
166
+
167
+ @staticmethod
168
+ def get_velocity_event(velocity):
169
+ velocity = DEFAULT_VELOCITY_BINS[np.argmin(abs(DEFAULT_VELOCITY_BINS - velocity))]
170
+ return Event("note", f"velocity_{velocity}")
171
+
172
+
173
+ class Vocab:
174
+ def __init__(self, vacab_file):
175
+ self.i2t = json.loads(vacab_file.read_text())
176
+ self.t2i = {}
177
+ for i, t in enumerate(self.i2t):
178
+ self.t2i[t] = i
179
+
180
+ def len(self):
181
+ return len(self.i2t)
182
+
183
+ def __len__(self):
184
+ return len(self.i2t)
185
+
186
+ def __repr__(self):
187
+ out = []
188
+ for i, t in enumerate(self.i2t):
189
+ out.append(f"{i}: {t}")
190
+ return "\n".join(out)
191
+
192
+
193
+ def midi_to_song(midi_obj, beat_div):
194
+ assert midi_obj.ticks_per_beat % beat_div == 0
195
+ grid_resol = midi_obj.ticks_per_beat // beat_div
196
+
197
+ # load notes
198
+ instr_notes = collections.defaultdict(list)
199
+ for instr in midi_obj.instruments:
200
+ for note in instr.notes:
201
+ instr_notes[instr.name].append(note)
202
+ instr_notes[instr.name].sort(key=lambda x: x.start)
203
+
204
+ # load chords
205
+ chords = []
206
+ for marker in midi_obj.markers:
207
+ if marker.text.split("_")[0] != "global" and "Boundary" not in marker.text.split("_")[0]:
208
+ chords.append(marker)
209
+ chords.sort(key=lambda x: x.time)
210
+
211
+ # load tempos
212
+ tempos = midi_obj.tempo_changes
213
+ tempos.sort(key=lambda x: x.time)
214
+
215
+ # load labels
216
+ labels = []
217
+ for marker in midi_obj.markers:
218
+ if "Boundary" in marker.text.split("_")[0]:
219
+ labels.append(marker)
220
+ labels.sort(key=lambda x: x.time)
221
+
222
+ # load global bpm
223
+ global_bpm = None
224
+ for marker in midi_obj.markers:
225
+ if marker.text.split("_")[0] == "global" and marker.text.split("_")[1] == "bpm":
226
+ global_bpm = int(marker.text.split("_")[2])
227
+
228
+ # process notes
229
+ intsr_gird = dict()
230
+ for key in instr_notes.keys():
231
+ notes = instr_notes[key]
232
+ note_grid = collections.defaultdict(list)
233
+ for note in notes:
234
+ # quantize start
235
+ quant_time = round(note.start / grid_resol)
236
+
237
+ # duration
238
+ note_duration = note.end - note.start
239
+ duration = round(note_duration / grid_resol)
240
+ duration = max(duration, 1) # dur >= 1
241
+
242
+ # append
243
+ note_grid[quant_time].append(
244
+ {
245
+ "note": note,
246
+ "pitch": note.pitch,
247
+ "duration": duration,
248
+ "velocity": note.velocity,
249
+ }
250
+ )
251
+
252
+ # sort
253
+ for time in note_grid.keys():
254
+ note_grid[time].sort(key=lambda x: -x["pitch"])
255
+
256
+ # set to track
257
+ intsr_gird[key] = note_grid.copy()
258
+
259
+ # process chords
260
+ chord_grid = collections.defaultdict(list)
261
+ for chord in chords:
262
+ quant_time = round(chord.time / grid_resol)
263
+ # chord_grid[quant_time] = [chord] # NOTE: only one chord per time
264
+ chord_grid[quant_time].append(chord)
265
+
266
+ # process tempo
267
+ tempo_grid = collections.defaultdict(list)
268
+ for tempo in tempos:
269
+ quant_time = round(tempo.time / grid_resol)
270
+ # tempo.tempo = DEFAULT_BPM_BINS[np.argmin(abs(DEFAULT_BPM_BINS-tempo.tempo))]
271
+ tempo_grid[quant_time] = [tempo] # NOTE: only one tempo per time
272
+
273
+ all_bpm = [tempo[0].tempo for _, tempo in tempo_grid.items()]
274
+ assert len(all_bpm) > 0, " No tempo changes in midi file."
275
+ average_bpm = sum(all_bpm) / len(all_bpm)
276
+ if global_bpm is None:
277
+ global_bpm = average_bpm
278
+
279
+ # process boundary
280
+ label_grid = collections.defaultdict(list)
281
+ for label in labels:
282
+ quant_time = round(label.time / grid_resol)
283
+ label_grid[quant_time] = [label]
284
+
285
+ # collect
286
+ song_data = {
287
+ "notes": intsr_gird,
288
+ "chords": chord_grid,
289
+ "tempos": tempo_grid,
290
+ "labels": label_grid,
291
+ "metadata": {
292
+ "global_bpm": global_bpm,
293
+ "average_bpm": average_bpm,
294
+ "beat_div": beat_div,
295
+ "beat_per_bar": midi_obj.time_signature_changes[0].numerator,
296
+ },
297
+ }
298
+ return song_data
299
+
300
+
301
+ def song_to_events(song):
302
+ beat_div = song["metadata"]["beat_div"]
303
+ beat_per_bar = song["metadata"]["beat_per_bar"]
304
+ grid_per_bar = beat_div * beat_per_bar
305
+
306
+ events = [Event("spec", "spec_ss")]
307
+ global_tempo = DEFAULT_BPM_BINS[
308
+ np.argmin(abs(DEFAULT_BPM_BINS - song["metadata"]["global_bpm"]))
309
+ ]
310
+ events.append(Event("metric", f"tempo_{global_tempo}"))
311
+
312
+ max_grid = list(chain(song["tempos"].keys(), song["chords"].keys()))
313
+ for _, v in song["notes"].items():
314
+ max_grid.extend(v.keys())
315
+ max_grid = max(max_grid)
316
+
317
+ for bar_i in range(0, max_grid + 1, grid_per_bar):
318
+ events.append(Event("bar", "bar_start"))
319
+ for i in range(bar_i, min(bar_i + grid_per_bar, max_grid + 1)):
320
+ pos = Event("metric", f"position_{i-bar_i}")
321
+ tmp = []
322
+ empty = True
323
+ if i in song["chords"]:
324
+ chord_items = song["chords"][i][0].text.split("_")
325
+ chord = f"{chord_items[0]}_{chord_items[1]}"
326
+ tmp.append(Event("metric", f"chord_{chord}"))
327
+ empty = False
328
+ if i in song["tempos"]:
329
+ tempo = DEFAULT_BPM_BINS[
330
+ np.argmin(abs(DEFAULT_BPM_BINS - song["tempos"][i][0].tempo))
331
+ ]
332
+ tmp.append(Event("metric", f"tempo_{tempo}"))
333
+ empty = False
334
+ for _, instr in song["notes"].items():
335
+ if i in instr:
336
+ for note in instr[i]:
337
+ duration = DEFAULT_DURATION_RANGE[
338
+ np.argmin(abs(DEFAULT_DURATION_RANGE - note["duration"]))
339
+ ]
340
+ velocity = DEFAULT_VELOCITY_BINS[
341
+ np.argmin(abs(DEFAULT_VELOCITY_BINS - note["velocity"]))
342
+ ]
343
+ tmp.append(Event("note", f'pitch_{note["pitch"]}'))
344
+ tmp.append(Event("note", f"duration_{duration}"))
345
+ tmp.append(Event("note", f"velocity_{velocity}"))
346
+ empty = False
347
+
348
+ if not empty:
349
+ events.append(pos)
350
+ events.extend(tmp)
351
+ events.append(Event("bar", "bar_end"))
352
+
353
+ events.append(Event("spec", "spec_se"))
354
+
355
+ return events
356
+
357
+
358
+ def events_to_midi(events, ticks_per_beat, grid_div):
359
+ bar_ranges = get_bar_ranges(events, from_start=False)
360
+
361
+ midi = miditoolkit.MidiFile()
362
+ midi.ticks_per_beat = ticks_per_beat
363
+ track = miditoolkit.Instrument(program=0, is_drum=False, name="piano")
364
+ midi.instruments = [track]
365
+
366
+ bar_tick = 0
367
+ subbeat_tick = 0
368
+
369
+ pitch, velocity, duration = 0, 0, 0
370
+ bar_len = 4
371
+
372
+ for i, (start, end) in enumerate(bar_ranges):
373
+ assert ticks_per_beat % grid_div == 0
374
+ ticks_per_subbeat = ticks_per_beat // grid_div
375
+
376
+ for event in events[start:end]:
377
+ if event.etype == "spec":
378
+ pass
379
+ elif event.etype == "bar":
380
+ if event.value in ["bar_start", "bar_end"]:
381
+ pass
382
+ else:
383
+ try:
384
+ bar_len = int(event.value.split("_")[1])
385
+ except ValueError:
386
+ assert event.value == "bar_N"
387
+
388
+ elif event.etype == "metric":
389
+ v = event.value
390
+ if v.startswith("position"):
391
+ pos = int(v.split("_")[1])
392
+ subbeat_tick = pos * ticks_per_subbeat
393
+ elif v.startswith("tempo"):
394
+ tempo = int(v.split("_")[1])
395
+ m = miditoolkit.TempoChange(time=bar_tick + subbeat_tick, tempo=tempo)
396
+ midi.tempo_changes.append(m)
397
+ elif v.startswith("chord"):
398
+ pass
399
+ else:
400
+ raise ValueError(f"Unknown metric: {v}")
401
+ elif event.etype == "note":
402
+ v = event.value
403
+ if v.startswith("pitch"):
404
+ pitch = int(v.split("_")[1])
405
+ elif v.startswith("duration"):
406
+ duration = int(v.split("_")[1]) * ticks_per_subbeat
407
+ elif v.startswith("velocity"):
408
+ velocity = int(v.split("_")[1])
409
+ n = miditoolkit.Note(
410
+ start=bar_tick + subbeat_tick,
411
+ end=bar_tick + subbeat_tick + duration,
412
+ pitch=pitch,
413
+ velocity=velocity,
414
+ )
415
+ midi.instruments[0].notes.append(n)
416
+ else:
417
+ raise ValueError(f"Unknown note: {v}")
418
+ else:
419
+ raise ValueError(f"Unknown event: {type(event)}")
420
+
421
+ bar_tick += ticks_per_beat * bar_len
422
+
423
+ return midi
424
+
425
+
426
+ def extract_leadsheet_from_events(
427
+ events, beat_per_bar, beat_div, cover_beat=0, min_pitch=60, no_chord=False
428
+ ):
429
+ # algorithm:
430
+ # - skyline
431
+ # - filter out notes with pitch < 60
432
+
433
+ grids = []
434
+ for event in events:
435
+ if event.etype == "bar":
436
+ for _ in range(beat_per_bar * beat_div):
437
+ grids.append(list())
438
+
439
+ grid_idx = 0
440
+ bar_count = 0
441
+ subbeat = 0
442
+ note_tmp = []
443
+ first_tempo = True
444
+ for event in events:
445
+ if event.etype == "spec":
446
+ grids[grid_idx].append(event)
447
+ elif event.etype == "bar":
448
+ if event.value == "bar_end":
449
+ bar_count += 1
450
+ grid_idx = bar_count * (beat_per_bar * beat_div)
451
+ subbeat = 0
452
+ grids[grid_idx].append(event)
453
+ elif event.etype == "metric":
454
+ if event.value.startswith("position"):
455
+ subbeat = int(event.value.split("_")[1])
456
+ if not event.value.startswith("tempo"):
457
+ grids[grid_idx + subbeat].append(event)
458
+ else: # tempo
459
+ if first_tempo:
460
+ grids[grid_idx + subbeat].append(event)
461
+ first_tempo = False
462
+ elif event.etype == "note":
463
+ note_tmp.append(event)
464
+ if event.value.startswith("velocity"):
465
+ pitch = note_tmp[0].value.split("_")[1]
466
+ if int(pitch) >= min_pitch: # only keep notes with pitch >= min_pitch
467
+ grids[grid_idx + subbeat].append(note_tmp[0])
468
+ grids[grid_idx + subbeat].append(note_tmp[1])
469
+ grids[grid_idx + subbeat].append(
470
+ Event("note", f"velocity_{LS_DEFAULT_VELOCITY}")
471
+ )
472
+ note_tmp = []
473
+
474
+ # select the highest note
475
+ for i, grid in enumerate(grids):
476
+ notes = [e for e in grid if e.etype == "note"]
477
+ notes = [(notes[i], notes[i + 1], notes[i + 2]) for i in range(0, len(notes), 3)]
478
+ notes.sort(key=lambda x: -x[0].unwrap(int)) # sort by pitch
479
+ grids[i] = [e for e in grid if not e.etype == "note"]
480
+ if len(notes) > 0:
481
+ grids[i].extend(notes[0])
482
+
483
+ # remove useless metric
484
+ for i, grid in enumerate(grids):
485
+ if len(grid) == 0 or not grid[-1].etype == "metric":
486
+ continue
487
+ metric = grid[-1]
488
+ assert metric.etype == "metric"
489
+ if metric.value.startswith("position"):
490
+ grids[i].pop()
491
+ assert len(grid) == 0 or grid[-1].etype == "bar"
492
+
493
+ return list(chain(*grids))
494
+
495
+
496
+ def get_bar_ranges(events, from_start=True):
497
+ bar_idx_list = []
498
+ for i, event in enumerate(events):
499
+ if event.etype == "bar" and event.value == "bar_start":
500
+ bar_idx_list.append(i)
501
+ bar_idx_list = bar_idx_list + [len(events)]
502
+ if from_start:
503
+ bar_idx_list[0] = 0 # the first bar starts at 0
504
+
505
+ bar_ranges = []
506
+ for i in range(len(bar_idx_list) - 1):
507
+ bar_ranges.append((bar_idx_list[i], bar_idx_list[i + 1]))
508
+ return bar_ranges
509
+
510
+
511
+ if __name__ == "__main__":
512
+ import argparse
513
+
514
+ parser = argparse.ArgumentParser()
515
+ subparsers = parser.add_subparsers(dest="command")
516
+ cmd_gen_vocab = subparsers.add_parser("gen_vocab")
517
+ cmd_gen_vocab.add_argument("--output_file", type=Path, required=True)
518
+ ca = parser.parse_args()
519
+
520
+ if ca.command is None:
521
+ parser.print_help()
522
+ exit()
523
+ elif ca.command == "gen_vocab":
524
+ vocab = gen_vocab()
525
+ ca.output_file.write_text(json.dumps(vocab, indent=2))
526
+ vocab = Vocab(ca.output_file)
527
+ print(vocab)
528
+ print("vocab size:", vocab.len())
529
+ else:
530
+ raise ValueError(f"Unknown command: {ca.command}")
picogen2/utils.py ADDED
@@ -0,0 +1,194 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import logging
3
+ import math
4
+ import pickle
5
+ import shutil
6
+ from dataclasses import dataclass, fields
7
+ from pathlib import Path
8
+
9
+ import numpy as np
10
+ import questionary
11
+ import torch
12
+ import torch.nn.functional as F
13
+
14
+ _logger = None
15
+ _level = None
16
+
17
+
18
+ @dataclass
19
+ class HyperParam:
20
+ beat_div: int
21
+ ticks_per_beat: int
22
+
23
+ seed: int
24
+ learning_rate: float
25
+ learning_rate_min: float
26
+ adam_b1: float
27
+ adam_b2: float
28
+ sched_T: int
29
+ warmup_epochs: int
30
+
31
+ vocab_size: int
32
+ token_class: int
33
+ condition_class: int
34
+ d_model: int
35
+ d_bottleneck: int
36
+ num_layers: int
37
+ num_layers_encoder: int
38
+ num_heads: int
39
+ activation: str
40
+ dropout: float
41
+ max_seq_len: int
42
+ max_position_embeddings: int
43
+
44
+ loss_weight: float = 1.0
45
+
46
+
47
+ def _get_logger():
48
+ global _logger
49
+ if _logger is None:
50
+ _logger = logging.getLogger("picogen2")
51
+
52
+ return _logger
53
+
54
+
55
+ class Logger:
56
+ def setLevel(self, level):
57
+ global _level, _logger
58
+ _level = level.upper()
59
+ _get_logger().setLevel(_level)
60
+
61
+ def __getattr__(self, name):
62
+ return getattr(_get_logger(), name)
63
+
64
+ def __repr__(self):
65
+ return repr(_get_logger())
66
+
67
+
68
+ logger = Logger()
69
+
70
+
71
+ def check_task_done(task: str, output_dir: Path):
72
+ done_file = output_dir / f"done_{task}"
73
+ return done_file.exists()
74
+
75
+
76
+ def mark_task_done(task: str, output_dir: Path):
77
+ done_file = output_dir / f"done_{task}"
78
+ done_file.touch()
79
+
80
+
81
+ def song_dir_name(index: int):
82
+ return "{:04d}".format(index)
83
+
84
+
85
+ def load_config(config_file):
86
+ config = json.loads(config_file.read_text())
87
+ hp = HyperParam(**config)
88
+ logger.info("checkpoint model config:")
89
+ for v in fields(hp):
90
+ logger.info(f"\t{v.name}: {getattr(hp, v.name)}")
91
+ return hp
92
+
93
+
94
+ def init_ckpt_dir(ckpt_dir, config_file, config_name="config"):
95
+ t_path = (ckpt_dir / config_name).with_suffix(config_file.suffix)
96
+ if not t_path.exists():
97
+ ckpt_dir.mkdir(exist_ok=True)
98
+ shutil.copyfile(config_file, t_path)
99
+ else:
100
+ # check if config is the same
101
+ if config_file.read_text() != t_path.read_text():
102
+ override = questionary.confirm(
103
+ f'Config file "{config_file}" is not same with checkpoint "{t_path}", override?',
104
+ default=False,
105
+ ).ask()
106
+ if override:
107
+ shutil.copyfile(config_file, t_path)
108
+ else:
109
+ print("Confliction between config file and checkpoint. Exit.")
110
+ exit()
111
+ # raise ValueError(f'config file {config_file} and {t_path} are not the same')
112
+
113
+
114
+ def save_checkpoint(filepath, obj, verbose=False):
115
+ print("Saving checkpoint to {} ... ".format(filepath), end="") if verbose else None
116
+ torch.save(obj, filepath)
117
+ print("Done.") if verbose else None
118
+
119
+
120
+ def scan_checkpoint(cp_dir, prefix):
121
+ # pattern = os.path.join(cp_dir, prefix + '????????')
122
+ # cp_list = glob.glob(pattern)
123
+ cp_list = list(cp_dir.glob(f"{prefix}*"))
124
+ if len(cp_list) == 0:
125
+ return None
126
+ return sorted(cp_list, key=lambda n: int(n.stem.split("_")[-1]))[-1]
127
+
128
+
129
+ def load_checkpoint(filepath: Path, device="cpu"):
130
+ assert filepath.is_file()
131
+ logger.info("Loading '{}'".format(filepath))
132
+ checkpoint_dict = torch.load(filepath, map_location=device, weights_only=False)
133
+ logger.info("Done.")
134
+ return checkpoint_dict
135
+
136
+
137
+ def downbeat_time_to_index(beats, downbeats):
138
+ downbeat_indices = []
139
+ beats = np.array(beats)
140
+ for downbeat in downbeats:
141
+ idx = np.argmin(np.abs(beats - downbeat))
142
+ downbeat_indices.append(idx)
143
+ return downbeat_indices
144
+
145
+
146
+ def top_p(logits, thres=0.9, temperature=1.0):
147
+ assert logits.dim() == 2, logits.shape
148
+
149
+ sorted_logits, sorted_indices = torch.sort(logits, descending=True)
150
+ cum_probs = torch.cumsum(F.softmax(sorted_logits / temperature, dim=-1), dim=-1)
151
+
152
+ sorted_indices_to_remove = cum_probs > thres
153
+ sorted_indices_to_remove[:, 0] = False
154
+ sorted_logits[sorted_indices_to_remove] = float("-inf")
155
+
156
+ return sorted_logits.scatter(1, sorted_indices, sorted_logits)
157
+
158
+
159
+ def top_k(logits, thres=0.9):
160
+ assert logits.dim() == 2
161
+
162
+ k = math.ceil((1 - thres) * logits.shape[-1])
163
+ val, ind = torch.topk(logits, k)
164
+ probs = torch.full_like(logits, float("-inf"))
165
+ probs.scatter_(1, ind, val)
166
+ return probs
167
+
168
+
169
+ def normalize(audio, min_y=-1.0, max_y=1.0, eps=1e-6):
170
+ assert len(audio.shape) == 1
171
+ max_y -= eps
172
+ min_y += eps
173
+ amax = audio.max()
174
+ amin = audio.min()
175
+ audio = (max_y - min_y) * (audio - amin) / (amax - amin) + min_y
176
+ return audio
177
+
178
+
179
+ def pickle_load(file):
180
+ return pickle.load(open(file, "rb"))
181
+
182
+
183
+ def pickle_save(data, file):
184
+ pickle.dump(data, open(file, "wb"))
185
+
186
+
187
+ def get_downbeat_indices(beats, downbeats):
188
+ beats = np.array(beats)
189
+ downbeats = np.array(downbeats)
190
+ downbeat_indices = []
191
+ for downbeat in downbeats:
192
+ idx = np.argmin(np.abs(beats - downbeat))
193
+ downbeat_indices.append(idx)
194
+ return np.array(downbeat_indices)
picogen2/version.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ _MAJOR = "0"
2
+ _MINOR = "1"
3
+ # On main and in a nightly release the patch should be one ahead of the last
4
+ # released build.
5
+ _PATCH = "1"
6
+ # This is mainly for nightly builds which have the suffix ".dev$DATE". See
7
+ # https://semver.org/#is-v123-a-semantic-version for the semantics.
8
+ _SUFFIX = ""
9
+
10
+ VERSION_SHORT = "{0}.{1}".format(_MAJOR, _MINOR)
11
+ VERSION = "{0}.{1}.{2}{3}".format(_MAJOR, _MINOR, _PATCH, _SUFFIX)
requirements.txt ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ git+https://github.com/TEAMuP-dev/pyharp.git@v0.3.0
2
+ # model-specific deps below:
3
+ torch
4
+ torchaudio
5
+ transformers
6
+ miditoolkit
7
+ questionary
8
+ soundfile
9
+ mpi4py
10
+ sheetsage @ git+https://github.com/tanchihpin0517/PiCoGen-sheetsage.git
11
+ beat_this @ https://github.com/CPJKU/beat_this/archive/main.zip
12
+ madmom @ git+https://github.com/CPJKU/madmom.git@0551aa8f48d71a367d92b5d3a347a0cf7cd97cc9