feat: add als generator chatbot and storage
This commit is contained in:
@@ -6,3 +6,11 @@ NODE_RED_FLOW_CREDENTIAL_SECRET=change-this-secret
|
|||||||
GITEA_TOKEN=put-your-token-here
|
GITEA_TOKEN=put-your-token-here
|
||||||
GITEA_USER=your-username
|
GITEA_USER=your-username
|
||||||
GITEA_URL=https://gitea.cbcren.online/
|
GITEA_URL=https://gitea.cbcren.online/
|
||||||
|
ANTHROPIC_BASE_URL=https://api.z.ai/api/anthropic
|
||||||
|
ANTHROPIC_AUTH_TOKEN=replace-with-your-token
|
||||||
|
ANTHROPIC_FALLBACK_BASE_URL=https://api.minimax.io/anthropic
|
||||||
|
ANTHROPIC_FALLBACK_TOKEN=replace-with-your-secondary-token
|
||||||
|
ANTHROPIC_MODEL=MiniMax-M2
|
||||||
|
API_TIMEOUT_MS=300000
|
||||||
|
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1
|
||||||
|
CHATBOT_UPLOAD_URL=http://localhost:1880/als/upload
|
||||||
|
|||||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -6,3 +6,9 @@ data/flows_cred.json
|
|||||||
data/*_cred.json
|
data/*_cred.json
|
||||||
als/
|
als/
|
||||||
data/.flows*
|
data/.flows*
|
||||||
|
data/library/
|
||||||
|
!data/library/.gitkeep
|
||||||
|
data/sources/*
|
||||||
|
!data/sources/.gitkeep
|
||||||
|
data/generated/*
|
||||||
|
!data/generated/.gitkeep
|
||||||
|
|||||||
18
README.md
18
README.md
@@ -100,3 +100,21 @@ Se añadió un flow en `data/flows.json` que usa Node-RED Dashboard para mostrar
|
|||||||
- Si intentas subir el mismo `.als`, el flujo detecta el hash duplicado, responde `409` y el dashboard avisa sin sobrescribir la biblioteca.
|
- Si intentas subir el mismo `.als`, el flujo detecta el hash duplicado, responde `409` y el dashboard avisa sin sobrescribir la biblioteca.
|
||||||
- Para consultar el inventario (por ejemplo, desde una IA) usa `GET /als/library`; el endpoint devuelve el arreglo completo de proyectos almacenados.
|
- Para consultar el inventario (por ejemplo, desde una IA) usa `GET /als/library`; el endpoint devuelve el arreglo completo de proyectos almacenados.
|
||||||
- Como el archivo vive dentro de `/data`, puedes versionarlo o respaldarlo fácilmente según tu flujo de trabajo.
|
- Como el archivo vive dentro de `/data`, puedes versionarlo o respaldarlo fácilmente según tu flujo de trabajo.
|
||||||
|
|
||||||
|
### Chatbot y generación de nuevos `.als`
|
||||||
|
|
||||||
|
- Coloca tus stems/loops en `data/sources/` (el contenedor los comparte con Node-RED). Los `.als` creados se guardan en `data/generated/` y las plantillas descomprimidas viven en `data/library/`.
|
||||||
|
- Configura en `.env` las credenciales de MiniMax/GLM (`ANTHROPIC_BASE_URL`, `ANTHROPIC_AUTH_TOKEN`, etc.). Si la IA falla, el generador recurre a heurísticas locales con las plantillas existentes.
|
||||||
|
- Nuevo endpoint `POST /als/chat`:
|
||||||
|
```json
|
||||||
|
{ "prompt": "generame un als de reggaeton 2001" }
|
||||||
|
```
|
||||||
|
Devuelve el resumen del proyecto creado (nombre, hash y ruta del archivo). En el dashboard aparece la tarjeta **Chatbot ALS** para conversar visualmente.
|
||||||
|
- Internamente se usa `data/lib/alsGenerator.js`: el bot elige una plantilla (`als-library.json`), edita el XML del `.als` (nombre del proyecto, tempo, anotaciones con los sources) y registra el resultado llamando otra vez a `/als/upload` para mantener la deduplicación.
|
||||||
|
- También puedes usarlo desde terminal:
|
||||||
|
```bash
|
||||||
|
set -a && source .env
|
||||||
|
node scripts/chatbot.js # modo interactivo
|
||||||
|
node scripts/generate-als.js "generame un als tribal"
|
||||||
|
```
|
||||||
|
Ambos comandos generan el archivo dentro de `data/generated/` y lo añaden automáticamente a la biblioteca.
|
||||||
|
|||||||
@@ -168,6 +168,351 @@
|
|||||||
"C:/VST2/Fabfilter/FabFilter Pro-C 2.dll"
|
"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",
|
"type": "function",
|
||||||
"z": "f68df1c4d2e4e1a9",
|
"z": "f68df1c4d2e4e1a9",
|
||||||
"name": "JSON base64 \u2192 Buffer",
|
"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,
|
"outputs": 2,
|
||||||
"noerr": 0,
|
"noerr": 0,
|
||||||
"initialize": "",
|
"initialize": "",
|
||||||
@@ -150,7 +150,7 @@
|
|||||||
"type": "function",
|
"type": "function",
|
||||||
"z": "f68df1c4d2e4e1a9",
|
"z": "f68df1c4d2e4e1a9",
|
||||||
"name": "Registrar ALS",
|
"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,
|
"outputs": 1,
|
||||||
"noerr": 0,
|
"noerr": 0,
|
||||||
"initialize": "",
|
"initialize": "",
|
||||||
@@ -159,6 +159,10 @@
|
|||||||
{
|
{
|
||||||
"var": "fs",
|
"var": "fs",
|
||||||
"module": "fs"
|
"module": "fs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"var": "path",
|
||||||
|
"module": "path"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"x": 980,
|
"x": 980,
|
||||||
@@ -308,5 +312,82 @@
|
|||||||
"x": 700,
|
"x": 700,
|
||||||
"y": 500,
|
"y": 500,
|
||||||
"wires": []
|
"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",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-xml-parser": "^5.3.2",
|
"fast-xml-parser": "^5.3.2",
|
||||||
|
"node-fetch": "^2.7.0",
|
||||||
"node-red-dashboard": "^3.6.6"
|
"node-red-dashboard": "^3.6.6"
|
||||||
},
|
}
|
||||||
"devDependencies": {}
|
|
||||||
},
|
},
|
||||||
"node_modules/@socket.io/component-emitter": {
|
"node_modules/@socket.io/component-emitter": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
@@ -327,6 +327,25 @@
|
|||||||
"node": ">= 0.6"
|
"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": {
|
"node_modules/node-red-dashboard": {
|
||||||
"version": "3.6.6",
|
"version": "3.6.6",
|
||||||
"resolved": "https://registry.npmjs.org/node-red-dashboard/-/node-red-dashboard-3.6.6.tgz",
|
"resolved": "https://registry.npmjs.org/node-red-dashboard/-/node-red-dashboard-3.6.6.tgz",
|
||||||
@@ -588,6 +607,11 @@
|
|||||||
"node": ">=0.6"
|
"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": {
|
"node_modules/undici-types": {
|
||||||
"version": "7.16.0",
|
"version": "7.16.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
||||||
@@ -601,6 +625,20 @@
|
|||||||
"node": ">= 0.8"
|
"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": {
|
"node_modules/ws": {
|
||||||
"version": "8.17.1",
|
"version": "8.17.1",
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz",
|
||||||
"integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w=="
|
"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": {
|
"node-red-dashboard": {
|
||||||
"version": "3.6.6",
|
"version": "3.6.6",
|
||||||
"resolved": "https://registry.npmjs.org/node-red-dashboard/-/node-red-dashboard-3.6.6.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||||
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="
|
"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": {
|
"undici-types": {
|
||||||
"version": "7.16.0",
|
"version": "7.16.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||||
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="
|
"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": {
|
"ws": {
|
||||||
"version": "8.17.1",
|
"version": "8.17.1",
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-xml-parser": "^5.3.2",
|
"fast-xml-parser": "^5.3.2",
|
||||||
|
"node-fetch": "^2.7.0",
|
||||||
"node-red-dashboard": "^3.6.6"
|
"node-red-dashboard": "^3.6.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -480,7 +480,7 @@ module.exports = {
|
|||||||
* global.get("os")
|
* global.get("os")
|
||||||
*/
|
*/
|
||||||
functionGlobalContext: {
|
functionGlobalContext: {
|
||||||
// os:require('os'),
|
alsGenerator: require('./lib/alsGenerator.js'),
|
||||||
},
|
},
|
||||||
|
|
||||||
/** The maximum number of messages nodes will buffer internally as part of their
|
/** 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
41
scripts/chatbot.js
Executable file
41
scripts/chatbot.js
Executable file
@@ -0,0 +1,41 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
const readline = require('readline');
|
||||||
|
const { generateFromPrompt } = require('../data/lib/alsGenerator');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const rl = readline.createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
output: process.stdout,
|
||||||
|
prompt: 'als-bot> '
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Chatbot ALS listo. Escribe "generame un als de ..." o "salir".');
|
||||||
|
rl.prompt();
|
||||||
|
|
||||||
|
rl.on('line', async (line) => {
|
||||||
|
const input = line.trim();
|
||||||
|
if (!input) {
|
||||||
|
rl.prompt();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (['salir', 'exit', 'quit'].includes(input.toLowerCase())) {
|
||||||
|
rl.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const result = await generateFromPrompt(input);
|
||||||
|
console.log('Nuevo ALS listo:', {
|
||||||
|
projectName: result.plan.projectName,
|
||||||
|
output: path.relative(process.cwd(), result.outputPath),
|
||||||
|
templateHash: result.plan.templateHash
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('No se pudo generar el ALS:', err.message);
|
||||||
|
}
|
||||||
|
rl.prompt();
|
||||||
|
});
|
||||||
|
|
||||||
|
rl.on('close', () => {
|
||||||
|
console.log('Hasta luego 👋');
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
25
scripts/generate-als.js
Executable file
25
scripts/generate-als.js
Executable file
@@ -0,0 +1,25 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
const path = require('path');
|
||||||
|
const { generateFromPrompt } = require('../data/lib/alsGenerator');
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const prompt = process.argv.slice(2).join(' ').trim();
|
||||||
|
if (!prompt) {
|
||||||
|
console.error('Uso: node scripts/generate-als.js \"generame un als de reggaeton\"');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const result = await generateFromPrompt(prompt);
|
||||||
|
console.log('✅ ALS generado:', {
|
||||||
|
projectName: result.plan.projectName,
|
||||||
|
outputPath: path.relative(process.cwd(), result.outputPath),
|
||||||
|
templateHash: result.plan.templateHash,
|
||||||
|
registered: result.registered
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('❌ Error generando ALS:', err.message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
Reference in New Issue
Block a user