Test
This commit is contained in:
@@ -369,7 +369,6 @@ export default function RecorderScreen() {
|
|||||||
|
|
||||||
if (data.transcription) {
|
if (data.transcription) {
|
||||||
setTranscriptionText(data.transcription);
|
setTranscriptionText(data.transcription);
|
||||||
textsToSpeak.push(data.transcription);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.llmResponse) {
|
if (data.llmResponse) {
|
||||||
|
|||||||
@@ -8,6 +8,13 @@ QUIBOT_TOKEN=MY_SECRET_TOKEN
|
|||||||
# Backend server config
|
# Backend server config
|
||||||
PORT=5000
|
PORT=5000
|
||||||
|
|
||||||
|
# Piper TTS config (optional — local model)
|
||||||
|
PIPER_MODELS_DIR=./piper
|
||||||
|
PIPER_MODEL=./src/ca_ES-upc_ona-medium.onnx
|
||||||
|
|
||||||
|
# Remote Piper TTS service (alternative to local model)
|
||||||
|
PIPER_URL=
|
||||||
|
|
||||||
LLAMA_CPP_URL=https://ollama.epsem.aranroig.com/v1/chat/completitions
|
LLAMA_CPP_URL=https://ollama.epsem.aranroig.com/v1/chat/completitions
|
||||||
LLAMA_PREAMBLE=./prompts/preamble.md
|
LLAMA_PREAMBLE=./prompts/preamble.md
|
||||||
LLAMA_API_KEY=your_api_key
|
LLAMA_API_KEY=your_api_key
|
||||||
687
backend/package-lock.json
generated
687
backend/package-lock.json
generated
@@ -8,6 +8,7 @@
|
|||||||
"name": "quibot-backend",
|
"name": "quibot-backend",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||||
"axios": "^1.7.0",
|
"axios": "^1.7.0",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
@@ -466,6 +467,388 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@hono/node-server": {
|
||||||
|
"version": "1.19.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz",
|
||||||
|
"integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.14.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"hono": "^4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@modelcontextprotocol/sdk": {
|
||||||
|
"version": "1.29.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz",
|
||||||
|
"integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@hono/node-server": "^1.19.9",
|
||||||
|
"ajv": "^8.17.1",
|
||||||
|
"ajv-formats": "^3.0.1",
|
||||||
|
"content-type": "^1.0.5",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"cross-spawn": "^7.0.5",
|
||||||
|
"eventsource": "^3.0.2",
|
||||||
|
"eventsource-parser": "^3.0.0",
|
||||||
|
"express": "^5.2.1",
|
||||||
|
"express-rate-limit": "^8.2.1",
|
||||||
|
"hono": "^4.11.4",
|
||||||
|
"jose": "^6.1.3",
|
||||||
|
"json-schema-typed": "^8.0.2",
|
||||||
|
"pkce-challenge": "^5.0.0",
|
||||||
|
"raw-body": "^3.0.0",
|
||||||
|
"zod": "^3.25 || ^4.0",
|
||||||
|
"zod-to-json-schema": "^3.25.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@cfworker/json-schema": "^4.1.1",
|
||||||
|
"zod": "^3.25 || ^4.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@cfworker/json-schema": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"zod": {
|
||||||
|
"optional": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@modelcontextprotocol/sdk/node_modules/accepts": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-types": "^3.0.0",
|
||||||
|
"negotiator": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@modelcontextprotocol/sdk/node_modules/body-parser": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bytes": "^3.1.2",
|
||||||
|
"content-type": "^2.0.0",
|
||||||
|
"debug": "^4.4.3",
|
||||||
|
"http-errors": "^2.0.1",
|
||||||
|
"iconv-lite": "^0.7.2",
|
||||||
|
"on-finished": "^2.4.1",
|
||||||
|
"qs": "^6.15.2",
|
||||||
|
"raw-body": "^3.0.2",
|
||||||
|
"type-is": "^2.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@modelcontextprotocol/sdk/node_modules/body-parser/node_modules/content-type": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@modelcontextprotocol/sdk/node_modules/cookie-signature": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@modelcontextprotocol/sdk/node_modules/debug": {
|
||||||
|
"version": "4.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
|
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@modelcontextprotocol/sdk/node_modules/express": {
|
||||||
|
"version": "5.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
|
||||||
|
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"accepts": "^2.0.0",
|
||||||
|
"body-parser": "^2.2.1",
|
||||||
|
"content-disposition": "^1.0.0",
|
||||||
|
"content-type": "^1.0.5",
|
||||||
|
"cookie": "^0.7.1",
|
||||||
|
"cookie-signature": "^1.2.1",
|
||||||
|
"debug": "^4.4.0",
|
||||||
|
"depd": "^2.0.0",
|
||||||
|
"encodeurl": "^2.0.0",
|
||||||
|
"escape-html": "^1.0.3",
|
||||||
|
"etag": "^1.8.1",
|
||||||
|
"finalhandler": "^2.1.0",
|
||||||
|
"fresh": "^2.0.0",
|
||||||
|
"http-errors": "^2.0.0",
|
||||||
|
"merge-descriptors": "^2.0.0",
|
||||||
|
"mime-types": "^3.0.0",
|
||||||
|
"on-finished": "^2.4.1",
|
||||||
|
"once": "^1.4.0",
|
||||||
|
"parseurl": "^1.3.3",
|
||||||
|
"proxy-addr": "^2.0.7",
|
||||||
|
"qs": "^6.14.0",
|
||||||
|
"range-parser": "^1.2.1",
|
||||||
|
"router": "^2.2.0",
|
||||||
|
"send": "^1.1.0",
|
||||||
|
"serve-static": "^2.2.0",
|
||||||
|
"statuses": "^2.0.1",
|
||||||
|
"type-is": "^2.0.1",
|
||||||
|
"vary": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@modelcontextprotocol/sdk/node_modules/finalhandler": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "^4.4.0",
|
||||||
|
"encodeurl": "^2.0.0",
|
||||||
|
"escape-html": "^1.0.3",
|
||||||
|
"on-finished": "^2.4.1",
|
||||||
|
"parseurl": "^1.3.3",
|
||||||
|
"statuses": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@modelcontextprotocol/sdk/node_modules/fresh": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@modelcontextprotocol/sdk/node_modules/iconv-lite": {
|
||||||
|
"version": "0.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
|
||||||
|
"integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@modelcontextprotocol/sdk/node_modules/media-typer": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@modelcontextprotocol/sdk/node_modules/merge-descriptors": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@modelcontextprotocol/sdk/node_modules/mime-db": {
|
||||||
|
"version": "1.54.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
|
||||||
|
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@modelcontextprotocol/sdk/node_modules/mime-types": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": "^1.54.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@modelcontextprotocol/sdk/node_modules/ms": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@modelcontextprotocol/sdk/node_modules/negotiator": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@modelcontextprotocol/sdk/node_modules/raw-body": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bytes": "~3.1.2",
|
||||||
|
"http-errors": "~2.0.1",
|
||||||
|
"iconv-lite": "~0.7.0",
|
||||||
|
"unpipe": "~1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@modelcontextprotocol/sdk/node_modules/send": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "^4.4.3",
|
||||||
|
"encodeurl": "^2.0.0",
|
||||||
|
"escape-html": "^1.0.3",
|
||||||
|
"etag": "^1.8.1",
|
||||||
|
"fresh": "^2.0.0",
|
||||||
|
"http-errors": "^2.0.1",
|
||||||
|
"mime-types": "^3.0.2",
|
||||||
|
"ms": "^2.1.3",
|
||||||
|
"on-finished": "^2.4.1",
|
||||||
|
"range-parser": "^1.2.1",
|
||||||
|
"statuses": "^2.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@modelcontextprotocol/sdk/node_modules/serve-static": {
|
||||||
|
"version": "2.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
|
||||||
|
"integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"encodeurl": "^2.0.0",
|
||||||
|
"escape-html": "^1.0.3",
|
||||||
|
"parseurl": "^1.3.3",
|
||||||
|
"send": "^1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@modelcontextprotocol/sdk/node_modules/type-is": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"content-type": "^2.0.0",
|
||||||
|
"media-typer": "^1.1.0",
|
||||||
|
"mime-types": "^3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@modelcontextprotocol/sdk/node_modules/type-is/node_modules/content-type": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/body-parser": {
|
"node_modules/@types/body-parser": {
|
||||||
"version": "1.19.6",
|
"version": "1.19.6",
|
||||||
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz",
|
||||||
@@ -652,6 +1035,39 @@
|
|||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/ajv": {
|
||||||
|
"version": "8.20.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
|
||||||
|
"integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"fast-deep-equal": "^3.1.3",
|
||||||
|
"fast-uri": "^3.0.1",
|
||||||
|
"json-schema-traverse": "^1.0.0",
|
||||||
|
"require-from-string": "^2.0.2"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/epoberezkin"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ajv-formats": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ajv": "^8.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"ajv": "^8.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"ajv": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/append-field": {
|
"node_modules/append-field": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
|
||||||
@@ -847,6 +1263,20 @@
|
|||||||
"url": "https://opencollective.com/express"
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cross-spawn": {
|
||||||
|
"version": "7.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
|
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"path-key": "^3.1.0",
|
||||||
|
"shebang-command": "^2.0.0",
|
||||||
|
"which": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "2.6.9",
|
"version": "2.6.9",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||||
@@ -1027,6 +1457,27 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/eventsource": {
|
||||||
|
"version": "3.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz",
|
||||||
|
"integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"eventsource-parser": "^3.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/eventsource-parser": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/express": {
|
"node_modules/express": {
|
||||||
"version": "4.22.2",
|
"version": "4.22.2",
|
||||||
"resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz",
|
"resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz",
|
||||||
@@ -1073,6 +1524,46 @@
|
|||||||
"url": "https://opencollective.com/express"
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/express-rate-limit": {
|
||||||
|
"version": "8.5.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz",
|
||||||
|
"integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ip-address": "^10.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 16"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/express-rate-limit"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"express": ">= 4.11"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fast-deep-equal": {
|
||||||
|
"version": "3.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
|
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/fast-uri": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/fastify"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/fastify"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
"node_modules/finalhandler": {
|
"node_modules/finalhandler": {
|
||||||
"version": "1.3.2",
|
"version": "1.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
|
||||||
@@ -1257,6 +1748,15 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/hono": {
|
||||||
|
"version": "4.12.26",
|
||||||
|
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.26.tgz",
|
||||||
|
"integrity": "sha512-uyZtpnYxM9CmQ7QsQknM4zN8EftNqhON1qYeIKM0Se67CCEe2c44xyGURwB0axX2fBDu1dqHrHAc1hmNT8ITkw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/http-errors": {
|
"node_modules/http-errors": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
||||||
@@ -1331,6 +1831,15 @@
|
|||||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/ip-address": {
|
||||||
|
"version": "10.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz",
|
||||||
|
"integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ipaddr.js": {
|
"node_modules/ipaddr.js": {
|
||||||
"version": "1.9.1",
|
"version": "1.9.1",
|
||||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||||
@@ -1340,12 +1849,45 @@
|
|||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-promise": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/isarray": {
|
"node_modules/isarray": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/isexe": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/jose": {
|
||||||
|
"version": "6.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz",
|
||||||
|
"integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/panva"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/json-schema-traverse": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/json-schema-typed": {
|
||||||
|
"version": "8.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz",
|
||||||
|
"integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==",
|
||||||
|
"license": "BSD-2-Clause"
|
||||||
|
},
|
||||||
"node_modules/math-intrinsics": {
|
"node_modules/math-intrinsics": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
@@ -1503,6 +2045,15 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/once": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"wrappy": "1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/openai": {
|
"node_modules/openai": {
|
||||||
"version": "6.44.0",
|
"version": "6.44.0",
|
||||||
"resolved": "https://registry.npmjs.org/openai/-/openai-6.44.0.tgz",
|
"resolved": "https://registry.npmjs.org/openai/-/openai-6.44.0.tgz",
|
||||||
@@ -1530,12 +2081,30 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/path-key": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/path-to-regexp": {
|
"node_modules/path-to-regexp": {
|
||||||
"version": "0.1.13",
|
"version": "0.1.13",
|
||||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
|
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
|
||||||
"integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
|
"integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/pkce-challenge": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.20.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/process-nextick-args": {
|
"node_modules/process-nextick-args": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||||
@@ -1624,6 +2193,64 @@
|
|||||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/require-from-string": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/router": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "^4.4.0",
|
||||||
|
"depd": "^2.0.0",
|
||||||
|
"is-promise": "^4.0.0",
|
||||||
|
"parseurl": "^1.3.3",
|
||||||
|
"path-to-regexp": "^8.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/router/node_modules/debug": {
|
||||||
|
"version": "4.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
|
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/router/node_modules/ms": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/router/node_modules/path-to-regexp": {
|
||||||
|
"version": "8.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
|
||||||
|
"integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/safe-buffer": {
|
"node_modules/safe-buffer": {
|
||||||
"version": "5.2.1",
|
"version": "5.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||||
@@ -1701,6 +2328,27 @@
|
|||||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/shebang-command": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"shebang-regex": "^3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/shebang-regex": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/side-channel": {
|
"node_modules/side-channel": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
|
||||||
@@ -1906,6 +2554,27 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/which": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"isexe": "^2.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"node-which": "bin/node-which"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/wrappy": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/xtend": {
|
"node_modules/xtend": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||||
@@ -1914,6 +2583,24 @@
|
|||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.4"
|
"node": ">=0.4"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zod": {
|
||||||
|
"version": "4.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
|
||||||
|
"integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zod-to-json-schema": {
|
||||||
|
"version": "3.25.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz",
|
||||||
|
"integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"peerDependencies": {
|
||||||
|
"zod": "^3.25.28 || ^4"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
"start": "node dist/index.js"
|
"start": "node dist/index.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||||
"axios": "^1.7.0",
|
"axios": "^1.7.0",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
|
|||||||
BIN
backend/piper/ca_ES-upc_ona-medium.onnx
Normal file
BIN
backend/piper/ca_ES-upc_ona-medium.onnx
Normal file
Binary file not shown.
493
backend/piper/ca_ES-upc_ona-medium.onnx.json
Normal file
493
backend/piper/ca_ES-upc_ona-medium.onnx.json
Normal file
@@ -0,0 +1,493 @@
|
|||||||
|
{
|
||||||
|
"audio": {
|
||||||
|
"sample_rate": 22050,
|
||||||
|
"quality": "medium"
|
||||||
|
},
|
||||||
|
"espeak": {
|
||||||
|
"voice": "ca"
|
||||||
|
},
|
||||||
|
"inference": {
|
||||||
|
"noise_scale": 0.667,
|
||||||
|
"length_scale": 1,
|
||||||
|
"noise_w": 0.8
|
||||||
|
},
|
||||||
|
"phoneme_type": "espeak",
|
||||||
|
"phoneme_map": {},
|
||||||
|
"phoneme_id_map": {
|
||||||
|
"_": [
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"^": [
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"$": [
|
||||||
|
2
|
||||||
|
],
|
||||||
|
" ": [
|
||||||
|
3
|
||||||
|
],
|
||||||
|
"!": [
|
||||||
|
4
|
||||||
|
],
|
||||||
|
"'": [
|
||||||
|
5
|
||||||
|
],
|
||||||
|
"(": [
|
||||||
|
6
|
||||||
|
],
|
||||||
|
")": [
|
||||||
|
7
|
||||||
|
],
|
||||||
|
",": [
|
||||||
|
8
|
||||||
|
],
|
||||||
|
"-": [
|
||||||
|
9
|
||||||
|
],
|
||||||
|
".": [
|
||||||
|
10
|
||||||
|
],
|
||||||
|
":": [
|
||||||
|
11
|
||||||
|
],
|
||||||
|
";": [
|
||||||
|
12
|
||||||
|
],
|
||||||
|
"?": [
|
||||||
|
13
|
||||||
|
],
|
||||||
|
"a": [
|
||||||
|
14
|
||||||
|
],
|
||||||
|
"b": [
|
||||||
|
15
|
||||||
|
],
|
||||||
|
"c": [
|
||||||
|
16
|
||||||
|
],
|
||||||
|
"d": [
|
||||||
|
17
|
||||||
|
],
|
||||||
|
"e": [
|
||||||
|
18
|
||||||
|
],
|
||||||
|
"f": [
|
||||||
|
19
|
||||||
|
],
|
||||||
|
"h": [
|
||||||
|
20
|
||||||
|
],
|
||||||
|
"i": [
|
||||||
|
21
|
||||||
|
],
|
||||||
|
"j": [
|
||||||
|
22
|
||||||
|
],
|
||||||
|
"k": [
|
||||||
|
23
|
||||||
|
],
|
||||||
|
"l": [
|
||||||
|
24
|
||||||
|
],
|
||||||
|
"m": [
|
||||||
|
25
|
||||||
|
],
|
||||||
|
"n": [
|
||||||
|
26
|
||||||
|
],
|
||||||
|
"o": [
|
||||||
|
27
|
||||||
|
],
|
||||||
|
"p": [
|
||||||
|
28
|
||||||
|
],
|
||||||
|
"q": [
|
||||||
|
29
|
||||||
|
],
|
||||||
|
"r": [
|
||||||
|
30
|
||||||
|
],
|
||||||
|
"s": [
|
||||||
|
31
|
||||||
|
],
|
||||||
|
"t": [
|
||||||
|
32
|
||||||
|
],
|
||||||
|
"u": [
|
||||||
|
33
|
||||||
|
],
|
||||||
|
"v": [
|
||||||
|
34
|
||||||
|
],
|
||||||
|
"w": [
|
||||||
|
35
|
||||||
|
],
|
||||||
|
"x": [
|
||||||
|
36
|
||||||
|
],
|
||||||
|
"y": [
|
||||||
|
37
|
||||||
|
],
|
||||||
|
"z": [
|
||||||
|
38
|
||||||
|
],
|
||||||
|
"æ": [
|
||||||
|
39
|
||||||
|
],
|
||||||
|
"ç": [
|
||||||
|
40
|
||||||
|
],
|
||||||
|
"ð": [
|
||||||
|
41
|
||||||
|
],
|
||||||
|
"ø": [
|
||||||
|
42
|
||||||
|
],
|
||||||
|
"ħ": [
|
||||||
|
43
|
||||||
|
],
|
||||||
|
"ŋ": [
|
||||||
|
44
|
||||||
|
],
|
||||||
|
"œ": [
|
||||||
|
45
|
||||||
|
],
|
||||||
|
"ǀ": [
|
||||||
|
46
|
||||||
|
],
|
||||||
|
"ǁ": [
|
||||||
|
47
|
||||||
|
],
|
||||||
|
"ǂ": [
|
||||||
|
48
|
||||||
|
],
|
||||||
|
"ǃ": [
|
||||||
|
49
|
||||||
|
],
|
||||||
|
"ɐ": [
|
||||||
|
50
|
||||||
|
],
|
||||||
|
"ɑ": [
|
||||||
|
51
|
||||||
|
],
|
||||||
|
"ɒ": [
|
||||||
|
52
|
||||||
|
],
|
||||||
|
"ɓ": [
|
||||||
|
53
|
||||||
|
],
|
||||||
|
"ɔ": [
|
||||||
|
54
|
||||||
|
],
|
||||||
|
"ɕ": [
|
||||||
|
55
|
||||||
|
],
|
||||||
|
"ɖ": [
|
||||||
|
56
|
||||||
|
],
|
||||||
|
"ɗ": [
|
||||||
|
57
|
||||||
|
],
|
||||||
|
"ɘ": [
|
||||||
|
58
|
||||||
|
],
|
||||||
|
"ə": [
|
||||||
|
59
|
||||||
|
],
|
||||||
|
"ɚ": [
|
||||||
|
60
|
||||||
|
],
|
||||||
|
"ɛ": [
|
||||||
|
61
|
||||||
|
],
|
||||||
|
"ɜ": [
|
||||||
|
62
|
||||||
|
],
|
||||||
|
"ɞ": [
|
||||||
|
63
|
||||||
|
],
|
||||||
|
"ɟ": [
|
||||||
|
64
|
||||||
|
],
|
||||||
|
"ɠ": [
|
||||||
|
65
|
||||||
|
],
|
||||||
|
"ɡ": [
|
||||||
|
66
|
||||||
|
],
|
||||||
|
"ɢ": [
|
||||||
|
67
|
||||||
|
],
|
||||||
|
"ɣ": [
|
||||||
|
68
|
||||||
|
],
|
||||||
|
"ɤ": [
|
||||||
|
69
|
||||||
|
],
|
||||||
|
"ɥ": [
|
||||||
|
70
|
||||||
|
],
|
||||||
|
"ɦ": [
|
||||||
|
71
|
||||||
|
],
|
||||||
|
"ɧ": [
|
||||||
|
72
|
||||||
|
],
|
||||||
|
"ɨ": [
|
||||||
|
73
|
||||||
|
],
|
||||||
|
"ɪ": [
|
||||||
|
74
|
||||||
|
],
|
||||||
|
"ɫ": [
|
||||||
|
75
|
||||||
|
],
|
||||||
|
"ɬ": [
|
||||||
|
76
|
||||||
|
],
|
||||||
|
"ɭ": [
|
||||||
|
77
|
||||||
|
],
|
||||||
|
"ɮ": [
|
||||||
|
78
|
||||||
|
],
|
||||||
|
"ɯ": [
|
||||||
|
79
|
||||||
|
],
|
||||||
|
"ɰ": [
|
||||||
|
80
|
||||||
|
],
|
||||||
|
"ɱ": [
|
||||||
|
81
|
||||||
|
],
|
||||||
|
"ɲ": [
|
||||||
|
82
|
||||||
|
],
|
||||||
|
"ɳ": [
|
||||||
|
83
|
||||||
|
],
|
||||||
|
"ɴ": [
|
||||||
|
84
|
||||||
|
],
|
||||||
|
"ɵ": [
|
||||||
|
85
|
||||||
|
],
|
||||||
|
"ɶ": [
|
||||||
|
86
|
||||||
|
],
|
||||||
|
"ɸ": [
|
||||||
|
87
|
||||||
|
],
|
||||||
|
"ɹ": [
|
||||||
|
88
|
||||||
|
],
|
||||||
|
"ɺ": [
|
||||||
|
89
|
||||||
|
],
|
||||||
|
"ɻ": [
|
||||||
|
90
|
||||||
|
],
|
||||||
|
"ɽ": [
|
||||||
|
91
|
||||||
|
],
|
||||||
|
"ɾ": [
|
||||||
|
92
|
||||||
|
],
|
||||||
|
"ʀ": [
|
||||||
|
93
|
||||||
|
],
|
||||||
|
"ʁ": [
|
||||||
|
94
|
||||||
|
],
|
||||||
|
"ʂ": [
|
||||||
|
95
|
||||||
|
],
|
||||||
|
"ʃ": [
|
||||||
|
96
|
||||||
|
],
|
||||||
|
"ʄ": [
|
||||||
|
97
|
||||||
|
],
|
||||||
|
"ʈ": [
|
||||||
|
98
|
||||||
|
],
|
||||||
|
"ʉ": [
|
||||||
|
99
|
||||||
|
],
|
||||||
|
"ʊ": [
|
||||||
|
100
|
||||||
|
],
|
||||||
|
"ʋ": [
|
||||||
|
101
|
||||||
|
],
|
||||||
|
"ʌ": [
|
||||||
|
102
|
||||||
|
],
|
||||||
|
"ʍ": [
|
||||||
|
103
|
||||||
|
],
|
||||||
|
"ʎ": [
|
||||||
|
104
|
||||||
|
],
|
||||||
|
"ʏ": [
|
||||||
|
105
|
||||||
|
],
|
||||||
|
"ʐ": [
|
||||||
|
106
|
||||||
|
],
|
||||||
|
"ʑ": [
|
||||||
|
107
|
||||||
|
],
|
||||||
|
"ʒ": [
|
||||||
|
108
|
||||||
|
],
|
||||||
|
"ʔ": [
|
||||||
|
109
|
||||||
|
],
|
||||||
|
"ʕ": [
|
||||||
|
110
|
||||||
|
],
|
||||||
|
"ʘ": [
|
||||||
|
111
|
||||||
|
],
|
||||||
|
"ʙ": [
|
||||||
|
112
|
||||||
|
],
|
||||||
|
"ʛ": [
|
||||||
|
113
|
||||||
|
],
|
||||||
|
"ʜ": [
|
||||||
|
114
|
||||||
|
],
|
||||||
|
"ʝ": [
|
||||||
|
115
|
||||||
|
],
|
||||||
|
"ʟ": [
|
||||||
|
116
|
||||||
|
],
|
||||||
|
"ʡ": [
|
||||||
|
117
|
||||||
|
],
|
||||||
|
"ʢ": [
|
||||||
|
118
|
||||||
|
],
|
||||||
|
"ʲ": [
|
||||||
|
119
|
||||||
|
],
|
||||||
|
"ˈ": [
|
||||||
|
120
|
||||||
|
],
|
||||||
|
"ˌ": [
|
||||||
|
121
|
||||||
|
],
|
||||||
|
"ː": [
|
||||||
|
122
|
||||||
|
],
|
||||||
|
"ˑ": [
|
||||||
|
123
|
||||||
|
],
|
||||||
|
"˞": [
|
||||||
|
124
|
||||||
|
],
|
||||||
|
"β": [
|
||||||
|
125
|
||||||
|
],
|
||||||
|
"θ": [
|
||||||
|
126
|
||||||
|
],
|
||||||
|
"χ": [
|
||||||
|
127
|
||||||
|
],
|
||||||
|
"ᵻ": [
|
||||||
|
128
|
||||||
|
],
|
||||||
|
"ⱱ": [
|
||||||
|
129
|
||||||
|
],
|
||||||
|
"0": [
|
||||||
|
130
|
||||||
|
],
|
||||||
|
"1": [
|
||||||
|
131
|
||||||
|
],
|
||||||
|
"2": [
|
||||||
|
132
|
||||||
|
],
|
||||||
|
"3": [
|
||||||
|
133
|
||||||
|
],
|
||||||
|
"4": [
|
||||||
|
134
|
||||||
|
],
|
||||||
|
"5": [
|
||||||
|
135
|
||||||
|
],
|
||||||
|
"6": [
|
||||||
|
136
|
||||||
|
],
|
||||||
|
"7": [
|
||||||
|
137
|
||||||
|
],
|
||||||
|
"8": [
|
||||||
|
138
|
||||||
|
],
|
||||||
|
"9": [
|
||||||
|
139
|
||||||
|
],
|
||||||
|
"̧": [
|
||||||
|
140
|
||||||
|
],
|
||||||
|
"̃": [
|
||||||
|
141
|
||||||
|
],
|
||||||
|
"̪": [
|
||||||
|
142
|
||||||
|
],
|
||||||
|
"̯": [
|
||||||
|
143
|
||||||
|
],
|
||||||
|
"̩": [
|
||||||
|
144
|
||||||
|
],
|
||||||
|
"ʰ": [
|
||||||
|
145
|
||||||
|
],
|
||||||
|
"ˤ": [
|
||||||
|
146
|
||||||
|
],
|
||||||
|
"ε": [
|
||||||
|
147
|
||||||
|
],
|
||||||
|
"↓": [
|
||||||
|
148
|
||||||
|
],
|
||||||
|
"#": [
|
||||||
|
149
|
||||||
|
],
|
||||||
|
"\"": [
|
||||||
|
150
|
||||||
|
],
|
||||||
|
"↑": [
|
||||||
|
151
|
||||||
|
],
|
||||||
|
"̺": [
|
||||||
|
152
|
||||||
|
],
|
||||||
|
"̻": [
|
||||||
|
153
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"num_symbols": 256,
|
||||||
|
"num_speakers": 1,
|
||||||
|
"speaker_id_map": {},
|
||||||
|
"piper_version": "1.0.0",
|
||||||
|
"language": {
|
||||||
|
"code": "ca_ES",
|
||||||
|
"family": "ca",
|
||||||
|
"region": "ES",
|
||||||
|
"name_native": "Català",
|
||||||
|
"name_english": "Catalan",
|
||||||
|
"country_english": "Spain"
|
||||||
|
},
|
||||||
|
"dataset": "upc_ona"
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ let _raspberryPort = Number(process.env.RASPBERRY_PI_PORT) || 8000;
|
|||||||
let _token = process.env.QUIBOT_TOKEN ?? 'MY_SECRET_TOKEN';
|
let _token = process.env.QUIBOT_TOKEN ?? 'MY_SECRET_TOKEN';
|
||||||
const APP_PORT = Number(process.env.PORT) || 5000;
|
const APP_PORT = Number(process.env.PORT) || 5000;
|
||||||
const piperUrl = process.env.PIPER_URL ?? '';
|
const piperUrl = process.env.PIPER_URL ?? '';
|
||||||
|
const mcpUrl = process.env.MCP_URL ?? '';
|
||||||
const llamacppUrl = process.env.LLAMA_CPP_URL ?? '';
|
const llamacppUrl = process.env.LLAMA_CPP_URL ?? '';
|
||||||
const llamacppApiKey = process.env.LLAMA_API_KEY ?? '';
|
const llamacppApiKey = process.env.LLAMA_API_KEY ?? '';
|
||||||
const llamaPreambleRaw = process.env.LLAMA_PREAMBLE ?? '';
|
const llamaPreambleRaw = process.env.LLAMA_PREAMBLE ?? '';
|
||||||
@@ -45,8 +46,11 @@ export const getLlamacppApiKey = () => llamacppApiKey;
|
|||||||
export const getLlamacppPreamble = () => llamacppPreamble;
|
export const getLlamacppPreamble = () => llamacppPreamble;
|
||||||
|
|
||||||
export const getPiperUrl = () => piperUrl;
|
export const getPiperUrl = () => piperUrl;
|
||||||
|
export const getPiperModelDir = () =>
|
||||||
|
process.env.PIPER_MODELS_DIR || join('/tmp', 'quibot-piper-models');
|
||||||
export const getPiperModel = () =>
|
export const getPiperModel = () =>
|
||||||
process.env.PIPER_MODEL ||
|
process.env.PIPER_MODEL ||
|
||||||
join('/tmp', 'quibot-piper-models', 'ca_ES-upc_ona-medium.onnx');
|
join(getPiperModelDir(), 'ca_ES-upc_ona-medium.onnx');
|
||||||
|
export const getMcpUrl = () => mcpUrl;
|
||||||
|
|
||||||
export const getAppPort = () => APP_PORT;
|
export const getAppPort = () => APP_PORT;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import router from './routes/router.js';
|
|||||||
import { getAppPort, getConfig } from './config.js';
|
import { getAppPort, getConfig } from './config.js';
|
||||||
import { whisperService } from './services/whisper.service.js';
|
import { whisperService } from './services/whisper.service.js';
|
||||||
import { piperService as piperWorker } from './services/piper.service.js';
|
import { piperService as piperWorker } from './services/piper.service.js';
|
||||||
|
import { mcpClient } from './services/mcpClient.service.js';
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
@@ -25,16 +26,22 @@ app.get('/health', (_req, res) => {
|
|||||||
res.json({ status: 'ok', settings });
|
res.json({ status: 'ok', settings });
|
||||||
});
|
});
|
||||||
|
|
||||||
const server = app.listen(getAppPort(), () => {
|
const server = app.listen(getAppPort(), async () => {
|
||||||
console.log(`QuiBot backend listening on port ${getAppPort()}`);
|
console.log(`QuiBot backend listening on port ${getAppPort()}`);
|
||||||
whisperService.spawn();
|
whisperService.spawn();
|
||||||
piperWorker.initWav().catch(() => { /* model may not exist yet → lazy init on first TTS call */ });
|
piperWorker.initWav().catch(() => { /* model may not exist yet → lazy init on first TTS call */ });
|
||||||
|
try {
|
||||||
|
await mcpClient.start();
|
||||||
|
console.log('[server] MCP client started');
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[server] MCP client failed to start: ${err instanceof Error ? err.message : String(err)}`);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
async function shutdown(signal: string) {
|
async function shutdown(signal: string) {
|
||||||
console.log(`[server] ${signal} received, shutting down...`);
|
console.log(`[server] ${signal} received, shutting down...`);
|
||||||
server.close(async () => {
|
server.close(async () => {
|
||||||
await Promise.all([whisperService.shutdown(), piperWorker.shutdown()]);
|
await Promise.all([whisperService.shutdown(), piperWorker.shutdown(), mcpClient.shutdown()]);
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Persistent Piper TTS worker – single subprocess, model loaded once."""
|
"""Persistent Piper TTS worker – single subprocess, model loaded once."""
|
||||||
|
|
||||||
|
import os
|
||||||
import sys
|
import sys
|
||||||
import json
|
import json
|
||||||
import wave
|
import wave
|
||||||
@@ -32,8 +33,9 @@ def main():
|
|||||||
voice = None
|
voice = None
|
||||||
|
|
||||||
# Defaults (will be overridden by init message)
|
# Defaults (will be overridden by init message)
|
||||||
DEFAULT_MODEL = "/tmp/quibot-piper-models/ca_ES-upc_ona-medium.onnx"
|
_default_dir = os.environ.get("PIPER_MODELS_DIR", "/tmp/quibot-piper-models")
|
||||||
DEFAULT_CONFIG = "/tmp/quibot-piper-models/ca_ES-upc_ona-medium.onnx.json"
|
DEFAULT_MODEL = os.path.join(_default_dir, "ca_ES-upc_ona-medium.onnx")
|
||||||
|
DEFAULT_CONFIG = os.path.join(_default_dir, "ca_ES-upc_ona-medium.onnx.json")
|
||||||
|
|
||||||
# Signal node that the process is alive and listening
|
# Signal node that the process is alive and listening
|
||||||
print(json.dumps({"type": "ready"}), flush=True)
|
print(json.dumps({"type": "ready"}), flush=True)
|
||||||
@@ -62,6 +64,10 @@ def main():
|
|||||||
elif msg.get("type") == "synthesize":
|
elif msg.get("type") == "synthesize":
|
||||||
text = msg.get("text", "")
|
text = msg.get("text", "")
|
||||||
msg_id = msg.get("msgId", "")
|
msg_id = msg.get("msgId", "")
|
||||||
|
|
||||||
|
# NEW: output file path from message
|
||||||
|
out_path = msg.get("outPath")
|
||||||
|
|
||||||
if not voice:
|
if not voice:
|
||||||
print(json.dumps({
|
print(json.dumps({
|
||||||
"type": "error",
|
"type": "error",
|
||||||
@@ -70,25 +76,38 @@ def main():
|
|||||||
}), flush=True)
|
}), flush=True)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
if not out_path:
|
||||||
|
print(json.dumps({
|
||||||
|
"type": "error",
|
||||||
|
"text": "Missing outPath",
|
||||||
|
"msgId": msg_id,
|
||||||
|
}), flush=True)
|
||||||
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
import io
|
||||||
|
import wave
|
||||||
|
|
||||||
buf = io.BytesIO()
|
buf = io.BytesIO()
|
||||||
wf = wave.open(buf, 'wb')
|
wf = wave.open(buf, 'wb')
|
||||||
# set_wav_config is called inside synthesize_wav via set_wav_format=True (default)
|
|
||||||
voice.synthesize_wav(text, wf)
|
voice.synthesize_wav(text, wf)
|
||||||
wf.close()
|
wf.close()
|
||||||
|
|
||||||
wav_bytes = buf.getvalue()
|
wav_bytes = buf.getvalue()
|
||||||
|
|
||||||
# Frame: send length prefix as ASCII number + newline, then raw bytes
|
# Write to file instead of stdout
|
||||||
header = f"{len(wav_bytes)}\n".encode('ascii')
|
os.makedirs(os.path.dirname(out_path) or ".", exist_ok=True)
|
||||||
sys.stdout.buffer.write(header)
|
with open(out_path, "wb") as f:
|
||||||
sys.stdout.buffer.write(wav_bytes)
|
f.write(wav_bytes)
|
||||||
sys.stdout.buffer.flush()
|
|
||||||
|
|
||||||
print(json.dumps({
|
print(json.dumps({
|
||||||
"type": "synthesized",
|
"type": "synthesized",
|
||||||
"bytes": len(wav_bytes),
|
"bytes": len(wav_bytes),
|
||||||
"msgId": msg_id,
|
"msgId": msg_id,
|
||||||
|
"outPath": out_path
|
||||||
}), flush=True)
|
}), flush=True)
|
||||||
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
err_msg = str(exc).replace('"', '\\"')
|
err_msg = str(exc).replace('"', '\\"')
|
||||||
print(json.dumps({
|
print(json.dumps({
|
||||||
@@ -97,6 +116,5 @@ def main():
|
|||||||
"msgId": msg_id,
|
"msgId": msg_id,
|
||||||
}), flush=True)
|
}), flush=True)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
74
backend/src/services/mcp.http.service.ts
Normal file
74
backend/src/services/mcp.http.service.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import { getMcpUrl } from '../config';
|
||||||
|
|
||||||
|
class McpHttpService {
|
||||||
|
private sessionId: string | null = null;
|
||||||
|
|
||||||
|
async callTool(name: string, args: Record<string, unknown>): Promise<{ text: string; isError?: boolean }> {
|
||||||
|
const baseUrl = getMcpUrl();
|
||||||
|
if (!baseUrl) {
|
||||||
|
throw new Error('MCP HTTP service not configured (set MCP_URL env var)');
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `${baseUrl}/mcp`;
|
||||||
|
|
||||||
|
if (!this.sessionId) {
|
||||||
|
const initRes = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
id: 1,
|
||||||
|
method: 'initialize',
|
||||||
|
params: {
|
||||||
|
protocolVersion: '2025-03-26',
|
||||||
|
capabilities: {},
|
||||||
|
clientInfo: { name: 'quibot-backend', version: '1.0.0' },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const initData = await initRes.json();
|
||||||
|
this.sessionId = String(initData.sessionId || initData.result?.sessionId);
|
||||||
|
|
||||||
|
await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
id: 2,
|
||||||
|
method: 'notifications/initialized',
|
||||||
|
}),
|
||||||
|
...(this.sessionId && { headers: { 'Mcp-SessionId': this.sessionId } }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(this.sessionId && { 'Mcp-SessionId': this.sessionId }),
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
jsonrpc: '2.0',
|
||||||
|
id: Date.now(),
|
||||||
|
method: 'tools/call',
|
||||||
|
params: { name, arguments: args },
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.error) {
|
||||||
|
return { text: JSON.stringify(data.error), isError: true };
|
||||||
|
}
|
||||||
|
const content = data.result?.content?.[0];
|
||||||
|
if (!content?.text) {
|
||||||
|
throw new Error('MCP tool returned no content');
|
||||||
|
}
|
||||||
|
return { text: content.text };
|
||||||
|
}
|
||||||
|
|
||||||
|
async shutdown(): Promise<void> {
|
||||||
|
this.sessionId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const mcpHttpService = new McpHttpService();
|
||||||
202
backend/src/services/mcpClient.service.ts
Normal file
202
backend/src/services/mcpClient.service.ts
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
import { spawn, ChildProcess } from 'child_process';
|
||||||
|
import { join } from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import { getMcpUrl } from '../config';
|
||||||
|
import { mcpHttpService } from './mcp.http.service';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = join(__filename, '..');
|
||||||
|
|
||||||
|
// Path to the compiled MCP server (two levels up from backend/src/)
|
||||||
|
const MCP_BIN = join(__dirname, '..', '..', 'mcp', 'dist', 'index.js');
|
||||||
|
|
||||||
|
let _proc: ChildProcess | null = null;
|
||||||
|
let nextId = 1;
|
||||||
|
let pending = new Map<number | string, { resolve: (v: unknown) => void; reject: (e: Error) => void }>();
|
||||||
|
|
||||||
|
function send(msg: Record<string, unknown>): number {
|
||||||
|
const id = nextId++;
|
||||||
|
_proc!.stdin!.write(JSON.stringify({ jsonrpc: '2.0', id, ...msg }) + '\n');
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const mcpClient = {
|
||||||
|
async start(): Promise<void> {
|
||||||
|
const hasMcpBin = (() => {
|
||||||
|
try {
|
||||||
|
require('fs').accessSync(MCP_BIN);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (!hasMcpBin) {
|
||||||
|
const url = getMcpUrl();
|
||||||
|
if (url) {
|
||||||
|
console.log('[mcp] Local MCP binary not found, using HTTP service at', url);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new Error('MCP local binary and HTTP URL both unavailable');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_proc) return;
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
_proc = spawn('node', [MCP_BIN], { stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env } });
|
||||||
|
|
||||||
|
_proc.stdout!.on('data', (chunk: Buffer) => {
|
||||||
|
const text = chunk.toString();
|
||||||
|
for (const line of text.split('\n')) {
|
||||||
|
if (!line.trim()) continue;
|
||||||
|
let parsed: { jsonrpc?: string; id?: number | string; method?: string; result?: unknown; error?: unknown };
|
||||||
|
try { parsed = JSON.parse(line); } catch { continue; }
|
||||||
|
if (parsed.jsonrpc !== '2.0') continue;
|
||||||
|
if (parsed.method) {
|
||||||
|
// notifications or responses without matching id — ignore for now
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!parsed.id) continue;
|
||||||
|
const p = pending.get(parsed.id);
|
||||||
|
if (!p) continue;
|
||||||
|
pending.delete(parsed.id);
|
||||||
|
if (parsed.error) {
|
||||||
|
p.reject(new Error(`MCP error: ${JSON.stringify(parsed.error)}`));
|
||||||
|
} else {
|
||||||
|
p.resolve(parsed.result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
_proc.stderr!.on('data', (chunk: Buffer) => {
|
||||||
|
console.log(`[mcp-client] stderr: ${chunk.toString().trim()}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
_proc.on('exit', (code, signal) => {
|
||||||
|
console.error(`[mcp-client] Exited code=${code} signal=${signal}`);
|
||||||
|
_proc = null;
|
||||||
|
for (const [, p] of pending) {
|
||||||
|
p.reject(new Error('MCP client process exited'));
|
||||||
|
}
|
||||||
|
pending.clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
_proc.on('error', (err: Error) => {
|
||||||
|
console.error(`[mcp-client] Error: ${err.message}`);
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send initialize request
|
||||||
|
const initId = send({
|
||||||
|
method: 'initialize',
|
||||||
|
params: {
|
||||||
|
protocolVersion: '2025-03-26',
|
||||||
|
capabilities: {},
|
||||||
|
clientInfo: { name: 'quibot-backend', version: '1.0.0' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
pending.set(initId, {
|
||||||
|
resolve: () => {
|
||||||
|
// Send initialized notification
|
||||||
|
_proc!.stdin!.write(
|
||||||
|
JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' }) + '\n',
|
||||||
|
);
|
||||||
|
resolve();
|
||||||
|
},
|
||||||
|
reject,
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
const p = pending.get(initId);
|
||||||
|
if (p) {
|
||||||
|
pending.delete(initId);
|
||||||
|
p.reject(new Error('MCP initialize timed out'));
|
||||||
|
}
|
||||||
|
}, 15_000);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async callTool(name: string, args: Record<string, unknown>): Promise<{ text: string; isError?: boolean }> {
|
||||||
|
if (!_proc) {
|
||||||
|
await this.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await this._callToolLocal(name, args);
|
||||||
|
} catch (localErr) {
|
||||||
|
const url = getMcpUrl();
|
||||||
|
if (url) {
|
||||||
|
console.log(`[mcp] Local MCP failed: ${localErr instanceof Error ? localErr.message : localErr}. Falling back to HTTP service.`);
|
||||||
|
return await mcpHttpService.callTool(name, args);
|
||||||
|
}
|
||||||
|
throw localErr;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async _callToolLocal(name: string, args: Record<string, unknown>): Promise<{ text: string; isError?: boolean }> {
|
||||||
|
if (!_proc?.stdin) {
|
||||||
|
throw new Error('MCP client not ready');
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const id = send({
|
||||||
|
method: 'tools/call',
|
||||||
|
params: { name, arguments: args },
|
||||||
|
});
|
||||||
|
|
||||||
|
let cleared = false;
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
if (cleared) return;
|
||||||
|
cleared = true;
|
||||||
|
pending.delete(id);
|
||||||
|
reject(new Error(`MCP tool "${name}" timed out`));
|
||||||
|
}, 180_000);
|
||||||
|
|
||||||
|
pending.set(id, {
|
||||||
|
resolve: (result: unknown) => {
|
||||||
|
if (cleared) return;
|
||||||
|
cleared = true;
|
||||||
|
clearTimeout(timer);
|
||||||
|
pending.delete(id);
|
||||||
|
const res = result as { content?: Array<{ type: string; text?: string }> };
|
||||||
|
if (res?.content?.[0]?.text !== undefined) {
|
||||||
|
resolve({ text: res.content[0].text });
|
||||||
|
} else {
|
||||||
|
reject(new Error('MCP tool returned no content'));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
reject: (err: Error) => {
|
||||||
|
if (cleared) return;
|
||||||
|
cleared = true;
|
||||||
|
clearTimeout(timer);
|
||||||
|
reject(err);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async shutdown(): Promise<void> {
|
||||||
|
if (!_proc) return;
|
||||||
|
_proc.kill('SIGTERM');
|
||||||
|
const current = _proc;
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
let done = false;
|
||||||
|
const cleanup = () => {
|
||||||
|
if (done) return;
|
||||||
|
done = true;
|
||||||
|
_proc = null;
|
||||||
|
for (const [, p] of pending) {
|
||||||
|
p.reject(new Error('MCP client shut down'));
|
||||||
|
}
|
||||||
|
pending.clear();
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
current.once('exit', () => cleanup());
|
||||||
|
setTimeout(() => {
|
||||||
|
const proc = _proc;
|
||||||
|
if (proc && !proc.killed) proc.kill('SIGKILL');
|
||||||
|
cleanup();
|
||||||
|
}, 3000);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -18,7 +18,7 @@ type PiperMsg =
|
|||||||
| { type: 'ready' }
|
| { type: 'ready' }
|
||||||
| { type: 'init_ok' }
|
| { type: 'init_ok' }
|
||||||
| { type: 'init_error'; error: string }
|
| { type: 'init_error'; error: string }
|
||||||
| { type: 'synthesized'; wavPath: string; bytes?: number; msgId: string }
|
| { type: 'synthesized'; outPath: string; bytes?: number; msgId: string }
|
||||||
| { type: 'error'; text: string; msgId: string };
|
| { type: 'error'; text: string; msgId: string };
|
||||||
|
|
||||||
class PiperLocalService {
|
class PiperLocalService {
|
||||||
@@ -45,41 +45,95 @@ class PiperLocalService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private handleLine(line: string): void {
|
private handleLine(line: string): void {
|
||||||
try { const msg = JSON.parse(line) as PiperMsg; /* handled below */ } catch { return; }
|
console.log('[RX]', line);
|
||||||
const msg = JSON.parse(line) as PiperMsg;
|
let msg: PiperMsg;
|
||||||
if (msg.type === 'ready') return;
|
|
||||||
if (msg.type === 'init_ok') { this.resolveInit(); }
|
try {
|
||||||
if (msg.type === 'init_error') {
|
msg = JSON.parse(line) as PiperMsg;
|
||||||
if (this.pendingInit) { this.pendingInit = null; this.initResolve = null; }
|
} catch {
|
||||||
this.initReject(new Error(msg.error));
|
console.warn('[piper-svc] Invalid JSON:', line);
|
||||||
}
|
return;
|
||||||
if (msg.type === 'synthesized') {
|
|
||||||
this.resolveResponse(msg.msgId, msg.wavPath);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switch (msg.type) {
|
||||||
|
case 'ready':
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'init_ok':
|
||||||
|
this.resolveInit();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'init_error':
|
||||||
|
this.resolveInitError(new Error(msg.error));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'synthesized':
|
||||||
|
this.resolveResponse(msg.msgId, msg.outPath);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'error':
|
||||||
|
this.rejectResponse(
|
||||||
|
msg.msgId,
|
||||||
|
new Error(msg.text)
|
||||||
|
);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async writeStdin(line: string): Promise<void> {
|
private async writeStdin(line: string): Promise<void> {
|
||||||
if (!this.proc?.stdin) throw new Error('piper-svc: stdin unavailable');
|
if (!this.proc?.stdin) {
|
||||||
this.proc.stdin.write(line + '\n');
|
throw new Error('piper-svc: stdin unavailable');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── pending-init promises (simplest possible design) ──
|
console.log('[TX]', line);
|
||||||
|
|
||||||
|
this.proc.stdin.write(line + '\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── pending-init promises (separate from synth to avoid clearing respMap on init failure) ──
|
||||||
private initResolve: (() => void) | null = null;
|
private initResolve: (() => void) | null = null;
|
||||||
private initReject: (e: Error) => void = () => { throw new Error('no reject handler'); };
|
private initReject: ((e: Error) => void) | null = null;
|
||||||
|
|
||||||
private resolveInit(): void {
|
private resolveInit(): void {
|
||||||
if (!this.pendingInit) return;
|
if (!this.pendingInit) return;
|
||||||
this.initResolve?.(); this.pendingInit = null;
|
|
||||||
this.initResolve = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── pending synth responses ──
|
this.initResolve?.();
|
||||||
private respMap = new Map<string, (wavPath: string) => void>();
|
|
||||||
|
this.pendingInit = null;
|
||||||
|
this.initResolve = null;
|
||||||
|
this.initReject = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private rejectResponse(msgId: string, err: Error): void {
|
||||||
|
const entry = this.respMap.get(msgId);
|
||||||
|
|
||||||
|
this.respMap.delete(msgId);
|
||||||
|
|
||||||
|
if (entry) {
|
||||||
|
entry.reject(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private resolveInitError(err: Error): void {
|
||||||
|
if (!this.pendingInit) return;
|
||||||
|
|
||||||
|
this.initReject?.(err);
|
||||||
|
|
||||||
|
this.pendingInit = null;
|
||||||
|
this.initResolve = null;
|
||||||
|
this.initReject = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── pending synth responses (separate from init so init failure doesn't clear them) ──
|
||||||
|
private respMap = new Map<string, {
|
||||||
|
resolve: (wavPath: string) => void;
|
||||||
|
reject: (e: Error) => void;
|
||||||
|
}>();
|
||||||
|
|
||||||
private resolveResponse(msgId: string, wavPath: string): void {
|
private resolveResponse(msgId: string, wavPath: string): void {
|
||||||
const fn = this.respMap.get(msgId);
|
const entry = this.respMap.get(msgId);
|
||||||
this.respMap.delete(msgId);
|
this.respMap.delete(msgId);
|
||||||
if (fn) fn(wavPath);
|
if (entry?.resolve) entry.resolve(wavPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── public spawn / initWav / synthWav ──
|
// ── public spawn / initWav / synthWav ──
|
||||||
@@ -87,21 +141,31 @@ class PiperLocalService {
|
|||||||
private async _spawn(): Promise<void> {
|
private async _spawn(): Promise<void> {
|
||||||
if (this.proc) return;
|
if (this.proc) return;
|
||||||
|
|
||||||
const workerPath = join(SCRIPT_DIR, 'src', 'piper-worker.py');
|
const workerPath = join(SCRIPT_DIR, 'piper-worker.py');
|
||||||
const venv = process.env.VIRTUAL_ENV || join(SCRIPT_DIR, '.venv', 'bin', 'python3');
|
const venv = process.env.VIRTUAL_ENV || join(SCRIPT_DIR, '..', '.venv', 'bin', 'python3');
|
||||||
this.proc = spawn(venv, [workerPath], { stdio: ['pipe', 'pipe', 'pipe'] });
|
this.proc = spawn(venv, [workerPath], { stdio: ['pipe', 'pipe', 'pipe'] });
|
||||||
|
|
||||||
this.setupStdout();
|
this.setupStdout();
|
||||||
|
|
||||||
|
if (this.proc.stderr) {
|
||||||
|
this.proc.stderr.on('data', (chunk: Buffer) => {
|
||||||
|
console.error(
|
||||||
|
'[piper-worker]',
|
||||||
|
chunk.toString().trim()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.proc.on('exit', () => {
|
this.proc.on('exit', () => {
|
||||||
// reject all pending
|
console.log('[piper-svc] Process exited, rejecting all pending synths');
|
||||||
for (const [, fn] of this.respMap) fn('');
|
// reject all pending (new format: {resolve, reject})
|
||||||
|
for (const [, entry] of this.respMap) entry.reject(new Error('piper process exited'));
|
||||||
this.respMap.clear();
|
this.respMap.clear();
|
||||||
if (this.pendingInit) { this.initReject(new Error('piper process exited')); this.pendingInit = null; }
|
if (this.pendingInit) { this.initReject(new Error('piper process exited')); this.pendingInit = null; }
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── cleanup old WAV files every 5 min ──
|
// ── cleanup old WAV files every 5 min ──
|
||||||
const timer = setInterval(() => {
|
this.cleanupTimer = setInterval(() => {
|
||||||
try {
|
try {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
for (const entry of readdirSync(TTS_DIR)) {
|
for (const entry of readdirSync(TTS_DIR)) {
|
||||||
@@ -120,17 +184,9 @@ class PiperLocalService {
|
|||||||
if (this.pendingInit) return this.pendingInit;
|
if (this.pendingInit) return this.pendingInit;
|
||||||
|
|
||||||
this.pendingInit = new Promise<void>((resolve, reject) => {
|
this.pendingInit = new Promise<void>((resolve, reject) => {
|
||||||
// override the global reject
|
|
||||||
const origReject = this.initReject;
|
|
||||||
this.initResolve = resolve;
|
this.initResolve = resolve;
|
||||||
this.initReject = (e: Error) => {
|
this.initReject = reject;
|
||||||
// cleanup
|
|
||||||
for (const [, fn] of this.respMap) fn('');
|
|
||||||
this.respMap.clear();
|
|
||||||
reject(e);
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const modelPath = getPiperModel() || join('/tmp', 'quibot-piper-models', 'ca_ES-upc_ona-medium.onnx');
|
const modelPath = getPiperModel() || join('/tmp', 'quibot-piper-models', 'ca_ES-upc_ona-medium.onnx');
|
||||||
const cfgPath = modelPath.replace(/\.onnx$/, '.onnx.json');
|
const cfgPath = modelPath.replace(/\.onnx$/, '.onnx.json');
|
||||||
await this.writeStdin(
|
await this.writeStdin(
|
||||||
@@ -155,8 +211,10 @@ class PiperLocalService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async synthWav(text: string): Promise<Buffer> {
|
async synthWav(text: string): Promise<Buffer> {
|
||||||
|
await this.initWav();
|
||||||
if (!this.proc) await this._spawn(); // auto-spawn; init runs concurrently
|
if (!this.proc) await this._spawn(); // auto-spawn; init runs concurrently
|
||||||
const msgId = randomUUID() + '-' + Date.now();
|
const msgId = randomUUID() + '-' + Date.now();
|
||||||
|
const outPath = join(TTS_DIR, `${msgId}.wav`);
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let cleared = false;
|
let cleared = false;
|
||||||
@@ -164,13 +222,15 @@ class PiperLocalService {
|
|||||||
if (cleared) return;
|
if (cleared) return;
|
||||||
cleared = true;
|
cleared = true;
|
||||||
this.respMap.delete(msgId);
|
this.respMap.delete(msgId);
|
||||||
reject(new Error('piper-svc: synthesis timed out'));
|
reject(new Error('piper-svc: synthesis timed out after 120s'));
|
||||||
}, 30_000);
|
}, 120_000);
|
||||||
|
|
||||||
this.respMap.set(msgId, (wavPath: string) => {
|
this.respMap.set(msgId, {
|
||||||
|
resolve: (wavPath: string) => {
|
||||||
if (cleared) return;
|
if (cleared) return;
|
||||||
cleared = true;
|
cleared = true;
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
|
console.log(`[piper-svc] Synthesized ${wavPath} (${Date.now()})`);
|
||||||
try {
|
try {
|
||||||
const buf = readFileSync(wavPath);
|
const buf = readFileSync(wavPath);
|
||||||
rmSync(wavPath, { force: true });
|
rmSync(wavPath, { force: true });
|
||||||
@@ -178,10 +238,19 @@ class PiperLocalService {
|
|||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
reject(err instanceof Error ? err : new Error('read WAV failed'));
|
reject(err instanceof Error ? err : new Error('read WAV failed'));
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
reject: (e: Error) => {
|
||||||
|
if (cleared) return;
|
||||||
|
cleared = true;
|
||||||
|
clearTimeout(timer);
|
||||||
|
this.respMap.delete(msgId);
|
||||||
|
reject(e);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log(`[piper-svc] synthesize ${text.substring(0, 40)}... (msgId=${msgId})`);
|
||||||
this.writeStdin(
|
this.writeStdin(
|
||||||
JSON.stringify({ type: 'synthesize', text, msgId }),
|
JSON.stringify({ type: 'synthesize', text, msgId, outPath }),
|
||||||
).catch((e) => {
|
).catch((e) => {
|
||||||
if (cleared) return;
|
if (cleared) return;
|
||||||
cleared = true;
|
cleared = true;
|
||||||
@@ -194,7 +263,10 @@ class PiperLocalService {
|
|||||||
// ── shutdown ──
|
// ── shutdown ──
|
||||||
|
|
||||||
async shutdown(): Promise<void> {
|
async shutdown(): Promise<void> {
|
||||||
this.cleanupTimer?.refresh(); this.cleanupTimer = null;
|
if (this.cleanupTimer) {
|
||||||
|
clearInterval(this.cleanupTimer);
|
||||||
|
this.cleanupTimer = null;
|
||||||
|
}
|
||||||
if (!this.proc) return;
|
if (!this.proc) return;
|
||||||
const p = this.proc; this.proc = null;
|
const p = this.proc; this.proc = null;
|
||||||
await new Promise<void>((res) => {
|
await new Promise<void>((res) => {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const __dirname = join(__filename, '..');
|
|||||||
|
|
||||||
const SCRIPT_DIR = join(__dirname, '..');
|
const SCRIPT_DIR = join(__dirname, '..');
|
||||||
|
|
||||||
const PYTHON = join(SCRIPT_DIR, '../.venv/bin/python3');
|
const PYTHON = join(SCRIPT_DIR, '..', '.venv', 'bin', 'python3');
|
||||||
|
|
||||||
const whisperModel = process.env.WHISPER_MODEL ?? 'base';
|
const whisperModel = process.env.WHISPER_MODEL ?? 'base';
|
||||||
const whisperLanguage = process.env.WHISPER_LANGUAGE ?? 'ca';
|
const whisperLanguage = process.env.WHISPER_LANGUAGE ?? 'ca';
|
||||||
@@ -153,7 +153,7 @@ class WhisperService {
|
|||||||
this.spawn();
|
this.spawn();
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.waitForInit();
|
// await this.waitForInit();
|
||||||
|
|
||||||
const msgId = randomUUID() + '-' + Date.now();
|
const msgId = randomUUID() + '-' + Date.now();
|
||||||
|
|
||||||
|
|||||||
2
mcp/.gitignore
vendored
Normal file
2
mcp/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
1895
mcp/package-lock.json
generated
Normal file
1895
mcp/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
26
mcp/package.json
Normal file
26
mcp/package.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"name": "quibot-mcp",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "QuiBot MCP server — exposes robot controls as MCP tools and resources",
|
||||||
|
"type": "module",
|
||||||
|
"bin": {
|
||||||
|
"quibot-mcp": "./dist/index.js"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"start": "node dist/index.js",
|
||||||
|
"dev": "tsx src/index.ts"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@cfworker/json-schema": "^4.1.1",
|
||||||
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||||
|
"axios": "^1.7.0",
|
||||||
|
"form-data": "^4.0.0",
|
||||||
|
"zod": "^3.25"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.19.21",
|
||||||
|
"tsx": "^4.19.0",
|
||||||
|
"typescript": "^5.6.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
581
mcp/src/index.ts
Normal file
581
mcp/src/index.ts
Normal file
@@ -0,0 +1,581 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||||
|
import axios, { AxiosError } from "axios";
|
||||||
|
import * as z from "zod";
|
||||||
|
import fs from "node:fs";
|
||||||
|
|
||||||
|
// --- Config from env (same as backend) ---
|
||||||
|
const RASPBERRY_PI_HOST = process.env.RASPBERRY_PI_HOST ?? "http://raspberrypi.local";
|
||||||
|
const RASPBERRY_PI_PORT = Number(process.env.RASPBERRY_PI_PORT) || 8000;
|
||||||
|
const QUIBOT_TOKEN = process.env.QUIBOT_TOKEN ?? "MY_SECRET_TOKEN";
|
||||||
|
|
||||||
|
const RPI_URL = `${RASPBERRY_PI_HOST}:${RASPBERRY_PI_PORT}`;
|
||||||
|
|
||||||
|
function rpiUrl(path: string, query?: Record<string, string>): string {
|
||||||
|
const url = `${RPI_URL}${path}`;
|
||||||
|
if (!query) return url;
|
||||||
|
const q = new URLSearchParams({ token: QUIBOT_TOKEN, ...query });
|
||||||
|
return `${url}?${q}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Helpers ---
|
||||||
|
async function rpiPost(path: string, query?: Record<string, string>, body?: unknown): Promise<unknown> {
|
||||||
|
try {
|
||||||
|
const res = await axios.post(rpiUrl(path, query), body, { timeout: 10000 });
|
||||||
|
return res.data;
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof AxiosError && err.response) {
|
||||||
|
throw new Error(`Pi error ${err.response.status}: ${JSON.stringify(err.response.data)}`);
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function rpiGet(path: string, query?: Record<string, string>): Promise<unknown> {
|
||||||
|
try {
|
||||||
|
const res = await axios.get(rpiUrl(path, query), { timeout: 10000 });
|
||||||
|
return res.data;
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof AxiosError && err.response) {
|
||||||
|
throw new Error(`Pi error ${err.response.status}: ${JSON.stringify(err.response.data)}`);
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function rpiPostMultipart(path: string, formData: FormData): Promise<unknown> {
|
||||||
|
try {
|
||||||
|
const res = await axios.post(rpiUrl(path), formData, {
|
||||||
|
timeout: 30000,
|
||||||
|
headers: { "Content-Type": "multipart/form-data" },
|
||||||
|
});
|
||||||
|
return res.data;
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof AxiosError && err.response) {
|
||||||
|
throw new Error(`Pi error ${err.response.status}: ${JSON.stringify(err.response.data)}`);
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- MCP Server ---
|
||||||
|
const server = new McpServer({
|
||||||
|
name: "quibot",
|
||||||
|
version: "1.0.0",
|
||||||
|
});
|
||||||
|
|
||||||
|
// === TOOLS ===
|
||||||
|
|
||||||
|
server.registerTool(
|
||||||
|
"motor_step",
|
||||||
|
{
|
||||||
|
description: "Move stepper motors in a direction (fire-and-forget — motor runs until stop)",
|
||||||
|
inputSchema: z.object({
|
||||||
|
direction: z.enum(["forward", "backward", "left", "right"]),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
async ({ direction }) => {
|
||||||
|
const piPath = direction === "backward" ? "/motor/step/backwards" : `/motor/step/${direction}`;
|
||||||
|
try {
|
||||||
|
const result = await rpiPost(piPath);
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: JSON.stringify(result) }],
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
const msg = err instanceof Error ? err.message : String(err);
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Error: ${msg}` }],
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
server.registerTool(
|
||||||
|
"motor_stop",
|
||||||
|
{
|
||||||
|
description: "Stop all stepper motors immediately (disables driver via GPIO EN)",
|
||||||
|
inputSchema: z.object({}),
|
||||||
|
},
|
||||||
|
async () => {
|
||||||
|
try {
|
||||||
|
const result = await rpiPost("/motor/stop");
|
||||||
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
server.registerTool(
|
||||||
|
"audio_upload_transcribe",
|
||||||
|
{
|
||||||
|
description:
|
||||||
|
"Upload an audio file to the Raspberry Pi and get transcription + LLM response. The Pi runs Whisper for transcription and llamacpp with preamble for response.",
|
||||||
|
inputSchema: z.object({
|
||||||
|
audioBase64: z.string().describe("Base64-encoded audio file"),
|
||||||
|
format: z.string().describe("Audio format (wav, m4a, mp3, etc.)"),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
async ({ audioBase64, format }) => {
|
||||||
|
try {
|
||||||
|
const buffer = Buffer.from(audioBase64, "base64");
|
||||||
|
const formData = new FormData();
|
||||||
|
const fname = `audio-upload-${Date.now()}.${format}`;
|
||||||
|
formData.append("file", new Blob([buffer]), fname);
|
||||||
|
|
||||||
|
const result = await rpiPostMultipart("/transcribe", formData);
|
||||||
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
server.registerTool(
|
||||||
|
"motor_upload",
|
||||||
|
{
|
||||||
|
description: "Upload an audio file to the Raspberry Pi for processing",
|
||||||
|
inputSchema: z.object({
|
||||||
|
filePath: z.string().describe("Path to audio file on local machine"),
|
||||||
|
format: z.string().describe("Audio format (wav, m4a, mp3, etc.)"),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
async ({ filePath, format }) => {
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
return { content: [{ type: "text", text: `File not found: ${filePath}` }], isError: true };
|
||||||
|
}
|
||||||
|
const fd = new (await import("form-data")).default();
|
||||||
|
fd.append("file", fs.createReadStream(filePath));
|
||||||
|
fd.append("format", format);
|
||||||
|
await axios.post(`${RPI_URL}/audio/upload?format=${format}&token=${QUIBOT_TOKEN}`, fd, {
|
||||||
|
headers: fd.getHeaders(),
|
||||||
|
timeout: 30000,
|
||||||
|
});
|
||||||
|
return { content: [{ type: "text", text: `Uploaded: ${filePath}` }] };
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
server.registerTool(
|
||||||
|
"audio_list",
|
||||||
|
{
|
||||||
|
description: "List incoming audio files on the Raspberry Pi",
|
||||||
|
inputSchema: z.object({}),
|
||||||
|
},
|
||||||
|
async () => {
|
||||||
|
try {
|
||||||
|
const files = await rpiGet("/audio/incoming");
|
||||||
|
return { content: [{ type: "text", text: JSON.stringify(files, null, 2) }] };
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
server.registerTool(
|
||||||
|
"audio_lifecycle",
|
||||||
|
{
|
||||||
|
description: "Manage audio file lifecycle: lock, unlock, cancel, or process a file",
|
||||||
|
inputSchema: z.object({
|
||||||
|
filename: z.string().describe("Audio filename"),
|
||||||
|
action: z.enum(["lock", "unlock", "cancel", "process"]).describe("Lifecycle action to perform"),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
async ({ filename, action }) => {
|
||||||
|
try {
|
||||||
|
const result = await rpiPost(`/audio/${action}/${filename}`);
|
||||||
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
server.registerTool(
|
||||||
|
"eye_set_shape",
|
||||||
|
{
|
||||||
|
description: "Set the LED eye shape on the robot's WS2811 matrix",
|
||||||
|
inputSchema: z.object({
|
||||||
|
shape: z.enum(["EYES_OPEN", "EYES_FW", "EYES_DOWN", "EYES_GESTURE"]).describe("Eye shape pattern"),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
async ({ shape }) => {
|
||||||
|
try {
|
||||||
|
const result = await rpiPost(`/eye/shape/${shape}`);
|
||||||
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
server.registerTool(
|
||||||
|
"eye_set_color",
|
||||||
|
{
|
||||||
|
description: "Set the LED eye color on the robot's WS2811 matrix",
|
||||||
|
inputSchema: z.object({
|
||||||
|
color: z.enum(["RED", "GREEN", "BLUE", "YELLOW", "CYAN", "MAGENTA", "WHITE", "OFF", "ORANGE", "VIOLET", "DARK_RED"]).describe("Eye color name"),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
async ({ color }) => {
|
||||||
|
try {
|
||||||
|
const result = await rpiPost(`/eye/color/${color}`);
|
||||||
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
server.registerTool(
|
||||||
|
"eye_toggle",
|
||||||
|
{
|
||||||
|
description: "Turn eyes on or off (enable/disable LED matrix breathing thread)",
|
||||||
|
inputSchema: z.object({
|
||||||
|
state: z.enum(["on", "off"]).describe("Eye power state"),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
async ({ state }) => {
|
||||||
|
try {
|
||||||
|
const result = await rpiPost(`/eye/${state}`);
|
||||||
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
server.registerTool(
|
||||||
|
"gesture_toggle_mode",
|
||||||
|
{
|
||||||
|
description: "Toggle between block mode and gesture mode on the robot",
|
||||||
|
inputSchema: z.object({}),
|
||||||
|
},
|
||||||
|
async () => {
|
||||||
|
try {
|
||||||
|
const result = await rpiPost("/gesture/toggle");
|
||||||
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
server.registerTool(
|
||||||
|
"gesture_on",
|
||||||
|
{
|
||||||
|
description: "Enable gesture sensor polling (PAJ7620U2)",
|
||||||
|
inputSchema: z.object({}),
|
||||||
|
},
|
||||||
|
async () => {
|
||||||
|
try {
|
||||||
|
const result = await rpiPost("/gesture/on");
|
||||||
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
server.registerTool(
|
||||||
|
"gesture_off",
|
||||||
|
{
|
||||||
|
description: "Disable gesture sensor polling",
|
||||||
|
inputSchema: z.object({}),
|
||||||
|
},
|
||||||
|
async () => {
|
||||||
|
try {
|
||||||
|
const result = await rpiPost("/gesture/off");
|
||||||
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
server.registerTool(
|
||||||
|
"run_command",
|
||||||
|
{
|
||||||
|
description: "Run a whitelisted system command on the Raspberry Pi",
|
||||||
|
inputSchema: z.object({
|
||||||
|
task: z.enum(["restart_nginx", "uptime", "update"]).describe("Command to run"),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
async ({ task }) => {
|
||||||
|
try {
|
||||||
|
const result = await rpiPost("/run", { task });
|
||||||
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
server.registerTool(
|
||||||
|
"tts_speak",
|
||||||
|
{
|
||||||
|
description: "Synthesize speech via Piper TTS and play it on the Raspberry Pi",
|
||||||
|
inputSchema: z.object({
|
||||||
|
text: z.string().describe("Text to speak"),
|
||||||
|
lang: z.string().default("ca").describe("Language code (ca, es, en)"),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
async ({ text, lang }) => {
|
||||||
|
try {
|
||||||
|
const result = await rpiGet("/tts", { text, lang: lang || "ca" });
|
||||||
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// === RESOURCES ===
|
||||||
|
|
||||||
|
server.registerResource(
|
||||||
|
"config",
|
||||||
|
"quibot://config",
|
||||||
|
{ description: "Current Raspberry Pi connection config and token" },
|
||||||
|
async () => ({
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
|
uri: "quibot://config",
|
||||||
|
name: "QuiBot Configuration",
|
||||||
|
mimeType: "application/json",
|
||||||
|
text: JSON.stringify(
|
||||||
|
{ raspberryPiHost: RASPBERRY_PI_HOST, raspberryPiPort: RASPBERRY_PI_PORT, token: QUIBOT_TOKEN },
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
server.registerResource(
|
||||||
|
"available-directions",
|
||||||
|
"quibot://directions",
|
||||||
|
{ description: "Available motor movement directions" },
|
||||||
|
async () => ({
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
|
uri: "quibot://directions",
|
||||||
|
name: "Available Motor Directions",
|
||||||
|
mimeType: "text/plain",
|
||||||
|
text: ["forward", "backward"].map((d) => ` POST /motor/step/${d}`).join("\n"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
server.registerResource(
|
||||||
|
"eye-shapes",
|
||||||
|
"quibot://eyes/shapes",
|
||||||
|
{ description: "Available LED eye shapes and their meanings" },
|
||||||
|
async () => ({
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
|
uri: "quibot://eyes/shapes",
|
||||||
|
name: "Available Eye Shapes",
|
||||||
|
mimeType: "text/plain",
|
||||||
|
text: [
|
||||||
|
" EYES_OPEN — Normal resting eyes (default)",
|
||||||
|
" EYES_FW — Forward-looking eyes",
|
||||||
|
" EYES_DOWN — Downward/downcast eyes",
|
||||||
|
" EYES_GESTURE — Gesture-acknowledge eyes",
|
||||||
|
].join("\n"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
server.registerResource(
|
||||||
|
"color-actions",
|
||||||
|
"quibot://blocks/colors",
|
||||||
|
{ description: "Color-to-action mapping for block recognition" },
|
||||||
|
async () => ({
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
|
uri: "quibot://blocks/colors",
|
||||||
|
name: "Color Block Actions",
|
||||||
|
mimeType: "text/plain",
|
||||||
|
text: [
|
||||||
|
" RED → Advance forward",
|
||||||
|
" GREEN → Turn right",
|
||||||
|
" BLUE → Turn left",
|
||||||
|
" YELLOW → Take / pick up block",
|
||||||
|
" ORANGE → Leave / eject block",
|
||||||
|
" VIOLET → Idle",
|
||||||
|
" BLACK → Reference / no block",
|
||||||
|
].join("\n"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
server.registerResource(
|
||||||
|
"gestures",
|
||||||
|
"quibot://gestures",
|
||||||
|
{ description: "Available PAJ7620U2 gestures" },
|
||||||
|
async () => ({
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
|
uri: "quibot://gestures",
|
||||||
|
name: "Available Gestures",
|
||||||
|
mimeType: "text/plain",
|
||||||
|
text: [
|
||||||
|
" GS_FORWARD → Hand moving forward (toward sensor)",
|
||||||
|
" GS_BACKWARD → Hand moving backward (away from sensor)",
|
||||||
|
" GS_LEFT → Hand moving left",
|
||||||
|
" GS_RIGHT → Hand moving right",
|
||||||
|
" GS_UP → Hand moving up",
|
||||||
|
" GS_DOWN → Hand moving down",
|
||||||
|
" GS_CLOCKWISE → Clockwise wave motion",
|
||||||
|
" GS_ANTICLOCKWISE→ Counter-clockwise wave motion",
|
||||||
|
" GS_WAVE → Wave hello gesture",
|
||||||
|
].join("\n"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
server.registerResource(
|
||||||
|
"pi-status",
|
||||||
|
"quibot://status/pi",
|
||||||
|
{ description: "Check if Raspberry Pi HTTP server is reachable" },
|
||||||
|
async () => {
|
||||||
|
try {
|
||||||
|
const res = await axios.get(`${RPI_URL}/health`, { timeout: 5000 });
|
||||||
|
return {
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
|
uri: "quibot://status/pi",
|
||||||
|
name: "Pi Status",
|
||||||
|
mimeType: "application/json",
|
||||||
|
text: JSON.stringify({ status: "connected", data: res.data }, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
contents: [
|
||||||
|
{
|
||||||
|
uri: "quibot://status/pi",
|
||||||
|
name: "Pi Status",
|
||||||
|
mimeType: "application/json",
|
||||||
|
text: JSON.stringify({ status: "disconnected", error: "Cannot reach Raspberry Pi" }, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// === PROMPTS ===
|
||||||
|
|
||||||
|
server.registerPrompt(
|
||||||
|
"quibot-setup",
|
||||||
|
{
|
||||||
|
description: "Get a complete reference for controlling the QuiBot robot via MCP",
|
||||||
|
},
|
||||||
|
() => ({
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
role: "user",
|
||||||
|
content: {
|
||||||
|
type: "text",
|
||||||
|
text: `# QuiBot MCP Server
|
||||||
|
|
||||||
|
You can control the physical QuiBot robot with these tools:
|
||||||
|
|
||||||
|
## Motor Control
|
||||||
|
- motor_step(direction) — Move forward, backward, left, or right
|
||||||
|
- motor_stop() — Stop all motors
|
||||||
|
- motor_upload(filePath, format) — Upload audio file to Pi
|
||||||
|
|
||||||
|
## Audio
|
||||||
|
- audio_upload_transcribe(audioBase64, format) — Upload + Whisper transcribe + llamacpp response
|
||||||
|
- audio_list() — List incoming audio files
|
||||||
|
- audio_lifecycle(filename, action) — lock/unlock/cancel/process audio
|
||||||
|
- tts_speak(text, lang) — Synthesize and play speech via Piper TTS
|
||||||
|
|
||||||
|
## Eyes (WS2811 LED Matrix)
|
||||||
|
- eye_set_shape(shape) — Set face expression shape
|
||||||
|
- eye_set_color(color) — Change eye color
|
||||||
|
- eye_toggle(state) — Turn eyes on/off
|
||||||
|
|
||||||
|
## Gesture Sensor
|
||||||
|
- gesture_on() / gesture_off() — Enable/disable gesture polling
|
||||||
|
- gesture_toggle_mode() — Toggle between block/gesture mode
|
||||||
|
|
||||||
|
## System
|
||||||
|
- run_command(task) — Run system commands on Pi (uptime, restart_nginx)
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
- quibot://status/pi — Check if Pi is reachable
|
||||||
|
- quibot://config — Current connection config
|
||||||
|
- quibot://blocks/colors — Color-to-action mapping
|
||||||
|
- quibot://gestures — Gesture reference
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// === START ===
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const transport = new StdioServerTransport();
|
||||||
|
await server.connect(transport);
|
||||||
|
console.error("[quibot-mcp] Server connected, waiting for requests via stdio...");
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error("[quibot-mcp] Failed to start:", err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
18
mcp/tsconfig.json
Normal file
18
mcp/tsconfig.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "Node16",
|
||||||
|
"moduleResolution": "Node16",
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"declaration": true,
|
||||||
|
"sourceMap": false
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
10
package-lock.json
generated
10
package-lock.json
generated
@@ -5,6 +5,7 @@
|
|||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"formdata-node": "^6.0.3",
|
||||||
"vue-i18n": "^11.4.5"
|
"vue-i18n": "^11.4.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -268,6 +269,15 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
|
"node_modules/formdata-node": {
|
||||||
|
"version": "6.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-6.0.3.tgz",
|
||||||
|
"integrity": "sha512-8e1++BCiTzUno9v5IZ2J6bv4RU+3UKDmqWUQD0MIMVCd9AdhWkO1gw57oo1mNEX1dMq2EGI+FbWz4B92pscSQg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/magic-string": {
|
"node_modules/magic-string": {
|
||||||
"version": "0.30.21",
|
"version": "0.30.21",
|
||||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"formdata-node": "^6.0.3",
|
||||||
"vue-i18n": "^11.4.5"
|
"vue-i18n": "^11.4.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user