tothepoweroftom commited on
Commit
823689b
·
verified ·
1 Parent(s): f20634c

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +239 -0
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()