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
+142 -3
View File
@@ -14,8 +14,9 @@
*/
import { openPrivateChannel, sendMessage } from "@vencord/types/utils";
import { findByPropsLazy } from "@vencord/types/webpack";
import { findByPropsLazy, findStoreLazy } from "@vencord/types/webpack";
import {
ChannelRTCStore,
ChannelStore,
GuildMemberStore,
GuildStore,
@@ -45,6 +46,63 @@ const ChannelActions = findByPropsLazy("selectVoiceChannel");
/** Discord's media engine actions, located lazily at runtime. */
const MediaEngineActions = findByPropsLazy("setSelfMute");
/** Per-user volume store, located lazily at runtime. */
const UserVolumeStore = findStoreLazy("UserVolumeStore");
/** Discord's per-user volume actions, located lazily at runtime. */
const UserVolumeActions = findByPropsLazy("setUserVolume");
/** Media engine store, for the client's own output volume. */
const MediaEngineStore = findStoreLazy("MediaEngineStore");
function userVolumeOf(userId: string): number | null {
const candidates: any[] = [UserVolumeStore, UserVolumeActions, MediaEngineStore, MediaEngineActions];
for (const candidate of candidates) {
try {
const getter = candidate?.getUserVolume;
if (typeof getter !== "function") continue;
const raw = getter.call(candidate, userId);
const value = typeof raw === "object" && raw !== null ? (raw as any).volume : raw;
const volume = Number(value);
if (Number.isFinite(volume)) return volume;
} catch {
// The lazy proxy throws when the module is absent.
}
}
return null;
}
function userVolumeSetter(): ((userId: string, volume: number) => void) | undefined {
const candidates: any[] = [UserVolumeStore, UserVolumeActions, MediaEngineStore, MediaEngineActions];
for (const candidate of candidates) {
try {
const setter = candidate?.setUserVolume;
if (typeof setter === "function") return setter.bind(candidate);
} catch {
// The lazy proxy throws when the module is absent.
}
}
return undefined;
}
function outputVolume(): number | null {
try {
const store = MediaEngineStore as any;
if (typeof store?.getLocalVolume === "function") {
const volume = Number(store.getLocalVolume());
return Number.isFinite(volume) ? volume : null;
}
} catch {
// Not available in this client build.
}
return null;
}
function compareIds(a: string, b: string): number {
try {
const left = BigInt(a);
@@ -193,7 +251,8 @@ function voiceMember(state: any, guildId?: string) {
selfDeaf: !!state.selfDeaf,
video: !!state.selfVideo,
stream: !!state.selfStream || !!state.stream,
suppress: !!state.suppress
suppress: !!state.suppress,
volume: userVolumeOf(state.userId)
};
}
@@ -373,6 +432,7 @@ export async function handlePuppetAction(action: string, params: Record<string,
guildId: channel?.guild_id ?? guildId ?? null,
channelId: voiceState.channelId,
channelName: channel?.name ?? null,
outputVolume: outputVolume(),
members: Object.values(states).map(state =>
voiceMember(state, (channel?.guild_id as string | undefined) ?? (guildId as string | undefined))
)
@@ -436,6 +496,72 @@ export async function handlePuppetAction(action: string, params: Record<string,
});
}
case PuppetAction.VoiceSpeaking: {
const targetId = UserStore.getCurrentUser()?.id;
assert(targetId, PuppetErrorCode.NotLoggedIn, "no user is logged in");
const voiceState =
VoiceStateStore.getVoiceStateForUser(targetId) ?? VoiceStateStore.getVoiceState(null, targetId);
if (!voiceState?.channelId) return ok({ channelId: null, userIds: [] });
const userIds = ChannelRTCStore.getSpeakingParticipants(voiceState.channelId)
.filter(participant => participant.speaking)
.map(participant => participant.user?.id ?? participant.id)
.filter(Boolean);
return ok({ channelId: voiceState.channelId, userIds });
}
case PuppetAction.UserVolume: {
const { userIds } = params;
assert(
Array.isArray(userIds) && userIds.length >= 1 && userIds.length <= 50,
PuppetErrorCode.InvalidParam,
"userIds must be an array of 1 to 50 user IDs"
);
assert(
(userIds as string[]).every(id => isSnowflake(id)),
PuppetErrorCode.InvalidSnowflake,
"every user ID must be a snowflake"
);
return ok({
outputVolume: outputVolume(),
users: (userIds as string[]).map(id => ({ id, volume: userVolumeOf(id) }))
});
}
case PuppetAction.UserSetVolume: {
const { userId, volume } = params;
assert(isSnowflake(userId), PuppetErrorCode.InvalidSnowflake, "userId must be a snowflake");
assert(
typeof volume === "number" && Number.isFinite(volume) && volume >= 0 && volume <= 200,
PuppetErrorCode.InvalidParam,
"volume must be a number from 0 to 200"
);
const setUserVolume = userVolumeSetter();
assert(
typeof setUserVolume === "function",
PuppetErrorCode.NotSupported,
"per-user volume is not available in this client build"
);
enforceOutboundRateLimit();
try {
setUserVolume(userId, volume);
} catch (error) {
throw new ActionError(
PuppetErrorCode.VoiceFailed,
`setting the user volume failed: ${String(error)}`
);
}
return ok({ userId, volume, outputVolume: outputVolume() });
}
case PuppetAction.VoiceOverview: {
const { guildId } = params;
assert(isSnowflake(guildId), PuppetErrorCode.InvalidSnowflake, "guildId must be a snowflake");
@@ -489,6 +615,18 @@ export async function handlePuppetAction(action: string, params: Record<string,
}
}
// Discord persists self-deafen across sessions. A deafened
// client receives no call audio, which breaks the monitor and
// the transcript, so joining clears it.
const setSelfDeaf = (MediaEngineActions as any)?.setSelfDeaf;
if (typeof setSelfDeaf === "function") {
try {
setSelfDeaf(false);
} catch {
// Best effort: the join still works.
}
}
try {
await selectVoiceChannel(channelId);
} catch (error) {
@@ -791,7 +929,8 @@ export async function handlePuppetAction(action: string, params: Record<string,
} catch (error) {
if (error instanceof ActionError) return fail(error.code, error.message);
const message = error instanceof Error ? error.message : String(error);
console.error("[puppet] action failed:", action, error);
return fail(PuppetErrorCode.Internal, "action failed");
return fail(PuppetErrorCode.Internal, `action failed: ${message}`);
}
}
+11 -3
View File
@@ -217,11 +217,19 @@ export function initPuppetEvents() {
dispatcher.subscribe("SPEAKING", (event: any) => {
try {
if (!event?.userId) return;
const userId = event?.userId ?? event?.user_id;
if (!userId) return;
// Discord sends a number bitfield (1 = microphone). A missing
// flag is treated as a start pulse: some dispatcher payloads
// carry no flag, and the UI decays on its own after silence.
const raw = event.speaking ?? event.speakingFlag;
const speaking = raw === undefined ? true : typeof raw === "boolean" ? raw : Number(raw) !== 0;
VesktopNative.puppet.emit({
type: "VOICE_SPEAKING",
userId: event.userId,
speaking: event.speaking !== false,
userId,
speaking,
channelId: event.channelId ?? event.channel_id ?? null
});
} catch (error) {