Guard Metame voice on human listeners

This commit is contained in:
Meta Vens 2026-06-13 20:14:56 +01:00
commit f338eb4854
3 changed files with 47 additions and 3 deletions

View file

@ -10,6 +10,7 @@ import {
type RepoDiscordIdentityRegistry,
WeksaSpeechClient,
} from "@voidbot/core";
import { ChannelType } from "discord.js";
import type { Message } from "discord.js";
const METAME_VOICE_DESIGN_PROMPT = [
@ -53,6 +54,12 @@ export async function maybeMirrorOwnerAquariumMessageAsMetameVoice(input: {
console.warn(`Skipped Metame owner voice bridge: ${metame.id} is not allowed in channel ${message.channelId}.`);
return;
}
if (!(await hasHumanListenerInAquariumVoice(message, config.repoFaceDiscordVoice.channelId))) {
console.log(
`Skipped Metame owner voice bridge for message ${message.id}: no human listeners in configured Aquarium voice channel.`,
);
return;
}
await appendMetameOwnerVoiceState({
statePath: config.metameOwnerVoice.statePath,
@ -170,3 +177,23 @@ async function appendMetameOwnerVoiceState(input: {
].join("\n");
await appendFile(input.statePath, `${header}${entry}`, "utf8");
}
async function hasHumanListenerInAquariumVoice(
message: Message,
voiceChannelId: string | undefined,
): Promise<boolean> {
if (!message.guild || !voiceChannelId) {
return false;
}
const channel = await message.guild.channels.fetch(voiceChannelId).catch((error) => {
const detail = error instanceof Error ? error.message : String(error);
console.warn(`Could not inspect Aquarium voice channel ${voiceChannelId}: ${detail}`);
return null;
});
if (!channel || (channel.type !== ChannelType.GuildVoice && channel.type !== ChannelType.GuildStageVoice)) {
return false;
}
return channel.members.some((member) => !member.user.bot);
}

View file

@ -10,7 +10,7 @@ import {
type AudioPlayer,
type VoiceConnection,
} from "@discordjs/voice";
import { ChannelType, type Client, type GuildBasedChannel } from "discord.js";
import { ChannelType, type Client, type VoiceBasedChannel } from "discord.js";
import {
appendRepoFaceVoicePlayedEntry,
@ -88,6 +88,12 @@ class RepoFaceVoicePlaybackRuntime {
this.active = true;
try {
const channel = await this.resolveVoiceChannel();
if (!hasHumanListener(channel)) {
console.log(
`Skipped repo Face voice playback for ${entry.id}: no human listeners in channel ${this.options.channelId}.`,
);
return;
}
const connection = await this.ensureConnection(channel);
const resource = createAudioResource(entry.audioPath, {
metadata: entry,
@ -108,7 +114,7 @@ class RepoFaceVoicePlaybackRuntime {
}
}
private async resolveVoiceChannel(): Promise<GuildBasedChannel> {
private async resolveVoiceChannel(): Promise<VoiceBasedChannel> {
const channel = await this.client.channels.fetch(this.options.channelId!);
if (!channel || !("guild" in channel)) {
throw new Error(`Configured voice channel ${this.options.channelId} was not found in a guild.`);
@ -119,7 +125,7 @@ class RepoFaceVoicePlaybackRuntime {
return channel;
}
private async ensureConnection(channel: GuildBasedChannel): Promise<VoiceConnection> {
private async ensureConnection(channel: VoiceBasedChannel): Promise<VoiceConnection> {
if (
this.connection &&
this.connection.joinConfig.channelId === channel.id &&
@ -136,7 +142,17 @@ class RepoFaceVoicePlaybackRuntime {
adapterCreator: channel.guild.voiceAdapterCreator,
selfDeaf: false,
});
this.connection.on("error", (error) => {
console.warn(`Repo Face voice connection error in channel ${channel.id}: ${error.message}`);
this.connection?.destroy();
this.connection = undefined;
this.active = false;
});
await entersState(this.connection, VoiceConnectionStatus.Ready, 30_000);
return this.connection;
}
}
function hasHumanListener(channel: VoiceBasedChannel): boolean {
return channel.members.some((member) => !member.user.bot);
}

View file

@ -357,3 +357,4 @@
{"ts":"2026-06-12T18:12:19Z","type":"implementation","status":"ok","note":"Added owner-authorized Aquarium-to-Metame voice bridge: owner messages in Aquarium can be mirrored exactly through shared Weksa/MiMo speech rendering, persisted to a Weksa-readable Metame voice-sync projection, and queued into the existing Discord voice outbox."}
{"ts":"2026-06-13T08:34:42Z","type":"implementation","status":"ok","note":"Created native Moobs Persona CultCache state at .voidbot/private/personas/moobs/moobs.cc from D:/Downloads/Moobs.txt; Moobs remains outside repo Face/swarm registries and is for iVixey's separate Discord."}
{"ts":"2026-06-13T12:09:58Z","type":"verification","status":"ok","note":"2026-06-13 local moderation heartbeat was re-enabled by setting VOIDBOT_MODERATION_HEARTBEAT_ENABLED=true while VOID_MODERATION_ENFORCEMENT_MODE=policy. A direct ModerationHeartbeatOnly pass completed ok in policy mode, observed 0 new messages past cursor 2026-06-12T18:40:31.845Z, proposed/applied 0 operations, and left openCaseCount=0."}
{"ts":"2026-06-13T19:11:52Z","type":"implementation","status":"ok","note":"Disabled Metame owner voice locally and hardened the voice path: when re-enabled, owner Aquarium text only renders through Weksa if the configured Aquarium voice channel currently has a non-bot listener; playback also skips empty voice channels and logs connection errors instead of crashing the bot."}