feat: add als generator chatbot and storage
This commit is contained in:
@@ -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"
|
||||
}
|
||||
]
|
||||
@@ -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
0
data/generated/.gitkeep
Normal file
345
data/lib/alsGenerator.js
Normal file
345
data/lib/alsGenerator.js
Normal 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
0
data/library/.gitkeep
Normal file
69
data/package-lock.json
generated
69
data/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"fast-xml-parser": "^5.3.2",
|
||||
"node-fetch": "^2.7.0",
|
||||
"node-red-dashboard": "^3.6.6"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
0
data/sources/.gitkeep
Normal file
Reference in New Issue
Block a user