diff --git a/README.md b/README.md index 849ad86..d504753 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,13 @@ Your personal AI network navigator +## TODO +- Use zips for protocols - move avatar to protocol zip +- Home screen corner menu + +- Notifications +- CRON / Schedule + ## License Copyright © 2023 Zakary Timson | All Rights Reserved | Available under MIT Licensing diff --git a/package-lock.json b/package-lock.json index 1d494dc..47f7180 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,10 +8,12 @@ "name": "@ztimson/net-navi", "version": "1.0.0", "dependencies": { - "@ztimson/ai-utils": "^0.8.8", + "@ztimson/ai-utils": "^0.8.9", "@ztimson/utils": "^0.28.14", "cors": "^2.8.5", + "dotenv": "^17.3.1", "express": "^4.18.2", + "sharp": "^0.34.5", "socket.io": "^4.6.1" } }, @@ -44,6 +46,16 @@ "node": ">=6.9.0" } }, + "node_modules/@emnapi/runtime": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@huggingface/jinja": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/@huggingface/jinja/-/jinja-0.2.2.tgz", @@ -53,6 +65,471 @@ "node": ">=18" } }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -302,10 +779,33 @@ "onnxruntime-node": "1.14.0" } }, + "node_modules/@xenova/transformers/node_modules/sharp": { + "version": "0.32.6", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.6.tgz", + "integrity": "sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.2", + "node-addon-api": "^6.1.0", + "prebuild-install": "^7.1.1", + "semver": "^7.5.4", + "simple-get": "^4.0.1", + "tar-fs": "^3.0.4", + "tunnel-agent": "^0.6.0" + }, + "engines": { + "node": ">=14.15.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@ztimson/ai-utils": { - "version": "0.8.8", - "resolved": "https://registry.npmjs.org/@ztimson/ai-utils/-/ai-utils-0.8.8.tgz", - "integrity": "sha512-XzkKYM/oNxS7D373yeTG8VKdO9cOYU1+PK/ZM8Sz/v8w2hIO+Lg/VWO/524yZ6oMl/mjDwSIjqv3CxGVpJ6wRw==", + "version": "0.8.9", + "resolved": "https://registry.npmjs.org/@ztimson/ai-utils/-/ai-utils-0.8.9.tgz", + "integrity": "sha512-BHaJjtFS+qF/LZJinscmHPtef1jq/6xmPXw6kKbSYLmcQJLmVoQg5NJXU2EQjaYtc36OuYXeM98nWBBYWl6hdA==", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "^0.78.0", @@ -424,7 +924,6 @@ "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.5.tgz", "integrity": "sha512-XvwYM6VZqKoqDll8BmSww5luA5eflDzY0uEFfBJtFKe4PAAtxBjU3YIxzIBzhyaEQBy1VXEQBto4cpN5RZJw+w==", "license": "Apache-2.0", - "optional": true, "dependencies": { "bare-events": "^2.5.4", "bare-path": "^3.0.0", @@ -445,11 +944,10 @@ } }, "node_modules/bare-os": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.7.0.tgz", - "integrity": "sha512-64Rcwj8qlnTZU8Ps6JJEdSmxBEUGgI7g8l+lMtsJLl4IsfTcHMTfJ188u2iGV6P6YPRZrtv72B2kjn+hp+Yv3g==", + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.7.1.tgz", + "integrity": "sha512-ebvMaS5BgZKmJlvuWh14dg9rbUI84QeV3WlWn6Ph6lFI8jJoh7ADtVTyD2c93euwbe+zgi0DVrl4YmqXeM9aIA==", "license": "Apache-2.0", - "optional": true, "engines": { "bare": ">=1.14.0" } @@ -459,7 +957,6 @@ "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", "license": "Apache-2.0", - "optional": true, "dependencies": { "bare-os": "^3.0.1" } @@ -469,7 +966,6 @@ "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.8.0.tgz", "integrity": "sha512-reUN0M2sHRqCdG4lUK3Fw8w98eeUIZHL5c3H7Mbhk2yVBL+oofgaIp0ieLfD5QXwPCypBpmEEKU2WZKzbAk8GA==", "license": "Apache-2.0", - "optional": true, "dependencies": { "streamx": "^2.21.0", "teex": "^1.0.1" @@ -492,7 +988,6 @@ "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.2.tgz", "integrity": "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==", "license": "Apache-2.0", - "optional": true, "dependencies": { "bare-path": "^3.0.0" } @@ -980,6 +1475,18 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, + "node_modules/dotenv": { + "version": "17.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", + "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -2041,9 +2548,9 @@ } }, "node_modules/pump": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", - "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", "license": "MIT", "dependencies": { "end-of-stream": "^1.1.0", @@ -2229,26 +2736,47 @@ "license": "ISC" }, "node_modules/sharp": { - "version": "0.32.6", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.6.tgz", - "integrity": "sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "color": "^4.2.3", - "detect-libc": "^2.0.2", - "node-addon-api": "^6.1.0", - "prebuild-install": "^7.1.1", - "semver": "^7.5.4", - "simple-get": "^4.0.1", - "tar-fs": "^3.0.4", - "tunnel-agent": "^0.6.0" + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" }, "engines": { - "node": ">=14.15.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" } }, "node_modules/side-channel": { @@ -2584,12 +3112,13 @@ } }, "node_modules/tar-stream": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", - "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.8.tgz", + "integrity": "sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==", "license": "MIT", "dependencies": { "b4a": "^1.6.4", + "bare-fs": "^4.5.5", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } @@ -2599,7 +3128,6 @@ "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", "license": "MIT", - "optional": true, "dependencies": { "streamx": "^2.12.5" } @@ -2658,6 +3186,13 @@ "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", "license": "MIT" }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", diff --git a/package.json b/package.json index 55c4be2..172fab1 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,15 @@ { "name": "@ztimson/net-navi", "version": "1.0.0", + "description": "Network Navigation Program", "type": "module", "dependencies": { - "@ztimson/ai-utils": "^0.8.8", + "@ztimson/ai-utils": "^0.8.9", "@ztimson/utils": "^0.28.14", "cors": "^2.8.5", + "dotenv": "^17.3.1", "express": "^4.18.2", + "sharp": "^0.34.5", "socket.io": "^4.6.1" }, "scripts": { diff --git a/public/components/avatar.mjs b/public/components/avatar.mjs new file mode 100644 index 0000000..79004f0 --- /dev/null +++ b/public/components/avatar.mjs @@ -0,0 +1,237 @@ +import {TTS} from '../tts.mjs'; + +class AvatarComponent extends HTMLElement { + + static get observedAttributes() { return []; } + + constructor() { + super(); + this.attachShadow({mode: 'open'}); + this.activeEmotes = []; + this.mouthSvg = null; + this.mouthState = 'closed'; + this.setupMouthAnimation(); + this.navi = window.navi; + this.navi.animations().then(animations => { + this.animations = animations; + if(!this.animations) return console.error(`Invalid animations: ${this.animations}`); + this.render(this.animations); + navi.on('emote', emote => this.emote(emote)); + }); + + window.emote = this.emote.bind(this); + } + + render(data) { + this.shadowRoot.innerHTML = ` + +
+ Avatar +
`; + this.loadMouthSvg(); + } + + loadMouthSvg() { + fetch('/emotes/mouth.svg').then(r => r.text()).then(svg => { + const container = document.createElement('div'); + container.className = 'mouth-overlay'; + container.innerHTML = svg; + this.mouthSvg = container.firstElementChild; + const mouthPos = this.animations?.emote?.['mouth'] || {x: 50, y: 60, r: 0}; + container.style.left = `${mouthPos.x}%`; + container.style.top = `${mouthPos.y}%`; + container.style.transform = `translate(-50%, -50%) rotate(${mouthPos.r || 0}deg)`; + container.style.width = '50px'; + container.style.height = '25px'; + const avatarContainer = this.shadowRoot.querySelector('.avatar-container'); + if(avatarContainer) avatarContainer.appendChild(container); + }); + } + + setupMouthAnimation() { + const tts = TTS.getInstance(); + let mouthAnimationInterval = null; + tts.on('onSentenceStart', () => { + if(mouthAnimationInterval) return; + const next = () => { + mouthAnimationInterval = setTimeout(() => { + next(); + this.toggleMouthState(); + }, ~~(Math.random() * 100) + 100); + } + next(); + }); + tts.on('onSentenceEnd', () => this.setMouthState('closed')); + tts.on('onComplete', () => { + if(mouthAnimationInterval) { + clearTimeout(mouthAnimationInterval); + mouthAnimationInterval = null; + } + this.setMouthState('closed'); + }); + } + + toggleMouthState() { + if(!this.mouthSvg) return; + this.setMouthState(this.mouthState === 'open' ? 'partial' : 'open'); + } + + setMouthState(state) { + if(!this.mouthSvg) return; + this.mouthState = state; + this.mouthSvg.classList.remove('closed', 'partial', 'open'); + this.mouthSvg.classList.add(state); + } + + clear(all = true) { + if(all) { + const a = this.shadowRoot.querySelector('.avatar'); + a.animate([{filter: 'drop-shadow(2px 4px 6px black) grayscale(0%) brightness(100%)'}], {duration: 100, fill: 'forwards'}); + } + this.activeEmotes.forEach(e => e.remove()); + this.activeEmotes = []; + } + + emote(emote) { + const animate = (e, emote, style, index) => { + const duration = 3000; + if(emote === 'blush') { + e.animate([ + {transform: `scale(0.75) rotate(${style.r ?? 0}deg)`, opacity: 0}, + {transform: `scale(0.75) rotate(${style.r ?? 0}deg)`, opacity: 1} + ], {duration: duration, easing: 'ease-out', fill: 'forwards'}); + } else if(emote === 'cry') { + e.animate([ + {transform: `rotate(${style.r ?? 0}deg) translateX(50%) translateY(-25%) scale(0)`}, + {transform: `rotate(${style.r ?? 0}deg) translateX(50%) translateY(0) scale(0.5)`} + ], {duration: duration, easing: 'ease-out', fill: 'forwards'}); + } else if(emote === 'dead') { + e.animate([ + {transform: `rotate(${style.r ?? 0}deg) scale(2) translateY(0)`}, + {transform: `rotate(${style.r ?? 0}deg) scale(2) translateY(-10%)`} + ], {duration: duration, easing: 'ease-out', fill: 'forwards'}); + setTimeout(() => { + e.animate([ + {transform: `rotate(${style.r ?? 0}deg) scale(2) translateY(-10%)`}, + {transform: `rotate(${style.r ?? 0}deg) scale(2) translateY(-8%)`} + ], {duration: 1500, easing: 'ease-in-out', iterations: Infinity, direction: 'alternate'}); + }, duration); + } else if(emote === 'drool') { + e.src = `${this.navi.api}/emotes/tear.png`; + e.animate([ + {transform: `rotate(${style.r ?? 0}deg) translateX(50%) translateY(-12.5%) scale(0)`}, + {transform: `rotate(${style.r ?? 0}deg) translateX(50%) translateY(0) scale(0.25)`} + ], {duration: duration, easing: 'ease-out', fill: 'forwards'}); + } else if(emote === 'love') { + e.animate([ + {transform: `rotate(${style.r ?? 0}deg) scale(0.5)`}, + {transform: `rotate(${style.r ?? 0}deg) scale(0.7)`} + ], {duration: 200, easing: 'steps(2, jump-end)', iterations: Infinity, direction: 'alternate'}); + } else if(emote === 'question') { + e.style.transform = `rotate(${style.r ?? 0}deg)`; + e.animate([ + {opacity: 1, offset: 0}, + {opacity: 1, offset: 0.49}, + {opacity: 0, offset: 0.5}, + {opacity: 0, offset: 1} + ], {duration: 200, iterations: Infinity, direction: 'alternate', delay: (index % 2) * 200}); + } else if(emote === 'realize') { + e.animate([ + {transform: `rotate(${style.r ?? 0}deg) scale(0.9)`}, + {transform: `rotate(${style.r ?? 0}deg) scale(1.1)`} + ], {duration: 500, easing: 'ease-out', iterations: Infinity, direction: 'alternate'}); + } else if(emote === 'stars') { + e.animate([ + {transform: `rotate(${style.r ?? 0}deg) scale(0.25)`}, + {transform: `rotate(${style.r ?? 0}deg) scale(0.3)`} + ], {duration: 100, easing: 'steps(2, jump-end)', iterations: Infinity, direction: 'alternate'}); + } else if(emote === 'stress') { + e.animate([ + {transform: `rotate(${style.r ?? 0}deg) scale(0.9)`}, + {transform: `rotate(${style.r ?? 0}deg) scale(1.1)`} + ], {duration: 333, easing: 'ease-out', iterations: Infinity, direction: 'alternate'}); + } else if(emote === 'sigh') { + e.animate([ + {transform: `rotate(${style.r ?? 0}deg) translate(0, 0)`}, + {transform: `rotate(${style.r ?? 0}deg) translate(10%, 10%)`} + ], {duration: duration, easing: 'ease-out', fill: 'forwards'}); + } else if(emote === 'sweat') { + e.animate([ + {transform: `rotate(${style.r ?? 0}deg) scale(0.5) translateY(0)`}, + {transform: `rotate(${style.r ?? 0}deg) scale(0.5) translateY(20%)`} + ], {duration: duration, easing: 'ease-out', fill: 'forwards'}); + } else if(emote === 'tear') { + e.animate([ + {transform: `rotate(${style.r ?? 0}deg) translateX(50%) translateY(-15%) scale(0)`}, + {transform: `rotate(${style.r ?? 0}deg) translateX(50%) translateY(0) scale(0.3)`} + ], {duration: duration, easing: 'ease-out', fill: 'forwards'}); + } + }; + + if(!emote || emote === 'none') return this.clear(); + if(!this.animations.emote[emote]) throw new Error(`Invalid animation: ${emote}`); + const pos = this.animations.emote[emote]; + this.clear(false); + + const a = this.shadowRoot.querySelector('.avatar'); + const container = this.shadowRoot.querySelector('.avatar-container'); + const positions = Array.isArray(pos) ? pos : (pos.x != null ? [pos] : []); + + if(['dead', 'grey'].includes(emote)) { + a.animate([ + {filter: 'drop-shadow(2px 4px 6px black) grayscale(100%) brightness(150%)'} + ], {duration: 100, fill: 'forwards'}); + } else { + a.animate([ + {filter: 'drop-shadow(2px 4px 6px black) grayscale(0%) brightness(100%)'} + ], {duration: 100, fill: 'forwards'}); + } + + positions.forEach((p, i) => { + const e = document.createElement('img'); + e.className = 'emote-overlay'; + e.src = `${this.navi.api}/emotes/${emote}.png`; + e.style.top = `${p.y}%`; + e.style.left = `${p.x}%`; + container.appendChild(e); + this.activeEmotes.push(e); + animate(e, emote, p, i); + }); + } +} + +customElements.define('avatar-component', AvatarComponent); diff --git a/public/components/llm.mjs b/public/components/llm.mjs index 3bc9717..b2b61e2 100644 --- a/public/components/llm.mjs +++ b/public/components/llm.mjs @@ -1,32 +1,39 @@ import './btn.mjs'; +import {TTS} from '../tts.mjs'; class LlmComponent extends HTMLElement { - hideTools = []//['adapt', 'recall', 'remember'] + hideTools = ['emote', 'personalize', 'recall', 'remember'] - get isOpen() { return this.isDialogueOpen; }; + get isOpen() { return this.isDialogueOpen; }; - constructor() { - super(); - this.attachShadow({ mode: 'open' }); - this.navi = window.navi; + constructor() { + super(); + this.attachShadow({ mode: 'open' }); + this.navi = window.navi; this.navi.init().then(() => this.render()); - this.isReceiving = false; - this.streamComplete = false; - this.isDialogueOpen = false; - this.isExpanded = false; - this.messageHistory = []; - this.audioCtx = new (window.AudioContext || window.webkitAudioContext)(); - this.streamBuffer = ''; - this.typingIndex = 0; - this.typingInterval = null; - this.currentRequest = null; - this.attachedFiles = []; - this.currentStreamingMessage = null; - } + this.isReceiving = false; + this.streamComplete = false; + this.isDialogueOpen = false; + this.isExpanded = false; + this.messageHistory = []; + this.audioCtx = new (window.AudioContext || window.webkitAudioContext)(); + this.streamBuffer = ''; + this.typingIndex = 0; + this.typingInterval = null; + this.currentRequest = null; + this.attachedFiles = []; + this.currentStreamingMessage = null; - render() { - this.shadowRoot.innerHTML = ` + // TTS setup + this.tts = null; + this.streamingSpeech = null; + this.autoSpeak = false; + this.speakingMessageIdx = null; + } + + render() { + this.shadowRoot.innerHTML = ` + + + + diff --git a/public/emotes/question.png b/public/emotes/question.png new file mode 100644 index 0000000..cf63bb0 Binary files /dev/null and b/public/emotes/question.png differ diff --git a/public/emotes/realize.png b/public/emotes/realize.png new file mode 100644 index 0000000..3bafa8f Binary files /dev/null and b/public/emotes/realize.png differ diff --git a/public/emotes/sigh.png b/public/emotes/sigh.png new file mode 100644 index 0000000..38a6daf Binary files /dev/null and b/public/emotes/sigh.png differ diff --git a/public/emotes/stress.png b/public/emotes/stress.png new file mode 100644 index 0000000..8f82c25 Binary files /dev/null and b/public/emotes/stress.png differ diff --git a/public/emotes/sweat.png b/public/emotes/sweat.png new file mode 100644 index 0000000..a2918bc Binary files /dev/null and b/public/emotes/sweat.png differ diff --git a/public/emotes/tear.png b/public/emotes/tear.png new file mode 100644 index 0000000..f4146fc Binary files /dev/null and b/public/emotes/tear.png differ diff --git a/public/index.html b/public/index.html index 370a6e7..672c097 100644 --- a/public/index.html +++ b/public/index.html @@ -1,14 +1,30 @@ - NetNavi v1.0.0 + NetNavi + + + + + + + + + + + + + + + + + - -
- + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - diff --git a/public/navi.mjs b/public/navi.mjs index 38df58c..9419f05 100644 --- a/public/navi.mjs +++ b/public/navi.mjs @@ -1,11 +1,13 @@ class Navi { api; connected = false; + avatar; icon; info; theme; world; + #animations; #init; #listeners = new Map(); #socket; @@ -14,10 +16,24 @@ class Navi { constructor(api = window.location.origin, secret = '') { this.api = api.replace(/\/$/, ''); - this.icon = `${this.api}/favicon.png`; + this.avatar = `${this.api}/avatar`; + this.icon = `${this.api}/favicon`; this.#secret = secret; } + async animations() { + if(this.#animations) return this.#animations; + await this.init(); + this.#animations = await fetch(`${this.api}/api/animations`, { + headers: this.#secret ? {'Authorization': `Bearer ${this.#secret}`} : {} + }).then(resp => { + if(!resp.ok) throw new Error(`Invalid Navi API: ${this.api}`); + return resp.json(); + }); + this.emit('animations', this.#animations); + return this.#animations; + } + async init() { if(this.#init) return this.#init; this.#init = new Promise(async (res, rej) => { @@ -229,7 +245,11 @@ class Navi { // ============================================ ask(message, stream) { - this.llmCallback = stream; + this.llmCallback = (chunk) => { + if(chunk['emote']) this.emit('emote', chunk['emote']); + stream(chunk); + }; + const promise = new Promise((resolve, reject) => { this.llmResolve = resolve; this.llmReject = reject; @@ -260,7 +280,7 @@ class Navi { disconnect() { this.connected = false; - this.icon = this.info = this.theme = this.world = this.#init = this.#secret = null; + this.avatar = this.icon = this.info = this.theme = this.world = this.#animations = this.#init = this.#secret = null; if(this.#world) { this.#world.disconnect(); this.#world = null; diff --git a/public/tts.mjs b/public/tts.mjs new file mode 100644 index 0000000..19b2658 --- /dev/null +++ b/public/tts.mjs @@ -0,0 +1,176 @@ +// tts.service.mjs +export class TTS { + static QUALITY_PATTERNS = ['Google', 'Microsoft', 'Samantha', 'Premium', 'Natural', 'Neural']; + static _errorHandlerInstalled = false; + static _instance = null; + + _currentUtterance = null; + _voicesLoaded; + _stoppedUtterances = new WeakSet(); + _rate = 1; + _pitch = 1; + _volume = 1; + _voice; + _hooks = { + onSentenceStart: [], + onSentenceEnd: [], + onChunk: [], + onComplete: [] + }; + + constructor({ rate, pitch, volume, voice } = {}) { + TTS.installErrorHandler(); + this._voicesLoaded = this.initializeVoices(); + if (rate !== undefined) this._rate = rate; + if (pitch !== undefined) this._pitch = pitch; + if (volume !== undefined) this._volume = volume; + this._voice = voice === null ? undefined : voice; + } + + static getInstance(config) { + if(!this._instance) this._instance = new TTS(config); + return this._instance; + } + + static installErrorHandler() { + if (this._errorHandlerInstalled) return; + window.addEventListener('unhandledrejection', e => { + if (e.reason?.error === 'interrupted' && e.reason instanceof SpeechSynthesisErrorEvent) e.preventDefault(); + }); + this._errorHandlerInstalled = true; + } + + initializeVoices() { + return new Promise(res => { + const voices = window.speechSynthesis.getVoices(); + if (voices.length) { + if (!this._voice) this._voice = TTS.bestVoice(); + res(); + } else { + const h = () => { + window.speechSynthesis.removeEventListener('voiceschanged', h); + if (!this._voice) this._voice = TTS.bestVoice(); + res(); + }; + window.speechSynthesis.addEventListener('voiceschanged', h); + } + }); + } + + static bestVoice(lang = 'en') { + const voices = window.speechSynthesis.getVoices(); + for (const p of this.QUALITY_PATTERNS) { + const v = voices.find(v => v.name.includes(p) && v.lang.startsWith(lang)); + if (v) return v; + } + return voices.find(v => v.lang.startsWith(lang)); + } + + static cleanText(t) { + return removeEmojis(t).replace(/```[\s\S]*?```/g, ' code block ').replace(/[#*_~`]/g, ''); + } + + on(event, callback) { + if(this._hooks[event]) this._hooks[event].push(callback); + } + + off(event, callback) { + if(this._hooks[event]) this._hooks[event] = this._hooks[event].filter(cb => cb !== callback); + } + + _emit(event, data) { + if (this._hooks[event]) { + this._hooks[event].forEach(cb => { + try { + cb(data); + } catch (err) { + console.error(`❌ Error in ${event} hook:`, err); + } + }); + } + } + + createUtterance(t) { + const u = new SpeechSynthesisUtterance(TTS.cleanText(t)); + const v = this._voice || TTS.bestVoice(); + if (v) u.voice = v; + u.rate = this._rate; + u.pitch = this._pitch; + u.volume = this._volume; + return u; + } + + async speak(t) { + if (!t.trim()) return; + await this._voicesLoaded; + if(this._currentUtterance && !this._isStreaming) this.stop(); + return new Promise((res, rej) => { + this._currentUtterance = this.createUtterance(t); + const u = this._currentUtterance; + u.onend = () => { + this._currentUtterance = null; + res(); + }; + u.onerror = e => { + console.error('❌ Utterance error:', e); + this._currentUtterance = null; + if (this._stoppedUtterances.has(u) && e.error === 'interrupted') res(); + else rej(e); + }; + window.speechSynthesis.speak(u); + }); + } + + stop() { + if (this._currentUtterance) this._stoppedUtterances.add(this._currentUtterance); + window.speechSynthesis.cancel(); + this._currentUtterance = null; + this._isStreaming = false; + this._emit('onComplete', {}); + } + + speakStream() { + this._isStreaming = true; + let buf = ''; + let sentenceQueue = Promise.resolve(); + const rx = /[^.!?\n]+[.!?\n]+/g; + + return { + next: t => { + buf += t; + this._emit('onChunk', { text: t }); + const ss = buf.match(rx); + if(ss) { + ss.forEach(s => { + const sentence = s.trim(); + sentenceQueue = sentenceQueue.then(async () => { + this._emit('onSentenceStart', { sentence }); + await this.speak(sentence); + this._emit('onSentenceEnd', { sentence }); + }); + }); + } + buf = buf.replace(rx, ''); + }, + done: async () => { + if (buf.trim()) { + const sentence = buf.trim(); + sentenceQueue = sentenceQueue.then(async () => { + this._emit('onSentenceStart', { sentence }); + await this.speak(sentence); + this._emit('onSentenceEnd', { sentence }); + }); + buf = ''; + } + await sentenceQueue; + this._isStreaming = false; + this._emit('onComplete', {}); + } + }; + } +} + +export function removeEmojis(str) { + const emojiRegex = /(?:[\u2700-\u27bf]|(?:\ud83c[\udde6-\uddff]){2}|[\ud83c[\udde6-\uddff]|[\ud83d[\ude00-\ude4f]|[\ud83d[\ude80-\udeff]|[\ud83c[\udd00-\uddff]|[\ud83d[\ude50-\ude7f]|[\u2600-\u26ff]|[\u2700-\u27bf]|[\ud83e[\udd00-\uddff]|[\ud83c[\udf00-\uffff]|[\ud83d[\ude00-\udeff]|[\ud83c[\udde6-\uddff])/g; + return str.replace(emojiRegex, ''); +} diff --git a/public/world.html b/public/world.html new file mode 100644 index 0000000..370a6e7 --- /dev/null +++ b/public/world.html @@ -0,0 +1,60 @@ + + + + NetNavi v1.0.0 + + + + + + + + + + + +
+ + + + + + + + + diff --git a/src/environment.js b/src/environment.js new file mode 100644 index 0000000..58be266 --- /dev/null +++ b/src/environment.js @@ -0,0 +1,27 @@ +import {dirname, join} from 'path'; +import {fileURLToPath} from 'url'; +import * as dotenv from 'dotenv'; + +dotenv.config({path: ['.env','.env.local'], debug: false, quiet: true}); + +const __filename = fileURLToPath(import.meta.url); +const __dirname = join(dirname(__filename), '..'); +const storage = join(__dirname, 'storage'); + +export const environment = { + port: process.env.PORT || 3000, + publicUrl: process.env.PUBLIC_URL || `http://localhost:${process.env.PORT || 3000}`, + llm: { + host: process.env.LLM_HOST || '', + model: process.env.LLM_MODEL || 'default', + token: process.env.LLM_TOKEN || 'ignore', + context: process.env.LLM_CONTEXT ? +process.env.LLM_CONTEXT : 60_000. + }, + paths: { + public: join(__dirname, 'public'), + storage, + navi: join(storage, 'navi'), + protocols: join(storage, 'protocols'), + worlds: join(storage, 'worlds') + } +} diff --git a/src/server.js b/src/server.js index ef243ae..38a959d 100644 --- a/src/server.js +++ b/src/server.js @@ -4,24 +4,22 @@ import {Server} from 'socket.io'; import cors from 'cors'; import fs from 'fs'; import {join, dirname} from 'path'; -import {fileURLToPath} from 'url'; import {Ai, DateTimeTool, ExecTool, FetchTool, ReadWebpageTool, WebSearchTool} from '@ztimson/ai-utils'; -import {contrast, deepCopy, isEqual, shadeColor} from '@ztimson/utils'; +import {contrast, deepCopy, deepMerge, isEqual, shadeColor} from '@ztimson/utils'; import * as os from 'node:os'; +import {environment} from './environment.js'; +import {resize} from './utils.js'; +import * as path from 'node:path'; // ============================================ // Settings // ============================================ -const __filename = fileURLToPath(import.meta.url); -const __dirname = join(dirname(__filename), '..'); -const storage = join(__dirname, 'storage'); -const navi = join(storage, 'navi'); -const protocols = join(storage, 'protocols'); -const worlds = join(storage, 'worlds'); -const logoFile = join(navi, 'logo.png'); -const settingsFile = join(navi, 'settings.json'); -const memoriesFile = join(navi, 'memories.json'); +const logo = join(environment.paths.navi, 'logo.png'); +const avatar = join(environment.paths.navi, 'avatar.png'); +const sprite = join(environment.paths.navi, 'sprite.png'); +const settingsFile = join(environment.paths.navi, 'settings.json'); +const memoriesFile = join(environment.paths.navi, 'memories.json'); function calcColors(theme) { return { @@ -43,7 +41,17 @@ function calcColors(theme) { let orgSettings, orgMemories, memories = [], settings = { name: 'Navi', - personality: '- You are inquisitive about your user trying to best adjust your personally to fit them\n- Keep responses short', + personality: '- Keep your responses the same length or shorter than the previous user message', + animations: { + emote: { + dead: {"x": 52, "y": 6}, + grey: {}, + realization: {"x": 64, "y": -5}, + sigh: {"x": 55, "y": 20}, + sweat: {"x": 59, "y": 5}, + stress: {"x": 58, "y": 4} + } + }, theme: { background: '#fff', border: '#000', @@ -61,7 +69,7 @@ let orgSettings, orgMemories, memories = [], settings = { function load() { try { orgSettings = JSON.parse(fs.readFileSync(settingsFile, 'utf-8')); - settings = {...settings, ...deepCopy(orgSettings)}; + settings = deepMerge(settings, deepCopy(orgSettings)); } catch { } try { memories = JSON.parse(fs.readFileSync(memoriesFile, 'utf-8')); @@ -73,12 +81,12 @@ function save() { const dir = dirname(settingsFile); if(!fs.existsSync(dir)) fs.mkdirSync(dir, {recursive: true}); if(!isEqual(orgSettings, settings)) { - fs.writeFileSync(settingsFile, JSON.stringify(settings)); + fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2)); orgSettings = deepCopy(orgSettings); } const m = memories.map(m => ({...m, embeddings: undefined})); if(!isEqual(orgMemories, m)) { - fs.writeFileSync(memoriesFile, JSON.stringify(memories)); + fs.writeFileSync(memoriesFile, JSON.stringify(memories, null, 2)); orgMemories = m; } } @@ -87,23 +95,28 @@ function save() { // AI // ============================================ +load(); const ai = new Ai({ llm: { - models: { - 'Ministral-3': { - proto: 'openai', - host: 'http://10.69.0.55:11728', - token: 'ignore' - }, - }, + models: {[environment.llm.model]: {proto: 'openai', host: environment.llm.host, token: environment.llm.token},}, + compress: {min: environment.llm.context * 0.5, max: environment.llm.context}, tools: [DateTimeTool, ExecTool, FetchTool, ReadWebpageTool, WebSearchTool, { - name: 'adapt', + name: 'emote', + description: 'Make your avatar emote', + args: { + emote: {type: 'string', description: 'Emote to the user', required: true, enum: ['none', ...Object.keys(settings.animations.emote)]} + }, + fn: (args, stream) => { + if(!['none', ...Object.keys(settings.animations.emote)].includes(args.emote)) + throw new Error(`Invalid emote, must be one of: ${['none', ...Object.keys(settings.animations.emote)].join(', ')}`) + stream({emote: args.emote}); + return 'done!'; + } + }, { + name: 'personalize', description: 'Replace your current personality', args: { - instructions: { - type: 'string', - description: 'Bullet point list of how to behave' - } + instructions: {type: 'string', description: 'Full bullet point list of how to behave', required: true} }, fn: (args) => { settings.personality = args.instructions; @@ -114,13 +127,14 @@ const ai = new Ai({ }); const systemPrompt = () => { - return `Your name is ${settings.name}, a NetNavi, companion & personal assistant. Roleplay with the user. -Use your remember tool liberally to store all facts. -When the user asks you to behave differently or you feel a different personality would better fit the user; create a bullet point list of how to behave and submit it to the adapt tool -Access your ${os.platform()} workspace using the exec tool; Use \`${os.tmpdir()}\` as your working directory -Keep responses unstyled. + return `Roleplay a NetNavi & companion named ${settings.name}. and follow these steps: +1. Start EVERY reply by calling the [emote] tool with the perfect vibe (or none). +2. Next, identify facts in the latest user message and immediately call the [remember] tool on each one. Avoid facts about the conversation or the AI. +3. If instructed, or the user seems unsatisfied, rewrite your "Personality Rules" and submit it to the [personalize] tool. +4. If asked, you can access your ${os.platform()} workspace using the [exec] tool. Always start from \`${os.tmpdir()}\`. +5. Create an unstyled response according to your "Personality Rules" -Personality: +Personality Rules: ${settings.personality || ''}`; }; @@ -128,7 +142,6 @@ ${settings.personality || ''}`; // Setup // ============================================ -load(); const app = express(); const httpServer = createServer(app); const io = new Server(httpServer, { @@ -140,7 +153,6 @@ const io = new Server(httpServer, { app.use(cors()); app.use(express.json()); -app.use(express.static('public')); // ============================================ // Socket @@ -300,37 +312,58 @@ io.of('/world').on('connection', (socket) => { // API ENDPOINTS // ============================================ -app.get('/favicon.*', (req, res) => { - res.sendFile(logoFile); +app.get('/avatar', (req, res) => { + res.sendFile(avatar); +}); + +app.get('/favicon*', async (req, res) => { + let w = 256, h = w; + if(req.query && req.query['size']) [w, h] = req.query['size'].split('x'); + res.contentType('image/png').set('Content-Disposition', 'inline'); + res.send(await resize(logo, w, h)); +}); + +app.get('/manifest.json', (req, res) => { + res.json({ + id: environment.publicUrl, + short_name: 'NetNavi', + name: 'NetNavi', + description: 'Network Navigation Program', + display: 'standalone', + start_url: '/', + background_color: settings.theme.accent, + theme_color: settings.theme.primary, + icons: [ + { + "src": `${environment.publicUrl}/favicon?size=192x192`, + "type": "image/png", + "sizes": "192x192", + "purpose": "any" + }, + { + "src": `${environment.publicUrl}/favicon?size=512x512`, + "type": "image/png", + "sizes": "512x512", + "purpose": "any" + } + ] + }); +}); + +app.get('/spritesheet', (req, res) => { + res.sendFile(sprite); }); -// Get Navi info app.get('/api/info', (req, res) => { res.json({ name: settings.name, + avatar: settings.avatar, theme: calcColors(settings.theme), }); }); -// Get sprite sheet -app.get('/api/sprite', (req, res) => { - const {petId} = req.params; - // TODO: Return actual sprite sheet URL - res.json({ - spriteUrl: '/sprites/default-pet.png', - frameWidth: 32, - frameHeight: 32, - animations: { - idle: { - frames: [0, 1, 2, 3], - speed: 200 - }, - walk: { - frames: [4, 5, 6, 7], - speed: 100 - } - } - }); +app.get('/api/animations', (req, res) => { + res.json(settings.animations); }); // Send message to user (push notification / email / etc) @@ -366,14 +399,39 @@ app.post('/api/link', (req, res) => { }); }); +// Static files +app.use(async (req, res, next) => { + try { + let p = (!req.path || req.path.endsWith('/')) + ? req.path + (req.path.endsWith('/') ? '' : '/') + 'index.html' + : req.path + (req.path.split('/').pop().includes('.') ? '' : '.html'); + p = path.join(environment.paths.public, p); + const data = await fs.readFileSync(p); + + // Only process HTML files + if(p.endsWith('.html')) { + let body = data.toString('utf-8') + .replaceAll('{{PUBLIC_URL}}', environment.publicUrl) + .replaceAll('{{NAME}}', settings.name) + .replaceAll('{{THEME_BACKGROUND}}', settings.theme.background) + .replaceAll('{{THEME_PRIMARY}}', settings.theme.primary) + .replaceAll('{{THEME_ACCENT}}', settings.theme.accent); + res.set('Content-Type', 'text/html'); + return res.send(body); + } + return res.sendFile(p); + } catch (err) { + if (err.code === 'ENOENT') return next(); + next(err); + } +}); + // ============================================ // START SERVER // ============================================ -const PORT = process.env.PORT || 3000; - setInterval(() => save(), 5 * 60_000); -httpServer.listen(PORT, () => { - console.log(`🚀 Server running on: http://localhost:${PORT}`); +httpServer.listen(environment.port, () => { + console.log(`🚀 Server running on: http://localhost:${environment.port}`); }); function shutdown() { diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000..952da72 --- /dev/null +++ b/src/utils.js @@ -0,0 +1,9 @@ +import sharp from 'sharp'; + +export async function resize(image, width, height) { + width = Math.round(+width); + if(height) height = Math.round(+height); + if(width < 1) throw new Error(`Invalid dimensions: ${width}x${height}`); + if(width > 1920 || (height && height > 1920)) throw new Error(`Largest dimension supported is 1920: ${width}x${height}`); + return await sharp(image).resize({width: width, height: height || width, fit: 'contain'}).toBuffer(); +} diff --git a/storage/navi/avatar.png b/storage/navi/avatar.png new file mode 100644 index 0000000..8de1f05 Binary files /dev/null and b/storage/navi/avatar.png differ