Spaces:
Running on Zero
Running on Zero
Vansh Chugh commited on
Commit ·
fe7e262
1
Parent(s): bf6c893
initial deploy
Browse files- .gitignore +5 -0
- README.md +3 -3
- app.py +167 -0
- packages.txt +3 -0
- picogen2/__init__.py +6 -0
- picogen2/assets.py +85 -0
- picogen2/infer.py +112 -0
- picogen2/mirtoolkit/__init__.py +0 -0
- picogen2/mirtoolkit/beat_this.py +78 -0
- picogen2/mirtoolkit/sheetsage.py +126 -0
- picogen2/mirtoolkit/utils.py +80 -0
- picogen2/model.py +230 -0
- picogen2/repr.py +530 -0
- picogen2/utils.py +194 -0
- picogen2/version.py +11 -0
- requirements.txt +12 -0
.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:
|
| 8 |
-
python_version: '3.
|
| 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
|