From 19df767f81cd6d1c65cb5f26c285f493b73d0641 Mon Sep 17 00:00:00 2001 From: Apher Date: Sun, 5 Jul 2026 06:44:13 +0000 Subject: [PATCH 01/14] Ignore python venv --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 2d7ec5c..49f898c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .env node_modules/ +.venv/ \ No newline at end of file From 059990cd312bf228e1560093afb9863159a6a163 Mon Sep 17 00:00:00 2001 From: Apher Date: Mon, 6 Jul 2026 06:12:05 +0000 Subject: [PATCH 02/14] Drafting with Abominable Intelligence --- .gitignore | 3 +- package-lock.json | 7 ++ package.json | 1 + src/scripts/py/config.json | 11 ++++ src/scripts/py/info.txt | 1 + src/scripts/py/requirements.txt | 4 ++ src/scripts/py/transcribe.js | 46 +++++++++++++ src/scripts/py/wakeword.py | 112 ++++++++++++++++++++++++++++++++ 8 files changed, 184 insertions(+), 1 deletion(-) create mode 100644 src/scripts/py/config.json create mode 100644 src/scripts/py/info.txt create mode 100644 src/scripts/py/requirements.txt create mode 100644 src/scripts/py/transcribe.js create mode 100644 src/scripts/py/wakeword.py diff --git a/.gitignore b/.gitignore index 49f898c..2a5b053 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .env node_modules/ -.venv/ \ No newline at end of file +.venv/ +src/scripts/py/vosk-model*/ \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 4b13c72..a4c8ad9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "mic": "^2.1.2", "redis": "^6.1.0" }, "devDependencies": { @@ -320,6 +321,12 @@ "node": ">=0.12.0" } }, + "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/minimatch": { "version": "10.2.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", diff --git a/package.json b/package.json index a69a95b..23ccdde 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "license": "ISC", "type": "commonjs", "dependencies": { + "mic": "^2.1.2", "redis": "^6.1.0" }, "devDependencies": { diff --git a/src/scripts/py/config.json b/src/scripts/py/config.json new file mode 100644 index 0000000..8f1cd46 --- /dev/null +++ b/src/scripts/py/config.json @@ -0,0 +1,11 @@ +{ + "wakeword": "alexa", + "wakewordThreshold": 0.80, + "cooldownSeconds": 2.5, + "confirmFrames": 3, + "listenSeconds": 12, + "silenceThreshold": 0.015, + "silenceFrames": 10, + "voskModelPath": "./vosk-model-small-en-us-0.15", + "sampleRate": 16000 +} diff --git a/src/scripts/py/info.txt b/src/scripts/py/info.txt new file mode 100644 index 0000000..c30b258 --- /dev/null +++ b/src/scripts/py/info.txt @@ -0,0 +1 @@ +PyAudio requires Python version <=3.13 \ No newline at end of file diff --git a/src/scripts/py/requirements.txt b/src/scripts/py/requirements.txt new file mode 100644 index 0000000..b787d28 --- /dev/null +++ b/src/scripts/py/requirements.txt @@ -0,0 +1,4 @@ +openwakeword +onnxruntime +numpy +pyaudio \ No newline at end of file diff --git a/src/scripts/py/transcribe.js b/src/scripts/py/transcribe.js new file mode 100644 index 0000000..50137ca --- /dev/null +++ b/src/scripts/py/transcribe.js @@ -0,0 +1,46 @@ +const { spawn } = require('child_process'); +const fs = require('fs'); + +const config = JSON.parse(fs.readFileSync('./config.json', 'utf8')); + +const py = spawn('python', ['wakeword.py', JSON.stringify(config)], { + stdio: ['ignore', 'pipe', 'pipe'] +}); + +let buffer = ''; + +py.stdout.on('data', (data) => { + buffer += data.toString(); + const lines = buffer.split('\n'); + buffer = lines.pop(); + + for (const line of lines) { + if (!line.trim()) continue; + + try { + const msg = JSON.parse(line); + + if (msg.event === 'ready') { + console.log('Python ready'); + } else if (msg.event === 'wakeword') { + console.log(`Wakeword detected: ${msg.model} (${msg.score})`); + } else if (msg.event === 'partial') { + console.log('Partial:', msg.text); + } else if (msg.event === 'text' || msg.event === 'final') { + console.log('Transcript:', msg.text); + } else if (msg.event === 'done') { + console.log('Back to wakeword mode'); + } + } catch { + console.log('[PY]', line); + } + } +}); + +py.stderr.on('data', (data) => { + console.error('[PY ERR]', data.toString()); +}); + +py.on('close', (code) => { + console.log('Python exited with code', code); +}); diff --git a/src/scripts/py/wakeword.py b/src/scripts/py/wakeword.py new file mode 100644 index 0000000..947b956 --- /dev/null +++ b/src/scripts/py/wakeword.py @@ -0,0 +1,112 @@ +import json +import sys +import time +import numpy as np +import pyaudio +import vosk +from openwakeword.model import Model + +CONFIG = json.loads(sys.argv[1]) + +RATE = CONFIG.get("sampleRate", 16000) +CHUNK = 1280 +WAKEWORD = CONFIG.get("wakeword", "alexa") +THRESHOLD = CONFIG.get("wakewordThreshold", 0.75) +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) + +oww = Model(wakeword_models=[WAKEWORD], inference_framework="onnx") + +audio = pyaudio.PyAudio() +stream = audio.open( + format=pyaudio.paInt16, + channels=1, + rate=RATE, + input=True, + frames_per_buffer=CHUNK, +) + +print(json.dumps({"event": "ready"}), flush=True) + +mode = "wakeword" +high_count = 0 +last_trigger = 0 +silence_count = 0 +listen_deadline = None + +def rms_level(samples): + x = samples.astype(np.float32) / 32768.0 + return np.sqrt(np.mean(x * x)) + +try: + while True: + data = stream.read(CHUNK, exception_on_overflow=False) + samples = np.frombuffer(data, dtype=np.int16) + now = time.time() + + if mode == "wakeword": + preds = oww.predict(samples) + score = preds.get(WAKEWORD, 0.0) + + if score >= THRESHOLD: + high_count += 1 + else: + high_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 + + 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) + + level = rms_level(samples) + if level < SILENCE_THRESHOLD: + silence_count += 1 + else: + silence_count = 0 + + 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) + + print(json.dumps({"event": "done"}), flush=True) + mode = "wakeword" + silence_count = 0 + high_count = 0 + listen_deadline = None + +except KeyboardInterrupt: + pass +finally: + stream.stop_stream() + stream.close() + audio.terminate() From 2275e8b23aa161885cb9bde6329861637ea64f8c Mon Sep 17 00:00:00 2001 From: Apher Date: Mon, 6 Jul 2026 10:09:11 +0000 Subject: [PATCH 03/14] simplify config variables --- src/config/redis.config.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/config/redis.config.js b/src/config/redis.config.js index 627c270..9504b71 100644 --- a/src/config/redis.config.js +++ b/src/config/redis.config.js @@ -1,4 +1,4 @@ module.exports = { - REDIS_HOST: process.env.REDIS_HOST, - REDIS_PORT: process.env.REDIS_PORT + REDIS_HOST: process.env.HOST, + REDIS_PORT: process.env.PORT } From c2d1c62595563624fa5f21ea9e8de0ae11299878 Mon Sep 17 00:00:00 2001 From: Apher Date: Mon, 6 Jul 2026 10:12:22 +0000 Subject: [PATCH 04/14] Use new config variables --- src/models/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/models/index.js b/src/models/index.js index c02adc1..1bcc29b 100644 --- a/src/models/index.js +++ b/src/models/index.js @@ -6,7 +6,7 @@ const Redis = require('redis'); const redis = {}; redis.client = Redis.createClient({ - url: `redis://default@${redisConfig.REDIS_HOST}:${redisConfig.REDIS_PORT}` + url: `redis://default@${redisConfig.HOST}:${redisConfig.PORT}` }); redis.ensureGroup = async (stream, group) => { From 575435e44415ff86a5f44b2e8e03dd7c71e8e54e Mon Sep 17 00:00:00 2001 From: Apher Date: Mon, 6 Jul 2026 10:12:42 +0000 Subject: [PATCH 05/14] Rename python script --- src/scripts/py/{wakeword.py => transcribe.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/scripts/py/{wakeword.py => transcribe.py} (100%) diff --git a/src/scripts/py/wakeword.py b/src/scripts/py/transcribe.py similarity index 100% rename from src/scripts/py/wakeword.py rename to src/scripts/py/transcribe.py From 3c8c33a9623e92c2480c4ff4ed43670925193028 Mon Sep 17 00:00:00 2001 From: Apher Date: Mon, 6 Jul 2026 10:13:16 +0000 Subject: [PATCH 06/14] create config module for transcription --- src/config/transcribe.config.js | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/config/transcribe.config.js diff --git a/src/config/transcribe.config.js b/src/config/transcribe.config.js new file mode 100644 index 0000000..18b343a --- /dev/null +++ b/src/config/transcribe.config.js @@ -0,0 +1,11 @@ +module.exports = { + wakeword: process.env.TRANS_WAKEWORD, + wakewordThreshold: process.env.TRANS_WAKEWORD_THRESHOLD, + cooldownSeconds: process.env.TRANS_COOLDOWN_SECONDS, + confirmFrames: process.env.TRANS_CONFIRM_FRAMES, + listenSeconds: process.env.TRANS_LISTEN_SECONDS, + silenceThreshold: process.env.TRANS_SILENCE_THRESHOLD, + silenceFrames: process.env.TRANS_SIELNCE_FRAMES, + voskModelPath: process.env.TRANS_VOSK_MODEL_PATH, + sampleRate: process.env.TRANS_SAMPLE_RATE +} \ No newline at end of file From 7b233734a60eefac8c10d23708acdc346caf94e6 Mon Sep 17 00:00:00 2001 From: Apher Date: Mon, 6 Jul 2026 23:00:58 +0000 Subject: [PATCH 07/14] Move vosk model directory --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 2a5b053..4f02d84 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ .env node_modules/ .venv/ -src/scripts/py/vosk-model*/ \ No newline at end of file +src/models/vosk* \ No newline at end of file From c361e62de3e0426fefaee0e510207b4ecebd6259 Mon Sep 17 00:00:00 2001 From: Apher Date: Mon, 6 Jul 2026 23:01:20 +0000 Subject: [PATCH 08/14] Fix environment variable selection --- src/config/redis.config.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/config/redis.config.js b/src/config/redis.config.js index 9504b71..fc0ff3e 100644 --- a/src/config/redis.config.js +++ b/src/config/redis.config.js @@ -1,4 +1,4 @@ module.exports = { - REDIS_HOST: process.env.HOST, - REDIS_PORT: process.env.PORT + HOST: process.env.REDIS_HOST, + PORT: process.env.REDIS_PORT } From 8041d5123cf23e70203ffcdc249705d6977b1944 Mon Sep 17 00:00:00 2001 From: Apher Date: Mon, 6 Jul 2026 23:01:45 +0000 Subject: [PATCH 09/14] Ensure correct variable types --- src/config/transcribe.config.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/config/transcribe.config.js b/src/config/transcribe.config.js index 18b343a..d7799c5 100644 --- a/src/config/transcribe.config.js +++ b/src/config/transcribe.config.js @@ -1,11 +1,11 @@ module.exports = { - wakeword: process.env.TRANS_WAKEWORD, - wakewordThreshold: process.env.TRANS_WAKEWORD_THRESHOLD, - cooldownSeconds: process.env.TRANS_COOLDOWN_SECONDS, - confirmFrames: process.env.TRANS_CONFIRM_FRAMES, - listenSeconds: process.env.TRANS_LISTEN_SECONDS, - silenceThreshold: process.env.TRANS_SILENCE_THRESHOLD, - silenceFrames: process.env.TRANS_SIELNCE_FRAMES, - voskModelPath: process.env.TRANS_VOSK_MODEL_PATH, - sampleRate: process.env.TRANS_SAMPLE_RATE -} \ No newline at end of file + wakeword: process.env.TRANS_WAKEWORD, + wakewordThreshold: Number(process.env.TRANS_WAKEWORD_THRESHOLD), + cooldownSeconds: Number(process.env.TRANS_COOLDOWN_SECONDS), + confirmFrames: Number(process.env.TRANS_CONFIRM_FRAMES), + listenSeconds: Number(process.env.TRANS_LISTEN_SECONDS), + silenceThreshold: Number(process.env.TRANS_SILENCE_THRESHOLD), + silenceFrames: Number(process.env.TRANS_SIELNCE_FRAMES), + voskModelPath: process.env.TRANS_VOSK_MODEL_PATH, + sampleRate: Number(process.env.TRANS_SAMPLE_RATE), +}; From 1f8c41e100f721dc1d0ddc873a83434a7fbadd2e Mon Sep 17 00:00:00 2001 From: Apher Date: Mon, 6 Jul 2026 23:02:27 +0000 Subject: [PATCH 10/14] Merge transcribe code with server.js --- src/scripts/py/transcribe.js | 46 ---------------------------------- src/server.js | 48 ++++++++++++++++++++++++++++++++++-- 2 files changed, 46 insertions(+), 48 deletions(-) delete mode 100644 src/scripts/py/transcribe.js diff --git a/src/scripts/py/transcribe.js b/src/scripts/py/transcribe.js deleted file mode 100644 index 50137ca..0000000 --- a/src/scripts/py/transcribe.js +++ /dev/null @@ -1,46 +0,0 @@ -const { spawn } = require('child_process'); -const fs = require('fs'); - -const config = JSON.parse(fs.readFileSync('./config.json', 'utf8')); - -const py = spawn('python', ['wakeword.py', JSON.stringify(config)], { - stdio: ['ignore', 'pipe', 'pipe'] -}); - -let buffer = ''; - -py.stdout.on('data', (data) => { - buffer += data.toString(); - const lines = buffer.split('\n'); - buffer = lines.pop(); - - for (const line of lines) { - if (!line.trim()) continue; - - try { - const msg = JSON.parse(line); - - if (msg.event === 'ready') { - console.log('Python ready'); - } else if (msg.event === 'wakeword') { - console.log(`Wakeword detected: ${msg.model} (${msg.score})`); - } else if (msg.event === 'partial') { - console.log('Partial:', msg.text); - } else if (msg.event === 'text' || msg.event === 'final') { - console.log('Transcript:', msg.text); - } else if (msg.event === 'done') { - console.log('Back to wakeword mode'); - } - } catch { - console.log('[PY]', line); - } - } -}); - -py.stderr.on('data', (data) => { - console.error('[PY ERR]', data.toString()); -}); - -py.on('close', (code) => { - console.log('Python exited with code', code); -}); diff --git a/src/server.js b/src/server.js index 1536db7..96de790 100644 --- a/src/server.js +++ b/src/server.js @@ -3,10 +3,19 @@ if (process.env.NODE_ENV !== "production") { require("dotenv").config(); } +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'] +}); + +let buffer = ''; // Send Speech to Text to Redis Stream const sendTranscriptEvent = async (streamKey, device_id, text) => { @@ -26,8 +35,6 @@ const sendTranscriptEvent = async (streamKey, device_id, text) => { const processor = async (group, consumer, streamKey) => { await redis.client.connect().catch(console.error); - sendTranscriptEvent("transcripts", config.DEVICE_ID, "TEST"); - while (true) { const res = await redis.client.xReadGroup( group, @@ -53,6 +60,43 @@ const processor = async (group, consumer, streamKey) => { const main = async () => { await redis.ensureGroup("commands", 'assistant'); + py.stdout.on('data', (data) => { + buffer += data.toString(); + const lines = buffer.split('\n'); + buffer = lines.pop(); + + for (const line of lines) { + if (!line.trim()) continue; + + try { + const msg = JSON.parse(line); + + if (msg.event === 'ready') { + console.log('Python ready'); + } else if (msg.event === 'wakeword') { + console.log(`Wakeword detected: ${msg.model} (${msg.score})`); + } else if (msg.event === 'partial') { + console.log('Partial:', msg.text); + } else if (msg.event === 'text' || msg.event === 'final') { + console.log('Transcript:', msg.text); + sendTranscriptEvent("transcripts", config.DEVICE_ID, msg.text); + } else if (msg.event === 'done') { + console.log('Back to wakeword mode'); + } + } catch { + console.log('[PY]', line); + } + } + }); + + py.stderr.on('data', (data) => { + console.error('[PY ERR]', data.toString()); + }); + + py.on('close', (code) => { + console.log('Python exited with code', code); + }); + processor('assistant', `${config.DEVICE_ID}`, "commands"); } From 8ee656c06798865c64eaea018706edb36bfe7638 Mon Sep 17 00:00:00 2001 From: Apher Date: Mon, 6 Jul 2026 23:45:06 +0000 Subject: [PATCH 11/14] Use function instead of const for functions and dont have functions open/close redis connection --- src/server.js | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/server.js b/src/server.js index 96de790..989a9e2 100644 --- a/src/server.js +++ b/src/server.js @@ -32,9 +32,7 @@ const sendTranscriptEvent = async (streamKey, device_id, text) => { } // Read Redis Stream group -const processor = async (group, consumer, streamKey) => { - await redis.client.connect().catch(console.error); - +async function processor (group, consumer, streamKey) { while (true) { const res = await redis.client.xReadGroup( group, @@ -53,12 +51,11 @@ const processor = async (group, consumer, streamKey) => { } } } - - await redis.client.quit(); } -const main = async () => { - await redis.ensureGroup("commands", 'assistant'); +async function main() { + await redis.client.connect().catch(console.error); + await redis.ensureGroup("commands", "assistant"); py.stdout.on('data', (data) => { buffer += data.toString(); From 14f5a85bd8e88634019b17d9b3e5fec3e7d53c37 Mon Sep 17 00:00:00 2001 From: Apher Date: Mon, 6 Jul 2026 23:45:29 +0000 Subject: [PATCH 12/14] Quotations and catch error --- src/server.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/server.js b/src/server.js index 989a9e2..39122c0 100644 --- a/src/server.js +++ b/src/server.js @@ -86,15 +86,15 @@ async function main() { } }); - 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"); + processor("assistant", `${config.DEVICE_ID}`, "commands").catch(console.error); } main().catch(console.error); \ No newline at end of file From 990589a776d5f5f487bb1aea920dc27d7b7c6350 Mon Sep 17 00:00:00 2001 From: Apher Date: Mon, 6 Jul 2026 23:46:12 +0000 Subject: [PATCH 13/14] Move sendTranscriptEvent to redis object --- src/models/index.js | 16 ++++++++++++--- src/server.js | 48 ++++++--------------------------------------- 2 files changed, 19 insertions(+), 45 deletions(-) diff --git a/src/models/index.js b/src/models/index.js index 1bcc29b..605d2f2 100644 --- a/src/models/index.js +++ b/src/models/index.js @@ -10,8 +10,6 @@ redis.client = Redis.createClient({ }); redis.ensureGroup = async (stream, group) => { - await redis.client.connect().catch(console.error); - try { await redis.client.xGroupCreate(stream, group, "0", {MKSTREAM: true}); @@ -23,7 +21,19 @@ redis.ensureGroup = async (stream, group) => { console.error(err); } } - await redis.client.quit(); +} + +redis.sendTranscriptEvent = async (streamKey, device_id, text) => { + try { + await redis.client.xAdd(streamKey, "*", { + text, + device_id: device_id, + }); + + console.log("Transcript added to stream"); + } catch (err) { + console.error(err); + } } module.exports = { diff --git a/src/server.js b/src/server.js index 39122c0..bdb4b2e 100644 --- a/src/server.js +++ b/src/server.js @@ -14,22 +14,7 @@ const transConfig = require("./config/transcribe.config") const py = spawn('python', ['./src/scripts/py/transcribe.py', JSON.stringify(transConfig)], { stdio: ['ignore', 'pipe', 'pipe'] }); - -let buffer = ''; - -// Send Speech to Text to Redis Stream -const sendTranscriptEvent = async (streamKey, device_id, text) => { - await redis.client.xAdd( - streamKey, - '*', - { - text: text, - device_id: device_id - } - ) - - console.log("Transcript added to stream"); -} +const { handlePythonOutput } = require("./scripts/py/pythonOutput"); // Read Redis Stream group async function processor (group, consumer, streamKey) { @@ -57,33 +42,12 @@ async function main() { await redis.client.connect().catch(console.error); await redis.ensureGroup("commands", "assistant"); - py.stdout.on('data', (data) => { - buffer += data.toString(); - const lines = buffer.split('\n'); - buffer = lines.pop(); - - for (const line of lines) { - if (!line.trim()) continue; - - try { - const msg = JSON.parse(line); - - if (msg.event === 'ready') { - console.log('Python ready'); - } else if (msg.event === 'wakeword') { - console.log(`Wakeword detected: ${msg.model} (${msg.score})`); - } else if (msg.event === 'partial') { - console.log('Partial:', msg.text); - } else if (msg.event === 'text' || msg.event === 'final') { - console.log('Transcript:', msg.text); - sendTranscriptEvent("transcripts", config.DEVICE_ID, msg.text); - } else if (msg.event === 'done') { - console.log('Back to wakeword mode'); + 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); } - } catch { - console.log('[PY]', line); - } - } + }); }); py.stderr.on("data", (data) => { From 091c4f9becfbe8506ebd0a16445bf7713f189902 Mon Sep 17 00:00:00 2001 From: Apher Date: Mon, 6 Jul 2026 23:46:37 +0000 Subject: [PATCH 14/14] Handle python output --- src/scripts/py/pythonOutput.js | 41 ++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 src/scripts/py/pythonOutput.js diff --git a/src/scripts/py/pythonOutput.js b/src/scripts/py/pythonOutput.js new file mode 100644 index 0000000..e09e33b --- /dev/null +++ b/src/scripts/py/pythonOutput.js @@ -0,0 +1,41 @@ +// pythonOutput.js +let buffer = ""; + +function handlePythonOutput(chunk, onMessage) { + buffer += chunk; + const lines = buffer.split("\n"); + buffer = lines.pop() || ""; + + for (const line of lines) { + if (!line.trim()) continue; + + try { + const msg = JSON.parse(line); + switch (msg.event) { + case "ready": + console.log("Python ready"); + break; + case "wakeword": + console.log(`Wakeword detected: ${msg.model} (${msg.score})`); + break; + case "partial": + console.log("Partial:", msg.text); + break; + case "text": + case "final": + console.log("Transcript:", msg.text); + onMessage?.(msg).catch(console.error); + break; + case "done": + console.log("Back to wakeword mode"); + break; + default: + console.log("[PY]", line); + } + } catch { + console.log("[PY]", line); + } + } +} + +module.exports = { handlePythonOutput };