mtf-assistant/src/server.js

182 lines
4.7 KiB
JavaScript

// Import dotenv file if not in production
if (process.env.NODE_ENV !== "production") {
require("dotenv").config();
}
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 {redis} = require('./modules/redis');
const config = require("./config");
const transConfig = require("./config/transcribe.config");
const { handlePythonOutput } = require("./scripts/py/pythonOutput");
let speaker = null;
let speakerChannels = null;
function ensureSpeaker(channels) {
if (speaker && speakerChannels === channels) return;
speakerChannels = channels;
if (speaker) {
try { speaker.end(); } catch {}
}
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;
} 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) {
if (msg.message.device_id !== config.DEVICE_ID) {
continue
}
console.log(`[${consumer}] Processing:`, msg);
switch (msg.message.command) {
case "greet":
await playAudio("Hello! I am the Many Tailed Fox.");
break;
case "remind":
await playAudio("Reminder, " + String(msg.message.value));
break;
default:
await playAudio("Command, " + String(msg.message.command) + " is undefined.")
}
await redis.client.xAck(streamKey, group, msg.id);
}
}
}
}
async function main() {
// 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") {
await redis.sendTranscriptEvent("transcripts", config.DEVICE_ID, msg.text);
}
});
});
py.stderr.on("data", (data) => {
console.error("[PY ERR]", data.toString());
});
py.on("close", (code) => {
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);