Spaces:
Sleeping
Sleeping
Create app.py
Browse files
app.py
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import re
|
| 2 |
+
import numpy as np
|
| 3 |
+
import gradio as gr
|
| 4 |
+
import pretty_midi
|
| 5 |
+
import subprocess
|
| 6 |
+
import random
|
| 7 |
+
from datasets import load_dataset
|
| 8 |
+
|
| 9 |
+
# ==========================================
|
| 10 |
+
# 1. DATA PREPARATION (MULTI-GENRE)
|
| 11 |
+
# ==========================================
|
| 12 |
+
print("Downloading and sorting dataset by genre... (This will take a minute on boot)")
|
| 13 |
+
dataset = load_dataset("ailsntua/Chordonomicon", split="train", streaming=True)
|
| 14 |
+
|
| 15 |
+
target_genres = ["pop", "rock", "jazz", "metal", "country", "blues", "r&b", "folk"]
|
| 16 |
+
corpus_by_genre = {genre: set() for genre in target_genres}
|
| 17 |
+
pattern = re.compile(r'<([^>]+)>\s*([^<]+)')
|
| 18 |
+
|
| 19 |
+
for row in dataset:
|
| 20 |
+
if all(len(progressions) >= 100 for progressions in corpus_by_genre.values()):
|
| 21 |
+
break
|
| 22 |
+
|
| 23 |
+
main_genre = str(row.get('main_genre', '')).lower()
|
| 24 |
+
genres_str = str(row.get('genres', '')).lower()
|
| 25 |
+
combined_genres = main_genre + " " + genres_str
|
| 26 |
+
|
| 27 |
+
matched_genre = None
|
| 28 |
+
for g in target_genres:
|
| 29 |
+
if g in combined_genres and len(corpus_by_genre[g]) < 100:
|
| 30 |
+
matched_genre = g
|
| 31 |
+
break
|
| 32 |
+
|
| 33 |
+
if not matched_genre: continue
|
| 34 |
+
|
| 35 |
+
chord_string = row.get('chords', '')
|
| 36 |
+
if not chord_string: continue
|
| 37 |
+
|
| 38 |
+
matches = pattern.findall(chord_string)
|
| 39 |
+
for tag, chords in matches:
|
| 40 |
+
tag = tag.lower().strip()
|
| 41 |
+
chords = " ".join(chords.split())
|
| 42 |
+
|
| 43 |
+
if chords and ('verse' in tag or 'chorus' in tag):
|
| 44 |
+
corpus_by_genre[matched_genre].add(chords)
|
| 45 |
+
if len(corpus_by_genre[matched_genre]) >= 100:
|
| 46 |
+
break
|
| 47 |
+
|
| 48 |
+
corpus_by_genre = {g: list(chords) for g, chords in corpus_by_genre.items()}
|
| 49 |
+
|
| 50 |
+
# ==========================================
|
| 51 |
+
# 2. MARKOV CHAIN LOGIC
|
| 52 |
+
# ==========================================
|
| 53 |
+
def train_markov_model(corpus, order=1):
|
| 54 |
+
markov_model = {}
|
| 55 |
+
art_start = "*S*"
|
| 56 |
+
art_end = "*E*"
|
| 57 |
+
|
| 58 |
+
for progression in corpus:
|
| 59 |
+
chords = progression.split()
|
| 60 |
+
if not chords: continue
|
| 61 |
+
current_state = tuple([art_start] * order)
|
| 62 |
+
|
| 63 |
+
for chord in chords:
|
| 64 |
+
if current_state not in markov_model: markov_model[current_state] = {}
|
| 65 |
+
if chord not in markov_model[current_state]: markov_model[current_state][chord] = 0
|
| 66 |
+
markov_model[current_state][chord] += 1
|
| 67 |
+
current_state = tuple(list(current_state)[1:] + [chord])
|
| 68 |
+
|
| 69 |
+
if current_state not in markov_model: markov_model[current_state] = {}
|
| 70 |
+
if art_end not in markov_model[current_state]: markov_model[current_state][art_end] = 0
|
| 71 |
+
markov_model[current_state][art_end] += 1
|
| 72 |
+
|
| 73 |
+
return markov_model
|
| 74 |
+
|
| 75 |
+
def get_next_chord(current_state, markov_model):
|
| 76 |
+
if current_state not in markov_model: return "*E*"
|
| 77 |
+
transitions = markov_model[current_state]
|
| 78 |
+
next_chords = list(transitions.keys())
|
| 79 |
+
counts = list(transitions.values())
|
| 80 |
+
total = sum(counts)
|
| 81 |
+
probs = [c / total for c in counts]
|
| 82 |
+
return np.random.choice(next_chords, p=probs)
|
| 83 |
+
|
| 84 |
+
def generate_progression(markov_model, target_length, order=1):
|
| 85 |
+
art_start = "*S*"
|
| 86 |
+
art_end = "*E*"
|
| 87 |
+
current_state = tuple([art_start] * order)
|
| 88 |
+
progression = []
|
| 89 |
+
|
| 90 |
+
max_attempts = target_length * 5
|
| 91 |
+
attempts = 0
|
| 92 |
+
|
| 93 |
+
while len(progression) < target_length and attempts < max_attempts:
|
| 94 |
+
attempts += 1
|
| 95 |
+
next_chord = get_next_chord(current_state, markov_model)
|
| 96 |
+
|
| 97 |
+
if next_chord == art_end:
|
| 98 |
+
current_state = tuple([art_start] * order)
|
| 99 |
+
continue
|
| 100 |
+
|
| 101 |
+
progression.append(next_chord)
|
| 102 |
+
current_state = tuple(list(current_state)[1:] + [next_chord])
|
| 103 |
+
|
| 104 |
+
return " ".join(progression)
|
| 105 |
+
|
| 106 |
+
# ==========================================
|
| 107 |
+
# 3. AUDIO SYNTHESIS & VOICING LOGIC
|
| 108 |
+
# ==========================================
|
| 109 |
+
NOTE_TO_MIDI = {'C': 60, 'Cs': 61, 'Db': 61, 'D': 62, 'Ds': 63, 'Eb': 63, 'E': 64, 'F': 65, 'Fs': 66, 'Gb': 66, 'G': 67, 'Gs': 68, 'Ab': 68, 'A': 69, 'As': 70, 'Bb': 70, 'B': 71}
|
| 110 |
+
MIDI_TO_NOTE = {60: 'C', 61: 'Db', 62: 'D', 63: 'Eb', 64: 'E', 65: 'F', 66: 'Gb', 67: 'G', 68: 'Ab', 69: 'A', 70: 'Bb', 71: 'B'}
|
| 111 |
+
CHORD_INTERVALS = {'maj': [0, 4, 7], 'min': [0, 3, 7], '7': [0, 4, 7, 10], 'maj7': [0, 4, 7, 11], 'min7': [0, 3, 7, 10], 'sus4': [0, 5, 7], 'sus2': [0, 2, 7], 'dim': [0, 3, 6], 'no3d': [0, 7], '5': [0, 7]}
|
| 112 |
+
|
| 113 |
+
def parse_chord_to_midi(chord_string):
|
| 114 |
+
if not chord_string or chord_string == 'N': return [], ""
|
| 115 |
+
chord_string = chord_string.split('/')[0]
|
| 116 |
+
root_note = chord_string[0]
|
| 117 |
+
remainder = chord_string[1:]
|
| 118 |
+
if remainder and remainder[0] in ['s', 'b']:
|
| 119 |
+
root_note += remainder[0]
|
| 120 |
+
remainder = remainder[1:]
|
| 121 |
+
|
| 122 |
+
root_midi = NOTE_TO_MIDI.get(root_note, 60)
|
| 123 |
+
quality = 'maj'
|
| 124 |
+
intervals = CHORD_INTERVALS['maj']
|
| 125 |
+
for q, ints in CHORD_INTERVALS.items():
|
| 126 |
+
if remainder.startswith(q):
|
| 127 |
+
intervals = ints; quality = q; break
|
| 128 |
+
return [root_midi + i for i in intervals], quality
|
| 129 |
+
|
| 130 |
+
def apply_voicing(pitches, voicing_type):
|
| 131 |
+
if not pitches: return pitches
|
| 132 |
+
pitches = sorted(pitches)
|
| 133 |
+
if voicing_type == "First Inversion" and len(pitches) > 1: pitches[0] += 12
|
| 134 |
+
elif voicing_type == "Second Inversion" and len(pitches) > 2: pitches[0] += 12; pitches[1] += 12
|
| 135 |
+
elif voicing_type == "Random Voice Leading":
|
| 136 |
+
choice = random.choice([0, 1, 2])
|
| 137 |
+
if choice == 1 and len(pitches) > 1: pitches[0] += 12
|
| 138 |
+
if choice == 2 and len(pitches) > 2: pitches[0] += 12; pitches[1] += 12
|
| 139 |
+
elif voicing_type == "Open / Spread" and len(pitches) >= 3: pitches[0] -= 12; pitches[1] += 12
|
| 140 |
+
return sorted(pitches) if voicing_type != "Open / Spread" else pitches
|
| 141 |
+
|
| 142 |
+
def generate_audio_file(progression_string, instrument, transpose_semitones, voicing_type):
|
| 143 |
+
if not progression_string.strip(): return None, None, ""
|
| 144 |
+
|
| 145 |
+
is_metal = (instrument == "Metal Guitar")
|
| 146 |
+
prog_num = 30 if is_metal else 0
|
| 147 |
+
velocity = 110 if is_metal else 85
|
| 148 |
+
|
| 149 |
+
midi = pretty_midi.PrettyMIDI(initial_tempo=120)
|
| 150 |
+
inst = pretty_midi.Instrument(program=prog_num)
|
| 151 |
+
current_time = 0.0
|
| 152 |
+
transposed_chord_names = []
|
| 153 |
+
|
| 154 |
+
for chord in progression_string.split():
|
| 155 |
+
pitches, quality = parse_chord_to_midi(chord)
|
| 156 |
+
if not pitches: continue
|
| 157 |
+
|
| 158 |
+
# Transpose
|
| 159 |
+
pitches = [p + transpose_semitones for p in pitches]
|
| 160 |
+
normalized_root = ((pitches[0] - 60) % 12) + 60
|
| 161 |
+
transposed_chord_names.append(MIDI_TO_NOTE.get(normalized_root, "C") + quality)
|
| 162 |
+
|
| 163 |
+
if is_metal: pitches = [p - 12 for p in pitches]
|
| 164 |
+
pitches = apply_voicing(pitches, voicing_type)
|
| 165 |
+
|
| 166 |
+
for pitch in pitches:
|
| 167 |
+
note = pretty_midi.Note(velocity=velocity, pitch=pitch, start=current_time, end=current_time + 0.5)
|
| 168 |
+
inst.notes.append(note)
|
| 169 |
+
current_time += 0.5
|
| 170 |
+
|
| 171 |
+
midi.instruments.append(inst)
|
| 172 |
+
midi_path = 'generated_progression.mid'
|
| 173 |
+
wav_path = 'generated_progression.wav'
|
| 174 |
+
|
| 175 |
+
midi.write(midi_path)
|
| 176 |
+
subprocess.run(['fluidsynth', '-ni', '/usr/share/sounds/sf2/FluidR3_GM.sf2', midi_path, '-F', wav_path, '-r', '44100'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
| 177 |
+
|
| 178 |
+
return wav_path, midi_path, " ".join(transposed_chord_names)
|
| 179 |
+
|
| 180 |
+
# ==========================================
|
| 181 |
+
# 4. GRADIO INTERFACE
|
| 182 |
+
# ==========================================
|
| 183 |
+
def app_logic(genre, order, length, instrument, transpose, voicing):
|
| 184 |
+
corpus = corpus_by_genre.get(genre, [])
|
| 185 |
+
|
| 186 |
+
if not corpus:
|
| 187 |
+
return f"Error: No chords found for {genre}.", "", None, None
|
| 188 |
+
|
| 189 |
+
model = train_markov_model(corpus, order=int(order))
|
| 190 |
+
raw_chords = generate_progression(model, target_length=int(length), order=int(order))
|
| 191 |
+
|
| 192 |
+
if not raw_chords.strip():
|
| 193 |
+
return "(Generation stopped. The Markov chain hit an early dead end. Try again or lower the Order.)", "", None, None
|
| 194 |
+
|
| 195 |
+
audio_path, midi_path, final_transposed_chords = generate_audio_file(raw_chords, instrument, int(transpose), voicing)
|
| 196 |
+
|
| 197 |
+
return raw_chords, final_transposed_chords, audio_path, midi_path
|
| 198 |
+
|
| 199 |
+
with gr.Blocks(theme=gr.themes.Monochrome()) as demo:
|
| 200 |
+
gr.Markdown("# 🎸 Markhords: AI Chord Progression Generator")
|
| 201 |
+
|
| 202 |
+
with gr.Row():
|
| 203 |
+
with gr.Column(scale=1):
|
| 204 |
+
gr.Markdown("### 1. Training Data")
|
| 205 |
+
genre_dropdown = gr.Dropdown(
|
| 206 |
+
choices=[g.capitalize() for g in target_genres],
|
| 207 |
+
value="Pop",
|
| 208 |
+
label="Dataset Genre (100 Progressions Each)"
|
| 209 |
+
)
|
| 210 |
+
|
| 211 |
+
gr.Markdown("### 2. Generation Settings")
|
| 212 |
+
order_slider = gr.Slider(minimum=1, maximum=3, step=1, value=1, label="Markov Chain Order")
|
| 213 |
+
length_slider = gr.Slider(minimum=2, maximum=16, step=1, value=8, label="Target Length (Chords)")
|
| 214 |
+
|
| 215 |
+
gr.Markdown("### 3. Post-Processing")
|
| 216 |
+
transpose_slider = gr.Slider(minimum=-12, maximum=12, step=1, value=0, label="Transpose (Semitones)")
|
| 217 |
+
voicing_dropdown = gr.Dropdown(
|
| 218 |
+
choices=["Root Position", "First Inversion", "Second Inversion", "Open / Spread", "Random Voice Leading"],
|
| 219 |
+
value="Root Position",
|
| 220 |
+
label="Chord Voicings"
|
| 221 |
+
)
|
| 222 |
+
instrument_dropdown = gr.Dropdown(choices=["Piano", "Metal Guitar"], value="Piano", label="Instrument")
|
| 223 |
+
|
| 224 |
+
generate_btn = gr.Button("Generate Chords", variant="primary")
|
| 225 |
+
|
| 226 |
+
with gr.Column(scale=1):
|
| 227 |
+
gr.Markdown("### Output")
|
| 228 |
+
output_raw_text = gr.Textbox(label="Original Generated Progression", lines=2, interactive=False)
|
| 229 |
+
output_final_text = gr.Textbox(label="Final Progression (After Transposition)", lines=2, interactive=False)
|
| 230 |
+
output_audio = gr.Audio(label="Playback", type="filepath", autoplay=True)
|
| 231 |
+
output_midi = gr.File(label="Download MIDI", interactive=False)
|
| 232 |
+
|
| 233 |
+
generate_btn.click(
|
| 234 |
+
fn=lambda g, o, l, i, t, v: app_logic(g.lower(), o, l, i, t, v),
|
| 235 |
+
inputs=[genre_dropdown, order_slider, length_slider, instrument_dropdown, transpose_slider, voicing_dropdown],
|
| 236 |
+
outputs=[output_raw_text, output_final_text, output_audio, output_midi]
|
| 237 |
+
)
|
| 238 |
+
|
| 239 |
+
demo.launch()
|