feat: add als generator chatbot and storage

This commit is contained in:
renato97
2025-12-01 03:03:22 +00:00
parent 5dafb7fcbf
commit a87347475a
14 changed files with 941 additions and 6 deletions

View File

@@ -168,6 +168,351 @@
"C:/VST2/Fabfilter/FabFilter Pro-C 2.dll"
]
},
"storedAt": "2025-12-01T02:46:38.916Z"
"storedAt": "2025-12-01T03:01:16.790Z",
"filePath": "library/9dbed77293415a951c43101ef30873d4.als"
},
{
"hash": "13a5d5ab84b6dead00d8eb01c3bfa194",
"projectName": "ai-generame-un-als-de-reggaeton-2001-1764558136687.als",
"meta": {
"creator": "Ableton Live 12.2",
"version": "5.12.0_12203",
"revision": "1c7a2c5dacd710ba28150f2c1534c22b1c158263",
"fileName": "ai-generame-un-als-de-reggaeton-2001-1764558136687.als",
"sizeBytes": 6665479,
"sizeHuman": "6.4 MB"
},
"liveSet": {
"tempo": 124,
"loopLengthBeats": 272,
"durationSeconds": 131.61290322580646,
"durationHuman": "2:12",
"loopStart": 0,
"scenes": []
},
"tracks": [
{
"name": "generame 1",
"type": "Audio",
"deviceCount": 3,
"color": "22"
},
{
"name": "un 2",
"type": "Audio",
"deviceCount": 0,
"color": "22"
},
{
"name": "als 3",
"type": "Audio",
"deviceCount": 1,
"color": "22"
},
{
"name": "de 4",
"type": "Audio",
"deviceCount": 2,
"color": "22"
},
{
"name": "reggaeton 5",
"type": "Audio",
"deviceCount": 1,
"color": "22"
},
{
"name": "2001 6",
"type": "MIDI",
"deviceCount": 1,
"color": "22"
},
{
"name": "Track 7",
"type": "MIDI",
"deviceCount": 4,
"color": "22"
},
{
"name": "Track 8",
"type": "MIDI",
"deviceCount": 2,
"color": "22"
},
{
"name": "Track 9",
"type": "MIDI",
"deviceCount": 3,
"color": "22"
},
{
"name": "Track 10",
"type": "MIDI",
"deviceCount": 2,
"color": "22"
},
{
"name": "Track 11",
"type": "MIDI",
"deviceCount": 2,
"color": "22"
},
{
"name": "Track 12",
"type": "MIDI",
"deviceCount": 3,
"color": "22"
},
{
"name": "Track 13",
"type": "MIDI",
"deviceCount": 3,
"color": "22"
},
{
"name": "Track 14",
"type": "MIDI",
"deviceCount": 3,
"color": "22"
},
{
"name": "Track 15",
"type": "MIDI",
"deviceCount": 5,
"color": "22"
},
{
"name": "Track 16",
"type": "MIDI",
"deviceCount": 2,
"color": "22"
},
{
"name": "Track 17",
"type": "Grupo",
"deviceCount": 1,
"color": "22"
},
{
"name": "Track 18",
"type": "Grupo",
"deviceCount": 3,
"color": "22"
},
{
"name": "Track 19",
"type": "Grupo",
"deviceCount": 1,
"color": "22"
}
],
"stats": {
"audio": 5,
"midi": 11,
"group": 3,
"totalTracks": 19,
"devices": 42,
"scenes": 0,
"samples": 8
},
"samples": {
"total": 8,
"relative": [
"Samples/Imported/RUFUS DU SOL - In the Moment (Adriatique Remix) Acapella.mp3",
"Samples/Processed/Bounce/Bounce KICK #1 [2025-08-30 144250]-3.wav",
"Samples/Recorded/12-Audio 0001 [2025-08-30 143952].wav",
"Presets/Audio Effects/Audio Effect Rack/Filter HI and Low.adg",
"Samples/Recorded/12-Audio 0001 [2025-08-30 144108].wav",
"Samples/Imported/Clap Fx.wav",
"Samples/Imported/Clap.wav",
"Swing and Groove/Swing/Swing 16-99.agr"
],
"absolute": [
"C:/Users/novik/Desktop/YOUTUBE RUFUS DUU SOL & ARTBAT/GHOSTPRODUCTION.PRO (ABLETON LIVE) (Style RUFUS DUU SOL) Project/Samples/Imported/RUFUS DU SOL - In the Moment (Adriatique Remix) Acapella.mp3",
"C:/VST2/Proximity-x64.dll",
"C:/Users/novik/Desktop/YOUTUBE RUFUS DUU SOL & ARTBAT/GHOSTPRODUCTION.PRO (ABLETON LIVE) (Style RUFUS DUU SOL) Project/Samples/Processed/Bounce/Bounce KICK #1 [2025-08-30 144250]-3.wav",
"C:/Users/novik/Desktop/YOUTUBE RUFUS DUU SOL & ARTBAT/GHOSTPRODUCTION.PRO (ABLETON LIVE) (Style RUFUS DUU SOL) Project/Samples/Recorded/12-Audio 0001 [2025-08-30 143952].wav",
"C:/Users/novik/OneDrive/Documenten/Ableton/User Library/Presets/Audio Effects/Audio Effect Rack/Filter HI and Low.adg",
"C:/Users/novik/Desktop/YOUTUBE RUFUS DUU SOL & ARTBAT/GHOSTPRODUCTION.PRO (ABLETON LIVE) (Style RUFUS DUU SOL) Project/Samples/Recorded/12-Audio 0001 [2025-08-30 144108].wav",
"C:/VST2/Fabfilter/FabFilter Pro-Q 4.dll",
"C:/Users/novik/Desktop/YOUTUBE RUFUS DUU SOL & ARTBAT/GHOSTPRODUCTION.PRO (ABLETON LIVE) (Style RUFUS DUU SOL) Project/Samples/Imported/Clap Fx.wav",
"C:/Users/novik/Desktop/YOUTUBE RUFUS DUU SOL & ARTBAT/GHOSTPRODUCTION.PRO (ABLETON LIVE) (Style RUFUS DUU SOL) Project/Samples/Imported/Clap.wav",
"C:/VST2/Fabfilter/FabFilter Pro-C 2.dll"
]
},
"storedAt": "2025-12-01T03:02:18.543Z",
"filePath": "library/13a5d5ab84b6dead00d8eb01c3bfa194.als"
},
{
"hash": "38427f49e8ffcfba20969319b9dda2f7",
"projectName": "ai-generame-un-als-tribal-1764558155174.als",
"meta": {
"creator": "Ableton Live 12.2",
"version": "5.12.0_12203",
"revision": "1c7a2c5dacd710ba28150f2c1534c22b1c158263",
"fileName": "ai-generame-un-als-tribal-1764558155174.als",
"sizeBytes": 6358425,
"sizeHuman": "6.1 MB"
},
"liveSet": {
"tempo": 124,
"loopLengthBeats": 272,
"durationSeconds": 131.61290322580646,
"durationHuman": "2:12",
"loopStart": 0,
"scenes": []
},
"tracks": [
{
"name": "generame 1",
"type": "Audio",
"deviceCount": 3,
"color": "22"
},
{
"name": "un 2",
"type": "Audio",
"deviceCount": 0,
"color": "22"
},
{
"name": "als 3",
"type": "Audio",
"deviceCount": 1,
"color": "22"
},
{
"name": "tribal 4",
"type": "Audio",
"deviceCount": 2,
"color": "22"
},
{
"name": "Track 5",
"type": "Audio",
"deviceCount": 1,
"color": "22"
},
{
"name": "Track 6",
"type": "MIDI",
"deviceCount": 1,
"color": "22"
},
{
"name": "Track 7",
"type": "MIDI",
"deviceCount": 4,
"color": "22"
},
{
"name": "Track 8",
"type": "MIDI",
"deviceCount": 2,
"color": "22"
},
{
"name": "Track 9",
"type": "MIDI",
"deviceCount": 3,
"color": "22"
},
{
"name": "Track 10",
"type": "MIDI",
"deviceCount": 2,
"color": "22"
},
{
"name": "Track 11",
"type": "MIDI",
"deviceCount": 2,
"color": "22"
},
{
"name": "Track 12",
"type": "MIDI",
"deviceCount": 3,
"color": "22"
},
{
"name": "Track 13",
"type": "MIDI",
"deviceCount": 3,
"color": "22"
},
{
"name": "Track 14",
"type": "MIDI",
"deviceCount": 3,
"color": "22"
},
{
"name": "Track 15",
"type": "MIDI",
"deviceCount": 5,
"color": "22"
},
{
"name": "Track 16",
"type": "MIDI",
"deviceCount": 2,
"color": "22"
},
{
"name": "Track 17",
"type": "Grupo",
"deviceCount": 1,
"color": "22"
},
{
"name": "Track 18",
"type": "Grupo",
"deviceCount": 3,
"color": "22"
},
{
"name": "Track 19",
"type": "Grupo",
"deviceCount": 1,
"color": "22"
}
],
"stats": {
"audio": 5,
"midi": 11,
"group": 3,
"totalTracks": 19,
"devices": 42,
"scenes": 0,
"samples": 8
},
"samples": {
"total": 8,
"relative": [
"Samples/Imported/RUFUS DU SOL - In the Moment (Adriatique Remix) Acapella.mp3",
"Samples/Processed/Bounce/Bounce KICK #1 [2025-08-30 144250]-3.wav",
"Samples/Recorded/12-Audio 0001 [2025-08-30 143952].wav",
"Presets/Audio Effects/Audio Effect Rack/Filter HI and Low.adg",
"Samples/Recorded/12-Audio 0001 [2025-08-30 144108].wav",
"Samples/Imported/Clap Fx.wav",
"Samples/Imported/Clap.wav",
"Swing and Groove/Swing/Swing 16-99.agr"
],
"absolute": [
"C:/Users/novik/Desktop/YOUTUBE RUFUS DUU SOL & ARTBAT/GHOSTPRODUCTION.PRO (ABLETON LIVE) (Style RUFUS DUU SOL) Project/Samples/Imported/RUFUS DU SOL - In the Moment (Adriatique Remix) Acapella.mp3",
"C:/VST2/Proximity-x64.dll",
"C:/Users/novik/Desktop/YOUTUBE RUFUS DUU SOL & ARTBAT/GHOSTPRODUCTION.PRO (ABLETON LIVE) (Style RUFUS DUU SOL) Project/Samples/Processed/Bounce/Bounce KICK #1 [2025-08-30 144250]-3.wav",
"C:/Users/novik/Desktop/YOUTUBE RUFUS DUU SOL & ARTBAT/GHOSTPRODUCTION.PRO (ABLETON LIVE) (Style RUFUS DUU SOL) Project/Samples/Recorded/12-Audio 0001 [2025-08-30 143952].wav",
"C:/Users/novik/OneDrive/Documenten/Ableton/User Library/Presets/Audio Effects/Audio Effect Rack/Filter HI and Low.adg",
"C:/Users/novik/Desktop/YOUTUBE RUFUS DUU SOL & ARTBAT/GHOSTPRODUCTION.PRO (ABLETON LIVE) (Style RUFUS DUU SOL) Project/Samples/Recorded/12-Audio 0001 [2025-08-30 144108].wav",
"C:/VST2/Fabfilter/FabFilter Pro-Q 4.dll",
"C:/Users/novik/Desktop/YOUTUBE RUFUS DUU SOL & ARTBAT/GHOSTPRODUCTION.PRO (ABLETON LIVE) (Style RUFUS DUU SOL) Project/Samples/Imported/Clap Fx.wav",
"C:/Users/novik/Desktop/YOUTUBE RUFUS DUU SOL & ARTBAT/GHOSTPRODUCTION.PRO (ABLETON LIVE) (Style RUFUS DUU SOL) Project/Samples/Imported/Clap.wav",
"C:/VST2/Fabfilter/FabFilter Pro-C 2.dll"
]
},
"storedAt": "2025-12-01T03:02:37.033Z",
"filePath": "library/38427f49e8ffcfba20969319b9dda2f7.als"
}
]

View File

@@ -96,7 +96,7 @@
"type": "function",
"z": "f68df1c4d2e4e1a9",
"name": "JSON base64 \u2192 Buffer",
"func": "const body = msg.payload;\nif (!body || typeof body.data !== 'string') {\n msg.error = 'No se recibi\u00f3 el archivo en el formato esperado.';\n msg.statusCode = 400;\n msg.analysis = null;\n return [null, msg];\n}\nconst commaIndex = body.data.indexOf(',');\nconst base64 = commaIndex > -1 ? body.data.slice(commaIndex + 1) : body.data;\ntry {\n const buffer = Buffer.from(base64, 'base64');\n msg.payload = buffer;\n msg.filename = body.filename || 'upload.als';\n msg.size = body.size || buffer.length;\n msg.statusText = 'Archivo recibido (' + Math.round(buffer.length / 1024) + ' KB)';\n return [msg, null];\n} catch (err) {\n node.error('No se pudo decodificar el archivo ALS', err);\n msg.error = 'No se pudo decodificar el archivo ALS.';\n msg.statusCode = 400;\n msg.analysis = null;\n return [null, msg];\n}",
"func": "const body = msg.payload;\nif (!body || typeof body.data !== 'string') {\n msg.error = 'No se recibi\u00f3 el archivo en el formato esperado.';\n msg.statusCode = 400;\n msg.analysis = null;\n return [null, msg];\n}\nconst commaIndex = body.data.indexOf(',');\nconst base64 = commaIndex > -1 ? body.data.slice(commaIndex + 1) : body.data;\ntry {\n const buffer = Buffer.from(base64, 'base64');\n msg.payload = buffer;\n msg.originalBuffer = buffer;\n msg.filename = body.filename || 'upload.als';\n msg.size = body.size || buffer.length;\n msg.statusText = 'Archivo recibido (' + Math.round(buffer.length / 1024) + ' KB)';\n return [msg, null];\n} catch (err) {\n node.error('No se pudo decodificar el archivo ALS', err);\n msg.error = 'No se pudo decodificar el archivo ALS.';\n msg.statusCode = 400;\n msg.analysis = null;\n return [null, msg];\n}",
"outputs": 2,
"noerr": 0,
"initialize": "",
@@ -150,7 +150,7 @@
"type": "function",
"z": "f68df1c4d2e4e1a9",
"name": "Registrar ALS",
"func": "const DB_PATH = '/data/als-library.json';\nfunction readLibrary() {\n try {\n const raw = fs.readFileSync(DB_PATH, 'utf8');\n return raw ? JSON.parse(raw) : [];\n } catch (err) {\n if (err.code === 'ENOENT') { return []; }\n node.error('No se pudo leer la biblioteca ALS', err);\n throw err;\n }\n}\nfunction writeLibrary(data) {\n fs.writeFileSync(DB_PATH, JSON.stringify(data, null, 2));\n}\nif (!msg.analysis || !msg.analysis.hash) {\n msg.error = 'No se pudo guardar el an\u00e1lisis (hash faltante).';\n msg.statusCode = msg.statusCode || 400;\n msg.analysis = null;\n return msg;\n}\nconst library = readLibrary();\nconst existing = library.find((entry) => entry.hash === msg.analysis.hash);\nif (existing) {\n msg.error = 'Este archivo ALS ya fue registrado anteriormente.';\n msg.statusCode = 409;\n msg.statusText = 'Archivo duplicado';\n msg.analysis = null;\n return msg;\n}\nconst record = {\n ...msg.analysis,\n storedAt: new Date().toISOString()\n};\nlibrary.push(record);\nwriteLibrary(library);\nmsg.analysis = record;\nmsg.error = null;\nmsg.statusCode = 200;\nmsg.statusText = 'An\u00e1lisis almacenado';\nreturn msg;",
"func": "const DB_PATH = '/data/als-library.json';\nconst LIB_DIR = '/data/library';\nfunction readLibrary() {\n try {\n const raw = fs.readFileSync(DB_PATH, 'utf8');\n return raw ? JSON.parse(raw) : [];\n } catch (err) {\n if (err.code === 'ENOENT') { return []; }\n node.error('No se pudo leer la biblioteca ALS', err);\n throw err;\n }\n}\nfunction writeLibrary(data) {\n fs.writeFileSync(DB_PATH, JSON.stringify(data, null, 2));\n}\nif (!msg.analysis || !msg.analysis.hash) {\n msg.error = 'No se pudo guardar el an\u00e1lisis (hash faltante).';\n msg.statusCode = msg.statusCode || 400;\n msg.analysis = null;\n return msg;\n}\nconst library = readLibrary();\nconst existing = library.find((entry) => entry.hash === msg.analysis.hash);\nif (existing) {\n msg.error = 'Este archivo ALS ya fue registrado anteriormente.';\n msg.statusCode = 409;\n msg.statusText = 'Archivo duplicado';\n msg.analysis = null;\n return msg;\n}\nconst fileName = msg.analysis.hash + '.als';\nconst filePath = path.join(LIB_DIR, fileName);\nfs.mkdirSync(LIB_DIR, { recursive: true });\nif (msg.originalBuffer && Buffer.isBuffer(msg.originalBuffer)) {\n fs.writeFileSync(filePath, msg.originalBuffer);\n}\nconst record = {\n ...msg.analysis,\n storedAt: new Date().toISOString(),\n filePath: path.posix.join('library', fileName)\n};\nlibrary.push(record);\nwriteLibrary(library);\nmsg.analysis = record;\nmsg.error = null;\nmsg.statusCode = 200;\nmsg.statusText = 'An\u00e1lisis almacenado';\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
@@ -159,6 +159,10 @@
{
"var": "fs",
"module": "fs"
},
{
"var": "path",
"module": "path"
}
],
"x": 980,
@@ -308,5 +312,82 @@
"x": 700,
"y": 500,
"wires": []
},
{
"id": "c87f0e0d1bdbf964",
"type": "ui_group",
"name": "Chatbot",
"tab": "c1ef7251baee3fe8",
"order": 3,
"disp": true,
"width": "12",
"collapse": false,
"className": ""
},
{
"id": "85899363d75b2e10",
"type": "ui_template",
"z": "f68df1c4d2e4e1a9",
"group": "c87f0e0d1bdbf964",
"name": "Chatbot UI",
"order": 0,
"width": "12",
"height": "10",
"format": "\n<md-card class=\"chatbot-card\">\n <md-card-title>\n <span class=\"md-headline\">Chatbot ALS</span>\n </md-card-title>\n <md-card-content>\n <div class=\"chat-log\">\n <div ng-repeat=\"item in messages\" class=\"message {{item.role}}\">\n <strong>{{item.role === 'user' ? 'T\u00fa' : (item.role === 'bot' ? 'ALS Bot' : 'Error')}}:</strong>\n <span>{{item.text}}</span>\n </div>\n </div>\n <form ng-submit=\"sendMessage()\">\n <md-input-container class=\"md-block\">\n <label>Escribe algo (ej. generame un als de reggaeton 2001)</label>\n <input ng-model=\"promptText\" required ng-disabled=\"busy\" />\n </md-input-container>\n <md-button class=\"md-raised md-primary\" type=\"submit\" ng-disabled=\"busy\">{{busy ? 'Generando...' : 'Enviar'}}</md-button>\n </form>\n </md-card-content>\n</md-card>\n<script>\n(function(scope) {\n scope.messages = [];\n scope.promptText = '';\n scope.busy = false;\n function pushMessage(role, text) {\n scope.messages.push({ role: role, text: text });\n if (scope.messages.length > 50) {\n scope.messages.shift();\n }\n scope.$applyAsync();\n }\n scope.sendMessage = function() {\n if (!scope.promptText || scope.busy) { return; }\n const text = scope.promptText;\n scope.promptText = '';\n pushMessage('user', text);\n scope.busy = true;\n fetch('/als/chat', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ prompt: text })\n })\n .then(function(res) {\n return res.text().then(function(body) {\n var data;\n try { data = body ? JSON.parse(body) : {}; } catch (err) { data = { error: body || err.message }; }\n return { ok: res.ok, data: data };\n });\n })\n .then(function(result) {\n if (result.ok) {\n var message = 'Proyecto: ' + result.data.projectName + '. Archivo: ' + result.data.outputPath;\n pushMessage('bot', message);\n } else {\n pushMessage('error', result.data && result.data.error ? result.data.error : 'Error generando ALS');\n }\n })\n .catch(function(err) {\n pushMessage('error', err.message || 'Error inesperado');\n })\n .finally(function() {\n scope.busy = false;\n scope.$applyAsync();\n });\n };\n})(scope);\n</script>\n<style>\n .chatbot-card .chat-log {\n max-height: 260px;\n overflow-y: auto;\n margin-bottom: 12px;\n padding: 8px;\n background: rgba(0,0,0,0.05);\n border-radius: 6px;\n }\n .chatbot-card .message { margin-bottom: 6px; }\n .chatbot-card .message.user strong { color: #1976d2; }\n .chatbot-card .message.bot strong { color: #2e7d32; }\n .chatbot-card .message.error strong { color: #c62828; }\n</style>\n",
"storeOutMessages": false,
"fwdInMessages": false,
"resendOnRefresh": true,
"templateScope": "local",
"className": "",
"x": 240,
"y": 520,
"wires": []
},
{
"id": "b6a5b6ec8f7a4c33",
"type": "http in",
"z": "f68df1c4d2e4e1a9",
"name": "POST /als/chat",
"url": "/als/chat",
"method": "post",
"upload": false,
"swaggerDoc": "",
"x": 220,
"y": 660,
"wires": [
[
"ca7c4bb1e81b009a"
]
]
},
{
"id": "ca7c4bb1e81b009a",
"type": "function",
"z": "f68df1c4d2e4e1a9",
"name": "Chatbot ALS",
"func": "\nconst raw = typeof msg.payload === 'string' ? msg.payload : JSON.stringify(msg.payload || {});\nlet body = {};\ntry {\n body = typeof msg.payload === 'object' ? msg.payload : JSON.parse(raw);\n} catch (err) {\n body = {};\n}\nconst prompt = (body.prompt || body.message || msg.prompt || '').trim();\nif (!prompt) {\n msg.statusCode = 400;\n msg.payload = { error: 'Falta el prompt del chatbot.' };\n return msg;\n}\nconst generator = global.get('alsGenerator');\nif (!generator || typeof generator.generateFromPrompt !== 'function') {\n msg.statusCode = 500;\n msg.payload = { error: 'Generador no disponible.' };\n return msg;\n}\nreturn (async () => {\n try {\n const result = await generator.generateFromPrompt(prompt);\n msg.statusCode = 200;\n msg.payload = {\n prompt: prompt,\n projectName: result.plan.projectName,\n templateHash: result.plan.templateHash,\n outputPath: result.outputPath,\n registered: result.registered\n };\n return msg;\n } catch (err) {\n node.error(err.message, msg);\n msg.statusCode = 500;\n msg.payload = { error: err.message };\n return msg;\n }\n})();\n",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 520,
"y": 660,
"wires": [
[
"e0b21f83f64a49c7"
]
]
},
{
"id": "e0b21f83f64a49c7",
"type": "http response",
"z": "f68df1c4d2e4e1a9",
"name": "",
"statusCode": "",
"headers": {},
"x": 720,
"y": 660,
"wires": []
}
]

0
data/generated/.gitkeep Normal file
View File

345
data/lib/alsGenerator.js Normal file
View File

@@ -0,0 +1,345 @@
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const zlib = require('zlib');
const { XMLParser, XMLBuilder } = require('fast-xml-parser');
const fetch = require('node-fetch');
const DATA_DIR = path.resolve(__dirname, '..');
const WORKSPACE_DIR = process.env.ALS_WORKSPACE_DIR
? path.resolve(process.env.ALS_WORKSPACE_DIR)
: DATA_DIR;
const LIBRARY_JSON = path.join(DATA_DIR, 'als-library.json');
const LIBRARY_DIR = path.join(DATA_DIR, 'library');
const SOURCES_DIR = path.join(WORKSPACE_DIR, 'sources');
const GENERATED_DIR = path.join(WORKSPACE_DIR, 'generated');
const UPLOAD_URL = process.env.CHATBOT_UPLOAD_URL || 'http://localhost:1880/als/upload';
const parser = new XMLParser({
ignoreAttributes: false,
attributeNamePrefix: '',
parseTagValue: false,
trimValues: false
});
const builder = new XMLBuilder({
ignoreAttributes: false,
attributeNamePrefix: '',
suppressEmptyNode: true
});
function ensureDirs() {
fs.mkdirSync(LIBRARY_DIR, { recursive: true });
fs.mkdirSync(SOURCES_DIR, { recursive: true });
fs.mkdirSync(GENERATED_DIR, { recursive: true });
}
function readLibrary() {
try {
const raw = fs.readFileSync(LIBRARY_JSON, 'utf8');
return raw ? JSON.parse(raw) : [];
} catch (err) {
if (err.code === 'ENOENT') {
return [];
}
throw err;
}
}
function listSources() {
const result = [];
if (!fs.existsSync(SOURCES_DIR)) {
return result;
}
function walk(dir) {
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
if (entry.name.startsWith('.')) {
continue;
}
const absolute = path.join(dir, entry.name);
const relative = path.relative(SOURCES_DIR, absolute);
if (entry.isDirectory()) {
walk(absolute);
} else {
result.push(relative);
}
}
}
walk(SOURCES_DIR);
return result;
}
function slugify(text) {
return text
.toLowerCase()
.replace(/[^a-z0-9]+/gi, '-')
.replace(/^-+|-+$/g, '')
.slice(0, 80) || 'als';
}
function scoreEntry(entry, promptTokens) {
let score = 0;
const haystack = [
entry.projectName,
entry.meta?.creator,
...(entry.tracks || []).map((t) => t.name)
]
.join(' ')
.toLowerCase();
for (const token of promptTokens) {
if (haystack.includes(token)) {
score += 2;
}
}
score += (entry.tracks || []).length;
score += entry.stats?.devices || 0;
return score;
}
function extractJson(text) {
if (!text) {
return null;
}
try {
return JSON.parse(text);
} catch (_) {
const match = text.match(/\{[\s\S]*\}/);
if (match) {
try {
return JSON.parse(match[0]);
} catch (err) {
return null;
}
}
}
return null;
}
async function planWithAI(prompt, library, sources) {
const base =
(process.env.ANTHROPIC_BASE_URL || '').trim() ||
(process.env.ANTHROPIC_FALLBACK_BASE_URL || '').trim();
const token =
(process.env.ANTHROPIC_AUTH_TOKEN || '').trim() ||
(process.env.ANTHROPIC_FALLBACK_TOKEN || '').trim();
if (!base || !token) {
return null;
}
const endpoint = `${base.replace(/\/$/, '')}/v1/messages`;
const contextEntries = library
.slice(0, 3)
.map((entry) => ({
projectName: entry.projectName,
hash: entry.hash,
tempo: entry.liveSet?.tempo,
tracks: (entry.tracks || []).slice(0, 5).map((t) => t.name)
}));
const payload = {
model: process.env.ANTHROPIC_MODEL || 'claude-3-5-sonnet-20241022',
max_tokens: 800,
temperature: 0.2,
system:
'Eres un generador de sesiones Ableton Live (.als). Devuelves un JSON con projectName, templateHash, tempo, trackNames (array) y notes. Usa solo los hashes ofrecidos.',
messages: [
{
role: 'user',
content: [
{
type: 'text',
text:
`Prompt del usuario: ${prompt}\n` +
`Plantillas disponibles: ${JSON.stringify(contextEntries, null, 2)}\n` +
`Sources disponibles (${sources.length}): ${sources.join(', ') || 'ninguna'}\n` +
'Respuesta esperada (JSON): {"projectName":"","templateHash":"","tempo":120,"trackNames":["..."],"notes":"..."}'
}
]
}
]
};
try {
const res = await fetch(endpoint, {
method: 'POST',
headers: {
'content-type': 'application/json',
'x-api-key': token,
'anthropic-version': '2023-06-01'
},
body: JSON.stringify(payload),
timeout: Number(process.env.API_TIMEOUT_MS) || 60000
});
if (!res.ok) {
throw new Error(await res.text());
}
const data = await res.json();
const text =
data?.content?.[0]?.text ||
data?.content?.[0]?.text ||
data?.content ||
'';
return extractJson(text);
} catch (err) {
console.warn('[alsGenerator] Error llamando a la IA:', err.message);
return null;
}
}
function setNameNode(nameNode, value) {
if (!nameNode) {
return;
}
if (nameNode.EffectiveName) {
nameNode.EffectiveName.Value = value;
}
if (nameNode.UserName) {
nameNode.UserName.Value = value;
}
if (nameNode.Value) {
nameNode.Value = value;
}
}
function collectTracks(liveSet) {
const tracksNode = liveSet?.Tracks || {};
const keys = ['AudioTrack', 'MidiTrack', 'GroupTrack'];
const result = [];
for (const key of keys) {
const arr = Array.isArray(tracksNode[key])
? tracksNode[key]
: tracksNode[key]
? [tracksNode[key]]
: [];
for (const track of arr) {
result.push(track);
}
}
return result;
}
function applyPlanToLiveSet(xmlObj, plan, prompt, sources) {
const ableton = xmlObj.Ableton || (xmlObj.Ableton = {});
const liveSet = ableton.LiveSet || (ableton.LiveSet = {});
liveSet.Name = liveSet.Name || {};
setNameNode(liveSet.Name, plan.projectName);
const annotationLines = [
`Prompt: ${prompt}`,
`Notas: ${plan.notes || 'generado automáticamente'}`,
`Sources (${sources.length}): ${sources.join(', ') || 'N/A'}`
];
liveSet.Annotation = liveSet.Annotation || {};
liveSet.Annotation.Value = annotationLines.join('\\n');
const tempoValue = plan.tempo || plan.targetTempo || liveSet?.MainTrack?.DeviceChain?.Mixer?.Tempo?.Manual?.Value;
if (tempoValue && liveSet.MainTrack?.DeviceChain?.Mixer?.Tempo?.Manual) {
liveSet.MainTrack.DeviceChain.Mixer.Tempo.Manual.Value = tempoValue;
}
const trackNames = plan.trackNames || [];
const tracks = collectTracks(liveSet);
trackNames.forEach((name, idx) => {
if (tracks[idx]) {
tracks[idx].Name = tracks[idx].Name || {};
setNameNode(tracks[idx].Name, name);
}
});
}
async function registerFile(filePath) {
try {
const buffer = fs.readFileSync(filePath);
const dataUrl = `data:application/octet-stream;base64,${buffer.toString('base64')}`;
const res = await fetch(UPLOAD_URL, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
filename: path.basename(filePath),
size: buffer.length,
data: dataUrl
}),
timeout: Number(process.env.API_TIMEOUT_MS) || 60000
});
const text = await res.text();
let payload = text;
try {
payload = JSON.parse(text);
} catch (_) {
// noop
}
if (!res.ok) {
throw new Error(typeof payload === 'string' ? payload : payload.error || 'Error desconocido');
}
return payload;
} catch (err) {
console.warn('[alsGenerator] No se pudo registrar el archivo generado:', err.message);
return null;
}
}
async function generateFromPrompt(prompt, options = {}) {
ensureDirs();
const library = readLibrary();
if (!library.length) {
throw new Error('No hay ALS en la biblioteca. Sube al menos uno antes de generar.');
}
const sources = listSources();
const tokens = prompt
.toLowerCase()
.split(/[^a-z0-9]+/)
.filter(Boolean);
const ranked = [...library].sort(
(a, b) => scoreEntry(b, tokens) - scoreEntry(a, tokens)
);
const fallbackEntry = ranked[0];
let plan = await planWithAI(prompt, ranked.slice(0, 5), sources);
if (!plan) {
plan = {
projectName: `AI ${prompt}`.trim().slice(0, 60),
templateHash: fallbackEntry.hash,
tempo: fallbackEntry.liveSet?.tempo || 120,
trackNames: (fallbackEntry.tracks || []).map((t, idx) => `${tokens[idx] || 'Track'} ${idx + 1}`),
notes: 'Plan generado con heurística local.'
};
}
if (!plan.templateHash || !library.find((e) => e.hash === plan.templateHash)) {
plan.templateHash = fallbackEntry.hash;
}
plan.projectName =
plan.projectName ||
`AI-${slugify(prompt)}-${plan.templateHash.slice(0, 4)}`;
const templatePath = path.join(LIBRARY_DIR, `${plan.templateHash}.als`);
if (!fs.existsSync(templatePath)) {
throw new Error(`No se encontró la plantilla ${plan.templateHash}.`);
}
const templateBuffer = fs.readFileSync(templatePath);
const xmlString = zlib.gunzipSync(templateBuffer).toString('utf8');
const xmlObj = parser.parse(xmlString);
applyPlanToLiveSet(xmlObj, plan, prompt, sources);
const newXmlString = builder.build(xmlObj);
const newBuffer = zlib.gzipSync(Buffer.from(newXmlString, 'utf8'));
const slug = `${slugify(plan.projectName)}-${Date.now()}`;
const outputPath = path.join(GENERATED_DIR, `${slug}.als`);
fs.writeFileSync(outputPath, newBuffer);
let registration = null;
if (options.register !== false) {
registration = await registerFile(outputPath);
}
return {
prompt,
plan,
outputPath,
registered: Boolean(registration),
registration
};
}
module.exports = {
generateFromPrompt
};

0
data/library/.gitkeep Normal file
View File

69
data/package-lock.json generated
View File

@@ -10,9 +10,9 @@
"license": "ISC",
"dependencies": {
"fast-xml-parser": "^5.3.2",
"node-fetch": "^2.7.0",
"node-red-dashboard": "^3.6.6"
},
"devDependencies": {}
}
},
"node_modules/@socket.io/component-emitter": {
"version": "3.1.2",
@@ -327,6 +327,25 @@
"node": ">= 0.6"
}
},
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/node-red-dashboard": {
"version": "3.6.6",
"resolved": "https://registry.npmjs.org/node-red-dashboard/-/node-red-dashboard-3.6.6.tgz",
@@ -588,6 +607,11 @@
"node": ">=0.6"
}
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
},
"node_modules/undici-types": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
@@ -601,6 +625,20 @@
"node": ">= 0.8"
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/ws": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
@@ -858,6 +896,14 @@
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz",
"integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w=="
},
"node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"requires": {
"whatwg-url": "^5.0.0"
}
},
"node-red-dashboard": {
"version": "3.6.6",
"resolved": "https://registry.npmjs.org/node-red-dashboard/-/node-red-dashboard-3.6.6.tgz",
@@ -1042,6 +1088,11 @@
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="
},
"tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
},
"undici-types": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
@@ -1052,6 +1103,20 @@
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="
},
"webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
},
"whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"requires": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"ws": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",

View File

@@ -15,6 +15,7 @@
"license": "ISC",
"dependencies": {
"fast-xml-parser": "^5.3.2",
"node-fetch": "^2.7.0",
"node-red-dashboard": "^3.6.6"
}
}

View File

@@ -480,7 +480,7 @@ module.exports = {
* global.get("os")
*/
functionGlobalContext: {
// os:require('os'),
alsGenerator: require('./lib/alsGenerator.js'),
},
/** The maximum number of messages nodes will buffer internally as part of their

0
data/sources/.gitkeep Normal file
View File