Add MicroslopSam TTS, Flextalk voice profiles, effects, and engine controls

This commit is contained in:
Fisto
2026-09-15 15:09:23 +02:00
parent eab94132bb
commit 302436f95f
42 changed files with 4675 additions and 180 deletions
+26 -5
View File
@@ -12,6 +12,9 @@ import { mainWin } from "./mainWindow";
const resolvers = new Map<string, Record<"resolve" | "reject", (data: any) => void>>();
/** Renderer commands that never answer must not leak their resolver or hang the caller. */
const RENDERER_TIMEOUT_MS = 15_000;
export interface IpcMessage {
nonce: string;
message: string;
@@ -31,18 +34,36 @@ export interface IpcResponse {
* You must add a handler for the message in the renderer process.
*/
export function sendRendererCommand<T = any>(message: string, data?: any) {
if (mainWin.isDestroyed()) {
console.warn("Main window is destroyed, cannot send IPC command:", message);
return Promise.reject(new Error("Main window is destroyed"));
if (mainWin.isDestroyed() || mainWin.webContents.isDestroyed() || mainWin.webContents.isCrashed()) {
return Promise.reject(new Error("the client renderer is not available"));
}
const nonce = randomUUID();
const promise = new Promise<T>((resolve, reject) => {
resolvers.set(nonce, { resolve, reject });
const timer = setTimeout(() => {
resolvers.delete(nonce);
reject(new Error(`the renderer did not answer ${message} in time`));
}, RENDERER_TIMEOUT_MS);
resolvers.set(nonce, {
resolve: value => {
clearTimeout(timer);
resolve(value);
},
reject: error => {
clearTimeout(timer);
reject(error);
}
});
});
mainWin.webContents.send(IpcEvents.IPC_COMMAND, { nonce, message, data });
try {
mainWin.webContents.send(IpcEvents.IPC_COMMAND, { nonce, message, data });
} catch (error) {
resolvers.delete(nonce);
return Promise.reject(error);
}
return promise;
}
+8 -1
View File
@@ -255,7 +255,14 @@ function initSettingsListeners(win: BrowserWindow) {
}
async function initSpellCheckLanguages(win: BrowserWindow, languages?: string[]) {
languages ??= await sendRendererCommand(IpcCommands.GET_LANGUAGES);
if (!languages) {
try {
languages = await sendRendererCommand(IpcCommands.GET_LANGUAGES);
} catch {
// The renderer may be loading or gone; spell check keeps its default.
return;
}
}
if (!languages) return;
const ses = session.defaultSession;
+19 -22
View File
@@ -21,6 +21,7 @@ import { IpcCommands, IpcEvents } from "shared/IpcEvents";
import {
PUPPET_LIMITS,
PuppetAction,
PuppetActions,
PuppetErrorCode,
type PuppetEvent,
type PuppetRequest,
@@ -36,30 +37,13 @@ export const PUPPET_SOCKET_PATH =
? "\\\\?\\pipe\\vesktop-puppet.sock"
: join(env.XDG_RUNTIME_DIR || env.TMPDIR || env.TMP || env.TEMP || "/tmp", "vesktop-puppet.sock");
const KNOWN_ACTIONS = new Set<string>([
PuppetAction.Ping,
PuppetAction.EventsSubscribe,
PuppetAction.StateGet,
PuppetAction.VoiceGet,
PuppetAction.VoiceOverview,
PuppetAction.VoiceJoin,
PuppetAction.VoiceLeave,
PuppetAction.VoiceMove,
PuppetAction.VoiceUsers,
PuppetAction.UserStatus,
PuppetAction.VoiceDevices,
PuppetAction.VoiceSetDevices,
PuppetAction.UserSearch,
PuppetAction.DmList,
PuppetAction.MessageSend,
PuppetAction.MessageRead,
PuppetAction.MessageFetch,
PuppetAction.ReactionAdd,
PuppetAction.DmSend
]);
// Derived from the shared list so a new action cannot be added in the protocol
// yet rejected by the bridge.
const KNOWN_ACTIONS = new Set<string>(PuppetActions);
let server: Server | undefined;
const subscribers = new Set<Socket>();
let rendererWarned = false;
export async function initPuppetBridge() {
await rm(PUPPET_SOCKET_PATH, { force: true });
@@ -148,11 +132,24 @@ async function handleLine(socket: Socket, line: string) {
try {
const result = await sendRendererCommand<PuppetResult>(IpcCommands.PUPPET_COMMAND, { action, params });
rendererWarned = false;
if (result?.ok === false) respond(socket, { id, ok: false, error: result.error });
else respond(socket, { id, ok: true, result: result?.result });
} catch (error) {
console.error("[puppet] action failed:", action, error);
const message = error instanceof Error ? error.message : String(error);
// A dead renderer is expected during a restart; the daemon polls every
// second, so log the outage once instead of once per command.
if (message === "the client renderer is not available") {
if (!rendererWarned) {
rendererWarned = true;
console.warn("[puppet] the client renderer is not available; commands fail until it reloads");
}
} else {
console.error("[puppet] action failed:", action, error);
}
respond(socket, {
id,
ok: false,