From 1bbc8f05c6c31091d64ba302ff51da08cec3e7c7 Mon Sep 17 00:00:00 2001 From: Apher Date: Fri, 10 Jul 2026 01:19:20 +0000 Subject: [PATCH] Add working TTS --- .gitignore | 3 +- package-lock.json | 277 +++++++++++++++++++++++++++++++++++++++++++++- package.json | 5 +- src/server.js | 176 ++++++++++++++++++++++------- 4 files changed, 416 insertions(+), 45 deletions(-) diff --git a/.gitignore b/.gitignore index 4f02d84..8882ec6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .env node_modules/ .venv/ -src/models/vosk* \ No newline at end of file +src/models/vosk* +output.wav \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index a4c8ad9..8487498 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,10 @@ "license": "ISC", "dependencies": { "mic": "^2.1.2", - "redis": "^6.1.0" + "redis": "^6.1.0", + "speaker": "^0.5.5", + "tiny-tts": "^5.0.1", + "wav-decoder": "^1.3.0" }, "devDependencies": { "dotenv": "^17.4.2", @@ -89,6 +92,14 @@ "@redis/client": "^6.1.0" } }, + "node_modules/adm-zip": { + "version": "0.5.18", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.18.tgz", + "integrity": "sha512-ufJnssQGbxzLNS1Ho9bCtX4rQKCCvoVuDLHoJyc3F9dOGDB4BkWs2Ci0kv53lqocAEQ/Cbi+I2XCsNYGqVYqng==", + "engines": { + "node": ">=12.0" + } + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -126,6 +137,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, "node_modules/brace-expansion": { "version": "5.0.7", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", @@ -152,6 +171,25 @@ "node": ">=8" } }, + "node_modules/buffer-alloc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", + "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "dependencies": { + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" + } + }, + "node_modules/buffer-alloc-unsafe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==" + }, + "node_modules/buffer-fill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==" + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -186,11 +224,18 @@ "node": ">=0.10.0" } }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "engines": { + "node": ">=18" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -204,6 +249,38 @@ } } }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/dotenv": { "version": "17.4.2", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", @@ -217,6 +294,38 @@ "url": "https://dotenvx.com" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "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", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -258,6 +367,46 @@ "node": ">= 6" } }, + "node_modules/global-agent": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/global-agent/-/global-agent-4.1.3.tgz", + "integrity": "sha512-KUJEViiuFT3I97t+GYMikLPJS2Lfo/S2F+DQuBWzuzaMPnvt5yyZePzArx36fBzpGTxZjIpDbXLeySLgh+k76g==", + "dependencies": { + "globalthis": "^1.0.2", + "matcher": "^4.0.0", + "semver": "^7.3.5", + "serialize-error": "^8.1.0" + }, + "engines": { + "node": ">=10.0" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -268,6 +417,17 @@ "node": ">=4" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/ignore-by-default": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", @@ -321,6 +481,20 @@ "node": ">=0.12.0" } }, + "node_modules/matcher": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/matcher/-/matcher-4.0.0.tgz", + "integrity": "sha512-S6x5wmcDmsDRRU/c2dkccDwQPXoFczc5+HpQ2lON8pnvHlnvHAHj5WlLVvw6n6vNyHuVugYrFohYxbS+pvFpKQ==", + "dependencies": { + "escape-string-regexp": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mic": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/mic/-/mic-2.1.2.tgz", @@ -347,7 +521,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/nodemon": { @@ -389,6 +562,35 @@ "node": ">=0.10.0" } }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/onnxruntime-common": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/onnxruntime-common/-/onnxruntime-common-1.27.0.tgz", + "integrity": "sha512-3KxL5wIVqa8Ex08jxSzncm9CMgw8CjOFyOQ7SxvG9o0cVLlhTNKXyIQuTbtX4tGPJEf73OER2xrjt4HJSBL4ow==" + }, + "node_modules/onnxruntime-node": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/onnxruntime-node/-/onnxruntime-node-1.27.0.tgz", + "integrity": "sha512-QEzGwrvNBgv4uPVdnbHsOGG4G6T96mdlcFI8aAKPjMU8wOPpVocPXb6k3QGkaZagVTv2G9Bnnbo6Z3JdXr1fQw==", + "hasInstallScript": true, + "os": [ + "win32", + "darwin", + "linux" + ], + "dependencies": { + "adm-zip": "^0.5.16", + "global-agent": "^4.1.3", + "onnxruntime-common": "1.27.0" + } + }, "node_modules/picomatch": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", @@ -442,7 +644,6 @@ "version": "7.8.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -451,6 +652,20 @@ "node": ">=10" } }, + "node_modules/serialize-error": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-8.1.0.tgz", + "integrity": "sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ==", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", @@ -464,6 +679,20 @@ "node": ">=10" } }, + "node_modules/speaker": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/speaker/-/speaker-0.5.5.tgz", + "integrity": "sha512-IBeMZUITigYBO139h0+1MAgBHNZF55GFJN4U/Box35Sg49cfqYkbCO92TXoCUy22Ast08zfqKuXLvPxq9CWwLw==", + "hasInstallScript": true, + "dependencies": { + "bindings": "^1.3.0", + "buffer-alloc": "^1.1.0", + "debug": "^4.0.0" + }, + "engines": { + "node": ">=8.6" + } + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -477,6 +706,19 @@ "node": ">=4" } }, + "node_modules/tiny-tts": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/tiny-tts/-/tiny-tts-5.0.1.tgz", + "integrity": "sha512-T7pTj5iZhanjcrABI1zPh/zh8iTPV2xsfeZSFjgbNmj0C21vW22jigmVQJ4mmbyCcNShyjJYu/35uci+ilxhaQ==", + "dependencies": { + "commander": "^12.0.0", + "onnxruntime-node": "^1.19.0", + "wavefile": "^11.0.0" + }, + "bin": { + "tiny-tts": "bin/cli.js" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -500,12 +742,39 @@ "nodetouch": "bin/nodetouch.js" } }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", "dev": true, "license": "MIT" + }, + "node_modules/wav-decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/wav-decoder/-/wav-decoder-1.3.0.tgz", + "integrity": "sha512-4U6O/JNb1dPO90CO2YMTQ5N2plJcntm39vNMvRq9VZ4Vy5FzS7Lnx95N2QcYUyKYcZfCbhI//W3dSHA8YnOQyQ==" + }, + "node_modules/wavefile": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/wavefile/-/wavefile-11.0.0.tgz", + "integrity": "sha512-/OBiAALgWU24IG7sC84cDO/KfFuvajWc5Uec0oV2zrpOOZZDgGdOwHwgEzOrwh8jkubBk7PtZfQBIcI1OaE5Ng==", + "bin": { + "wavefile": "bin/wavefile.js" + }, + "engines": { + "node": ">=8" + } } } } diff --git a/package.json b/package.json index 23ccdde..02b88e3 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,10 @@ "type": "commonjs", "dependencies": { "mic": "^2.1.2", - "redis": "^6.1.0" + "redis": "^6.1.0", + "speaker": "^0.5.5", + "tiny-tts": "^5.0.1", + "wav-decoder": "^1.3.0" }, "devDependencies": { "dotenv": "^17.4.2", diff --git a/src/server.js b/src/server.js index bdb4b2e..3f0e1f7 100644 --- a/src/server.js +++ b/src/server.js @@ -1,64 +1,162 @@ // Import dotenv file if not in production if (process.env.NODE_ENV !== "production") { - require("dotenv").config(); + require("dotenv").config(); } -const { spawn } = require('child_process'); +const TinyTTS = require("tiny-tts"); +const tts = new TinyTTS(); + +const fs = require("fs"); +const os = require("os"); +const path = require("path"); +const wav = require("wav-decoder"); +const Speaker = require("speaker"); +const { spawn } = require("child_process"); const models = require("./models"); const redis = models.redis; const config = require("./config"); -const transConfig = require("./config/transcribe.config") - -const py = spawn('python', ['./src/scripts/py/transcribe.py', JSON.stringify(transConfig)], { - stdio: ['ignore', 'pipe', 'pipe'] -}); +const transConfig = require("./config/transcribe.config"); const { handlePythonOutput } = require("./scripts/py/pythonOutput"); -// Read Redis Stream group -async function processor (group, consumer, streamKey) { - while (true) { - const res = await redis.client.xReadGroup( - group, - consumer, - [{key: streamKey, id: '>'}], - {COUNT: 5, BLOCK:3000} - ); +let speaker = null; +let speakerChannels = null; - if (!res) continue; +function ensureSpeaker(channels) { + if (speaker && speakerChannels === channels) return; + speakerChannels = channels; - for (const stream of res) { - for (const msg of stream.messages) { - console.log(`[${consumer}] Processing:`, msg); + if (speaker) { + try { speaker.end(); } catch {} + } - await redis.client.xAck(streamKey, group, msg.id); - } - } + const candidates = [ + { device: "default" }, + { device: "plughw:0,0" }, + { device: "hw:0,0" }, + ]; + + let lastErr = null; + for (const c of candidates) { + try { + speaker = new Speaker({ + channels, + bitDepth: 16, + sampleRate: 44100, + device: c.device, + }); + return; // success + } catch (e) { + lastErr = e; } + } + + throw lastErr || new Error("Failed to open output device"); +} + +function floatsToPCM16LE(channelData) { + const nCh = channelData.length; + const n = channelData[0].length; + const out = Buffer.alloc(n * nCh * 2); + + for (let i = 0; i < n; i++) { + for (let ch = 0; ch < nCh; ch++) { + let s = channelData[ch][i]; + s = Math.max(-1, Math.min(1, s)); + out.writeInt16LE((s * 0x7fff) | 0, (i * nCh + ch) * 2); + } + } + return out; +} + +async function playAudio(text) { + const outFile = path.join( + os.tmpdir(), + `tinytts-${Date.now()}-${Math.random().toString(16).slice(2)}.wav` + ); + + try { + // Force tiny-tts to write the WAV to disk (works across versions) + const maybeBytes = await tts.speak(text, { output: outFile }); + + // Some versions return bytes, some return nothing. Always read the file if needed. + const wavBytes = Buffer.isBuffer(maybeBytes) + ? maybeBytes + : await fs.promises.readFile(outFile); + + if (!Buffer.isBuffer(wavBytes) || wavBytes.length < 44) { + throw new Error("tiny-tts did not produce a valid WAV buffer"); + } + if (wavBytes.toString("ascii", 0, 4) !== "RIFF") { + throw new Error("tiny-tts WAV buffer missing RIFF header"); + } + + const decoded = await wav.decode(wavBytes); + if (!decoded || decoded.sampleRate !== 44100 || !decoded.channelData?.length) { + throw new Error(`Unsupported WAV: ${decoded?.sampleRate} Hz`); + } + + ensureSpeaker(decoded.channelData.length); + const pcm = floatsToPCM16LE(decoded.channelData); + speaker.write(pcm); + } finally { + fs.promises.unlink(outFile).catch(() => {}); + } +} + +const py = spawn("python", ["./src/scripts/py/transcribe.py", JSON.stringify(transConfig)], { + stdio: ["ignore", "pipe", "pipe"], +}); + +async function processor(group, consumer, streamKey) { + while (true) { + const res = await redis.client.xReadGroup( + group, + consumer, + [{ key: streamKey, id: ">" }], + { COUNT: 5, BLOCK: 3000 } + ); + + if (!res) continue; + + for (const stream of res) { + for (const msg of stream.messages) { + console.log(`[${consumer}] Processing:`, msg); + + if (msg.message.command === "play_music") { + await playAudio("Playing music."); + } + + await redis.client.xAck(streamKey, group, msg.id); + } + } + } } async function main() { - await redis.client.connect().catch(console.error); - await redis.ensureGroup("commands", "assistant"); + await redis.client.connect().catch(console.error); + await redis.ensureGroup("commands", "assistant"); - py.stdout.on("data", (data) => { - handlePythonOutput(data.toString(), async (msg) => { - if (msg.event === "text" || msg.event === "final") { - await redis.sendTranscriptEvent("transcripts", config.DEVICE_ID, msg.text); - } - }); + await playAudio("Assistant, online.") + + py.stdout.on("data", (data) => { + handlePythonOutput(data.toString(), async (msg) => { + if (msg.event === "text" || msg.event === "final") { + await redis.sendTranscriptEvent("transcripts", config.DEVICE_ID, msg.text); + } }); + }); - py.stderr.on("data", (data) => { - console.error("[PY ERR]", data.toString()); - }); + py.stderr.on("data", (data) => { + console.error("[PY ERR]", data.toString()); + }); - py.on("close", (code) => { - console.log("Python exited with code", code); - }); + py.on("close", (code) => { + console.log("Python exited with code", code); + }); - processor("assistant", `${config.DEVICE_ID}`, "commands").catch(console.error); + processor("assistant", `${config.DEVICE_ID}`, "commands").catch(console.error); } -main().catch(console.error); \ No newline at end of file +main().catch(console.error);