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 = `
+
+
+

+
`;
+ 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