@kernel.chat/kbot 3.69.1 → 3.71.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/a2a.d.ts +50 -5
- package/dist/a2a.js +305 -44
- package/dist/auth.d.ts +4 -0
- package/dist/auth.js +43 -4
- package/dist/doctor.js +53 -7
- package/dist/integrations/ableton-bridge.d.ts +158 -0
- package/dist/integrations/ableton-bridge.js +486 -0
- package/dist/integrations/ableton-m4l.d.ts +94 -0
- package/dist/integrations/ableton-m4l.js +252 -1
- package/dist/integrations/install-remote-script.d.ts +23 -0
- package/dist/integrations/install-remote-script.js +121 -0
- package/dist/machine.d.ts +1 -0
- package/dist/machine.js +17 -1
- package/dist/serve.js +3 -2
- package/dist/tools/a2a.d.ts +2 -0
- package/dist/tools/a2a.js +233 -0
- package/dist/tools/ableton-bridge-tools.d.ts +14 -0
- package/dist/tools/ableton-bridge-tools.js +327 -0
- package/dist/tools/ai-analysis.d.ts +2 -0
- package/dist/tools/ai-analysis.js +677 -0
- package/dist/tools/financial-analysis.d.ts +2 -0
- package/dist/tools/financial-analysis.js +945 -0
- package/dist/tools/index.js +6 -0
- package/dist/tools/music-gen.d.ts +2 -0
- package/dist/tools/music-gen.js +1006 -0
- package/dist/tools/threat-intel.d.ts +2 -0
- package/dist/tools/threat-intel.js +1619 -0
- package/package.json +2 -2
|
@@ -0,0 +1,1006 @@
|
|
|
1
|
+
// kbot Music Generation Engine — AI-powered songwriting + pattern generation
|
|
2
|
+
//
|
|
3
|
+
// Suno-style creative music generation using local Ollama models ($0 cost).
|
|
4
|
+
// Outputs are compatible with kbot's Ableton tools so users can pipe:
|
|
5
|
+
// music_idea -> generate_drum_pattern -> ableton_midi
|
|
6
|
+
//
|
|
7
|
+
// Tools:
|
|
8
|
+
// generate_lyrics — AI lyrics from genre + mood + topic
|
|
9
|
+
// generate_melody_pattern — MIDI melody from key + scale + BPM + genre
|
|
10
|
+
// generate_drum_pattern — MIDI drums from genre + BPM + feel
|
|
11
|
+
// generate_song_structure — Full arrangement plan from genre + BPM + key
|
|
12
|
+
// music_idea — Creative prompt -> full production blueprint
|
|
13
|
+
//
|
|
14
|
+
// All generation uses local Ollama (kernel:latest) — zero API cost.
|
|
15
|
+
// Music theory computations use music-theory.ts — zero AI needed.
|
|
16
|
+
import { registerTool } from './index.js';
|
|
17
|
+
import { SCALES, NAMED_PROGRESSIONS, GENRE_DRUM_PATTERNS, GM_DRUMS, RHYTHM_PATTERNS, getScaleNotes, noteNameToMidi, midiToNoteName, parseProgression, quantizeToScale, } from './music-theory.js';
|
|
18
|
+
// ── Ollama Integration ─────────────────────────────────────────────────────
|
|
19
|
+
const OLLAMA_URL = process.env.OLLAMA_HOST || 'http://localhost:11434';
|
|
20
|
+
const DEFAULT_MODEL = 'kernel:latest';
|
|
21
|
+
/**
|
|
22
|
+
* Call local Ollama for creative generation. Returns raw text.
|
|
23
|
+
* Falls back gracefully if Ollama is offline.
|
|
24
|
+
*/
|
|
25
|
+
async function ollamaGenerate(prompt, opts) {
|
|
26
|
+
const model = opts?.model || DEFAULT_MODEL;
|
|
27
|
+
const temperature = opts?.temperature ?? 0.8;
|
|
28
|
+
const maxTokens = opts?.maxTokens ?? 1024;
|
|
29
|
+
try {
|
|
30
|
+
const res = await fetch(`${OLLAMA_URL}/api/generate`, {
|
|
31
|
+
method: 'POST',
|
|
32
|
+
headers: { 'Content-Type': 'application/json' },
|
|
33
|
+
body: JSON.stringify({
|
|
34
|
+
model,
|
|
35
|
+
prompt,
|
|
36
|
+
stream: false,
|
|
37
|
+
options: { num_predict: maxTokens, temperature },
|
|
38
|
+
}),
|
|
39
|
+
signal: AbortSignal.timeout(120_000),
|
|
40
|
+
});
|
|
41
|
+
if (!res.ok) {
|
|
42
|
+
return `[Ollama error: HTTP ${res.status}. Is Ollama running? Try: ollama serve]`;
|
|
43
|
+
}
|
|
44
|
+
const data = (await res.json());
|
|
45
|
+
// Strip thinking tags that some models emit
|
|
46
|
+
return (data.response || '')
|
|
47
|
+
.replace(/<think>[\s\S]*?<\/think>/g, '')
|
|
48
|
+
.trim();
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
if (err instanceof Error && err.name === 'AbortError') {
|
|
52
|
+
return '[Ollama timed out after 120s. Model may still be loading.]';
|
|
53
|
+
}
|
|
54
|
+
return `[Ollama offline. Start with: ollama serve]`;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Call Ollama and parse the response as JSON.
|
|
59
|
+
* Extracts the first JSON object or array from the response.
|
|
60
|
+
*/
|
|
61
|
+
async function ollamaGenerateJSON(prompt, opts) {
|
|
62
|
+
const raw = await ollamaGenerate(prompt, opts);
|
|
63
|
+
if (raw.startsWith('[Ollama'))
|
|
64
|
+
return null;
|
|
65
|
+
// Try to extract JSON from the response
|
|
66
|
+
const jsonMatch = raw.match(/[\[{][\s\S]*[\]}]/);
|
|
67
|
+
if (!jsonMatch)
|
|
68
|
+
return null;
|
|
69
|
+
try {
|
|
70
|
+
return JSON.parse(jsonMatch[0]);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
77
|
+
function pick(arr) {
|
|
78
|
+
return arr[Math.floor(Math.random() * arr.length)];
|
|
79
|
+
}
|
|
80
|
+
function randomInRange(min, max) {
|
|
81
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
82
|
+
}
|
|
83
|
+
function clamp(value, min, max) {
|
|
84
|
+
return Math.max(min, Math.min(max, value));
|
|
85
|
+
}
|
|
86
|
+
/** Parse a key string like "Cm", "F#m", "Bb" into root + scale */
|
|
87
|
+
function parseKeyString(keyStr) {
|
|
88
|
+
const match = keyStr.match(/^([A-Ga-g][#b]?)(m|min|minor)?$/i);
|
|
89
|
+
if (!match)
|
|
90
|
+
return { root: 'C', scale: 'natural_minor' };
|
|
91
|
+
const root = match[1].charAt(0).toUpperCase() + match[1].slice(1);
|
|
92
|
+
const isMinor = !!match[2];
|
|
93
|
+
return { root, scale: isMinor ? 'natural_minor' : 'major' };
|
|
94
|
+
}
|
|
95
|
+
/** Get available genre names from GENRE_DRUM_PATTERNS */
|
|
96
|
+
function availableGenres() {
|
|
97
|
+
return Object.keys(GENRE_DRUM_PATTERNS);
|
|
98
|
+
}
|
|
99
|
+
/** Format a MidiNote array as readable JSON for output */
|
|
100
|
+
function formatMidiNotes(notes) {
|
|
101
|
+
return JSON.stringify(notes.map(n => ({
|
|
102
|
+
note: midiToNoteName(n.pitch),
|
|
103
|
+
midi: n.pitch,
|
|
104
|
+
velocity: n.velocity,
|
|
105
|
+
start_beat: Math.round(n.start * 1000) / 1000,
|
|
106
|
+
duration: Math.round(n.duration * 1000) / 1000,
|
|
107
|
+
})), null, 2);
|
|
108
|
+
}
|
|
109
|
+
const GENRE_PROFILES = {
|
|
110
|
+
trap: {
|
|
111
|
+
bpmRange: [130, 170],
|
|
112
|
+
preferredKeys: ['Cm', 'Dm', 'Am', 'Em', 'Fm'],
|
|
113
|
+
preferredScales: ['natural_minor', 'phrygian', 'harmonic_minor'],
|
|
114
|
+
feel: 'straight',
|
|
115
|
+
progressions: ['i bVI bVII i', 'i bVII bVI V', 'i iv bVI bVII'],
|
|
116
|
+
instruments: ['808 bass', 'hi-hats', 'snare/clap', 'dark pads', 'bells', 'plucks'],
|
|
117
|
+
energyProfile: 'half-time feel with rapid hi-hats, sparse arrangement, heavy 808',
|
|
118
|
+
description: 'Modern trap: half-time drums, 808 bass slides, rapid hi-hat patterns, dark minor tonality',
|
|
119
|
+
},
|
|
120
|
+
drill: {
|
|
121
|
+
bpmRange: [140, 150],
|
|
122
|
+
preferredKeys: ['Cm', 'Dm', 'Bbm', 'Gm'],
|
|
123
|
+
preferredScales: ['natural_minor', 'harmonic_minor', 'phrygian'],
|
|
124
|
+
feel: 'straight',
|
|
125
|
+
progressions: ['i bVI bVII i', 'i bII bVII i', 'i bVI V i'],
|
|
126
|
+
instruments: ['sliding 808', 'clap', 'hi-hats', 'dark piano', 'strings'],
|
|
127
|
+
energyProfile: 'relentless hi-hats, syncopated kicks, sliding 808 bass',
|
|
128
|
+
description: 'UK/NY drill: sliding 808s, displaced snare, aggressive hi-hat patterns, minor keys',
|
|
129
|
+
},
|
|
130
|
+
house: {
|
|
131
|
+
bpmRange: [120, 130],
|
|
132
|
+
preferredKeys: ['Am', 'Cm', 'Dm', 'Gm'],
|
|
133
|
+
preferredScales: ['natural_minor', 'dorian', 'major'],
|
|
134
|
+
feel: 'straight',
|
|
135
|
+
progressions: ['i bVII bVI bVII', 'i iv bVI V', 'vi IV I V'],
|
|
136
|
+
instruments: ['four-on-floor kick', 'open hats', 'claps', 'synth bass', 'piano stabs', 'vocal chops'],
|
|
137
|
+
energyProfile: 'steady four-on-floor, build-and-release dynamics',
|
|
138
|
+
description: 'House: four-on-floor kick, offbeat hi-hats, piano chords, deep bassline',
|
|
139
|
+
},
|
|
140
|
+
lofi: {
|
|
141
|
+
bpmRange: [70, 90],
|
|
142
|
+
preferredKeys: ['Em', 'Am', 'Dm', 'Cm', 'Gm'],
|
|
143
|
+
preferredScales: ['natural_minor', 'dorian', 'pentatonic_minor'],
|
|
144
|
+
feel: 'swing',
|
|
145
|
+
progressions: ['ii7 V7 Imaj7', 'Imaj7 vi7 ii7 V7', 'i iv bVI V'],
|
|
146
|
+
instruments: ['dusty drums', 'jazz piano', 'Rhodes', 'vinyl crackle', 'bass guitar', 'guitar'],
|
|
147
|
+
energyProfile: 'chill, relaxed, nostalgic, lo-fi warmth',
|
|
148
|
+
description: 'Lo-fi hip hop: jazzy chords, dusty drums, vinyl texture, mellow bass',
|
|
149
|
+
},
|
|
150
|
+
pop: {
|
|
151
|
+
bpmRange: [100, 130],
|
|
152
|
+
preferredKeys: ['C', 'G', 'D', 'A', 'F'],
|
|
153
|
+
preferredScales: ['major', 'mixolydian'],
|
|
154
|
+
feel: 'straight',
|
|
155
|
+
progressions: ['I V vi IV', 'vi IV I V', 'I IV vi V'],
|
|
156
|
+
instruments: ['drums', 'bass', 'piano', 'synth pad', 'guitar', 'vocal'],
|
|
157
|
+
energyProfile: 'verse-chorus dynamics, catchy hooks, bright tones',
|
|
158
|
+
description: 'Pop: standard backbeat, singable melodies, major keys, verse-chorus structure',
|
|
159
|
+
},
|
|
160
|
+
rnb: {
|
|
161
|
+
bpmRange: [65, 95],
|
|
162
|
+
preferredKeys: ['Dm', 'Am', 'Em', 'Cm', 'Gm'],
|
|
163
|
+
preferredScales: ['dorian', 'natural_minor', 'pentatonic_minor'],
|
|
164
|
+
feel: 'swing',
|
|
165
|
+
progressions: ['Imaj7 iii7 vi7 ii7 V7', 'i iv bVI V', 'ii7 V7 Imaj7'],
|
|
166
|
+
instruments: ['Rhodes', 'bass guitar', 'drums', 'strings', 'vocal harmonies', 'synth pad'],
|
|
167
|
+
energyProfile: 'smooth, laid-back groove with lush harmony',
|
|
168
|
+
description: 'R&B: jazzy extended chords, smooth bass, expressive melody, intimate feel',
|
|
169
|
+
},
|
|
170
|
+
phonk: {
|
|
171
|
+
bpmRange: [130, 145],
|
|
172
|
+
preferredKeys: ['Cm', 'Dm', 'Em', 'Am'],
|
|
173
|
+
preferredScales: ['natural_minor', 'blues', 'phrygian'],
|
|
174
|
+
feel: 'straight',
|
|
175
|
+
progressions: ['i bVI bVII i', 'i bII i bVII', 'i iv i V'],
|
|
176
|
+
instruments: ['Memphis drums', 'cowbell', '808 bass', 'distorted vocals', 'chopped soul samples'],
|
|
177
|
+
energyProfile: 'dark, aggressive, lo-fi Memphis rap aesthetic',
|
|
178
|
+
description: 'Phonk: cowbells, dark melodies, distorted 808, Memphis rap influence',
|
|
179
|
+
},
|
|
180
|
+
ambient: {
|
|
181
|
+
bpmRange: [60, 90],
|
|
182
|
+
preferredKeys: ['Am', 'Em', 'Dm', 'C', 'G'],
|
|
183
|
+
preferredScales: ['major', 'lydian', 'dorian', 'pentatonic_major'],
|
|
184
|
+
feel: 'straight',
|
|
185
|
+
progressions: ['I II', 'i bVII', 'Imaj7 IVmaj7', 'i bVI'],
|
|
186
|
+
instruments: ['evolving pads', 'granular textures', 'reverb piano', 'field recordings', 'drones'],
|
|
187
|
+
energyProfile: 'slow evolution, wide stereo, textural layers',
|
|
188
|
+
description: 'Ambient: long evolving textures, wide reverb, minimal rhythm, atmospheric',
|
|
189
|
+
},
|
|
190
|
+
techno: {
|
|
191
|
+
bpmRange: [130, 145],
|
|
192
|
+
preferredKeys: ['Am', 'Dm', 'Em', 'Cm'],
|
|
193
|
+
preferredScales: ['natural_minor', 'phrygian', 'dorian'],
|
|
194
|
+
feel: 'straight',
|
|
195
|
+
progressions: ['i bVII', 'i iv', 'i bVI bVII i'],
|
|
196
|
+
instruments: ['four-on-floor kick', 'clap', 'ride cymbal', 'acid bass', 'synth stabs', 'noise sweeps'],
|
|
197
|
+
energyProfile: 'hypnotic, driving, build-and-release over long sections',
|
|
198
|
+
description: 'Techno: driving kick, industrial textures, acid basslines, hypnotic repetition',
|
|
199
|
+
},
|
|
200
|
+
jazz: {
|
|
201
|
+
bpmRange: [100, 180],
|
|
202
|
+
preferredKeys: ['F', 'Bb', 'Eb', 'Ab', 'Db', 'C'],
|
|
203
|
+
preferredScales: ['major', 'dorian', 'mixolydian', 'bebop_dominant'],
|
|
204
|
+
feel: 'swing',
|
|
205
|
+
progressions: ['ii7 V7 Imaj7', 'Imaj7 vi7 ii7 V7', 'Imaj7 bIIImaj7 IVmaj7 bVImaj7'],
|
|
206
|
+
instruments: ['piano/Rhodes', 'upright bass', 'drums (brushes/sticks)', 'saxophone', 'trumpet'],
|
|
207
|
+
energyProfile: 'conversational dynamics, improvisation, swing feel',
|
|
208
|
+
description: 'Jazz: extended chords, swing rhythm, walking bass, improvisation-friendly',
|
|
209
|
+
},
|
|
210
|
+
dnb: {
|
|
211
|
+
bpmRange: [160, 180],
|
|
212
|
+
preferredKeys: ['Am', 'Dm', 'Em', 'Cm'],
|
|
213
|
+
preferredScales: ['natural_minor', 'harmonic_minor', 'phrygian'],
|
|
214
|
+
feel: 'straight',
|
|
215
|
+
progressions: ['i bVII bVI V', 'i bVI bIII bVII', 'i iv bVI bVII'],
|
|
216
|
+
instruments: ['breakbeat drums', 'reese bass', 'pads', 'vocal samples', 'synth leads'],
|
|
217
|
+
energyProfile: 'fast breaks, heavy bass, liquid or aggressive textures',
|
|
218
|
+
description: 'Drum & bass: breakbeat at 170+, deep reese bass, atmospheric pads',
|
|
219
|
+
},
|
|
220
|
+
reggaeton: {
|
|
221
|
+
bpmRange: [88, 100],
|
|
222
|
+
preferredKeys: ['Am', 'Dm', 'Em', 'Cm'],
|
|
223
|
+
preferredScales: ['natural_minor', 'harmonic_minor', 'phrygian_dominant'],
|
|
224
|
+
feel: 'straight',
|
|
225
|
+
progressions: ['vi IV I V', 'i bVII bVI V', 'i iv V i'],
|
|
226
|
+
instruments: ['dembow drums', 'bass', 'reggaeton snare', 'synth lead', 'vocal', 'perc'],
|
|
227
|
+
energyProfile: 'dembow rhythm, steady energy, danceable groove',
|
|
228
|
+
description: 'Reggaeton: dembow beat pattern, Latin percussion, catchy vocal hooks',
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
function getStructureForGenre(genre, bpm) {
|
|
232
|
+
// Adapt bar counts to BPM — faster tempos need fewer bars per section to stay in typical song length
|
|
233
|
+
const tempoFactor = bpm > 150 ? 1.5 : bpm > 120 ? 1.0 : 0.75;
|
|
234
|
+
const structures = {
|
|
235
|
+
trap: [
|
|
236
|
+
{ name: 'Intro', bars: 4, energy: 0.2, instruments: ['pads', 'melody'], description: 'Atmospheric intro, filtered melody teaser' },
|
|
237
|
+
{ name: 'Verse 1', bars: 8, energy: 0.5, instruments: ['drums', 'bass', 'melody'], description: 'Drums enter, half-time feel, bass holds' },
|
|
238
|
+
{ name: 'Pre-Hook', bars: 4, energy: 0.7, instruments: ['drums', 'bass', 'melody', 'perc'], description: 'Build energy with hi-hat rolls, rising 808' },
|
|
239
|
+
{ name: 'Hook', bars: 8, energy: 0.9, instruments: ['drums', 'bass', 'melody', 'pads', 'perc'], description: 'Full arrangement, catchiest melody, heavy 808' },
|
|
240
|
+
{ name: 'Verse 2', bars: 8, energy: 0.5, instruments: ['drums', 'bass', 'melody', 'perc'], description: 'Variation on verse 1, added percussion' },
|
|
241
|
+
{ name: 'Hook 2', bars: 8, energy: 0.95, instruments: ['drums', 'bass', 'melody', 'pads', 'perc', 'fx'], description: 'Full arrangement with extra energy layers' },
|
|
242
|
+
{ name: 'Bridge', bars: 4, energy: 0.4, instruments: ['pads', 'melody', 'bass'], description: 'Stripped back, emotional break, no drums' },
|
|
243
|
+
{ name: 'Final Hook', bars: 8, energy: 1.0, instruments: ['drums', 'bass', 'melody', 'pads', 'perc', 'fx'], description: 'Maximum energy, double-time hats, full stack' },
|
|
244
|
+
{ name: 'Outro', bars: 4, energy: 0.15, instruments: ['pads', 'melody'], description: 'Elements drop out, reverb tail, fade' },
|
|
245
|
+
],
|
|
246
|
+
house: [
|
|
247
|
+
{ name: 'Intro', bars: 8, energy: 0.3, instruments: ['kick', 'hats'], description: 'Kick and hats build, hi-pass filter opening' },
|
|
248
|
+
{ name: 'Build A', bars: 8, energy: 0.5, instruments: ['drums', 'bass'], description: 'Bass enters, groove established' },
|
|
249
|
+
{ name: 'Main A', bars: 16, energy: 0.8, instruments: ['drums', 'bass', 'chords', 'melody'], description: 'Full groove, main hook, chord stabs' },
|
|
250
|
+
{ name: 'Breakdown', bars: 8, energy: 0.3, instruments: ['pads', 'melody', 'fx'], description: 'Kick drops, pads swell, tension builds' },
|
|
251
|
+
{ name: 'Build B', bars: 4, energy: 0.6, instruments: ['drums', 'bass', 'fx'], description: 'Riser, snare roll, kick returns' },
|
|
252
|
+
{ name: 'Main B', bars: 16, energy: 0.9, instruments: ['drums', 'bass', 'chords', 'melody', 'perc'], description: 'Peak energy, all elements, extra percussion' },
|
|
253
|
+
{ name: 'Breakdown 2', bars: 8, energy: 0.35, instruments: ['pads', 'chords'], description: 'Second breakdown, different melodic element' },
|
|
254
|
+
{ name: 'Main C', bars: 16, energy: 0.85, instruments: ['drums', 'bass', 'chords', 'melody'], description: 'Return to groove, slight variation' },
|
|
255
|
+
{ name: 'Outro', bars: 8, energy: 0.2, instruments: ['kick', 'hats'], description: 'Elements drop, kick and hats ride out' },
|
|
256
|
+
],
|
|
257
|
+
pop: [
|
|
258
|
+
{ name: 'Intro', bars: 4, energy: 0.3, instruments: ['guitar/piano', 'light drums'], description: 'Instrumental hook or vocal ad-lib' },
|
|
259
|
+
{ name: 'Verse 1', bars: 8, energy: 0.5, instruments: ['drums', 'bass', 'guitar/piano'], description: 'Verse melody, simple arrangement' },
|
|
260
|
+
{ name: 'Pre-Chorus', bars: 4, energy: 0.7, instruments: ['drums', 'bass', 'synth', 'guitar/piano'], description: 'Harmonic lift, building anticipation' },
|
|
261
|
+
{ name: 'Chorus', bars: 8, energy: 0.9, instruments: ['drums', 'bass', 'synth', 'guitar/piano', 'vocal layers'], description: 'Biggest hook, full arrangement, singable melody' },
|
|
262
|
+
{ name: 'Verse 2', bars: 8, energy: 0.55, instruments: ['drums', 'bass', 'guitar/piano', 'perc'], description: 'Slightly bigger than verse 1' },
|
|
263
|
+
{ name: 'Pre-Chorus 2', bars: 4, energy: 0.75, instruments: ['drums', 'bass', 'synth', 'guitar/piano'], description: 'Same lift, slight variation' },
|
|
264
|
+
{ name: 'Chorus 2', bars: 8, energy: 0.95, instruments: ['drums', 'bass', 'synth', 'guitar/piano', 'vocal layers', 'strings'], description: 'Bigger chorus with added elements' },
|
|
265
|
+
{ name: 'Bridge', bars: 8, energy: 0.4, instruments: ['piano', 'strings', 'vocal'], description: 'New harmony, emotional contrast, stripped back' },
|
|
266
|
+
{ name: 'Final Chorus', bars: 8, energy: 1.0, instruments: ['drums', 'bass', 'synth', 'guitar/piano', 'vocal layers', 'strings', 'perc'], description: 'Key change optional, maximum energy' },
|
|
267
|
+
{ name: 'Outro', bars: 4, energy: 0.2, instruments: ['guitar/piano', 'vocal'], description: 'Instrumental hook callback, fade or hard stop' },
|
|
268
|
+
],
|
|
269
|
+
ambient: [
|
|
270
|
+
{ name: 'Opening', bars: 8, energy: 0.1, instruments: ['drone', 'field recording'], description: 'Single texture emerges from silence' },
|
|
271
|
+
{ name: 'Evolution A', bars: 16, energy: 0.3, instruments: ['pad', 'drone', 'granular'], description: 'Slowly evolving textures layer in' },
|
|
272
|
+
{ name: 'Peak', bars: 16, energy: 0.6, instruments: ['pad', 'piano', 'granular', 'bass drone'], description: 'Richest harmonic content, widest stereo' },
|
|
273
|
+
{ name: 'Dissolution', bars: 16, energy: 0.35, instruments: ['pad', 'drone'], description: 'Elements fade, space opens up' },
|
|
274
|
+
{ name: 'Coda', bars: 8, energy: 0.05, instruments: ['drone'], description: 'Return to near-silence, single texture' },
|
|
275
|
+
],
|
|
276
|
+
};
|
|
277
|
+
// Default structure for genres not explicitly defined
|
|
278
|
+
const defaultStructure = [
|
|
279
|
+
{ name: 'Intro', bars: 4, energy: 0.25, instruments: ['melody', 'pads'], description: 'Establish mood and key' },
|
|
280
|
+
{ name: 'Verse 1', bars: 8, energy: 0.5, instruments: ['drums', 'bass', 'melody'], description: 'Main groove, primary melody' },
|
|
281
|
+
{ name: 'Chorus/Hook', bars: 8, energy: 0.85, instruments: ['drums', 'bass', 'melody', 'chords', 'perc'], description: 'Full energy, hook section' },
|
|
282
|
+
{ name: 'Verse 2', bars: 8, energy: 0.55, instruments: ['drums', 'bass', 'melody', 'perc'], description: 'Variation with added elements' },
|
|
283
|
+
{ name: 'Chorus/Hook 2', bars: 8, energy: 0.9, instruments: ['drums', 'bass', 'melody', 'chords', 'perc', 'fx'], description: 'Bigger hook with layers' },
|
|
284
|
+
{ name: 'Bridge', bars: 4, energy: 0.35, instruments: ['pads', 'melody'], description: 'Contrast section, stripped back' },
|
|
285
|
+
{ name: 'Final Hook', bars: 8, energy: 1.0, instruments: ['drums', 'bass', 'melody', 'chords', 'perc', 'fx'], description: 'Maximum energy climax' },
|
|
286
|
+
{ name: 'Outro', bars: 4, energy: 0.15, instruments: ['melody', 'pads'], description: 'Wind down, elements exit' },
|
|
287
|
+
];
|
|
288
|
+
const sections = structures[genre] || defaultStructure;
|
|
289
|
+
// Scale bar counts by tempo factor
|
|
290
|
+
const scaled = sections.map(s => ({
|
|
291
|
+
...s,
|
|
292
|
+
bars: Math.max(2, Math.round(s.bars * tempoFactor)),
|
|
293
|
+
}));
|
|
294
|
+
const totalBars = scaled.reduce((sum, s) => sum + s.bars, 0);
|
|
295
|
+
return { sections: scaled, totalBars };
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Generate a melodic pattern using music theory rules.
|
|
299
|
+
* Uses scale-constrained note selection with genre-appropriate rhythmic patterns.
|
|
300
|
+
*/
|
|
301
|
+
function generateMelodyFromTheory(config) {
|
|
302
|
+
const { key, scale, bars, octave, genre, density } = config;
|
|
303
|
+
const scaleData = getScaleNotes(key, scale, octave);
|
|
304
|
+
const scaleMidi = scaleData.midi;
|
|
305
|
+
// Add upper octave notes for melodic range
|
|
306
|
+
const extendedScale = [...scaleMidi, ...scaleMidi.map(n => n + 12)];
|
|
307
|
+
const notes = [];
|
|
308
|
+
const beatsPerBar = 4;
|
|
309
|
+
const totalBeats = bars * beatsPerBar;
|
|
310
|
+
// Genre-specific rhythm selection
|
|
311
|
+
let rhythmKey;
|
|
312
|
+
if (genre === 'trap' || genre === 'drill' || genre === 'phonk') {
|
|
313
|
+
rhythmKey = density === 'dense' ? 'sixteenth' : density === 'moderate' ? 'eighth' : 'quarter';
|
|
314
|
+
}
|
|
315
|
+
else if (genre === 'lofi' || genre === 'jazz' || genre === 'rnb') {
|
|
316
|
+
rhythmKey = density === 'dense' ? 'swing' : density === 'moderate' ? 'eighth' : 'dotted_quarter';
|
|
317
|
+
}
|
|
318
|
+
else if (genre === 'ambient') {
|
|
319
|
+
rhythmKey = density === 'dense' ? 'quarter' : 'whole';
|
|
320
|
+
}
|
|
321
|
+
else if (genre === 'house' || genre === 'techno') {
|
|
322
|
+
rhythmKey = density === 'dense' ? 'sixteenth' : 'eighth';
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
rhythmKey = density === 'dense' ? 'eighth' : 'quarter';
|
|
326
|
+
}
|
|
327
|
+
const rhythmPositions = RHYTHM_PATTERNS[rhythmKey] || RHYTHM_PATTERNS.quarter;
|
|
328
|
+
// Walk through bars generating notes
|
|
329
|
+
let prevScaleIdx = Math.floor(scaleMidi.length / 2); // Start near middle of scale
|
|
330
|
+
const scaleLen = extendedScale.length;
|
|
331
|
+
for (let bar = 0; bar < bars; bar++) {
|
|
332
|
+
const barStart = bar * beatsPerBar;
|
|
333
|
+
for (const pos of rhythmPositions) {
|
|
334
|
+
const beat = barStart + pos;
|
|
335
|
+
if (beat >= totalBeats)
|
|
336
|
+
break;
|
|
337
|
+
// Probability of placing a note (rest probability)
|
|
338
|
+
const restChance = genre === 'ambient' ? 0.5 : genre === 'lofi' ? 0.25 : 0.15;
|
|
339
|
+
if (Math.random() < restChance)
|
|
340
|
+
continue;
|
|
341
|
+
// Step-wise motion with occasional leaps (good melody writing)
|
|
342
|
+
const stepSize = Math.random() < 0.7
|
|
343
|
+
? (Math.random() < 0.5 ? 1 : -1) // step
|
|
344
|
+
: (Math.random() < 0.5 ? randomInRange(2, 4) : randomInRange(-4, -2)); // leap
|
|
345
|
+
prevScaleIdx = clamp(prevScaleIdx + stepSize, 0, scaleLen - 1);
|
|
346
|
+
const pitch = extendedScale[prevScaleIdx];
|
|
347
|
+
// Velocity shaping
|
|
348
|
+
const isDownbeat = pos === 0;
|
|
349
|
+
const isBackbeat = pos === 1 || pos === 3;
|
|
350
|
+
let velocity = randomInRange(60, 90);
|
|
351
|
+
if (isDownbeat)
|
|
352
|
+
velocity = randomInRange(85, 110);
|
|
353
|
+
else if (isBackbeat)
|
|
354
|
+
velocity = randomInRange(70, 95);
|
|
355
|
+
velocity = clamp(velocity, 1, 127);
|
|
356
|
+
// Note duration varies by density
|
|
357
|
+
const maxDur = density === 'sparse' ? 2.0 : density === 'moderate' ? 1.0 : 0.5;
|
|
358
|
+
const minDur = density === 'sparse' ? 0.5 : density === 'moderate' ? 0.25 : 0.125;
|
|
359
|
+
const duration = minDur + Math.random() * (maxDur - minDur);
|
|
360
|
+
notes.push({ pitch, start: beat, duration, velocity });
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
// Quantize all notes to scale (safety)
|
|
364
|
+
return notes.map(n => ({
|
|
365
|
+
...n,
|
|
366
|
+
pitch: quantizeToScale(n.pitch, key, scale),
|
|
367
|
+
}));
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Generate a drum pattern as MidiNote[].
|
|
371
|
+
* Uses GENRE_DRUM_PATTERNS as a base, then applies feel/humanize/density.
|
|
372
|
+
*/
|
|
373
|
+
function generateDrumPattern(config) {
|
|
374
|
+
const { genre, bars, feel, humanize, density } = config;
|
|
375
|
+
const pattern = GENRE_DRUM_PATTERNS[genre] || GENRE_DRUM_PATTERNS.hiphop;
|
|
376
|
+
const notes = [];
|
|
377
|
+
for (let bar = 0; bar < bars; bar++) {
|
|
378
|
+
const barStart = bar * 4; // 4 beats per bar
|
|
379
|
+
for (const [instrument, positions] of Object.entries(pattern.pattern)) {
|
|
380
|
+
const midiNote = GM_DRUMS[instrument];
|
|
381
|
+
if (!midiNote)
|
|
382
|
+
continue;
|
|
383
|
+
for (const pos16th of positions) {
|
|
384
|
+
// Density filter: randomly skip hits based on density parameter
|
|
385
|
+
// Low density = more skips, high density = fewer skips
|
|
386
|
+
if (density < 1.0 && Math.random() > density + 0.3)
|
|
387
|
+
continue;
|
|
388
|
+
// Convert 16th note position to beat position
|
|
389
|
+
let beatPos = pos16th / 4;
|
|
390
|
+
// Apply feel
|
|
391
|
+
if (feel === 'swing' || feel === 'triplet') {
|
|
392
|
+
// Swing the even 16th notes (the "e" and "a")
|
|
393
|
+
const sub = pos16th % 4;
|
|
394
|
+
if (sub === 1 || sub === 3) {
|
|
395
|
+
beatPos = (pos16th - sub) / 4 + (sub === 1 ? 2 / 3 : 3.5 / 4);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
const start = barStart + beatPos;
|
|
399
|
+
// Humanize: slight timing and velocity variation
|
|
400
|
+
const timingOffset = humanize ? (Math.random() - 0.5) * 0.02 : 0;
|
|
401
|
+
const velocityBase = instrument === 'kick' || instrument === 'snare' || instrument === 'clap'
|
|
402
|
+
? randomInRange(90, 120)
|
|
403
|
+
: instrument.includes('hihat') || instrument.includes('closed')
|
|
404
|
+
? randomInRange(60, 100)
|
|
405
|
+
: randomInRange(70, 105);
|
|
406
|
+
const velocityOffset = humanize ? randomInRange(-10, 10) : 0;
|
|
407
|
+
// Hi-hat velocity patterns for trap/drill: accent rolls
|
|
408
|
+
let velocity = clamp(velocityBase + velocityOffset, 1, 127);
|
|
409
|
+
if ((genre === 'trap' || genre === 'drill' || genre === 'phonk') &&
|
|
410
|
+
(instrument === 'closed_hihat') && humanize) {
|
|
411
|
+
// Create a rolling accent pattern
|
|
412
|
+
const accent = (pos16th % 4 === 0) ? 20 : (pos16th % 2 === 0) ? 10 : 0;
|
|
413
|
+
velocity = clamp(velocity + accent, 1, 127);
|
|
414
|
+
}
|
|
415
|
+
notes.push({
|
|
416
|
+
pitch: midiNote,
|
|
417
|
+
start: start + timingOffset,
|
|
418
|
+
duration: 0.1, // drums are short
|
|
419
|
+
velocity,
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
// Sort by start time
|
|
425
|
+
return notes.sort((a, b) => a.start - b.start);
|
|
426
|
+
}
|
|
427
|
+
// ── Tool Registration ──────────────────────────────────────────────────────
|
|
428
|
+
export function registerMusicGenTools() {
|
|
429
|
+
// ─── 1. Generate Lyrics ─────────────────────────────────────────────
|
|
430
|
+
registerTool({
|
|
431
|
+
name: 'generate_lyrics',
|
|
432
|
+
description: '[Music Gen] Generate song lyrics using local AI (Ollama, $0 cost). Produces verse/chorus/bridge structure with genre-appropriate language and flow. Output is plain text with section markers.',
|
|
433
|
+
parameters: {
|
|
434
|
+
genre: {
|
|
435
|
+
type: 'string',
|
|
436
|
+
description: 'Music genre (e.g., "trap", "pop", "rnb", "rock", "country", "jazz", "indie", "reggaeton")',
|
|
437
|
+
required: true,
|
|
438
|
+
},
|
|
439
|
+
mood: {
|
|
440
|
+
type: 'string',
|
|
441
|
+
description: 'Emotional mood (e.g., "melancholic", "hype", "romantic", "aggressive", "dreamy", "uplifting")',
|
|
442
|
+
required: true,
|
|
443
|
+
},
|
|
444
|
+
topic: {
|
|
445
|
+
type: 'string',
|
|
446
|
+
description: 'What the song is about (e.g., "late night drives", "heartbreak", "grinding for success", "summer love")',
|
|
447
|
+
required: true,
|
|
448
|
+
},
|
|
449
|
+
structure: {
|
|
450
|
+
type: 'string',
|
|
451
|
+
description: 'Song structure — comma-separated sections. Default: "verse,chorus,verse,chorus,bridge,chorus". Other options: "verse,chorus", "intro,verse,hook,verse,hook,outro"',
|
|
452
|
+
},
|
|
453
|
+
style_reference: {
|
|
454
|
+
type: 'string',
|
|
455
|
+
description: 'Optional style reference — artist or song to channel (e.g., "Drake", "Billie Eilish", "Frank Ocean"). The AI uses this for tone/cadence, not copying.',
|
|
456
|
+
},
|
|
457
|
+
},
|
|
458
|
+
tier: 'free',
|
|
459
|
+
timeout: 120_000,
|
|
460
|
+
async execute(args) {
|
|
461
|
+
const genre = String(args.genre);
|
|
462
|
+
const mood = String(args.mood);
|
|
463
|
+
const topic = String(args.topic);
|
|
464
|
+
const structure = String(args.structure || 'verse,chorus,verse,chorus,bridge,chorus');
|
|
465
|
+
const styleRef = args.style_reference ? ` Channel the writing style/cadence of ${args.style_reference} (tone only, not copying lyrics).` : '';
|
|
466
|
+
const sections = structure.split(',').map(s => s.trim());
|
|
467
|
+
const prompt = `You are a professional songwriter. Write ${genre} song lyrics about "${topic}" with a ${mood} mood.${styleRef}
|
|
468
|
+
|
|
469
|
+
Structure: ${sections.map(s => `[${s.toUpperCase()}]`).join(' -> ')}
|
|
470
|
+
|
|
471
|
+
Rules:
|
|
472
|
+
- Each section should be 4-8 lines
|
|
473
|
+
- Use genre-appropriate language, cadence, and syllable patterns for ${genre}
|
|
474
|
+
- The chorus should be catchy and memorable — the emotional peak
|
|
475
|
+
- The bridge should offer contrast (new perspective, key change feel, or stripped-back moment)
|
|
476
|
+
- Include natural rhythm and internal rhyme schemes
|
|
477
|
+
- Mark each section with [SECTION_NAME] headers
|
|
478
|
+
- Do NOT include any explanation — only output the lyrics
|
|
479
|
+
|
|
480
|
+
Write the lyrics now:`;
|
|
481
|
+
const lyrics = await ollamaGenerate(prompt, { temperature: 0.85, maxTokens: 1500 });
|
|
482
|
+
if (lyrics.startsWith('[Ollama')) {
|
|
483
|
+
return `Error: ${lyrics}\n\nTo use generate_lyrics, ensure Ollama is running with kernel:latest:\n ollama serve\n ollama pull kernel:latest`;
|
|
484
|
+
}
|
|
485
|
+
return `# Generated Lyrics\n\n**Genre:** ${genre} | **Mood:** ${mood} | **Topic:** ${topic}\n**Structure:** ${sections.join(' -> ')}${styleRef ? `\n**Style ref:** ${args.style_reference}` : ''}\n\n---\n\n${lyrics}`;
|
|
486
|
+
},
|
|
487
|
+
});
|
|
488
|
+
// ─── 2. Generate Melody Pattern ─────────────────────────────────────
|
|
489
|
+
registerTool({
|
|
490
|
+
name: 'generate_melody_pattern',
|
|
491
|
+
description: '[Music Gen] Generate a MIDI melody pattern using music theory + local AI for creative decisions. Output is a JSON array of {note, midi, velocity, start_beat, duration} compatible with kbot\'s ableton_midi tool. Uses scale quantization and genre-appropriate rhythm.',
|
|
492
|
+
parameters: {
|
|
493
|
+
key: {
|
|
494
|
+
type: 'string',
|
|
495
|
+
description: 'Musical key (e.g., "Cm", "F#m", "Bb", "Em"). Append "m" for minor.',
|
|
496
|
+
required: true,
|
|
497
|
+
},
|
|
498
|
+
scale: {
|
|
499
|
+
type: 'string',
|
|
500
|
+
description: `Scale type. Default: auto-detected from key. Options: ${Object.keys(SCALES).join(', ')}`,
|
|
501
|
+
},
|
|
502
|
+
bpm: {
|
|
503
|
+
type: 'number',
|
|
504
|
+
description: 'Tempo in BPM (20-300). Default: 120.',
|
|
505
|
+
},
|
|
506
|
+
genre: {
|
|
507
|
+
type: 'string',
|
|
508
|
+
description: `Genre for rhythm/feel selection. Options: ${Object.keys(GENRE_PROFILES).join(', ')}. Default: "pop".`,
|
|
509
|
+
},
|
|
510
|
+
bars: {
|
|
511
|
+
type: 'number',
|
|
512
|
+
description: 'Number of bars to generate (1-16). Default: 4.',
|
|
513
|
+
},
|
|
514
|
+
octave: {
|
|
515
|
+
type: 'number',
|
|
516
|
+
description: 'Starting octave (2-6). Default: 4 (middle C range).',
|
|
517
|
+
},
|
|
518
|
+
density: {
|
|
519
|
+
type: 'string',
|
|
520
|
+
description: 'Note density: "sparse" (whole/half notes), "moderate" (quarter/eighth), "dense" (sixteenth). Default: "moderate".',
|
|
521
|
+
},
|
|
522
|
+
creative_prompt: {
|
|
523
|
+
type: 'string',
|
|
524
|
+
description: 'Optional: describe the melody character for AI-assisted note choices (e.g., "ascending hopeful melody", "dark descending phrase"). If provided, Ollama shapes the melodic contour.',
|
|
525
|
+
},
|
|
526
|
+
},
|
|
527
|
+
tier: 'free',
|
|
528
|
+
timeout: 60_000,
|
|
529
|
+
async execute(args) {
|
|
530
|
+
const keyStr = String(args.key || 'Cm');
|
|
531
|
+
const { root, scale: autoScale } = parseKeyString(keyStr);
|
|
532
|
+
const scale = String(args.scale || autoScale);
|
|
533
|
+
const bpm = clamp(Number(args.bpm) || 120, 20, 300);
|
|
534
|
+
const genre = String(args.genre || 'pop');
|
|
535
|
+
const bars = clamp(Number(args.bars) || 4, 1, 16);
|
|
536
|
+
const octave = clamp(Number(args.octave) || 4, 2, 6);
|
|
537
|
+
const density = (String(args.density || 'moderate'));
|
|
538
|
+
const creativePrompt = args.creative_prompt ? String(args.creative_prompt) : null;
|
|
539
|
+
if (!SCALES[scale]) {
|
|
540
|
+
return `Error: Unknown scale "${scale}". Available: ${Object.keys(SCALES).join(', ')}`;
|
|
541
|
+
}
|
|
542
|
+
let melodyNotes;
|
|
543
|
+
if (creativePrompt) {
|
|
544
|
+
// Use Ollama to get a melodic contour suggestion, then realize it with music theory
|
|
545
|
+
const scaleNotes = getScaleNotes(root, scale, octave);
|
|
546
|
+
const aiPrompt = `You are a music theory expert. Given this request:
|
|
547
|
+
Genre: ${genre}, Key: ${root} ${scale}, BPM: ${bpm}
|
|
548
|
+
Creative direction: "${creativePrompt}"
|
|
549
|
+
Available notes: ${scaleNotes.names.join(', ')} (octave ${octave}) and ${scaleNotes.names.map(n => n.replace(/\d/, String(octave + 1))).join(', ')} (octave ${octave + 1})
|
|
550
|
+
|
|
551
|
+
Generate a ${bars}-bar melody as a JSON array. Each element: {"note": "C4", "velocity": 80, "start_beat": 0.0, "duration": 0.5}
|
|
552
|
+
- start_beat is absolute position (0 = bar 1 beat 1, 4 = bar 2 beat 1, etc.)
|
|
553
|
+
- Total beats: ${bars * 4}
|
|
554
|
+
- Use genre-appropriate rhythm for ${genre}
|
|
555
|
+
- velocity: 1-127 (louder on downbeats)
|
|
556
|
+
- Only use notes from the ${root} ${scale} scale
|
|
557
|
+
- ${density} density: ${density === 'sparse' ? '8-16 notes' : density === 'moderate' ? '16-32 notes' : '32-64 notes'} total
|
|
558
|
+
|
|
559
|
+
Output ONLY the JSON array, no explanation:`;
|
|
560
|
+
const aiResult = await ollamaGenerateJSON(aiPrompt, { temperature: 0.7, maxTokens: 2048 });
|
|
561
|
+
if (aiResult && Array.isArray(aiResult) && aiResult.length > 0) {
|
|
562
|
+
// Convert AI output to MidiNote, quantizing to scale for safety
|
|
563
|
+
melodyNotes = aiResult
|
|
564
|
+
.filter(n => n.note && typeof n.start_beat === 'number')
|
|
565
|
+
.map(n => {
|
|
566
|
+
let pitch;
|
|
567
|
+
try {
|
|
568
|
+
pitch = noteNameToMidi(n.note);
|
|
569
|
+
}
|
|
570
|
+
catch {
|
|
571
|
+
pitch = 60;
|
|
572
|
+
}
|
|
573
|
+
pitch = quantizeToScale(pitch, root, scale);
|
|
574
|
+
return {
|
|
575
|
+
pitch,
|
|
576
|
+
start: Math.max(0, n.start_beat),
|
|
577
|
+
duration: Math.max(0.0625, n.duration || 0.5),
|
|
578
|
+
velocity: clamp(n.velocity || 80, 1, 127),
|
|
579
|
+
};
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
else {
|
|
583
|
+
// Fallback to theory-based generation
|
|
584
|
+
melodyNotes = generateMelodyFromTheory({ key: root, scale, bpm, bars, octave, genre, density });
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
else {
|
|
588
|
+
melodyNotes = generateMelodyFromTheory({ key: root, scale, bpm, bars, octave, genre, density });
|
|
589
|
+
}
|
|
590
|
+
const formatted = formatMidiNotes(melodyNotes);
|
|
591
|
+
const scaleInfo = getScaleNotes(root, scale, octave);
|
|
592
|
+
return [
|
|
593
|
+
`# Melody Pattern`,
|
|
594
|
+
``,
|
|
595
|
+
`**Key:** ${root} ${scale} | **BPM:** ${bpm} | **Genre:** ${genre}`,
|
|
596
|
+
`**Bars:** ${bars} | **Octave:** ${octave} | **Density:** ${density}`,
|
|
597
|
+
`**Scale notes:** ${scaleInfo.names.join(', ')}`,
|
|
598
|
+
`**Notes generated:** ${melodyNotes.length}`,
|
|
599
|
+
creativePrompt ? `**Creative direction:** ${creativePrompt}` : '',
|
|
600
|
+
``,
|
|
601
|
+
`## MIDI Data (compatible with ableton_midi)`,
|
|
602
|
+
``,
|
|
603
|
+
'```json',
|
|
604
|
+
formatted,
|
|
605
|
+
'```',
|
|
606
|
+
``,
|
|
607
|
+
`## Usage`,
|
|
608
|
+
`Pipe this to ableton_midi to write directly to a clip in Ableton Live.`,
|
|
609
|
+
].filter(Boolean).join('\n');
|
|
610
|
+
},
|
|
611
|
+
});
|
|
612
|
+
// ─── 3. Generate Drum Pattern ───────────────────────────────────────
|
|
613
|
+
registerTool({
|
|
614
|
+
name: 'generate_drum_pattern',
|
|
615
|
+
description: `[Music Gen] Generate a genre-specific drum pattern as MIDI-compatible JSON. Includes genre defaults (trap: 808 kick, clap on 2&4, hi-hat rolls; house: four-on-floor, open hats on &; drill: sliding 808, syncopated kicks). Output is compatible with kbot's ableton_midi tool. Available genres: ${availableGenres().join(', ')}.`,
|
|
616
|
+
parameters: {
|
|
617
|
+
genre: {
|
|
618
|
+
type: 'string',
|
|
619
|
+
description: `Drum genre/style. Options: ${availableGenres().join(', ')}. Default: "trap".`,
|
|
620
|
+
required: true,
|
|
621
|
+
},
|
|
622
|
+
bpm: {
|
|
623
|
+
type: 'number',
|
|
624
|
+
description: 'Tempo in BPM. Default: auto from genre.',
|
|
625
|
+
},
|
|
626
|
+
bars: {
|
|
627
|
+
type: 'number',
|
|
628
|
+
description: 'Number of bars to generate (1-16). Default: 4.',
|
|
629
|
+
},
|
|
630
|
+
feel: {
|
|
631
|
+
type: 'string',
|
|
632
|
+
description: 'Rhythmic feel: "straight" (default), "swing", "triplet".',
|
|
633
|
+
},
|
|
634
|
+
humanize: {
|
|
635
|
+
type: 'string',
|
|
636
|
+
description: 'Humanize timing and velocity: "true" or "false". Default: "true".',
|
|
637
|
+
},
|
|
638
|
+
density: {
|
|
639
|
+
type: 'number',
|
|
640
|
+
description: 'Pattern density 0.0 (minimal) to 1.0 (full). Default: 0.8. Lower values randomly omit hits for sparser grooves.',
|
|
641
|
+
},
|
|
642
|
+
variation: {
|
|
643
|
+
type: 'string',
|
|
644
|
+
description: 'If "true", add subtle variations each bar (fills, ghost notes, hat opens). Default: "true".',
|
|
645
|
+
},
|
|
646
|
+
},
|
|
647
|
+
tier: 'free',
|
|
648
|
+
timeout: 10_000,
|
|
649
|
+
async execute(args) {
|
|
650
|
+
const genre = String(args.genre || 'trap').toLowerCase();
|
|
651
|
+
const pattern = GENRE_DRUM_PATTERNS[genre];
|
|
652
|
+
const genreProfile = GENRE_PROFILES[genre];
|
|
653
|
+
if (!pattern) {
|
|
654
|
+
return `Error: Unknown genre "${genre}". Available genres: ${availableGenres().join(', ')}`;
|
|
655
|
+
}
|
|
656
|
+
const bpmRange = pattern.bpm;
|
|
657
|
+
const bpm = clamp(Number(args.bpm) || randomInRange(bpmRange[0], bpmRange[1]), 20, 300);
|
|
658
|
+
const bars = clamp(Number(args.bars) || 4, 1, 16);
|
|
659
|
+
const feel = (String(args.feel || genreProfile?.feel || 'straight'));
|
|
660
|
+
const humanize = String(args.humanize ?? 'true') !== 'false';
|
|
661
|
+
const density = clamp(Number(args.density ?? 0.8), 0.0, 1.0);
|
|
662
|
+
const variation = String(args.variation ?? 'true') !== 'false';
|
|
663
|
+
let notes = generateDrumPattern({ genre, bpm, bars, feel, humanize, density });
|
|
664
|
+
// Add variations if requested
|
|
665
|
+
if (variation && bars > 1) {
|
|
666
|
+
const additionalNotes = [];
|
|
667
|
+
for (let bar = 1; bar < bars; bar++) {
|
|
668
|
+
const barStart = bar * 4;
|
|
669
|
+
// Occasional ghost snare
|
|
670
|
+
if (Math.random() < 0.3) {
|
|
671
|
+
const ghostPos = barStart + pick([0.75, 1.75, 2.75, 3.75]);
|
|
672
|
+
additionalNotes.push({
|
|
673
|
+
pitch: GM_DRUMS.snare,
|
|
674
|
+
start: ghostPos,
|
|
675
|
+
duration: 0.1,
|
|
676
|
+
velocity: randomInRange(25, 45), // very quiet ghost
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
// Fill on last bar before section change
|
|
680
|
+
if (bar === bars - 1 && Math.random() < 0.6) {
|
|
681
|
+
// Snare roll fill on beat 4
|
|
682
|
+
for (let i = 0; i < 4; i++) {
|
|
683
|
+
additionalNotes.push({
|
|
684
|
+
pitch: GM_DRUMS.snare,
|
|
685
|
+
start: barStart + 3 + (i * 0.25),
|
|
686
|
+
duration: 0.1,
|
|
687
|
+
velocity: randomInRange(70 + i * 10, 90 + i * 10),
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
// Occasional open hat replacing closed hat
|
|
692
|
+
if (Math.random() < 0.2) {
|
|
693
|
+
const openPos = barStart + pick([1.5, 3.5]);
|
|
694
|
+
additionalNotes.push({
|
|
695
|
+
pitch: GM_DRUMS.open_hihat,
|
|
696
|
+
start: openPos,
|
|
697
|
+
duration: 0.25,
|
|
698
|
+
velocity: randomInRange(70, 95),
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
notes = [...notes, ...additionalNotes].sort((a, b) => a.start - b.start);
|
|
703
|
+
}
|
|
704
|
+
// Map MIDI numbers to drum names for readable output
|
|
705
|
+
const reverseDrumMap = new Map();
|
|
706
|
+
for (const [name, midi] of Object.entries(GM_DRUMS)) {
|
|
707
|
+
if (!reverseDrumMap.has(midi))
|
|
708
|
+
reverseDrumMap.set(midi, name);
|
|
709
|
+
}
|
|
710
|
+
const formatted = JSON.stringify(notes.map(n => ({
|
|
711
|
+
drum: reverseDrumMap.get(n.pitch) || `midi_${n.pitch}`,
|
|
712
|
+
midi: n.pitch,
|
|
713
|
+
velocity: n.velocity,
|
|
714
|
+
start_beat: Math.round(n.start * 1000) / 1000,
|
|
715
|
+
duration: Math.round(n.duration * 1000) / 1000,
|
|
716
|
+
})), null, 2);
|
|
717
|
+
const patternDesc = Object.entries(pattern.pattern)
|
|
718
|
+
.map(([inst, positions]) => ` ${inst}: hits on 16th positions [${positions.join(', ')}]`)
|
|
719
|
+
.join('\n');
|
|
720
|
+
return [
|
|
721
|
+
`# Drum Pattern: ${genre}`,
|
|
722
|
+
``,
|
|
723
|
+
`**BPM:** ${bpm} | **Bars:** ${bars} | **Feel:** ${feel}`,
|
|
724
|
+
`**Humanize:** ${humanize} | **Density:** ${density} | **Variation:** ${variation}`,
|
|
725
|
+
`**Total hits:** ${notes.length}`,
|
|
726
|
+
``,
|
|
727
|
+
`## Base Pattern (per bar)`,
|
|
728
|
+
patternDesc,
|
|
729
|
+
``,
|
|
730
|
+
`## MIDI Data (compatible with ableton_midi)`,
|
|
731
|
+
``,
|
|
732
|
+
'```json',
|
|
733
|
+
formatted,
|
|
734
|
+
'```',
|
|
735
|
+
``,
|
|
736
|
+
genreProfile ? `## Genre Notes\n${genreProfile.description}` : '',
|
|
737
|
+
``,
|
|
738
|
+
`## Usage`,
|
|
739
|
+
`Pipe to ableton_midi to write to a drum rack clip in Ableton Live.`,
|
|
740
|
+
].filter(Boolean).join('\n');
|
|
741
|
+
},
|
|
742
|
+
});
|
|
743
|
+
// ─── 4. Generate Song Structure ─────────────────────────────────────
|
|
744
|
+
registerTool({
|
|
745
|
+
name: 'generate_song_structure',
|
|
746
|
+
description: '[Music Gen] Generate a complete song structure with section plan, bar counts, energy curve, and instrument suggestions based on genre conventions. Useful for planning a full production before writing any notes.',
|
|
747
|
+
parameters: {
|
|
748
|
+
genre: {
|
|
749
|
+
type: 'string',
|
|
750
|
+
description: `Genre for structure conventions. Options: ${Object.keys(GENRE_PROFILES).join(', ')}. Default: "trap".`,
|
|
751
|
+
required: true,
|
|
752
|
+
},
|
|
753
|
+
bpm: {
|
|
754
|
+
type: 'number',
|
|
755
|
+
description: 'Tempo in BPM. Default: auto from genre.',
|
|
756
|
+
},
|
|
757
|
+
key: {
|
|
758
|
+
type: 'string',
|
|
759
|
+
description: 'Musical key (e.g., "Cm", "F#m", "Bb"). Default: auto from genre.',
|
|
760
|
+
},
|
|
761
|
+
progression: {
|
|
762
|
+
type: 'string',
|
|
763
|
+
description: 'Chord progression as Roman numerals (e.g., "I V vi IV") or named preset. If omitted, a genre-appropriate progression is chosen.',
|
|
764
|
+
},
|
|
765
|
+
target_minutes: {
|
|
766
|
+
type: 'number',
|
|
767
|
+
description: 'Target song length in minutes. Default: 3.5.',
|
|
768
|
+
},
|
|
769
|
+
},
|
|
770
|
+
tier: 'free',
|
|
771
|
+
timeout: 30_000,
|
|
772
|
+
async execute(args) {
|
|
773
|
+
const genre = String(args.genre || 'trap').toLowerCase();
|
|
774
|
+
const profile = GENRE_PROFILES[genre];
|
|
775
|
+
if (!profile) {
|
|
776
|
+
return `Error: Unknown genre "${genre}". Available: ${Object.keys(GENRE_PROFILES).join(', ')}`;
|
|
777
|
+
}
|
|
778
|
+
const bpm = clamp(Number(args.bpm) || randomInRange(profile.bpmRange[0], profile.bpmRange[1]), 20, 300);
|
|
779
|
+
const keyStr = String(args.key || pick(profile.preferredKeys));
|
|
780
|
+
const { root, scale } = parseKeyString(keyStr);
|
|
781
|
+
const targetMinutes = clamp(Number(args.target_minutes) || 3.5, 1, 10);
|
|
782
|
+
// Calculate bars from target duration
|
|
783
|
+
const barsPerMinute = bpm / 4; // 4 beats per bar in 4/4
|
|
784
|
+
const targetBars = Math.round(targetMinutes * barsPerMinute);
|
|
785
|
+
// Select chord progression
|
|
786
|
+
let progressionStr;
|
|
787
|
+
if (args.progression) {
|
|
788
|
+
const namedProg = NAMED_PROGRESSIONS[String(args.progression).toLowerCase()];
|
|
789
|
+
progressionStr = namedProg ? namedProg.numerals : String(args.progression);
|
|
790
|
+
}
|
|
791
|
+
else {
|
|
792
|
+
progressionStr = pick(profile.progressions);
|
|
793
|
+
}
|
|
794
|
+
// Parse the progression to verify it works
|
|
795
|
+
let chordNames;
|
|
796
|
+
try {
|
|
797
|
+
const parsedChords = parseProgression(progressionStr, root, scale);
|
|
798
|
+
chordNames = parsedChords.map(chord => chord.map(n => midiToNoteName(n)).join('/'));
|
|
799
|
+
}
|
|
800
|
+
catch {
|
|
801
|
+
chordNames = progressionStr.split(/\s+/);
|
|
802
|
+
}
|
|
803
|
+
// Get song structure
|
|
804
|
+
const structure = getStructureForGenre(genre, bpm);
|
|
805
|
+
// Scale to target bars
|
|
806
|
+
const currentTotal = structure.totalBars;
|
|
807
|
+
const scaleFactor = targetBars / currentTotal;
|
|
808
|
+
const scaledSections = structure.sections.map(s => ({
|
|
809
|
+
...s,
|
|
810
|
+
bars: Math.max(2, Math.round(s.bars * scaleFactor)),
|
|
811
|
+
}));
|
|
812
|
+
const actualBars = scaledSections.reduce((sum, s) => sum + s.bars, 0);
|
|
813
|
+
const actualMinutes = Math.round((actualBars / barsPerMinute) * 10) / 10;
|
|
814
|
+
// Get scale info
|
|
815
|
+
const scaleInfo = getScaleNotes(root, scale);
|
|
816
|
+
// Energy curve as ASCII art
|
|
817
|
+
const maxWidth = 30;
|
|
818
|
+
const energyCurve = scaledSections.map(s => {
|
|
819
|
+
const barWidth = Math.max(1, Math.round(s.energy * maxWidth));
|
|
820
|
+
const bar = '#'.repeat(barWidth) + '.'.repeat(maxWidth - barWidth);
|
|
821
|
+
return ` ${s.name.padEnd(16)} |${bar}| ${Math.round(s.energy * 100)}%`;
|
|
822
|
+
}).join('\n');
|
|
823
|
+
// Suggested instruments
|
|
824
|
+
const instrumentPalette = profile.instruments.join(', ');
|
|
825
|
+
// Available progressions for this genre
|
|
826
|
+
const suggestedProgs = profile.progressions.map((p, i) => ` ${i + 1}. ${p}`).join('\n');
|
|
827
|
+
return [
|
|
828
|
+
`# Song Structure: ${genre.toUpperCase()}`,
|
|
829
|
+
``,
|
|
830
|
+
`**Key:** ${root} ${scale} | **BPM:** ${bpm} | **Duration:** ~${actualMinutes} min (${actualBars} bars)`,
|
|
831
|
+
`**Chord Progression:** ${progressionStr}`,
|
|
832
|
+
`**Chords (MIDI):** ${chordNames.join(' | ')}`,
|
|
833
|
+
`**Scale:** ${scaleInfo.names.join(', ')}`,
|
|
834
|
+
``,
|
|
835
|
+
`## Sections`,
|
|
836
|
+
``,
|
|
837
|
+
scaledSections.map((s, i) => [
|
|
838
|
+
`### ${i + 1}. ${s.name} (${s.bars} bars)`,
|
|
839
|
+
`**Energy:** ${Math.round(s.energy * 100)}% | **Instruments:** ${s.instruments.join(', ')}`,
|
|
840
|
+
`${s.description}`,
|
|
841
|
+
].join('\n')).join('\n\n'),
|
|
842
|
+
``,
|
|
843
|
+
`## Energy Curve`,
|
|
844
|
+
``,
|
|
845
|
+
'```',
|
|
846
|
+
energyCurve,
|
|
847
|
+
'```',
|
|
848
|
+
``,
|
|
849
|
+
`## Instrument Palette`,
|
|
850
|
+
instrumentPalette,
|
|
851
|
+
``,
|
|
852
|
+
`## Alternative Progressions for ${genre}`,
|
|
853
|
+
suggestedProgs,
|
|
854
|
+
``,
|
|
855
|
+
`## Production Notes`,
|
|
856
|
+
profile.energyProfile,
|
|
857
|
+
``,
|
|
858
|
+
`## Next Steps`,
|
|
859
|
+
`1. \`generate_drum_pattern\` with genre="${genre}", bpm=${bpm}`,
|
|
860
|
+
`2. \`generate_melody_pattern\` with key="${keyStr}", genre="${genre}", bpm=${bpm}`,
|
|
861
|
+
`3. \`generate_lyrics\` with genre="${genre}" for vocal content`,
|
|
862
|
+
`4. Pipe MIDI data to \`ableton_midi\` to build the session`,
|
|
863
|
+
].join('\n');
|
|
864
|
+
},
|
|
865
|
+
});
|
|
866
|
+
// ─── 5. Music Idea ──────────────────────────────────────────────────
|
|
867
|
+
registerTool({
|
|
868
|
+
name: 'music_idea',
|
|
869
|
+
description: '[Music Gen] Creative idea generator — describe a vibe, reference track, or mood and get a complete production blueprint: BPM, key, genre, chord progression, instrument palette, drum pattern style, and melody approach. Uses local Ollama for creative reasoning ($0 cost). Perfect starting point for any production.',
|
|
870
|
+
parameters: {
|
|
871
|
+
prompt: {
|
|
872
|
+
type: 'string',
|
|
873
|
+
description: 'Describe the vibe, mood, reference track, or feeling (e.g., "late night drive through Tokyo", "something like Playboi Carti but with jazz chords", "aggressive dark energy, 808 heavy")',
|
|
874
|
+
required: true,
|
|
875
|
+
},
|
|
876
|
+
},
|
|
877
|
+
tier: 'free',
|
|
878
|
+
timeout: 120_000,
|
|
879
|
+
async execute(args) {
|
|
880
|
+
const userPrompt = String(args.prompt);
|
|
881
|
+
const availableProgressions = Object.entries(NAMED_PROGRESSIONS)
|
|
882
|
+
.slice(0, 20) // limit for prompt size
|
|
883
|
+
.map(([id, p]) => `${id}: "${p.numerals}" (${p.name})`)
|
|
884
|
+
.join('\n');
|
|
885
|
+
const genreList = Object.entries(GENRE_PROFILES)
|
|
886
|
+
.map(([id, p]) => `${id}: BPM ${p.bpmRange[0]}-${p.bpmRange[1]}, ${p.description}`)
|
|
887
|
+
.join('\n');
|
|
888
|
+
const aiPrompt = `You are a music producer and creative director. Given this creative brief, design a complete production blueprint.
|
|
889
|
+
|
|
890
|
+
Creative brief: "${userPrompt}"
|
|
891
|
+
|
|
892
|
+
Available genres:
|
|
893
|
+
${genreList}
|
|
894
|
+
|
|
895
|
+
Some named chord progressions:
|
|
896
|
+
${availableProgressions}
|
|
897
|
+
|
|
898
|
+
Respond in this EXACT JSON format (no markdown, no explanation):
|
|
899
|
+
{
|
|
900
|
+
"genre": "one of: ${Object.keys(GENRE_PROFILES).join(', ')}",
|
|
901
|
+
"bpm": 140,
|
|
902
|
+
"key": "Cm",
|
|
903
|
+
"scale": "natural_minor",
|
|
904
|
+
"mood": "dark, aggressive, nocturnal",
|
|
905
|
+
"chord_progression": "i bVI bVII i",
|
|
906
|
+
"progression_name": "name if using a named one, or 'custom'",
|
|
907
|
+
"instruments": ["808 bass", "dark pads", "bells", "hi-hats", "clap"],
|
|
908
|
+
"drum_style": "description of drum approach",
|
|
909
|
+
"melody_approach": "description of melodic strategy",
|
|
910
|
+
"production_notes": "2-3 sentences on overall production direction",
|
|
911
|
+
"reference_artists": ["Artist 1", "Artist 2"],
|
|
912
|
+
"energy_arc": "description of the song's energy journey"
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
Be specific and creative. Match the user's vibe exactly.`;
|
|
916
|
+
const aiResult = await ollamaGenerateJSON(aiPrompt, { temperature: 0.85, maxTokens: 1024 });
|
|
917
|
+
// Build the blueprint — use AI result if available, fall back to smart defaults
|
|
918
|
+
const genre = aiResult?.genre && GENRE_PROFILES[aiResult.genre] ? aiResult.genre : 'trap';
|
|
919
|
+
const profile = GENRE_PROFILES[genre];
|
|
920
|
+
const bpm = aiResult?.bpm ? clamp(aiResult.bpm, 20, 300) : randomInRange(profile.bpmRange[0], profile.bpmRange[1]);
|
|
921
|
+
const keyStr = aiResult?.key || pick(profile.preferredKeys);
|
|
922
|
+
const { root, scale: autoScale } = parseKeyString(keyStr);
|
|
923
|
+
const scale = aiResult?.scale && SCALES[aiResult.scale] ? aiResult.scale : autoScale;
|
|
924
|
+
const progression = aiResult?.chord_progression || pick(profile.progressions);
|
|
925
|
+
const instruments = aiResult?.instruments || profile.instruments;
|
|
926
|
+
const drumStyle = aiResult?.drum_style || profile.energyProfile;
|
|
927
|
+
const melodyApproach = aiResult?.melody_approach || 'Genre-appropriate melody following scale intervals';
|
|
928
|
+
const mood = aiResult?.mood || 'not specified';
|
|
929
|
+
const productionNotes = aiResult?.production_notes || profile.description;
|
|
930
|
+
const references = aiResult?.reference_artists || [];
|
|
931
|
+
const energyArc = aiResult?.energy_arc || 'Standard verse-chorus energy dynamics';
|
|
932
|
+
// Verify chord progression
|
|
933
|
+
let chordNotes = [];
|
|
934
|
+
try {
|
|
935
|
+
const parsed = parseProgression(progression, root, scale);
|
|
936
|
+
chordNotes = parsed.map(chord => chord.map(n => midiToNoteName(n)).join('/'));
|
|
937
|
+
}
|
|
938
|
+
catch {
|
|
939
|
+
chordNotes = progression.split(/\s+/);
|
|
940
|
+
}
|
|
941
|
+
// Get scale info
|
|
942
|
+
const scaleInfo = getScaleNotes(root, scale);
|
|
943
|
+
// Get drum pattern info
|
|
944
|
+
const drumPattern = GENRE_DRUM_PATTERNS[genre];
|
|
945
|
+
const drumInfo = drumPattern
|
|
946
|
+
? Object.entries(drumPattern.pattern).map(([inst, pos]) => ` ${inst}: [${pos.join(', ')}]`).join('\n')
|
|
947
|
+
: ' (custom pattern needed)';
|
|
948
|
+
return [
|
|
949
|
+
`# Music Idea Blueprint`,
|
|
950
|
+
``,
|
|
951
|
+
`> "${userPrompt}"`,
|
|
952
|
+
``,
|
|
953
|
+
`## Core Parameters`,
|
|
954
|
+
`| Parameter | Value |`,
|
|
955
|
+
`|---|---|`,
|
|
956
|
+
`| **Genre** | ${genre} |`,
|
|
957
|
+
`| **BPM** | ${bpm} |`,
|
|
958
|
+
`| **Key** | ${root} ${scale} |`,
|
|
959
|
+
`| **Mood** | ${mood} |`,
|
|
960
|
+
`| **Scale** | ${scaleInfo.names.join(', ')} |`,
|
|
961
|
+
``,
|
|
962
|
+
`## Chord Progression`,
|
|
963
|
+
`**Roman numerals:** ${progression}`,
|
|
964
|
+
`**Notes:** ${chordNotes.join(' | ')}`,
|
|
965
|
+
aiResult?.progression_name ? `**Based on:** ${aiResult.progression_name}` : '',
|
|
966
|
+
``,
|
|
967
|
+
`## Instrument Palette`,
|
|
968
|
+
instruments.map(i => `- ${i}`).join('\n'),
|
|
969
|
+
``,
|
|
970
|
+
`## Drum Pattern`,
|
|
971
|
+
`**Style:** ${drumStyle}`,
|
|
972
|
+
`**Base pattern (16th positions):**`,
|
|
973
|
+
'```',
|
|
974
|
+
drumInfo,
|
|
975
|
+
'```',
|
|
976
|
+
``,
|
|
977
|
+
`## Melody Approach`,
|
|
978
|
+
melodyApproach,
|
|
979
|
+
``,
|
|
980
|
+
`## Production Notes`,
|
|
981
|
+
productionNotes,
|
|
982
|
+
``,
|
|
983
|
+
`## Energy Arc`,
|
|
984
|
+
energyArc,
|
|
985
|
+
``,
|
|
986
|
+
references.length > 0 ? `## Reference Artists\n${references.map(r => `- ${r}`).join('\n')}\n` : '',
|
|
987
|
+
`## Quick Start Commands`,
|
|
988
|
+
``,
|
|
989
|
+
'```',
|
|
990
|
+
`# 1. Generate drums`,
|
|
991
|
+
`generate_drum_pattern genre="${genre}" bpm=${bpm}`,
|
|
992
|
+
``,
|
|
993
|
+
`# 2. Generate melody`,
|
|
994
|
+
`generate_melody_pattern key="${keyStr}" genre="${genre}" bpm=${bpm} density="moderate"`,
|
|
995
|
+
``,
|
|
996
|
+
`# 3. Generate song structure`,
|
|
997
|
+
`generate_song_structure genre="${genre}" bpm=${bpm} key="${keyStr}" progression="${progression}"`,
|
|
998
|
+
``,
|
|
999
|
+
`# 4. Generate lyrics`,
|
|
1000
|
+
`generate_lyrics genre="${genre}" mood="${mood}" topic="${userPrompt}"`,
|
|
1001
|
+
'```',
|
|
1002
|
+
].filter(Boolean).join('\n');
|
|
1003
|
+
},
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
//# sourceMappingURL=music-gen.js.map
|