2024-08-13 16:46:37 -06:00
|
|
|
import { exec, ChildProcess } from "child_process";
|
2024-11-11 07:38:52 -08:00
|
|
|
import os from "node:os";
|
|
|
|
|
|
2024-12-04 17:37:30 -08:00
|
|
|
import { removeCodeBlocksAndTrim } from ".";
|
2024-12-04 17:21:04 -08:00
|
|
|
|
2024-12-05 21:53:09 -08:00
|
|
|
import type { IMessenger } from "../protocol/messenger";
|
2024-08-14 10:52:57 -06:00
|
|
|
import type { FromCoreProtocol, ToCoreProtocol } from "../protocol";
|
2024-08-13 16:46:37 -06:00
|
|
|
|
|
|
|
|
// The amount of time before a process is declared
|
|
|
|
|
// a zombie after executing .kill()
|
|
|
|
|
const ttsKillTimeout: number = 5000;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Cleans a message text to safely be used in 'exec' context on host.
|
|
|
|
|
*
|
|
|
|
|
* Return modified message text.
|
|
|
|
|
*/
|
2024-12-04 17:21:04 -08:00
|
|
|
export function sanitizeMessageForTTS(message: string): string {
|
|
|
|
|
message = removeCodeBlocksAndTrim(message);
|
2024-08-13 16:46:37 -06:00
|
|
|
|
|
|
|
|
// Remove or replace problematic characters
|
|
|
|
|
message = message
|
|
|
|
|
.replace(/"/g, "")
|
|
|
|
|
.replace(/`/g, "")
|
|
|
|
|
.replace(/\$/g, "")
|
|
|
|
|
.replace(/\\/g, "")
|
|
|
|
|
.replace(/[&|;()<>]/g, "");
|
|
|
|
|
|
|
|
|
|
message = message.trim().replace(/\s+/g, " ");
|
|
|
|
|
|
|
|
|
|
return message;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export class TTS {
|
|
|
|
|
static os: string | undefined = undefined;
|
|
|
|
|
static handle: ChildProcess | undefined = undefined;
|
2024-08-14 10:52:57 -06:00
|
|
|
static messenger: IMessenger<ToCoreProtocol, FromCoreProtocol>;
|
2024-08-13 16:46:37 -06:00
|
|
|
|
|
|
|
|
static async read(message: string) {
|
|
|
|
|
message = sanitizeMessageForTTS(message);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Kill any active TTS processes
|
|
|
|
|
await TTS.kill();
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.warn("Error killing TTS process: ", e);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch (TTS.os) {
|
|
|
|
|
case "darwin":
|
|
|
|
|
TTS.handle = exec(`say "${message}"`);
|
|
|
|
|
break;
|
|
|
|
|
case "win32":
|
|
|
|
|
// Replace single quotes on windows
|
|
|
|
|
TTS.handle = exec(
|
|
|
|
|
`powershell -Command "Add-Type -AssemblyName System.Speech; (New-Object System.Speech.Synthesis.SpeechSynthesizer).Speak('${message.replace(
|
|
|
|
|
/'/g,
|
|
|
|
|
"''",
|
|
|
|
|
)}')"`,
|
|
|
|
|
);
|
|
|
|
|
break;
|
|
|
|
|
case "linux":
|
|
|
|
|
TTS.handle = exec(`espeak "${message}"`);
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
console.log(
|
|
|
|
|
"Text-to-speech is not supported on this operating system.",
|
|
|
|
|
);
|
2024-08-14 10:52:57 -06:00
|
|
|
return;
|
2024-08-13 16:46:37 -06:00
|
|
|
}
|
2024-08-14 10:52:57 -06:00
|
|
|
|
2024-11-15 12:28:04 -08:00
|
|
|
void TTS.messenger.request("setTTSActive", true);
|
2024-08-14 10:52:57 -06:00
|
|
|
|
|
|
|
|
TTS.handle?.once("exit", () => {
|
2024-11-15 12:28:04 -08:00
|
|
|
void TTS.messenger.request("setTTSActive", false);
|
2024-08-14 10:52:57 -06:00
|
|
|
});
|
2024-08-13 16:46:37 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static async kill(): Promise<void> {
|
|
|
|
|
return new Promise<void>((resolve, reject) => {
|
|
|
|
|
// Only kill a TTS process if it's still running
|
|
|
|
|
if (TTS.handle && TTS.handle.exitCode === null) {
|
|
|
|
|
// Use a timeout in case of zombie processes
|
|
|
|
|
let killTimeout: NodeJS.Timeout = setTimeout(() => {
|
|
|
|
|
reject(`Unable to kill TTS process: ${TTS.handle?.pid}`);
|
|
|
|
|
}, ttsKillTimeout);
|
|
|
|
|
|
|
|
|
|
// Resolve our promise once the program has exited
|
|
|
|
|
TTS.handle.once("exit", () => {
|
|
|
|
|
clearTimeout(killTimeout);
|
|
|
|
|
TTS.handle = undefined;
|
|
|
|
|
resolve();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
TTS.handle.kill();
|
|
|
|
|
} else {
|
|
|
|
|
resolve();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static async setup() {
|
|
|
|
|
TTS.os = os.platform();
|
|
|
|
|
}
|
|
|
|
|
}
|