Compare commits
10 Commits
6e8c689639
...
4a2c6aa761
| Author | SHA1 | Date | |
|---|---|---|---|
| 4a2c6aa761 | |||
| 8a65d22c12 | |||
| 2c975003b0 | |||
| 8472485283 | |||
| fcf94f7ef5 | |||
| d25cc2adf3 | |||
| 22bcfa31f3 | |||
| b92ffb7a73 | |||
| f99ffe9772 | |||
| 882bc00916 |
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,5 +1,5 @@
|
||||
.env
|
||||
node_modules/
|
||||
.venv/
|
||||
src/models/vosk*
|
||||
src/scripts/py/zipformer*
|
||||
output.wav
|
||||
279
package-lock.json
generated
279
package-lock.json
generated
@ -9,6 +9,7 @@
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"axios": "^1.18.1",
|
||||
"mic": "^2.1.2",
|
||||
"redis": "^6.1.0",
|
||||
"speaker": "^0.5.5",
|
||||
@ -100,6 +101,18 @@
|
||||
"node": ">=12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/agent-base": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||
"integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/anymatch": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
|
||||
@ -114,6 +127,24 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.18.1",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz",
|
||||
"integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.16.0",
|
||||
"form-data": "^4.0.5",
|
||||
"https-proxy-agent": "^5.0.1",
|
||||
"proxy-from-env": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||
@ -190,6 +221,19 @@
|
||||
"resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz",
|
||||
"integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ=="
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||
@ -224,6 +268,18 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "12.1.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
|
||||
@ -281,6 +337,15 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "17.4.2",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz",
|
||||
@ -294,6 +359,20 @@
|
||||
"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",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
@ -310,6 +389,33 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
|
||||
"integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-set-tostringtag": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.6",
|
||||
"has-tostringtag": "^1.0.2",
|
||||
"hasown": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-string-regexp": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||
@ -339,6 +445,42 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.16.0",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
|
||||
"integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz",
|
||||
"integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.4",
|
||||
"mime-types": "^2.1.35"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
@ -354,6 +496,52 @@
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"es-define-property": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"es-object-atoms": "^1.1.1",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-proto": "^1.0.1",
|
||||
"gopd": "^1.2.0",
|
||||
"has-symbols": "^1.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"math-intrinsics": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
"es-object-atoms": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/glob-parent": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
@ -428,6 +616,58 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-tostringtag": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
|
||||
"integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/https-proxy-agent": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
|
||||
"integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "6",
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/ignore-by-default": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
|
||||
@ -495,12 +735,42 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/mic": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/mic/-/mic-2.1.2.tgz",
|
||||
"integrity": "sha512-rpl4tgdXX24sAzYwjRc5OZfGNAuhUIIjdd0cw8+Ubq7rp3iGhi40AdqcwurDWhEZADk60tPOxb3E2MpoeLeyxw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "10.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
|
||||
@ -604,6 +874,15 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
|
||||
"integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/pstree.remy": {
|
||||
"version": "1.1.8",
|
||||
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
|
||||
|
||||
@ -12,6 +12,7 @@
|
||||
"license": "ISC",
|
||||
"type": "commonjs",
|
||||
"dependencies": {
|
||||
"axios": "^1.18.1",
|
||||
"mic": "^2.1.2",
|
||||
"redis": "^6.1.0",
|
||||
"speaker": "^0.5.5",
|
||||
|
||||
@ -2,5 +2,7 @@ module.exports = {
|
||||
FORCE_DB_SYNC: process.env.FORCE_DB_SYNC,
|
||||
NODE_ENV: process.env.NODE_ENV,
|
||||
PORT: process.env.PORT,
|
||||
DEVICE_ID: process.env.DEVICE_ID
|
||||
DEVICE_ID: process.env.DEVICE_ID,
|
||||
HA_URL: process.env.HA_URL,
|
||||
HA_TOKEN: process.env.HA_TOKEN
|
||||
};
|
||||
|
||||
@ -1,4 +0,0 @@
|
||||
module.exports = {
|
||||
HOST: process.env.REDIS_HOST,
|
||||
PORT: process.env.REDIS_PORT
|
||||
}
|
||||
@ -6,6 +6,11 @@ module.exports = {
|
||||
listenSeconds: Number(process.env.TRANS_LISTEN_SECONDS),
|
||||
silenceThreshold: Number(process.env.TRANS_SILENCE_THRESHOLD),
|
||||
silenceFrames: Number(process.env.TRANS_SILENCE_FRAMES),
|
||||
voskModelPath: process.env.TRANS_VOSK_MODEL_PATH,
|
||||
sampleRate: Number(process.env.TRANS_SAMPLE_RATE),
|
||||
sherpaModelPath: {
|
||||
encoder: process.env.TRANS_SHERPA_ENCODER,
|
||||
decoder: process.env.TRANS_SHERPA_DECODER,
|
||||
joiner: process.env.TRANS_SHERPA_JOINER,
|
||||
tokens: process.env.TRANS_SHERPA_TOKENS,
|
||||
},
|
||||
};
|
||||
|
||||
@ -1 +1 @@
|
||||
Subproject commit e41a75e7436a059ab963c5db7b62ca524cacc4e5
|
||||
Subproject commit b335675847af7b92b3464d807939d1dbe558968d
|
||||
@ -6,6 +6,11 @@
|
||||
"listenSeconds": 12,
|
||||
"silenceThreshold": 0.015,
|
||||
"silenceFrames": 10,
|
||||
"voskModelPath": "./vosk-model-small-en-us-0.15",
|
||||
"sampleRate": 16000
|
||||
"sampleRate": 16000,
|
||||
"sherpaModelPath": {
|
||||
"encoder": "./zipformer-small/encoder.onnx",
|
||||
"decoder": "./zipformer-small/decoder.onnx",
|
||||
"joiner": "./zipformer-small/joiner.onnx",
|
||||
"tokens": "./zipformer-small/data/tokens.txt"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
// pythonOutput.js
|
||||
let buffer = "";
|
||||
|
||||
function handlePythonOutput(chunk, onMessage) {
|
||||
|
||||
@ -3,3 +3,4 @@ onnxruntime
|
||||
numpy
|
||||
pyaudio
|
||||
vosk
|
||||
sherpa_onnx
|
||||
@ -1,116 +1,229 @@
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
import collections, json, os, sys
|
||||
from dataclasses import dataclass
|
||||
from time import monotonic
|
||||
|
||||
import numpy as np
|
||||
import pyaudio
|
||||
import vosk
|
||||
import openwakeword
|
||||
import pyaudio
|
||||
import sherpa_onnx
|
||||
from openwakeword.model import Model
|
||||
|
||||
CONFIG = json.loads(sys.argv[1])
|
||||
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
RATE = CONFIG.get("sampleRate", 16000)
|
||||
CHUNK = 1280
|
||||
WAKEWORD = CONFIG.get("wakeword", "alexa")
|
||||
THRESHOLD = CONFIG.get("wakewordThreshold", 0.75)
|
||||
CHUNK = 640
|
||||
|
||||
# Wakeword
|
||||
WAKEWORD = CONFIG.get("wakeword", "hey jarvis")
|
||||
THRESHOLD = CONFIG.get("wakewordThreshold", 0.80)
|
||||
COOLDOWN = CONFIG.get("cooldownSeconds", 2.5)
|
||||
CONFIRM_FRAMES = CONFIG.get("confirmFrames", 3)
|
||||
LISTEN_SECONDS = CONFIG.get("listenSeconds", 12)
|
||||
SILENCE_THRESHOLD = CONFIG.get("silenceThreshold", 0.02)
|
||||
SILENCE_FRAMES = CONFIG.get("silenceFrames", 12)
|
||||
VOSK_MODEL_PATH = CONFIG["voskModelPath"]
|
||||
|
||||
vosk.SetLogLevel(-1)
|
||||
vosk_model = vosk.Model(VOSK_MODEL_PATH)
|
||||
recognizer = vosk.KaldiRecognizer(vosk_model, RATE)
|
||||
recognizer.SetWords(True)
|
||||
# Speech/silence gating (prevents wakeword firing right after transcript)
|
||||
# Values assume threshold is on a normalized amplitude scale (0..1).
|
||||
SILENCE_THRESHOLD = CONFIG.get("silenceThreshold", 0.03)
|
||||
SILENCE_FRAMES = CONFIG.get("silenceFrames", 6)
|
||||
|
||||
# Download all models (TODO: Import single model)
|
||||
openwakeword.utils.download_models()
|
||||
# After transcribe finishes, ignore wakeword for a short period
|
||||
POST_TRANSCRIPT_IGNORE_SECONDS = CONFIG.get("postTranscriptIgnoreSeconds", 1.5)
|
||||
|
||||
# ---- Transcription endpointing ----
|
||||
ENDPOINT_RULE1 = CONFIG.get("endpoint_rule1_min_trailing_silence", 2.4)
|
||||
ENDPOINT_RULE2 = CONFIG.get("endpoint_rule2_min_trailing_silence", 1.2)
|
||||
ENDPOINT_RULE3 = CONFIG.get("endpoint_rule3_min_utterance_length", 30)
|
||||
MIN_TRANSCRIBE_SECONDS_BEFORE_ENDPOINT = CONFIG.get("min_transcribe_before_endpoint", 1.0)
|
||||
ENABLE_ENDPOINT_DETECTION = CONFIG.get("enable_endpoint_detection", True)
|
||||
|
||||
MODE_WAKE = "wakeword"
|
||||
MODE_TRANSCRIBE = "transcribe"
|
||||
|
||||
# ---- Sherpa model paths ----
|
||||
mc = CONFIG["sherpaModelPath"]
|
||||
paths = {k: os.path.join(SCRIPT_DIR, v.lstrip("./")) for k, v in mc.items()}
|
||||
|
||||
recognizer = sherpa_onnx.OnlineRecognizer.from_transducer(
|
||||
encoder=paths["encoder"],
|
||||
decoder=paths["decoder"],
|
||||
joiner=paths["joiner"],
|
||||
tokens=paths["tokens"],
|
||||
sample_rate=RATE,
|
||||
feature_dim=80,
|
||||
decoding_method="greedy_search",
|
||||
num_threads=max(1, (os.cpu_count() or 1) // 2),
|
||||
|
||||
enable_endpoint_detection=ENABLE_ENDPOINT_DETECTION,
|
||||
rule1_min_trailing_silence=ENDPOINT_RULE1,
|
||||
rule2_min_trailing_silence=ENDPOINT_RULE2,
|
||||
rule3_min_utterance_length=ENDPOINT_RULE3,
|
||||
)
|
||||
|
||||
# ---- openwakeword model ----
|
||||
oww = Model(wakeword_models=[WAKEWORD], inference_framework="onnx")
|
||||
|
||||
audio = pyaudio.PyAudio()
|
||||
stream = audio.open(
|
||||
# ---- Audio ----
|
||||
pa = pyaudio.PyAudio()
|
||||
mic = pa.open(
|
||||
format=pyaudio.paInt16,
|
||||
channels=1,
|
||||
rate=RATE,
|
||||
input=True,
|
||||
frames_per_buffer=CHUNK,
|
||||
frames_per_buffer=CHUNK
|
||||
)
|
||||
|
||||
print(json.dumps({"event": "ready"}), flush=True)
|
||||
@dataclass
|
||||
class State:
|
||||
mode: str = MODE_WAKE
|
||||
high_count: int = 0
|
||||
last_trigger: float = 0.0
|
||||
|
||||
mode = "wakeword"
|
||||
high_count = 0
|
||||
last_trigger = 0
|
||||
silence_count = 0
|
||||
listen_deadline = None
|
||||
# Transcription timing
|
||||
listen_deadline: float | None = None
|
||||
transcribe_start: float | None = None
|
||||
|
||||
def rms_level(samples):
|
||||
x = samples.astype(np.float32) / 32768.0
|
||||
return np.sqrt(np.mean(x * x))
|
||||
# Wakeword anti-double-trigger
|
||||
post_transcript_ignore_deadline: float | None = None
|
||||
|
||||
# Silence gate
|
||||
silence_count: int = 0
|
||||
|
||||
sherpa_stream: object = None
|
||||
last_partial: str = ""
|
||||
|
||||
state = State()
|
||||
|
||||
float_buf = np.empty(CHUNK, np.float32)
|
||||
|
||||
def emit(event, **kw):
|
||||
print(json.dumps({"event": event, **kw}, ensure_ascii=False), flush=True)
|
||||
|
||||
def decode_pending(st):
|
||||
while recognizer.is_ready(st):
|
||||
recognizer.decode_stream(st)
|
||||
|
||||
def reset_oww():
|
||||
if hasattr(oww, "accumulated_predictions") and WAKEWORD in oww.accumulated_predictions:
|
||||
oww.accumulated_predictions[WAKEWORD].clear()
|
||||
|
||||
def flush():
|
||||
# Drain mic so next phase starts with fresh audio
|
||||
while True:
|
||||
try:
|
||||
available = mic.get_read_available()
|
||||
except Exception:
|
||||
break
|
||||
if available <= 0:
|
||||
break
|
||||
mic.read(min(available, CHUNK), exception_on_overflow=False)
|
||||
|
||||
def start(now):
|
||||
state.mode = MODE_TRANSCRIBE
|
||||
state.high_count = 0
|
||||
state.last_trigger = now
|
||||
|
||||
state.listen_deadline = now + LISTEN_SECONDS
|
||||
state.transcribe_start = now
|
||||
|
||||
state.last_partial = ""
|
||||
state.sherpa_stream = recognizer.create_stream()
|
||||
|
||||
def stop(now):
|
||||
# Reset back to wake mode
|
||||
reset_oww()
|
||||
flush()
|
||||
|
||||
state.mode = MODE_WAKE
|
||||
state.high_count = 0
|
||||
|
||||
state.listen_deadline = None
|
||||
state.transcribe_start = None
|
||||
state.last_partial = ""
|
||||
state.sherpa_stream = None
|
||||
|
||||
# Cooldown baseline should start AFTER the transcript ends
|
||||
state.last_trigger = now
|
||||
|
||||
# And ignore wakeword for a short grace period
|
||||
state.post_transcript_ignore_deadline = now + POST_TRANSCRIPT_IGNORE_SECONDS
|
||||
|
||||
emit("ready")
|
||||
|
||||
try:
|
||||
while True:
|
||||
data = stream.read(CHUNK, exception_on_overflow=False)
|
||||
samples = np.frombuffer(data, dtype=np.int16)
|
||||
now = time.time()
|
||||
pcm_i16 = mic.read(CHUNK, exception_on_overflow=False)
|
||||
samples_i16 = np.frombuffer(pcm_i16, dtype=np.int16)
|
||||
|
||||
if mode == "wakeword":
|
||||
preds = oww.predict(samples)
|
||||
score = preds.get(WAKEWORD, 0.0)
|
||||
now = monotonic()
|
||||
|
||||
if score >= THRESHOLD:
|
||||
high_count += 1
|
||||
if state.mode == MODE_WAKE:
|
||||
# -------- Silence gate (prevents wake right after transcript) --------
|
||||
# normalized mean absolute amplitude
|
||||
amp = float(np.mean(np.abs(samples_i16)) / 32768.0)
|
||||
|
||||
if amp <= SILENCE_THRESHOLD:
|
||||
state.silence_count += 1
|
||||
else:
|
||||
high_count = 0
|
||||
state.silence_count = 0
|
||||
|
||||
if high_count >= CONFIRM_FRAMES and (now - last_trigger) > COOLDOWN:
|
||||
last_trigger = now
|
||||
high_count = 0
|
||||
print(json.dumps({
|
||||
"event": "wakeword",
|
||||
"model": WAKEWORD,
|
||||
"score": float(score)
|
||||
}), flush=True)
|
||||
mode = "transcribe"
|
||||
recognizer.Reset()
|
||||
silence_count = 0
|
||||
listen_deadline = now + LISTEN_SECONDS
|
||||
silence_ok = state.silence_count >= SILENCE_FRAMES
|
||||
|
||||
else:
|
||||
if recognizer.AcceptWaveform(data):
|
||||
result = json.loads(recognizer.Result())
|
||||
if result.get("text"):
|
||||
print(json.dumps({"event": "final", "text": result["text"]}), flush=True)
|
||||
listen_deadline = now + LISTEN_SECONDS
|
||||
silence_count = 0
|
||||
else:
|
||||
partial = json.loads(recognizer.PartialResult()).get("partial", "")
|
||||
if partial:
|
||||
print(json.dumps({"event": "partial", "text": partial}), flush=True)
|
||||
# Ignore wakeword right after transcript ends
|
||||
ignore_ok = not (
|
||||
state.post_transcript_ignore_deadline is not None
|
||||
and now < state.post_transcript_ignore_deadline
|
||||
)
|
||||
|
||||
level = rms_level(samples)
|
||||
if level < SILENCE_THRESHOLD:
|
||||
silence_count += 1
|
||||
else:
|
||||
silence_count = 0
|
||||
if not (silence_ok and ignore_ok):
|
||||
continue
|
||||
|
||||
if silence_count >= SILENCE_FRAMES or now > listen_deadline:
|
||||
final = json.loads(recognizer.FinalResult())
|
||||
if final.get("text"):
|
||||
print(json.dumps({"event": "final", "text": final["text"]}), flush=True)
|
||||
# -------- Wakeword scoring --------
|
||||
score = oww.predict(samples_i16).get(WAKEWORD, 0.0)
|
||||
|
||||
print(json.dumps({"event": "done"}), flush=True)
|
||||
mode = "wakeword"
|
||||
silence_count = 0
|
||||
high_count = 0
|
||||
listen_deadline = None
|
||||
state.high_count = (state.high_count + 1) if score >= THRESHOLD else 0
|
||||
|
||||
if state.high_count >= CONFIRM_FRAMES and (now - state.last_trigger) > COOLDOWN:
|
||||
start(now)
|
||||
emit("wakeword", model=WAKEWORD, score=float(score))
|
||||
|
||||
continue
|
||||
|
||||
# ---- Transcribe mode ----
|
||||
np.multiply(samples_i16, 1 / 32768.0, out=float_buf, casting="unsafe")
|
||||
|
||||
st = state.sherpa_stream
|
||||
st.accept_waveform(RATE, float_buf)
|
||||
decode_pending(st)
|
||||
|
||||
txt = recognizer.get_result(st)
|
||||
if txt and txt != state.last_partial:
|
||||
state.last_partial = txt
|
||||
emit("partial", text=txt)
|
||||
|
||||
endpoint_ok = False
|
||||
if state.listen_deadline is not None and now >= state.listen_deadline:
|
||||
endpoint_ok = True
|
||||
elif ENABLE_ENDPOINT_DETECTION and recognizer.is_endpoint(st):
|
||||
# Don’t allow endpointing immediately after switching to transcribe
|
||||
if state.transcribe_start is not None and now >= (state.transcribe_start + MIN_TRANSCRIBE_SECONDS_BEFORE_ENDPOINT):
|
||||
endpoint_ok = True
|
||||
|
||||
if not endpoint_ok:
|
||||
continue
|
||||
|
||||
st.input_finished()
|
||||
decode_pending(st)
|
||||
|
||||
final = recognizer.get_result(st)
|
||||
if final:
|
||||
emit("final", text=final)
|
||||
|
||||
emit("done")
|
||||
stop(now)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
stream.stop_stream()
|
||||
stream.close()
|
||||
audio.terminate()
|
||||
mic.stop_stream()
|
||||
mic.close()
|
||||
pa.terminate()
|
||||
|
||||
@ -3,9 +3,14 @@ if (process.env.NODE_ENV !== "production") {
|
||||
require("dotenv").config();
|
||||
}
|
||||
|
||||
// Axios
|
||||
const axios = require("axios");
|
||||
|
||||
// TinyTTS
|
||||
const TinyTTS = require("tiny-tts");
|
||||
const tts = new TinyTTS();
|
||||
|
||||
// General imports
|
||||
const fs = require("fs");
|
||||
const os = require("os");
|
||||
const path = require("path");
|
||||
@ -13,12 +18,26 @@ const wav = require("wav-decoder");
|
||||
const Speaker = require("speaker");
|
||||
const { spawn } = require("child_process");
|
||||
|
||||
// Modules
|
||||
const {redis} = require('./modules/redis');
|
||||
|
||||
// Configs
|
||||
const config = require("./config");
|
||||
const transConfig = require("./config/transcribe.config");
|
||||
const { handlePythonOutput } = require("./scripts/py/pythonOutput");
|
||||
|
||||
// === HomeAssistant ===
|
||||
const ha = {}
|
||||
|
||||
ha.client = axios.create({
|
||||
baseURL: config.HA_URL,
|
||||
headers: {
|
||||
Authorization: `Bearer ${config.HA_TOKEN}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
// === Speaker ===
|
||||
let speaker = null;
|
||||
let speakerChannels = null;
|
||||
|
||||
@ -69,6 +88,7 @@ function floatsToPCM16LE(channelData) {
|
||||
return out;
|
||||
}
|
||||
|
||||
// === TTS ===
|
||||
async function playAudio(text) {
|
||||
const outFile = path.join(
|
||||
os.tmpdir(),
|
||||
@ -104,6 +124,7 @@ async function playAudio(text) {
|
||||
}
|
||||
}
|
||||
|
||||
// === Transcriber ===
|
||||
const py = spawn("python", ["./src/scripts/py/transcribe.py", JSON.stringify(transConfig)], {
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
@ -136,6 +157,26 @@ async function processor(group, consumer, streamKey) {
|
||||
await playAudio("Reminder, " + String(msg.message.value));
|
||||
break;
|
||||
|
||||
case "light_on":
|
||||
await playAudio("Turning on light.");
|
||||
await ha.client.post("/api/services/homeassistant/turn_on", {
|
||||
entity_id: "light.living_room_living_room"
|
||||
}).catch(async (err) => {
|
||||
await playAudio("I have failed you father.");
|
||||
console.error(err);
|
||||
});
|
||||
break;
|
||||
|
||||
case "light_off":
|
||||
await playAudio("Turning off light.");
|
||||
await ha.client.post("/api/services/homeassistant/turn_off", {
|
||||
entity_id: "light.living_room_living_room"
|
||||
}).catch(async (err) => {
|
||||
await playAudio("I have failed you father.");
|
||||
console.error(err);
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
await playAudio("Command, " + String(msg.message.command) + " is undefined.")
|
||||
}
|
||||
@ -146,12 +187,9 @@ async function processor(group, consumer, streamKey) {
|
||||
}
|
||||
}
|
||||
|
||||
// === Main ===
|
||||
async function main() {
|
||||
await redis.connect();
|
||||
await redis.ensureGroup("commands", "assistant");
|
||||
|
||||
await playAudio("Assistant, online.");
|
||||
|
||||
// Attach python listeners FIRST to avoid missing logs
|
||||
py.stdout.on("data", (data) => {
|
||||
handlePythonOutput(data.toString(), async (msg) => {
|
||||
if (msg.event === "text" || msg.event === "final") {
|
||||
@ -168,7 +206,18 @@ async function main() {
|
||||
console.log("Python exited with code", code);
|
||||
});
|
||||
|
||||
try {
|
||||
await redis.connect();
|
||||
await redis.ensureGroup("commands", "assistant");
|
||||
await playAudio("Assistant, online.");
|
||||
|
||||
// Start the command processor
|
||||
processor("assistant", `${config.DEVICE_ID}`, "commands").catch(console.error);
|
||||
|
||||
console.log("System fully initialized.");
|
||||
} catch (err) {
|
||||
console.error("Initialization failed:", err);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user